Merge remote-tracking branch 'origin/master' into 868-dynamic-prompt

This commit is contained in:
sherifButt 2024-03-13 12:31:37 +00:00
commit 23fa3297cf
59 changed files with 1191 additions and 640 deletions

View File

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

View File

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

View File

@ -75,7 +75,7 @@ mintplexlabs/anythingllm
# Run this in powershell terminal
$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\.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 `
--cap-add SYS_ADMIN `
-v "$env:STORAGE_LOCATION`:/app/server/storage" `

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,15 +31,7 @@ const HistoricalMessage = ({
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{
uid:
role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
<ProfileImage role={role} workspace={workspace} />
{error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<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);

View File

@ -14,7 +14,6 @@ const PromptReply = ({
closed = true,
}) => {
const assistantBackgroundColor = "bg-historical-msg-system";
if (!reply && sources.length === 0 && !pending && !error) return null;
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="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<WorkspaceProfileImage workspace={workspace} />
<div className="mt-3 ml-5 dot-falling"></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="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<WorkspaceProfileImage workspace={workspace} />
<span
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="flex gap-x-5">
<Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />
<WorkspaceProfileImage workspace={workspace} />
<span
className={`reply flex flex-col gap-y-1 mt-2`}
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);

View File

@ -0,0 +1,50 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { Tooltip } from "react-tooltip";
export default function StopGenerationButton() {
function emitHaltEvent() {
window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));
}
return (
<>
<button
type="button"
onClick={emitHaltEvent}
data-tooltip-id="stop-generation-button"
data-tooltip-content="Stop generating response"
className="border-none text-white/60 cursor-pointer group"
>
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="group-hover:stroke-[#46C8FF] stroke-white"
cx="10"
cy="10.562"
r="9"
stroke-width="2"
/>
<rect
className="group-hover:fill-[#46C8FF] fill-white"
x="6.3999"
y="6.96204"
width="7.2"
height="7.2"
rx="2"
/>
</svg>
</button>
<Tooltip
id="stop-generation-button"
place="bottom"
delayShow={300}
className="tooltip !text-xs invert"
/>
</>
);
}

View File

@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10.8984" cy="10.562" r="9" stroke="white" stroke-width="2"/>
<rect x="7.29846" y="6.96204" width="7.2" height="7.2" rx="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -1,4 +1,3 @@
import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
import React, { useState, useRef } from "react";
import SlashCommandsButton, {
SlashCommands,
@ -6,6 +5,8 @@ import SlashCommandsButton, {
} from "./SlashCommands";
import { isMobile } from "react-device-detect";
import debounce from "lodash.debounce";
import { PaperPlaneRight } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton";
export default function PromptInput({
className = "bottom-0",
@ -86,19 +87,18 @@ export default function PromptInput({
className="cursor-text max-h-[100px] md:min-h-[40px] mx-2 md:mx-0 py-2 w-full text-[16px] md:text-md text-white bg-transparent placeholder:text-white/60 resize-none active:outline-none focus:outline-none flex-grow"
placeholder={"Send a message"}
/>
<button
ref={formRef}
type="submit"
disabled={buttonDisabled}
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
>
{buttonDisabled ? (
<CircleNotch className="w-6 h-6 animate-spin" />
) : (
{buttonDisabled ? (
<StopGenerationButton />
) : (
<button
ref={formRef}
type="submit"
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
>
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
)}
<span className="sr-only">Send message</span>
</button>
<span className="sr-only">Send message</span>
</button>
)}
</div>
<div className="flex justify-between py-3.5">
<div className="flex gap-x-2">

View File

@ -79,11 +79,7 @@ export default function ChatContainer({
const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];
var _chatHistory = [...remHistory];
if (!promptMessage || !promptMessage?.userMessage) {
setLoadingResponse(false);
return false;
}
if (!promptMessage || !promptMessage?.userMessage) return false;
if (!!threadSlug) {
await Workspace.threads.streamChat(
{ workspaceSlug: workspace.slug, threadSlug },

View File

@ -3,6 +3,7 @@ import { baseHeaders } from "@/utils/request";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import WorkspaceThread from "@/models/workspaceThread";
import { v4 } from "uuid";
import { ABORT_STREAM_EVENT } from "@/utils/chat";
const Workspace = {
new: async function (data = {}) {
@ -75,6 +76,16 @@ const Workspace = {
},
streamChat: async function ({ slug }, message, handleChat) {
const ctrl = new AbortController();
// Listen for the ABORT_STREAM_EVENT key to be emitted by the client
// to early abort the streaming response. On abort we send a special `stopGeneration`
// event to be handled which resets the UI for us to be able to send another message.
// The backend response abort handling is done in each LLM's handleStreamResponse.
window.addEventListener(ABORT_STREAM_EVENT, () => {
ctrl.abort();
handleChat({ id: v4(), type: "stopGeneration" });
});
await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, {
method: "POST",
body: JSON.stringify({ message }),
@ -238,6 +249,54 @@ const Workspace = {
});
},
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;

View File

@ -1,3 +1,4 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
import { fetchEventSource } from "@microsoft/fetch-event-source";
@ -80,6 +81,16 @@ const WorkspaceThread = {
handleChat
) {
const ctrl = new AbortController();
// Listen for the ABORT_STREAM_EVENT key to be emitted by the client
// to early abort the streaming response. On abort we send a special `stopGeneration`
// event to be handled which resets the UI for us to be able to send another message.
// The backend response abort handling is done in each LLM's handleStreamResponse.
window.addEventListener(ABORT_STREAM_EVENT, () => {
ctrl.abort();
handleChat({ id: v4(), type: "stopGeneration" });
});
await fetchEventSource(
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`,
{

View File

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

View File

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

View File

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

View File

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

View File

@ -13,27 +13,28 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminWorkspaces() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Instance workspaces
<p className="text-lg leading-6 font-bold text-white">
Instance Workspaces
</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<BookOpen className="h-4 w-4" /> New Workspace
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the workspaces that exist on this instance. Removing
a workspace will delete all of it's associated chats and settings.
</p>
@ -80,8 +81,8 @@ function WorkspacesContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@ export default function GeneralLLMPreference() {
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient p-[18px] h-full overflow-y-scroll animate-pulse border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -208,33 +208,33 @@ export default function GeneralLLMPreference() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form onSubmit={handleSubmit} className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
LLM Preference
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for your preferred LLM
chat & embedding provider. Its important these keys are
current and correct or else AnythingLLM will not function
properly.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
<div className="text-sm font-medium text-white mt-6 mb-4">
LLM Providers
</div>
<div className="w-full">

View File

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

View File

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

View File

@ -101,7 +101,7 @@ export default function SuggestedChatMessages({ slug }) {
</div>
);
return (
<div className="w-screen">
<div className="w-screen mt-6">
<div className="flex flex-col">
<label className="block input-label">Suggested Chat Messages</label>
<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 SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
import WorkspacePfp from "./WorkspacePfp";
export default function GeneralInfo({ slug }) {
const [workspace, setWorkspace] = useState(null);
@ -66,9 +67,8 @@ export default function GeneralInfo({ slug }) {
</button>
)}
</form>
<div className="mt-6">
<SuggestedChatMessages slug={workspace.slug} />
</div>
<SuggestedChatMessages slug={workspace.slug} />
<WorkspacePfp workspace={workspace} slug={slug} />
<DeleteWorkspace workspace={workspace} />
</>
);

View File

@ -1,3 +1,5 @@
export const ABORT_STREAM_EVENT = "abort-chat-stream";
// For handling of chat responses in the frontend by their various types.
export default function handleChat(
chatResult,
@ -108,6 +110,22 @@ export default function handleChat(
_chatHistory[chatIdx] = updatedHistory;
}
setChatHistory([..._chatHistory]);
setLoadingResponse(false);
} else if (type === "stopGeneration") {
const chatIdx = _chatHistory.length - 1;
const existingHistory = { ..._chatHistory[chatIdx] };
const updatedHistory = {
...existingHistory,
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
};
_chatHistory[chatIdx] = updatedHistory;
setChatHistory([..._chatHistory]);
setLoadingResponse(false);
}
}

View File

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

View File

@ -19,10 +19,21 @@ const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
const { convertToChatHistory } = require("../utils/helpers/chat/responses");
const { CollectorApi } = require("../utils/collectorApi");
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) {
if (!app) return;
const responseCache = new Map();
app.post(
"/workspace/new",
[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 };

View File

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

View File

@ -26,7 +26,7 @@
"@google/generative-ai": "^0.1.3",
"@googleapis/youtube": "^9.0.0",
"@pinecone-database/pinecone": "^2.0.1",
"@prisma/client": "5.3.0",
"@prisma/client": "5.3.1",
"@qdrant/js-client-rest": "^1.4.0",
"@xenova/transformers": "^2.14.0",
"@zilliz/milvus2-sdk-node": "^2.3.5",
@ -52,7 +52,7 @@
"openai": "^3.2.1",
"pinecone-client": "^1.1.0",
"posthog-node": "^3.1.1",
"prisma": "^5.3.1",
"prisma": "5.3.1",
"slugify": "^1.6.6",
"sqlite": "^4.2.1",
"sqlite3": "^5.1.6",
@ -78,4 +78,4 @@
"nodemon": "^2.0.22",
"prettier": "^3.0.3"
}
}
}

View File

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

View File

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

View File

@ -1,6 +1,9 @@
const { v4 } = require("uuid");
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
class AnthropicLLM {
constructor(embedder = null, modelPreference = null) {
if (!process.env.ANTHROPIC_API_KEY)
@ -150,6 +153,13 @@ class AnthropicLLM {
let fullText = "";
const { uuid = v4(), sources = [] } = responseProps;
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
stream.on("streamEvent", (message) => {
const data = message;
if (
@ -181,6 +191,7 @@ class AnthropicLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
}
});

View File

@ -1,6 +1,9 @@
const { AzureOpenAiEmbedder } = require("../../EmbeddingEngines/azureOpenAi");
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
class AzureOpenAiLLM {
constructor(embedder = null, _modelPreference = null) {
@ -174,6 +177,14 @@ class AzureOpenAiLLM {
return new Promise(async (resolve) => {
let fullText = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
for await (const event of stream) {
for (const choice of event.choices) {
const delta = choice.delta?.content;
@ -198,6 +209,7 @@ class AzureOpenAiLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
});
}

View File

@ -1,5 +1,8 @@
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
class GeminiLLM {
constructor(embedder = null, modelPreference = null) {
@ -198,6 +201,14 @@ class GeminiLLM {
return new Promise(async (resolve) => {
let fullText = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
for await (const chunk of stream) {
fullText += chunk.text();
writeResponseChunk(response, {
@ -218,6 +229,7 @@ class GeminiLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
});
}

View File

@ -1,7 +1,10 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const { OpenAiEmbedder } = require("../../EmbeddingEngines/openAi");
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
class HuggingFaceLLM {
constructor(embedder = null, _modelPreference = null) {
@ -172,6 +175,14 @@ class HuggingFaceLLM {
return new Promise((resolve) => {
let fullText = "";
let chunk = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
stream.data.on("data", (data) => {
const lines = data
?.toString()
@ -218,6 +229,7 @@ class HuggingFaceLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
} else {
let error = null;
@ -241,6 +253,7 @@ class HuggingFaceLLM {
close: true,
error,
});
response.removeListener("close", handleAbort);
resolve("");
return;
}
@ -266,6 +279,7 @@ class HuggingFaceLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
}
}

View File

@ -2,7 +2,10 @@ const fs = require("fs");
const path = require("path");
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
// Docs: https://api.js.langchain.com/classes/chat_models_llama_cpp.ChatLlamaCpp.html
const ChatLlamaCpp = (...args) =>
@ -176,6 +179,14 @@ class NativeLLM {
return new Promise(async (resolve) => {
let fullText = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
for await (const chunk of stream) {
if (chunk === undefined)
throw new Error(
@ -202,6 +213,7 @@ class NativeLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
});
}

View File

@ -1,6 +1,9 @@
const { chatPrompt } = require("../../chats");
const { StringOutputParser } = require("langchain/schema/output_parser");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
// Docs: https://github.com/jmorganca/ollama/blob/main/docs/api.md
class OllamaAILLM {
@ -180,8 +183,16 @@ class OllamaAILLM {
const { uuid = uuidv4(), sources = [] } = responseProps;
return new Promise(async (resolve) => {
let fullText = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
try {
let fullText = "";
for await (const chunk of stream) {
if (chunk === undefined)
throw new Error(
@ -210,6 +221,7 @@ class OllamaAILLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
} catch (error) {
writeResponseChunk(response, {
@ -222,6 +234,7 @@ class OllamaAILLM {
error?.cause ?? error.message
}`,
});
response.removeListener("close", handleAbort);
}
});
}

View File

@ -1,7 +1,10 @@
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
const { chatPrompt } = require("../../chats");
const { v4: uuidv4 } = require("uuid");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
function openRouterModels() {
const { MODELS } = require("./models.js");
@ -195,6 +198,13 @@ class OpenRouterLLM {
let chunk = "";
let lastChunkTime = null; // null when first token is still not received.
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
// NOTICE: Not all OpenRouter models will return a stop reason
// which keeps the connection open and so the model never finalizes the stream
// like the traditional OpenAI response schema does. So in the case the response stream
@ -220,6 +230,7 @@ class OpenRouterLLM {
error: false,
});
clearInterval(timeoutCheck);
response.removeListener("close", handleAbort);
resolve(fullText);
}
}, 500);
@ -269,6 +280,7 @@ class OpenRouterLLM {
error: false,
});
clearInterval(timeoutCheck);
response.removeListener("close", handleAbort);
resolve(fullText);
} else {
let finishReason = null;
@ -305,6 +317,7 @@ class OpenRouterLLM {
error: false,
});
clearInterval(timeoutCheck);
response.removeListener("close", handleAbort);
resolve(fullText);
}
}

View File

@ -1,5 +1,8 @@
const { chatPrompt } = require("../../chats");
const { writeResponseChunk } = require("../../helpers/chat/responses");
const {
writeResponseChunk,
clientAbortedHandler,
} = require("../../helpers/chat/responses");
function togetherAiModels() {
const { MODELS } = require("./models.js");
@ -185,6 +188,14 @@ class TogetherAiLLM {
return new Promise((resolve) => {
let fullText = "";
let chunk = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
stream.data.on("data", (data) => {
const lines = data
?.toString()
@ -230,6 +241,7 @@ class TogetherAiLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
} else {
let finishReason = null;
@ -263,6 +275,7 @@ class TogetherAiLLM {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
}
}

View File

@ -3,6 +3,7 @@ const fs = require("fs");
const { getType } = require("mime");
const { User } = require("../../models/user");
const { normalizePath } = require(".");
const { Workspace } = require("../../models/workspace");
function fetchPfp(pfpPath) {
if (!fs.existsSync(pfpPath)) {
@ -38,7 +39,21 @@ async function determinePfpFilepath(id) {
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 = {
fetchPfp,
determinePfpFilepath,
determineWorkspacePfpFilepath,
};

View File

@ -1,6 +1,14 @@
const { v4: uuidv4 } = require("uuid");
const moment = require("moment");
function clientAbortedHandler(resolve, fullText) {
console.log(
"\x1b[43m\x1b[34m[STREAM ABORTED]\x1b[0m Client requested to abort stream. Exiting LLM stream handler early."
);
resolve(fullText);
return;
}
// The default way to handle a stream response. Functions best with OpenAI.
// Currently used for LMStudio, LocalAI, Mistral API, and OpenAI
function handleDefaultStreamResponse(response, stream, responseProps) {
@ -9,6 +17,14 @@ function handleDefaultStreamResponse(response, stream, responseProps) {
return new Promise((resolve) => {
let fullText = "";
let chunk = "";
// Establish listener to early-abort a streaming response
// in case things go sideways or the user does not like the response.
// We preserve the generated text but continue as if chat was completed
// to preserve previously generated content.
const handleAbort = () => clientAbortedHandler(resolve, fullText);
response.on("close", handleAbort);
stream.data.on("data", (data) => {
const lines = data
?.toString()
@ -52,6 +68,7 @@ function handleDefaultStreamResponse(response, stream, responseProps) {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
} else {
let finishReason = null;
@ -85,6 +102,7 @@ function handleDefaultStreamResponse(response, stream, responseProps) {
close: true,
error: false,
});
response.removeListener("close", handleAbort);
resolve(fullText);
}
}
@ -141,4 +159,5 @@ module.exports = {
convertToChatHistory,
convertToPromptHistory,
writeResponseChunk,
clientAbortedHandler,
};

View File

@ -649,17 +649,17 @@
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06"
integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==
"@prisma/client@5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.3.0.tgz#47f07e5639993cffcf1c740a144495410562f279"
integrity sha512-cduYBlwj6oBfAUx2OI5i7t3NlpVeOtkN7pAqv0cw0B6gs4y8cY1mr8ZYywja0NUCOCqEWDkcZWBTVBwm6mnRIw==
"@prisma/client@5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.3.1.tgz#fc7fc2d91e814cc4fe18a4bc5e78bf851c26985e"
integrity sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==
dependencies:
"@prisma/engines-version" "5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4"
"@prisma/engines-version" "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
"@prisma/engines-version@5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4":
version "5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4.tgz#46ee2884e04cdba1163461ef856cec882d31c836"
integrity sha512-uftIog5FQ/OUR8Vb9TzpNBJ6L+zJnBgmd1A0uPJUzuvGMU32UmeyobpdXVzST5UprKryTdWOYXQFVyiQ2OU4Nw==
"@prisma/engines-version@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59":
version "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz#7eb6f5c6b7628b8b39df55c903f411528a6f761c"
integrity sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==
"@prisma/engines@5.3.1":
version "5.3.1"
@ -4455,7 +4455,7 @@ prettier@^3.0.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"
integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==
prisma@^5.3.1:
prisma@5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.3.1.tgz#a0932c1c1a5ed4ff449d064b193d9c7e94e8bf77"
integrity sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==