diff --git a/frontend/src/components/Generic/Badges/Badge/index.jsx b/frontend/src/components/Generic/Badges/Badge/index.jsx
new file mode 100644
index 00000000..53c62049
--- /dev/null
+++ b/frontend/src/components/Generic/Badges/Badge/index.jsx
@@ -0,0 +1,108 @@
+import React from "react";
+
+// Updated utility function for dark theme
+const colorMapping = (bg) => {
+ const mappings = {
+ "emerald-600": { text: "text-emerald-100", icon: "text-emerald-200 group-hover:text-emerald-50" },
+ "red-600": { text: "text-red-100", icon: "text-red-200 group-hover:text-red-50" },
+ "blue-600": { text: "text-blue-100", icon: "text-blue-200 group-hover:text-blue-50" },
+ "yellow-600": { text: "text-yellow-100", icon: "text-yellow-200 group-hover:text-yellow-50" },
+ "gray-600": { text: "text-gray-100", icon: "text-gray-200 group-hover:text-gray-50" },
+ "purple-600": { text: "text-purple-100", icon: "text-purple-200 group-hover:text-purple-50" },
+ "pink-600": { text: "text-pink-100", icon: "text-pink-200 group-hover:text-pink-50" },
+ "indigo-600": { text: "text-indigo-100", icon: "text-indigo-200 group-hover:text-indigo-50" },
+ };
+
+ return mappings[bg] || { text: "text-gray-100", icon: "text-gray-200" };
+};
+
+// Badge Component
+
+export default function Badge({
+ label = "Beta",
+ size = "sm", // "sm", "md", "lg" or "xl"
+ rounded = "full", // "none", "sm", "md", "lg", "full"
+ shadow = "none", // "none", "sm", "md", "lg", "xl"
+ showDot = false,
+ showClose = false,
+ bg = "emerald-600",
+ animated = false,
+ onClose = () => {}, // Callback for close icon
+}) {
+ // Adjustments based on props
+ const { text: textColor, icon: iconColor } = colorMapping(bg);
+ const animatedClasses = animated ? "animate-pulse" : "";
+ const sizeClasses = {
+ sm: "py-0.5 pl-2 pr-0.5 text-xs",
+ md: "py-1 pl-2 pr-1 text-sm",
+ lg: "py-1 px-3 text-sm",
+ xl: "py-1.5 px-4 text-base",
+ }[size];
+ const iconSizeClasses = {
+ sm: "h-2 w-2",
+ md: "h-3 w-3",
+ lg: "h-4 w-4",
+ xl: "h-4 w-4",
+ }[size];
+ const roundedClasses = {
+ none: "rounded-none",
+ sm: "rounded-sm",
+ md: "rounded-md",
+ lg: "rounded-lg",
+ full: "rounded-full",
+ }[rounded];
+ const shadowClasses = {
+ none: "",
+ sm: "shadow-sm",
+ md: "shadow-md",
+ lg: "shadow-lg",
+ xl: "shadow-xl",
+ }[shadow];
+ const backgroundClasses = `bg-${bg}`;
+
+ // SVG Icons
+ const DotIcon = () => (
+
+
+
+ );
+
+ const CloseIcon = () => (
+
+
+
+ );
+
+ return (
+
+ {showDot && (
+
+
+
+ )}
+
+ {label}
+
+ {showClose && (
+
+
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/Generic/Blocks/ToggleBlock/index.jsx b/frontend/src/components/Generic/Blocks/ToggleBlock/index.jsx
new file mode 100644
index 00000000..4ca19cee
--- /dev/null
+++ b/frontend/src/components/Generic/Blocks/ToggleBlock/index.jsx
@@ -0,0 +1,40 @@
+import React from "react";
+import ToggleButton from "../../Buttons/ToggleButton";
+import Badge from "../../Badges/Badge";
+
+// ToggleBlock: A component that includes a ToggleButton with additional context
+export default function ToggleBlock({
+ initialChecked,
+ label,
+ onToggle,
+ description,
+ name,
+}) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/Generic/Buttons/ToggleButton/index.jsx b/frontend/src/components/Generic/Buttons/ToggleButton/index.jsx
new file mode 100644
index 00000000..90a4f1e9
--- /dev/null
+++ b/frontend/src/components/Generic/Buttons/ToggleButton/index.jsx
@@ -0,0 +1,30 @@
+import React, { useState, useEffect } from "react";
+
+// ToggleButton: A reusable and semi-controlled toggle button
+export default function ToggleButton({ initialChecked, onToggle, name }) {
+ const [isChecked, setIsChecked] = useState(initialChecked);
+
+ useEffect(() => {
+ setIsChecked(initialChecked);
+ }, [initialChecked]);
+
+ const handleToggle = () => {
+ setIsChecked(!isChecked);
+ if (onToggle) {
+ onToggle(!isChecked);
+ }
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/src/models/metaResponse.js b/frontend/src/models/metaResponse.js
new file mode 100644
index 00000000..8319287a
--- /dev/null
+++ b/frontend/src/models/metaResponse.js
@@ -0,0 +1,314 @@
+import { API_BASE } from "@/utils/constants";
+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 MetaResponse = {
+ toggle: async function (slug) {
+ const result = await fetch(
+ `${API_BASE}/workspace/${slug}/metaResponse/toggle`,
+ {
+ method: "PATCH",
+ headers: baseHeaders(),
+ }
+ )
+ .then((res) => res.ok)
+ .catch(() => false);
+ return result;
+ },
+ new: async function (data = {}) {
+ const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .catch((e) => {
+ return { workspace: null, message: e.message };
+ });
+
+ return { workspace, message };
+ },
+ update: async function (slug, data = {}) {
+ const { workspace, message } = await fetch(
+ `${API_BASE}/workspace/${slug}/update`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: baseHeaders(),
+ }
+ )
+ .then((res) => res.json())
+ .catch((e) => {
+ return { workspace: null, message: e.message };
+ });
+
+ return { workspace, message };
+ },
+ modifyEmbeddings: async function (slug, changes = {}) {
+ const { workspace, message } = await fetch(
+ `${API_BASE}/workspace/${slug}/update-embeddings`,
+ {
+ method: "POST",
+ body: JSON.stringify(changes), // contains 'adds' and 'removes' keys that are arrays of filepaths
+ headers: baseHeaders(),
+ }
+ )
+ .then((res) => res.json())
+ .catch((e) => {
+ return { workspace: null, message: e.message };
+ });
+
+ return { workspace, message };
+ },
+ chatHistory: async function (slug) {
+ const history = await fetch(`${API_BASE}/workspace/${slug}/chats`, {
+ method: "GET",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .then((res) => res.history || [])
+ .catch(() => []);
+ return history;
+ },
+ updateChatFeedback: async function (chatId, slug, feedback) {
+ const result = await fetch(
+ `${API_BASE}/workspace/${slug}/chat-feedback/${chatId}`,
+ {
+ method: "POST",
+ headers: baseHeaders(),
+ body: JSON.stringify({ feedback }),
+ }
+ )
+ .then((res) => res.ok)
+ .catch(() => false);
+ return result;
+ },
+ 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 }),
+ headers: baseHeaders(),
+ signal: ctrl.signal,
+ openWhenHidden: true,
+ async onopen(response) {
+ if (response.ok) {
+ return; // everything's good
+ } else if (
+ response.status >= 400 &&
+ response.status < 500 &&
+ response.status !== 429
+ ) {
+ handleChat({
+ id: v4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `An error occurred while streaming response. Code ${response.status}`,
+ });
+ ctrl.abort();
+ throw new Error("Invalid Status code response.");
+ } else {
+ handleChat({
+ id: v4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `An error occurred while streaming response. Unknown Error.`,
+ });
+ ctrl.abort();
+ throw new Error("Unknown error");
+ }
+ },
+ async onmessage(msg) {
+ try {
+ const chatResult = JSON.parse(msg.data);
+ handleChat(chatResult);
+ } catch { }
+ },
+ onerror(err) {
+ handleChat({
+ id: v4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `An error occurred while streaming response. ${err.message}`,
+ });
+ ctrl.abort();
+ throw new Error();
+ },
+ });
+ },
+ all: async function () {
+ const workspaces = await fetch(`${API_BASE}/workspaces`, {
+ method: "GET",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .then((res) => res.workspaces || [])
+ .catch(() => []);
+
+ return workspaces;
+ },
+ bySlug: async function (slug = "") {
+ const workspace = await fetch(`${API_BASE}/workspace/${slug}`, {
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .then((res) => res.workspace)
+ .catch(() => null);
+ return workspace;
+ },
+ delete: async function (slug) {
+ const result = await fetch(`${API_BASE}/workspace/${slug}`, {
+ method: "DELETE",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.ok)
+ .catch(() => false);
+
+ return result;
+ },
+ uploadFile: async function (slug, formData) {
+ const response = await fetch(`${API_BASE}/workspace/${slug}/upload`, {
+ method: "POST",
+ body: formData,
+ headers: baseHeaders(),
+ });
+
+ const data = await response.json();
+ return { response, data };
+ },
+ uploadLink: async function (slug, link) {
+ const response = await fetch(`${API_BASE}/workspace/${slug}/upload-link`, {
+ method: "POST",
+ body: JSON.stringify({ link }),
+ headers: baseHeaders(),
+ });
+
+ const data = await response.json();
+ return { response, data };
+ },
+
+ getSuggestedMessages: async function (slug) {
+ return await fetch(`${API_BASE}/workspace/${slug}/suggested-messages`, {
+ method: "GET",
+ cache: "no-cache",
+ headers: baseHeaders(),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error("Could not fetch suggested messages.");
+ return res.json();
+ })
+ .then((res) => res.suggestedMessages)
+ .catch((e) => {
+ console.error(e);
+ return null;
+ });
+ },
+ setSuggestedMessages: async function (slug, messages) {
+ return fetch(`${API_BASE}/workspace/${slug}/suggested-messages`, {
+ method: "POST",
+ headers: baseHeaders(),
+ body: JSON.stringify({ messages }),
+ })
+ .then((res) => {
+ if (!res.ok) {
+ throw new Error(
+ res.statusText || "Error setting suggested messages."
+ );
+ }
+ return { success: true, ...res.json() };
+ })
+ .catch((e) => {
+ console.error(e);
+ return { success: false, error: e.message };
+ });
+ },
+ setPinForDocument: async function (slug, docPath, pinStatus) {
+ return fetch(`${API_BASE}/workspace/${slug}/update-pin`, {
+ method: "POST",
+ headers: baseHeaders(),
+ body: JSON.stringify({ docPath, pinStatus }),
+ })
+ .then((res) => {
+ if (!res.ok) {
+ throw new Error(
+ res.statusText || "Error setting pin status for document."
+ );
+ }
+ return true;
+ })
+ .catch((e) => {
+ console.error(e);
+ return false;
+ });
+ },
+ 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 MetaResponse;
diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatEnableMetaResponse/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatEnableMetaResponse/index.jsx
new file mode 100644
index 00000000..3ed41f04
--- /dev/null
+++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatEnableMetaResponse/index.jsx
@@ -0,0 +1,25 @@
+import GenericBadge from "@/components/Generic/Badges/Badge";
+import ToggleBlock from "@/components/Generic/Blocks/ToggleBlock";
+
+export default function ChatEnableMetaResponse({ workspace, setHasChanges }) {
+ // Toggle metaResponse value
+
+ const toggleMetaResponse = () => {
+ setHasChanges(true);
+ };
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx
index 3004b871..06f0163f 100644
--- a/frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx
@@ -8,6 +8,7 @@ import ChatHistorySettings from "./ChatHistorySettings";
import ChatPromptSettings from "./ChatPromptSettings";
import ChatTemperatureSettings from "./ChatTemperatureSettings";
import ChatModeSelection from "./ChatModeSelection";
+import ChatEnableMetaResponse from "./ChatEnableMetaResponse";
export default function ChatSettings({ workspace }) {
const [settings, setSettings] = useState({});
@@ -28,7 +29,14 @@ export default function ChatSettings({ workspace }) {
e.preventDefault();
const data = {};
const form = new FormData(formEl.current);
- for (var [key, value] of form.entries()) data[key] = castToType(key, value);
+ data["metaResponse"] = form.get("metaResponse") === "on" ? true : false;
+ for (var [key, value] of form.entries()) {
+ if (key === "metaResponse") {
+ data[key] = value === "on" ? true : false;
+ } else {
+ data[key] = castToType(key, value);
+ }
+ }
const { workspace: updatedWorkspace, message } = await Workspace.update(
workspace.slug,
data
@@ -65,6 +73,10 @@ export default function ChatSettings({ workspace }) {
workspace={workspace}
setHasChanges={setHasChanges}
/>
+
{hasChanges && (
0
? `${failedToEmbed.length} documents failed to add.\n\n${errors
- .map((msg) => `${msg}`)
- .join("\n\n")}`
+ .map((msg) => `${msg}`)
+ .join("\n\n")}`
: null,
});
} catch (e) {
@@ -567,4 +584,76 @@ function workspaceEndpoints(app) {
);
}
+const metaResponseDefaultSettings = {
+ inputs: {
+ isEnabled: false,
+ config: {
+ systemPrompt: {
+ isEnabled: false,
+ content: "",
+ openAiPrompt: "",
+ overrideSystemPrompt: false,
+ suggestionsList: [
+ {
+ title: "",
+ content: "",
+ },
+ ],
+ canEdit: ["admin", "manager"],
+ },
+ promptSchema: {
+ content: "",
+ suggestionsList: [
+ {
+ title: "",
+ content: "",
+ },
+ ],
+ overrideWorkspacePrompt: false,
+ canEdit: ["admin", "manager"],
+ },
+ components: {
+ dropDownMenu: {
+ isEnabled: false,
+ options: [],
+
+ },
+ optionsList: {
+ isEnabled: false,
+ options: [],
+ },
+ optionsButtons: {
+ isEnabled: false,
+ options: [],
+ },
+ multiSelectCheckboxes: {
+ isEnabled: false,
+ options: [],
+ },
+ },
+ },
+ permissions: ["user"],
+ },
+ sentiments: {
+ isEnabled: false,
+ config: {
+ sentimentAnalysis: {
+ isEnabled: false,
+
+ scoreThreshold: 0.5,
+ },
+ },
+ permissions: ["user"],
+ },
+ avatars: {
+ isEnabled: false,
+ config: {
+ avatarType: "circle",
+ avatarSize: "medium",
+ avatarName: "user",
+ },
+ permissions: ["user"],
+ },
+};
+
module.exports = { workspaceEndpoints };