diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx
new file mode 100644
index 00000000..e9fb8303
--- /dev/null
+++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx
@@ -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 (
+
+
+
+
+ Customize the profile image of the assistant for this workspace.
+
+
+
+
+
+ {pfp && (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
index ee00143e..b6d5b84a 100644
--- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx
@@ -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 }) {
)}
-
-
-
+
+
>
);
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index a36777c8..74b83688 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -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,
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index 54228bba..2fe63e58 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -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 };
diff --git a/server/models/workspace.js b/server/models/workspace.js
index 92c2f9e3..48952c63 100644
--- a/server/models/workspace.js
+++ b/server/models/workspace.js
@@ -19,6 +19,7 @@ const Workspace = {
"chatModel",
"topN",
"chatMode",
+ "pfpFilename",
],
new: async function (name = null, creatorId = null) {
diff --git a/server/prisma/migrations/20240301002308_init/migration.sql b/server/prisma/migrations/20240301002308_init/migration.sql
new file mode 100644
index 00000000..5847beaf
--- /dev/null
+++ b/server/prisma/migrations/20240301002308_init/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "workspaces" ADD COLUMN "pfpFilename" TEXT;
diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma
index 8cd3a1d3..e6121e29 100644
--- a/server/prisma/schema.prisma
+++ b/server/prisma/schema.prisma
@@ -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[]
diff --git a/server/utils/files/pfp.js b/server/utils/files/pfp.js
index dd6ba0fe..0d1dd9f8 100644
--- a/server/utils/files/pfp.js
+++ b/server/utils/files/pfp.js
@@ -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,
};