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 ( +
+
+
+
+
+
+
+ + +
+ +
+
+
+
+

+ {description} +

+
+
+
+ ); +} 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 && (