- added metaResponse toggle button to ChatSettings

- generic components folder
- generic togglebutton,toggleblock,badge
This commit is contained in:
sherifButt 2024-03-19 06:33:09 +00:00
parent afc52c643e
commit 872db95dfe
7 changed files with 621 additions and 3 deletions

View File

@ -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 = () => (
<svg
viewBox="0 0 8 8"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={`${iconSizeClasses} ${iconColor} ${animatedClasses}`}
>
<circle cx="4" cy="4" r="4" />
</svg>
);
const CloseIcon = () => (
<svg
viewBox="0 0 12 12"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={`${iconSizeClasses} ${iconColor}`}
>
<path d="M6 4.586L2.929 1.515 1.515 2.929 4.586 6l-3.071 3.071 1.414 1.414L6 7.414l3.071 3.071 1.414-1.414L7.414 6l3.071-3.071-1.414-1.414L6 4.586z" />
</svg>
);
return (
<div
className={`flex flex-row gap-0.5 w-fit h-fit justify-center items-center group ${sizeClasses} ${backgroundClasses} ${roundedClasses} ${shadowClasses}`}
>
{showDot && (
<div>
<DotIcon />
</div>
)}
<p className={`block text-center font-medium px-1 ${textColor}`}>
{label}
</p>
{showClose && (
<div
className="flex flex-row justify-start items-start p-1 rounded-lg cursor-pointer"
onClick={onClose}
>
<CloseIcon />
</div>
)}
</div>
);
};

View File

@ -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 (
<div className="relative w-full max-h-full">
<div className="relative rounded-lg">
<div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<div className="flex flex-row gap-3" >
<label className="block input-label mb-4">{label}</label>
<Badge showDot animated />
</div>
<ToggleButton
initialChecked={initialChecked}
onToggle={onToggle}
name={name}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between space-x-14">
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{description}
</p>
</div>
</div>
</div>
);
}

View File

@ -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 (
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name={name}
onChange={handleToggle}
checked={isChecked}
className="peer sr-only pointer-events-none"
/>
<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>
</label>
);
}

View File

@ -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;

View File

@ -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 (
<div className="relative w-full max-h-full">
<ToggleBlock
initialChecked={workspace?.metaResponse}
label={
workspace.metaResponse
? "Meta Response is Enabled"
: "Enable Meta Response"
}
onToggle={toggleMetaResponse}
name="metaResponse"
description="Turn on this feature to dynamically adjust the chat interface based on conversation context, using options like dropdowns, sliders, and suggestions for a tailored user experience."
/>
</div>
);
}

View File

@ -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}
/>
<ChatEnableMetaResponse
workspace={workspace}
setHasChanges={setHasChanges}
/>
{hasChanges && (
<button
type="submit"

View File

@ -87,6 +87,23 @@ function workspaceEndpoints(app) {
response.sendStatus(400).end();
return;
}
if (
!currWorkspace.metaResponse &&
!currWorkspace.metaResponseSettings
) {
metaResponseDefaultSettings.inputs.config.systemPrompt.openAiPrompt =
currWorkspace.openAiPrompt || "";
data.metaResponseSettings = JSON.stringify(
metaResponseDefaultSettings
);
await EventLogs.logEvent(
"workspace_meta_response_enabled",
{
workspaceName: currWorkspace?.name || "Unknown Workspace",
},
user?.id
);
}
const { workspace, message } = await Workspace.update(
currWorkspace.id,
@ -212,8 +229,8 @@ function workspaceEndpoints(app) {
message:
failedToEmbed.length > 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 };