Merge branch 'master' of github.com:Mintplex-Labs/anything-llm into render

This commit is contained in:
timothycarambat 2024-03-12 12:29:57 -07:00
commit 429ea0c805
42 changed files with 942 additions and 600 deletions

View File

@ -40,7 +40,7 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"wavefile": "^11.0.0", "wavefile": "^11.0.0",
"youtube-transcript": "^1.0.6", "youtube-transcript": "^1.0.6",
"youtubei.js": "^8.0.0" "youtubei.js": "^9.1.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.22", "nodemon": "^2.0.22",

View File

@ -40,9 +40,9 @@
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@fastify/busboy@^2.0.0": "@fastify/busboy@^2.0.0":
version "2.1.0" version "2.1.1"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
"@googleapis/youtube@^9.0.0": "@googleapis/youtube@^9.0.0":
version "9.0.0" version "9.0.0"
@ -258,9 +258,9 @@ accepts@~1.3.8:
negotiator "0.6.3" negotiator "0.6.3"
acorn@^8.8.0: acorn@^8.8.0:
version "8.11.2" version "8.11.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
agent-base@6: agent-base@6:
version "6.0.2" version "6.0.2"
@ -3152,9 +3152,9 @@ undici-types@~5.26.4:
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^5.19.1: undici@^5.19.1:
version "5.28.2" version "5.28.3"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b"
integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w== integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==
dependencies: dependencies:
"@fastify/busboy" "^2.0.0" "@fastify/busboy" "^2.0.0"
@ -3322,10 +3322,10 @@ youtube-transcript@^1.0.6:
dependencies: dependencies:
phin "^3.5.0" phin "^3.5.0"
youtubei.js@^8.0.0: youtubei.js@^9.1.0:
version "8.0.0" version "9.1.0"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-8.0.0.tgz#0fcbe332e263d9be6afe4e3d1917e9ddc1ffbed3" resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.1.0.tgz#bcf154c9fa21d3c8c1d00a5e10360d0a065c660e"
integrity sha512-kUwHvqoB5vfaGaY1quAGcX5JPIyjr5fjj9Zj/ZwUDCrermz/r5uIkNiJ5cNHkmAJbZP9fdygzNMvGHd7fM445g== integrity sha512-C5GBJ4LgnS6vGAUkdIdQNOFFb5EZ1p3xBvUELNXmIG3Idr6vxWrKNBNy8ClZT3SuDVXaAJqDgF9b5jvY8lNKcg==
dependencies: dependencies:
jintr "^1.1.0" jintr "^1.1.0"
tslib "^2.5.0" tslib "^2.5.0"

View File

@ -75,7 +75,7 @@ mintplexlabs/anythingllm
# Run this in powershell terminal # Run this in powershell terminal
$env:STORAGE_LOCATION="$HOME\Documents\anythingllm"; ` $env:STORAGE_LOCATION="$HOME\Documents\anythingllm"; `
If(!(Test-Path $env:STORAGE_LOCATION)) {New-Item $env:STORAGE_LOCATION -ItemType Directory}; ` If(!(Test-Path $env:STORAGE_LOCATION)) {New-Item $env:STORAGE_LOCATION -ItemType Directory}; `
If(!(Test-Path "$env:STORAGE_LOCATION\.env")) {New-Item "$env:STORAGE_LOCATION\.env"}; ` If(!(Test-Path "$env:STORAGE_LOCATION\.env")) {New-Item "$env:STORAGE_LOCATION\.env" -ItemType File}; `
docker run -d -p 3001:3001 ` docker run -d -p 3001:3001 `
--cap-add SYS_ADMIN ` --cap-add SYS_ADMIN `
-v "$env:STORAGE_LOCATION`:/app/server/storage" ` -v "$env:STORAGE_LOCATION`:/app/server/storage" `

View File

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

View File

@ -57,8 +57,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
reason: file.errors[0].code, reason: file.errors[0].code,
}; };
}); });
setFiles([...newAccepted, ...newRejected]);
setFiles([...files, ...newAccepted, ...newRejected]);
}; };
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

@ -38,10 +38,10 @@ export default function Sidebar() {
<div <div
ref={sidebarRef} ref={sidebarRef}
style={{ height: "calc(100% - 76px)" }} 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 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 flex-col gap-y-2 pb-8 overflow-y-scroll no-scroll">
<div className="flex gap-x-2 items-center justify-between"> <div className="flex gap-x-2 items-center justify-between">
{(!user || user?.role !== "default") && ( {(!user || user?.role !== "default") && (
@ -144,9 +144,11 @@ export function SidebarMobileHeader() {
style={{ objectFit: "contain" }} style={{ objectFit: "contain" }}
/> />
</div> </div>
<div className="flex gap-x-2 items-center text-slate-500 shrink-0"> {(!user || user?.role !== "default") && (
<SettingsButton /> <div className="flex gap-x-2 items-center text-slate-500 shink-0">
</div> <SettingsButton />
</div>
)}
</div> </div>
{/* Primary Body */} {/* Primary Body */}

View File

@ -31,15 +31,7 @@ const HistoricalMessage = ({
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`} className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
> >
<div className="flex gap-x-5"> <div className="flex gap-x-5">
<Jazzicon <ProfileImage role={role} workspace={workspace} />
size={36}
user={{
uid:
role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
{error ? ( {error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500"> <div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className={`inline-block `}> <span className={`inline-block `}>
@ -76,4 +68,28 @@ const HistoricalMessage = ({
); );
}; };
function ProfileImage({ role, workspace }) {
if (role === "assistant" && workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return (
<Jazzicon
size={36}
user={{
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
);
}
export default memo(HistoricalMessage); export default memo(HistoricalMessage);

View File

@ -14,7 +14,6 @@ const PromptReply = ({
closed = true, closed = true,
}) => { }) => {
const assistantBackgroundColor = "bg-historical-msg-system"; const assistantBackgroundColor = "bg-historical-msg-system";
if (!reply && sources.length === 0 && !pending && !error) return null; if (!reply && sources.length === 0 && !pending && !error) return null;
if (pending) { if (pending) {
@ -24,11 +23,7 @@ const PromptReply = ({
> >
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col"> <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5"> <div className="flex gap-x-5">
<Jazzicon <WorkspaceProfileImage workspace={workspace} />
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<div className="mt-3 ml-5 dot-falling"></div> <div className="mt-3 ml-5 dot-falling"></div>
</div> </div>
</div> </div>
@ -43,11 +38,7 @@ const PromptReply = ({
> >
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col"> <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5"> <div className="flex gap-x-5">
<Jazzicon <WorkspaceProfileImage workspace={workspace} />
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<span <span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`} className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
> >
@ -68,7 +59,7 @@ const PromptReply = ({
> >
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col"> <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5"> <div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" /> <WorkspaceProfileImage workspace={workspace} />
<span <span
className={`reply flex flex-col gap-y-1 mt-2`} className={`reply flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }} dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
@ -80,4 +71,20 @@ const PromptReply = ({
); );
}; };
function WorkspaceProfileImage({ workspace }) {
if (!!workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return <Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />;
}
export default memo(PromptReply); export default memo(PromptReply);

View File

@ -238,6 +238,54 @@ const Workspace = {
}); });
}, },
threads: WorkspaceThread, threads: WorkspaceThread,
uploadPfp: async function (formData, slug) {
return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, {
method: "POST",
body: formData,
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Error uploading pfp.");
return { success: true, error: null };
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
fetchPfp: async function (slug) {
return await fetch(`${API_BASE}/workspace/${slug}/pfp`, {
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
})
.then((res) => {
if (res.ok && res.status !== 204) return res.blob();
throw new Error("Failed to fetch pfp.");
})
.then((blob) => (blob ? URL.createObjectURL(blob) : null))
.catch((e) => {
console.log(e);
return null;
});
},
removePfp: async function (slug) {
return await fetch(`${API_BASE}/workspace/${slug}/remove-pfp`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => {
if (res.ok) return { success: true, error: null };
throw new Error("Failed to remove pfp.");
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
}; };
export default Workspace; export default Workspace;

View File

@ -13,25 +13,29 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminInvites() { export default function AdminInvites() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <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 <button
onClick={openModal} 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> </button>
</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">
Create invitation links for people in your organization to accept Create invitation links for people in your organization to accept
and sign up with. Invitations can only be used by a single user. and sign up with. Invitations can only be used by a single user.
</p> </p>
@ -50,6 +54,7 @@ function InvitationsContainer() {
const darkMode = usePrefersDarkMode(); const darkMode = usePrefersDarkMode();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [invites, setInvites] = useState([]); const [invites, setInvites] = useState([]);
useEffect(() => { useEffect(() => {
async function fetchInvites() { async function fetchInvites() {
const _invites = await Admin.invites(); const _invites = await Admin.invites();
@ -74,13 +79,13 @@ function InvitationsContainer() {
} }
return ( return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5"> <table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Status Status
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3">
Accepted By Accepted By
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">

View File

@ -30,20 +30,22 @@ export default function AdminLogs() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <div className="flex gap-x-4 items-center">
<p className="text-2xl font-semibold text-white">Event Logs</p> <p className="text-lg leading-6 font-bold text-white">
Event Logs
</p>
<button <button
onClick={handleResetLogs} 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 Clear event logs
</button> </button>
</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">
View all actions and events happening on this instance for View all actions and events happening on this instance for
monitoring. monitoring.
</p> </p>
@ -95,10 +97,10 @@ function LogsContainer() {
return ( return (
<> <>
<table className="md:w-5/6 w-full text-sm text-left rounded-lg mt-5"> <table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Event Type Event Type
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
@ -116,7 +118,7 @@ function LogsContainer() {
{!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)} {!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)}
</tbody> </tbody>
</table> </table>
<div className="flex w-full justify-between items-center"> <div className="flex w-full justify-between items-center mt-6">
<button <button
onClick={handlePrevious} 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" 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, enabled: false,
limit: 10, limit: 10,
}); });
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setSaving(true); setSaving(true);
@ -43,46 +44,35 @@ export default function AdminSystem() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
onChange={() => setHasChanges(true)} 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="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="items-center">
<div className="items-center flex gap-x-4"> <p className="text-lg leading-6 font-bold text-white">
<p className="text-2xl font-semibold text-white"> System Preferences
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.
</p> </p>
</div> </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="mt-6 mb-8">
<div className="flex flex-col gap-y-2 mb-2.5"> <div className="flex flex-col gap-y-1">
<label className="leading-tight font-semibold text-white"> <h2 className="text-base leading-6 font-bold text-white">
Users can delete workspaces Users can delete workspaces
</label> </h2>
<p className="leading-tight text-sm text-white text-opacity-60 w-96"> <p className="text-xs leading-[18px] font-base text-white/60">
Allow non-admin users to delete workspaces that they are a Allow non-admin users to delete workspaces that they are a part
part of. This would delete the workspace for everyone. of. This would delete the workspace for everyone.
</p> </p>
</div> <label className="relative inline-flex cursor-pointer items-center mt-2">
<label className="relative inline-flex cursor-pointer items-center">
<input <input
type="checkbox" type="checkbox"
name="users_can_delete_workspaces" 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> <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label> </label>
</div> </div>
</div>
<div className="my-4"> <div className="mb-8">
<div className="flex flex-col gap-y-2 mb-2.5"> <div className="flex flex-col gap-y-1">
<label className="leading-tight font-medium text-black dark:text-white"> <h2 className="text-base leading-6 font-bold text-white">
Limit messages per user per day 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> </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> </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> </div>
{messageLimit.enabled && ( {messageLimit.enabled && (
<div className="mb-4"> <div className="mt-4">
<label className=" block flex items-center gap-x-1 font-medium text-black dark:text-white"> <label className="block text-sm font-medium text-white">
Message limit per day Message limit per day
</label> </label>
<div className="relative"> <div className="relative mt-2">
<input <input
type="number" type="number"
name="message_limit" name="message_limit"
@ -143,12 +135,24 @@ export default function AdminSystem() {
value={messageLimit.limit} value={messageLimit.limit}
min={1} min={1}
max={300} 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> </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> </form>
</div> </div>
</div> </div>

View File

@ -13,25 +13,26 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminUsers() { export default function AdminUsers() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <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 <button
onClick={openModal} 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 <UserPlus className="h-4 w-4" /> Add user
</button> </button>
</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 accounts which have an account on this instance. These are all the accounts which have an account on this instance.
Removing an account will instantly remove their access to this Removing an account will instantly remove their access to this
instance. instance.
@ -51,6 +52,7 @@ function UsersContainer() {
const { user: currUser } = useUser(); const { user: currUser } = useUser();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
useEffect(() => { useEffect(() => {
async function fetchUsers() { async function fetchUsers() {
const _users = await Admin.users(); const _users = await Admin.users();
@ -75,8 +77,8 @@ function UsersContainer() {
} }
return ( return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5"> <table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Username Username
@ -120,7 +122,7 @@ const ROLE_HINT = {
export function RoleHintDisplay({ role }) { export function RoleHintDisplay({ role }) {
return ( return (
<div className="flex flex-col gap-y-1 py-1 pb-4"> <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"> <ul className="flex flex-col gap-y-1 list-disc px-4">
{ROLE_HINT[role ?? "default"].map((hints, i) => { {ROLE_HINT[role ?? "default"].map((hints, i) => {
return ( return (

View File

@ -13,27 +13,28 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminWorkspaces() { export default function AdminWorkspaces() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <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">
Instance workspaces Instance Workspaces
</p> </p>
<button <button
onClick={openModal} 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 <BookOpen className="h-4 w-4" /> New Workspace
</button> </button>
</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 workspaces that exist on this instance. Removing These are all the workspaces that exist on this instance. Removing
a workspace will delete all of it's associated chats and settings. a workspace will delete all of it's associated chats and settings.
</p> </p>
@ -80,8 +81,8 @@ function WorkspacesContainer() {
} }
return ( return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5"> <table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Name Name

View File

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

View File

@ -1,7 +1,7 @@
import useLogo from "@/hooks/useLogo"; import useLogo from "@/hooks/useLogo";
import System from "@/models/system"; import System from "@/models/system";
import showToast from "@/utils/toast"; 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 AnythingLLM from "@/media/logo/anything-llm.png";
import { Plus } from "@phosphor-icons/react"; import { Plus } from "@phosphor-icons/react";
@ -9,6 +9,7 @@ export default function CustomLogo() {
const { logo: _initLogo, setLogo: _setLogo } = useLogo(); const { logo: _initLogo, setLogo: _setLogo } = useLogo();
const [logo, setLogo] = useState(""); const [logo, setLogo] = useState("");
const [isDefaultLogo, setIsDefaultLogo] = useState(true); const [isDefaultLogo, setIsDefaultLogo] = useState(true);
const fileInputRef = useRef(null);
useEffect(() => { useEffect(() => {
async function logoInit() { async function logoInit() {
@ -62,61 +63,88 @@ export default function CustomLogo() {
showToast("Image successfully removed.", "success"); showToast("Image successfully removed.", "success");
}; };
const triggerFileInputClick = () => {
fileInputRef.current?.click();
};
return ( return (
<div className="my-6"> <div className="mt-6 mb-8">
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-1">
<h2 className="leading-tight font-medium text-white">Custom Logo</h2> <h2 className="text-base leading-6 font-bold text-white">
<p className="text-sm font-base text-white/60"> Custom Logo
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Upload your custom logo to make your chatbot yours. Upload your custom logo to make your chatbot yours.
</p> </p>
</div> </div>
<div className="flex md:flex-row flex-col items-center"> {isDefaultLogo ? (
<img <div className="flex md:flex-row flex-col items-center">
src={logo} <div className="flex flex-row gap-x-8">
alt="Uploaded Logo" <label
className="w-48 h-48 object-contain mr-6" className="mt-3 transition-all duration-300 hover:opacity-60"
hidden={isDefaultLogo} 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"
> >
<div className="flex flex-col items-center justify-center"> <input
<div className="rounded-full bg-white/40"> id="logo-upload"
<Plus className="w-6 h-6 text-black/80 m-2" /> type="file"
</div> accept="image/*"
<div className="text-white text-opacity-80 text-sm font-semibold py-1"> className="hidden"
Add a custom logo onChange={handleFileUpload}
</div> />
<div className="text-white text-opacity-60 text-xs font-medium py-1"> <div
Recommended size: 800 x 200 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> </div>
</div> </label>
</label> </div>
{!isDefaultLogo && (
<button
onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60"
>
Delete
</button>
)}
</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> </div>
); );
} }

View File

@ -53,16 +53,16 @@ export default function CustomMessages() {
}; };
return ( return (
<div className="mb-6"> <div className="mb-8">
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-1">
<h2 className="leading-tight font-medium text-white"> <h2 className="text-base leading-6 font-bold text-white">
Custom Messages Custom Messages
</h2> </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. Customize the automatic messages displayed to your users.
</p> </p>
</div> </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) => ( {messages.map((message, index) => (
<div key={index} className="flex flex-col gap-y-2"> <div key={index} className="flex flex-col gap-y-2">
{message.user && ( {message.user && (
@ -85,27 +85,34 @@ export default function CustomMessages() {
)} )}
</div> </div>
))} ))}
<div className="flex gap-4 mt-12 justify-between pb-7"> <div className="flex gap-4 mt-12 justify-between pb-[15px]">
<button <button
className="self-end text-white hover:text-white/60 transition" className="self-end text-white hover:text-white/60 transition"
onClick={() => addMessage("response")} onClick={() => addMessage("response")}
> >
<div className="flex items-center justify-start"> <div className="flex items-center justify-start text-sm font-normal -ml-2">
<Plus className="w-5 h-5 m-2" weight="fill" /> New System Message <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> </div>
</button> </button>
<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")} onClick={() => addMessage("user")}
> >
<div className="flex items-center"> <div className="flex items-center justify-start text-sm font-normal">
<Plus className="w-5 h-5 m-2" weight="fill" /> New User Message <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> </div>
</button> </button>
</div> </div>
</div> </div>
{hasChanges && ( {hasChanges && (
<div className="flex justify-center py-6"> <div className="flex justify-start pt-6">
<button <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" 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} onClick={handleMessageSave}

View File

@ -1,11 +1,20 @@
import { ICON_COMPONENTS } from "@/components/Footer"; import { ICON_COMPONENTS } from "@/components/Footer";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Plus, X } from "@phosphor-icons/react";
export default function NewIconForm({ handleSubmit, showing }) { export default function NewIconForm({ icon, url, onSave, onRemove }) {
const [selectedIcon, setSelectedIcon] = useState("Info"); const [selectedIcon, setSelectedIcon] = useState(icon || "Plus");
const [selectedUrl, setSelectedUrl] = useState(url || "");
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isEdited, setIsEdited] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
useEffect(() => {
setSelectedIcon(icon || "Plus");
setSelectedUrl(url || "");
setIsEdited(false);
}, [icon, url]);
useEffect(() => { useEffect(() => {
function handleClickOutside(event) { function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -17,82 +26,90 @@ export default function NewIconForm({ handleSubmit, showing }) {
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, [dropdownRef]); }, [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 ( return (
<form onSubmit={handleSubmit} className="flex justify-start"> <form onSubmit={handleSubmit} className="flex items-center gap-x-1.5">
<div className="mt-6 mb-6 flex flex-col bg-zinc-900 rounded-lg px-6 py-4"> <div className="relative" ref={dropdownRef}>
<div className="flex gap-x-4 items-center"> <div
<div className="h-[34px] w-[34px] bg-[#1C1E21] rounded-full flex items-center justify-center cursor-pointer"
className="relative flex flex-col items-center gap-y-4" onClick={() => setIsDropdownOpen(!isDropdownOpen)}
ref={dropdownRef} >
> {React.createElement(ICON_COMPONENTS[selectedIcon] || Plus, {
<input type="hidden" name="icon" value={selectedIcon} /> className: "h-5 w-5 text-white",
<label className="text-sm font-medium text-white">Icon</label> 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 <button
type="button" type="button"
className={`${ onClick={handleRemove}
isDropdownOpen className="hover:text-red-500 text-white/80 px-2 py-2 rounded-md text-sm font-bold"
? "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);
}}
> >
{React.createElement(ICON_COMPONENTS[selectedIcon], { <X size={20} />
className: "h-5 w-5 text-white",
weight: "fill",
})}
</button> </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> </form>
); );
} }

View File

@ -1,36 +1,37 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import showToast from "@/utils/toast"; 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 { safeJsonParse } from "@/utils/request";
import NewIconForm from "./NewIconForm"; import NewIconForm from "./NewIconForm";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import System from "@/models/system"; import System from "@/models/system";
export default function FooterCustomization() { export default function FooterCustomization() {
const [loading, setLoading] = useState(true); const [footerIcons, setFooterIcons] = useState(Array(3).fill(null));
const [footerIcons, setFooterIcons] = useState([]);
const [showForm, setShowForm] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchFooterIcons() { async function fetchFooterIcons() {
const settings = (await Admin.systemPreferences())?.settings; const settings = (await Admin.systemPreferences())?.settings;
if (settings && settings.footer_data) { 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(); fetchFooterIcons();
}, []); }, []);
const removeFooterIcon = async (index) => { const updateFooterIcons = async (updatedIcons) => {
const updatedIcons = footerIcons.filter((_, i) => i !== index);
const { success, error } = await Admin.updateSystemPreferences({ const { success, error } = await Admin.updateSystemPreferences({
footer_data: JSON.stringify(updatedIcons), footer_data: JSON.stringify(updatedIcons.filter((icon) => icon !== null)),
}); });
if (!success) { if (!success) {
showToast(`Failed to remove footer icon - ${error}`, "error", { showToast(`Failed to update footer icons - ${error}`, "error", {
clear: true, clear: true,
}); });
return; return;
@ -38,103 +39,44 @@ export default function FooterCustomization() {
window.localStorage.removeItem(System.cacheKeys.footerIcons); window.localStorage.removeItem(System.cacheKeys.footerIcons);
setFooterIcons(updatedIcons); setFooterIcons(updatedIcons);
showToast("Successfully removed footer icon.", "success", { clear: true }); showToast("Successfully updated footer icons.", "success", { clear: true });
}; };
const onSubmit = async (e) => { const handleRemoveIcon = (index) => {
e.preventDefault(); const updatedIcons = [...footerIcons];
const form = new FormData(e.target); updatedIcons[index] = null;
const icon = form.get("icon"); updateFooterIcons(updatedIcons);
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 });
}; };
return ( return (
<div className="mb-6"> <div className="mb-8">
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-1">
<h2 className="leading-tight font-medium text-white"> <h2 className="text-base leading-6 font-bold text-white">
Custom Footer Icons Custom Footer Icons
</h2> </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. Customize the footer icons displayed on the bottom of the sidebar.
</p> </p>
</div> </div>
<CurrentIcons footerIcons={footerIcons} remove={removeFooterIcon} /> <div className="mt-3 flex gap-x-3 font-bold text-white text-sm">
<NewIconForm <div>Icon</div>
handleSubmit={onSubmit} <div>Link</div>
showing={footerIcons.length < MAX_ICONS && showForm} </div>
/> <div className="mt-2 flex flex-col gap-y-[10px]">
<div hidden={!(!showForm && footerIcons.length < MAX_ICONS) || loading}> {footerIcons.map((icon, index) => (
<div className="flex gap-2 mt-6"> <NewIconForm
<button key={index}
onClick={() => setShowForm(true)} icon={icon?.icon}
className="flex gap-x-2 items-center justify-center text-white text-sm hover:text-sky-400 transition-all duration-300" url={icon?.url}
> onSave={(newIcon, newUrl) => {
Add new footer icon const updatedIcons = [...footerIcons];
<Plus className="" size={24} weight="fill" /> updatedIcons[index] = { icon: newIcon, url: newUrl };
</button> updateFooterIcons(updatedIcons);
</div> }}
onRemove={() => handleRemoveIcon(index)}
/>
))}
</div> </div>
</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; if (loading || !user?.role) return null;
return ( return (
<form className="mb-6" onSubmit={updateSupportEmail}> <form className="mb-6" onSubmit={updateSupportEmail}>
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-1">
<h2 className="leading-tight font-medium text-white">Support Email</h2> <h2 className="text-base leading-6 font-bold text-white">
<p className="text-sm font-base text-white/60"> 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 Set the support email address that shows up in the user menu while
logged into this instance. logged into this instance.
</p> </p>
@ -64,7 +66,7 @@ export default function SupportEmail() {
<input <input
name="supportEmail" name="supportEmail"
type="email" 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" placeholder="support@mycompany.com"
required={true} required={true}
autoComplete="off" autoComplete="off"

View File

@ -11,16 +11,16 @@ export default function Appearance() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <div className="items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Appearance Settings Appearance
</p> </p>
</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">
Customize the appearance settings of your platform. Customize the appearance settings of your platform.
</p> </p>
</div> </div>

View File

@ -7,7 +7,7 @@ import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow"; import ChatRow from "./ChatRow";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import System from "@/models/system"; import System from "@/models/system";
import { CaretDown } from "@phosphor-icons/react"; import { CaretDown, Download } from "@phosphor-icons/react";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
const exportOptions = { const exportOptions = {
@ -47,11 +47,9 @@ const exportOptions = {
export default function WorkspaceChats() { export default function WorkspaceChats() {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [exportType, setExportType] = useState("jsonl");
const menuRef = useRef(); const menuRef = useRef();
const openMenuButton = useRef(); const openMenuButton = useRef();
const handleDumpChats = async (exportType) => {
const handleDumpChats = async () => {
const chats = await System.exportChats(exportType); const chats = await System.exportChats(exportType);
if (!!chats) { if (!!chats) {
const { name, mimeType, fileExtension, filenameFunc } = const { name, mimeType, fileExtension, filenameFunc } =
@ -90,56 +88,48 @@ export default function WorkspaceChats() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <div className="flex gap-x-4 items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Workspace Chats Workspace Chats
</p> </p>
<div className="flex gap-x-1 relative"> <div className="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>
<button <button
ref={openMenuButton} ref={openMenuButton}
onClick={toggleMenu} 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 ${ 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]"
showMenu ? "bg-slate-200 text-slate-800" : ""
}`}
> >
<CaretDown weight="bold" className="h-4 w-4" /> <Download size={18} weight="bold" />
Export
<CaretDown size={18} weight="bold" />
</button> </button>
<div <div
ref={menuRef} ref={menuRef}
className={`${ className={`${
showMenu ? "slide-down" : "slide-up hidden" 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"> <div className="py-2">
{Object.entries(exportOptions) {Object.entries(exportOptions).map(([key, data]) => (
.filter(([type, _]) => type !== exportType) <button
.map(([key, data]) => ( key={key}
<button onClick={() => {
key={key} handleDumpChats(key);
onClick={() => { setShowMenu(false);
setExportType(key); }}
setShowMenu(false); className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
}} >
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md" {data.name}
> </button>
{data.name} ))}
</button>
))}
</div> </div>
</div> </div>
</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 These are all the recorded chats and messages that have been sent
by users ordered by their creation date. by users ordered by their creation date.
</p> </p>
@ -195,8 +185,8 @@ function ChatsContainer() {
return ( return (
<> <>
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5"> <table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Id Id
@ -228,7 +218,7 @@ function ChatsContainer() {
))} ))}
</tbody> </tbody>
</table> </table>
<div className="flex w-full justify-between items-center"> <div className="flex w-full justify-between items-center mt-6">
<button <button
onClick={handlePrevious} 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" 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 /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 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="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10"> <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" /> <img src={image} alt="Github" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1"> <div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4"> <div className="items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Import GitHub Repository Import GitHub Repository
</p> </p>
</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">
Import all files from a public or private Github repository Import all files from a public or private Github repository
and have its files be available in your workspace. and have its files be available in your workspace.
</p> </p>
@ -88,7 +88,7 @@ export default function GithubConnectorSetup() {
<form className="w-full" onSubmit={handleSubmit}> <form className="w-full" onSubmit={handleSubmit}>
{!accessToken && ( {!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 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"> <div className="flex items-center gap-x-2">
<Info size={20} className="shrink-0 text-blue-400" /> <Info size={20} className="shrink-0 text-blue-400" />

View File

@ -48,19 +48,19 @@ export default function YouTubeTranscriptConnectorSetup() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 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="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10"> <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" /> <img src={image} alt="YouTube" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1"> <div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4"> <div className="items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Import YouTube transcription Import YouTube transcription
</p> </p>
</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">
From a youtube link, import the entire transcript of that From a youtube link, import the entire transcript of that
video for embedding. video for embedding.
</p> </p>

View File

@ -9,26 +9,31 @@ export default function DataConnectors() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 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="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"> <div className="items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Data Connectors Data Connectors
</p> </p>
</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">
Verified data connectors allow you to add more content to your Verified data connectors allow you to add more content to your
AnythingLLM workspaces with no custom code or complexity. AnythingLLM workspaces with no custom code or complexity.
<br /> <br />
Guaranteed to work with your AnythingLLM instance. Guaranteed to work with your AnythingLLM instance.
</p> </p>
</div> </div>
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full"> <div className="text-sm font-medium text-white mt-6 mb-4">
<DataConnectorOption slug="github" /> Available Data Connectors
<DataConnectorOption slug="youtube-transcript" /> </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> </div>
</div> </div>

View File

@ -14,14 +14,16 @@ export default function EmbedChats() {
<Sidebar /> <Sidebar />
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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="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"> <div className="flex gap-x-4 items-center">
<p className="text-2xl font-semibold text-white">Embed Chats</p> <p className="text-lg leading-6 font-bold text-white">
Embed Chats
</p>
</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 from any embed that These are all the recorded chats and messages from any embed that
you have published. you have published.
</p> </p>

View File

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

View File

@ -128,18 +128,11 @@ export default function GeneralEmbeddingPreference() {
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <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 /> <Sidebar />
{loading ? ( {loading ? (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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"> <div className="w-full h-full flex justify-center items-center">
<PreLoader /> <PreLoader />
@ -148,30 +141,30 @@ export default function GeneralEmbeddingPreference() {
) : ( ) : (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 <form
id="embedding-form" id="embedding-form"
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex w-full" 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="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"> <div className="flex gap-x-4 items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Embedding Preference Embedding Preference
</p> </p>
{hasChanges && ( {hasChanges && (
<button <button
type="submit" type="submit"
disabled={saving} 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"} {saving ? "Saving..." : "Save changes"}
</button> </button>
)} )}
</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">
When using an LLM that does not natively support an embedding When using an LLM that does not natively support an embedding
engine - you may need to additionally specify credentials to engine - you may need to additionally specify credentials to
for embedding text. for embedding text.
@ -181,63 +174,67 @@ export default function GeneralEmbeddingPreference() {
format which AnythingLLM can use to process. format which AnythingLLM can use to process.
</p> </p>
</div> </div>
<div className="text-sm font-medium text-white mt-6 mb-4">
<> Embedding Providers
<div className="text-white text-sm font-medium py-4"> </div>
Embedding Providers <div className="w-full">
</div> <div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full"> <div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> <div className="w-full flex items-center sticky top-0">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm"> <MagnifyingGlass
<div className="w-full flex items-center sticky top-0 z-20"> size={16}
<MagnifyingGlass weight="bold"
size={16} className="absolute left-4 z-30 text-white"
weight="bold" />
className="absolute left-4 z-30 text-white" <input
/> type="text"
<input placeholder="Search Embedding providers"
type="text" 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"
placeholder="Search Embedding providers" onChange={(e) => setSearchQuery(e.target.value)}
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" autoComplete="off"
onChange={(e) => setSearchQuery(e.target.value)} onKeyDown={(e) => {
autoComplete="off" if (e.key === "Enter") e.preventDefault();
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> </div>
</div> </div>
<div <div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
onChange={() => setHasChanges(true)} {filteredEmbedders.map((embedder) => {
className="mt-4 flex flex-col gap-y-1" return (
> <EmbedderItem
{selectedEmbedder && key={embedder.name}
EMBEDDERS.find( name={embedder.name}
(embedder) => embedder.value === selectedEmbedder value={embedder.value}
)?.options} image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
</div> </div>
</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> </div>
</form> </form>
</div> </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> </div>
); );
} }

View File

@ -189,7 +189,7 @@ export default function GeneralLLMPreference() {
{loading ? ( {loading ? (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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"> <div className="w-full h-full flex justify-center items-center">
<PreLoader /> <PreLoader />
@ -198,33 +198,33 @@ export default function GeneralLLMPreference() {
) : ( ) : (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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"> <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="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"> <div className="flex gap-x-4 items-center">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
LLM Preference LLM Preference
</p> </p>
{hasChanges && ( {hasChanges && (
<button <button
type="submit" type="submit"
disabled={saving} 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"} {saving ? "Saving..." : "Save changes"}
</button> </button>
)} )}
</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 the credentials and settings for your preferred LLM These are the credentials and settings for your preferred LLM
chat & embedding provider. Its important these keys are chat & embedding provider. Its important these keys are
current and correct or else AnythingLLM will not function current and correct or else AnythingLLM will not function
properly. properly.
</p> </p>
</div> </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 LLM Providers
</div> </div>
<div className="w-full"> <div className="w-full">

View File

@ -154,18 +154,11 @@ export default function GeneralVectorDatabase() {
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <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 /> <Sidebar />
{loading ? ( {loading ? (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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"> <div className="w-full h-full flex justify-center items-center">
<PreLoader /> <PreLoader />
@ -174,42 +167,42 @@ export default function GeneralVectorDatabase() {
) : ( ) : (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 <form
id="vectordb-form" id="vectordb-form"
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex w-full" 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="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"> <div className="flex items-center gap-x-4">
<p className="text-2xl font-semibold text-white"> <p className="text-lg leading-6 font-bold text-white">
Vector Database Vector Database
</p> </p>
{hasChanges && ( {hasChanges && (
<button <button
type="submit" type="submit"
disabled={saving} 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"} {saving ? "Saving..." : "Save changes"}
</button> </button>
)} )}
</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 the credentials and settings for how your These are the credentials and settings for how your
AnythingLLM instance will function. It's important these keys AnythingLLM instance will function. It's important these keys
are current and correct. are current and correct.
</p> </p>
</div> </div>
<div className="text-white text-sm font-medium py-4"> <div className="text-sm font-medium text-white mt-6 mb-4">
Select your preferred vector database provider Vector Database Providers
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> <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 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 <MagnifyingGlass
size={16} size={16}
weight="bold" weight="bold"
@ -257,6 +250,13 @@ export default function GeneralVectorDatabase() {
</form> </form>
</div> </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> </div>
); );
} }

View File

@ -19,7 +19,7 @@ export default function WorkspaceChat() {
} }
function ShowWorkspaceChat() { function ShowWorkspaceChat() {
const { slug, threadSlug = null } = useParams(); const { slug } = useParams();
const [workspace, setWorkspace] = useState(null); const [workspace, setWorkspace] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -32,9 +32,11 @@ function ShowWorkspaceChat() {
return; return;
} }
const suggestedMessages = await Workspace.getSuggestedMessages(slug); const suggestedMessages = await Workspace.getSuggestedMessages(slug);
const pfpUrl = await Workspace.fetchPfp(slug);
setWorkspace({ setWorkspace({
..._workspace, ..._workspace,
suggestedMessages, suggestedMessages,
pfpUrl,
}); });
setLoading(false); setLoading(false);
} }

View File

@ -101,7 +101,7 @@ export default function SuggestedChatMessages({ slug }) {
</div> </div>
); );
return ( return (
<div className="w-screen"> <div className="w-screen mt-6">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block input-label">Suggested Chat Messages</label> <label className="block input-label">Suggested Chat Messages</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -0,0 +1,96 @@
import Workspace from "@/models/workspace";
import showToast from "@/utils/toast";
import { Plus } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
export default function WorkspacePfp({ workspace, slug }) {
const [pfp, setPfp] = useState(null);
useEffect(() => {
async function fetchWorkspace() {
const pfpUrl = await Workspace.fetchPfp(slug);
setPfp(pfpUrl);
}
fetchWorkspace();
}, [slug]);
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return false;
const formData = new FormData();
formData.append("file", file);
const { success, error } = await Workspace.uploadPfp(
formData,
workspace.slug
);
if (!success) {
showToast(`Failed to upload profile picture: ${error}`, "error");
return;
}
const pfpUrl = await Workspace.fetchPfp(workspace.slug);
setPfp(pfpUrl);
showToast("Profile picture uploaded.", "success");
};
const handleRemovePfp = async () => {
const { success, error } = await Workspace.removePfp(workspace.slug);
if (!success) {
showToast(`Failed to remove profile picture: ${error}`, "error");
return;
}
setPfp(null);
};
return (
<div className="mt-6">
<div className="flex flex-col">
<label className="block input-label">Assistant Profile Image</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the profile image of the assistant for this workspace.
</p>
</div>
<div className="flex flex-col md:flex-row items-center gap-8">
<div className="flex flex-col items-center">
<label className="w-36 h-36 flex flex-col items-center justify-center bg-zinc-900/50 transition-all duration-300 rounded-full mt-8 border-2 border-dashed border-white border-opacity-60 cursor-pointer hover:opacity-60">
<input
id="workspace-pfp-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
{pfp ? (
<img
src={pfp}
alt="User profile picture"
className="w-36 h-36 rounded-full object-cover bg-white"
/>
) : (
<div className="flex flex-col items-center justify-center p-3">
<Plus className="w-8 h-8 text-white/80 m-2" />
<span className="text-white text-opacity-80 text-xs font-semibold">
Workspace Image
</span>
<span className="text-white text-opacity-60 text-xs">
800 x 800
</span>
</div>
)}
</label>
{pfp && (
<button
type="button"
onClick={handleRemovePfp}
className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline"
>
Remove Workspace Image
</button>
)}
</div>
</div>
</div>
);
}

View File

@ -6,6 +6,7 @@ import VectorCount from "./VectorCount";
import WorkspaceName from "./WorkspaceName"; import WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages"; import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace"; import DeleteWorkspace from "./DeleteWorkspace";
import WorkspacePfp from "./WorkspacePfp";
export default function GeneralInfo({ slug }) { export default function GeneralInfo({ slug }) {
const [workspace, setWorkspace] = useState(null); const [workspace, setWorkspace] = useState(null);
@ -66,9 +67,8 @@ export default function GeneralInfo({ slug }) {
</button> </button>
)} )}
</form> </form>
<div className="mt-6"> <SuggestedChatMessages slug={workspace.slug} />
<SuggestedChatMessages slug={workspace.slug} /> <WorkspacePfp workspace={workspace} slug={slug} />
</div>
<DeleteWorkspace workspace={workspace} /> <DeleteWorkspace workspace={workspace} />
</> </>
); );

View File

@ -553,8 +553,6 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id }); const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename; const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) { if (oldPfpFilename) {
const oldPfpPath = path.join( const oldPfpPath = path.join(
__dirname, __dirname,

View File

@ -19,10 +19,21 @@ const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
const { convertToChatHistory } = require("../utils/helpers/chat/responses"); const { convertToChatHistory } = require("../utils/helpers/chat/responses");
const { CollectorApi } = require("../utils/collectorApi"); const { CollectorApi } = require("../utils/collectorApi");
const { handleUploads } = setupMulter(); const { handleUploads } = setupMulter();
const { setupPfpUploads } = require("../utils/files/multer");
const { normalizePath } = require("../utils/files");
const { handlePfpUploads } = setupPfpUploads();
const path = require("path");
const fs = require("fs");
const {
determineWorkspacePfpFilepath,
fetchPfp,
} = require("../utils/files/pfp");
function workspaceEndpoints(app) { function workspaceEndpoints(app) {
if (!app) return; if (!app) return;
const responseCache = new Map();
app.post( app.post(
"/workspace/new", "/workspace/new",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
@ -422,6 +433,138 @@ function workspaceEndpoints(app) {
} }
} }
); );
app.get(
"/workspace/:slug/pfp",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async function (request, response) {
try {
const { slug } = request.params;
const cachedResponse = responseCache.get(slug);
if (cachedResponse) {
response.writeHead(200, {
"Content-Type": cachedResponse.mime || "image/png",
});
response.end(cachedResponse.buffer);
return;
}
const pfpPath = await determineWorkspacePfpFilepath(slug);
if (!pfpPath) {
response.sendStatus(204).end();
return;
}
const { found, buffer, mime } = fetchPfp(pfpPath);
if (!found) {
response.sendStatus(204).end();
return;
}
responseCache.set(slug, { buffer, mime });
response.writeHead(200, {
"Content-Type": mime || "image/png",
});
response.end(buffer);
return;
} catch (error) {
console.error("Error processing the logo request:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.post(
"/workspace/:slug/upload-pfp",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
handlePfpUploads.single("file"),
async function (request, response) {
try {
const { slug } = request.params;
const uploadedFileName = request.randomFileName;
if (!uploadedFileName) {
return response.status(400).json({ message: "File upload failed." });
}
const workspaceRecord = await Workspace.get({
slug,
});
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(
workspaceRecord.pfpFilename
)}`
);
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
const { workspace, message } = await Workspace.update(
workspaceRecord.id,
{
pfpFilename: uploadedFileName,
}
);
return response.status(workspace ? 200 : 500).json({
message: workspace
? "Profile picture uploaded successfully."
: message,
});
} catch (error) {
console.error("Error processing the profile picture upload:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.delete(
"/workspace/:slug/remove-pfp",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async function (request, response) {
try {
const { slug } = request.params;
const workspaceRecord = await Workspace.get({
slug,
});
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
);
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
const { workspace, message } = await Workspace.update(
workspaceRecord.id,
{
pfpFilename: null,
}
);
// Clear the cache
responseCache.delete(slug);
return response.status(workspace ? 200 : 500).json({
message: workspace
? "Profile picture removed successfully."
: message,
});
} catch (error) {
console.error("Error processing the profile picture removal:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
} }
module.exports = { workspaceEndpoints }; module.exports = { workspaceEndpoints };

View File

@ -19,6 +19,7 @@ const Workspace = {
"chatModel", "chatModel",
"topN", "topN",
"chatMode", "chatMode",
"pfpFilename",
], ],
new: async function (name = null, creatorId = null) { new: async function (name = null, creatorId = null) {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "pfpFilename" TEXT;

View File

@ -100,6 +100,7 @@ model workspaces {
chatModel String? chatModel String?
topN Int? @default(4) topN Int? @default(4)
chatMode String? @default("chat") chatMode String? @default("chat")
pfpFilename String?
workspace_users workspace_users[] workspace_users workspace_users[]
documents workspace_documents[] documents workspace_documents[]
workspace_suggested_messages workspace_suggested_messages[] workspace_suggested_messages workspace_suggested_messages[]

View File

@ -3,6 +3,7 @@ const fs = require("fs");
const { getType } = require("mime"); const { getType } = require("mime");
const { User } = require("../../models/user"); const { User } = require("../../models/user");
const { normalizePath } = require("."); const { normalizePath } = require(".");
const { Workspace } = require("../../models/workspace");
function fetchPfp(pfpPath) { function fetchPfp(pfpPath) {
if (!fs.existsSync(pfpPath)) { if (!fs.existsSync(pfpPath)) {
@ -38,7 +39,21 @@ async function determinePfpFilepath(id) {
return pfpFilepath; return pfpFilepath;
} }
async function determineWorkspacePfpFilepath(slug) {
const workspace = await Workspace.get({ slug });
const pfpFilename = workspace?.pfpFilename || null;
if (!pfpFilename) return null;
const basePath = process.env.STORAGE_DIR
? path.join(process.env.STORAGE_DIR, "assets/pfp")
: path.join(__dirname, "../../storage/assets/pfp");
const pfpFilepath = path.join(basePath, normalizePath(pfpFilename));
if (!fs.existsSync(pfpFilepath)) return null;
return pfpFilepath;
}
module.exports = { module.exports = {
fetchPfp, fetchPfp,
determinePfpFilepath, determinePfpFilepath,
determineWorkspacePfpFilepath,
}; };