Breakout Chat/Query mode as a workspace setting (#734)

Remove useless icons in prompt bar
Add chatMode column to workspaces that defaults to chat
Add UI for toggle of chat mode with hint
Update UI for workspace settings to match designs
This commit is contained in:
Timothy Carambat 2024-02-16 14:50:40 -08:00 committed by GitHub
parent 32233974c2
commit 51dbff0dcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 105 additions and 138 deletions

View File

@ -1,21 +1,10 @@
import { import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
Chats,
CircleNotch,
Gear,
PaperPlaneRight,
Quotes,
} from "@phosphor-icons/react";
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import ManageWorkspace, {
useManageWorkspaceModal,
} from "../../../Modals/MangeWorkspace";
import useUser from "@/hooks/useUser";
import SlashCommandsButton, { import SlashCommandsButton, {
SlashCommands, SlashCommands,
useSlashCommands, useSlashCommands,
} from "./SlashCommands"; } from "./SlashCommands";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { Tooltip } from "react-tooltip";
export default function PromptInput({ export default function PromptInput({
workspace, workspace,
@ -27,10 +16,8 @@ export default function PromptInput({
sendCommand, sendCommand,
}) { }) {
const { showSlashCommand, setShowSlashCommand } = useSlashCommands(); const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const formRef = useRef(null); const formRef = useRef(null);
const [_, setFocused] = useState(false); const [_, setFocused] = useState(false);
const { user } = useUser();
const handleSubmit = (e) => { const handleSubmit = (e) => {
setFocused(false); setFocused(false);
@ -100,24 +87,6 @@ export default function PromptInput({
</div> </div>
<div className="flex justify-between py-3.5"> <div className="flex justify-between py-3.5">
<div className="flex gap-x-2"> <div className="flex gap-x-2">
{user?.role !== "default" && (
<div>
<Gear
onClick={showModal}
data-tooltip-id="tooltip-workspace-settings-prompt"
data-tooltip-content={`Open the ${workspace.name} workspace settings`}
className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
weight="fill"
/>
<Tooltip
id="tooltip-workspace-settings-prompt"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
)}
<ChatModeSelector workspace={workspace} />
<SlashCommandsButton <SlashCommandsButton
showing={showSlashCommand} showing={showSlashCommand}
setShowSlashCommand={setShowSlashCommand} setShowSlashCommand={setShowSlashCommand}
@ -127,44 +96,6 @@ export default function PromptInput({
</div> </div>
</div> </div>
</form> </form>
{showing && (
<ManageWorkspace hideModal={hideModal} providedSlug={workspace.slug} />
)}
</div>
);
}
function ChatModeSelector({ workspace }) {
const STORAGE_KEY = `workspace_chat_mode_${workspace.slug}`;
const [chatMode, setChatMode] = useState(
window.localStorage.getItem(STORAGE_KEY) ?? "chat"
);
function toggleMode() {
const newChatMode = chatMode === "chat" ? "query" : "chat";
setChatMode(newChatMode);
window.localStorage.setItem(STORAGE_KEY, newChatMode);
}
const ModeIcon = chatMode === "chat" ? Chats : Quotes;
return (
<div
data-tooltip-id="chat-mode-toggle"
data-tooltip-content={`You are currently in ${chatMode} mode. Click to switch to ${
chatMode === "chat" ? "query" : "chat"
} mode.`}
>
<ModeIcon
onClick={toggleMode}
className="w-7 h-7 text-white/60 hover:text-white cursor-pointer"
weight="fill"
/>
<Tooltip
id="chat-mode-toggle"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div> </div>
); );
} }

View File

@ -77,9 +77,6 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
await Workspace.threads.streamChat( await Workspace.threads.streamChat(
{ workspaceSlug: workspace.slug, threadSlug }, { workspaceSlug: workspace.slug, threadSlug },
promptMessage.userMessage, promptMessage.userMessage,
window.localStorage.getItem(
`workspace_chat_mode_${workspace.slug}`
) ?? "chat",
(chatResult) => (chatResult) =>
handleChat( handleChat(
chatResult, chatResult,
@ -93,9 +90,6 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
await Workspace.streamChat( await Workspace.streamChat(
workspace, workspace,
promptMessage.userMessage, promptMessage.userMessage,
window.localStorage.getItem(
`workspace_chat_mode_${workspace.slug}`
) ?? "chat",
(chatResult) => (chatResult) =>
handleChat( handleChat(
chatResult, chatResult,

View File

@ -439,3 +439,7 @@ dialog::backdrop {
.slide-up { .slide-up {
animation: slideUp 0.3s ease-out forwards; animation: slideUp 0.3s ease-out forwards;
} }
.input-label {
@apply text-[14px] font-bold text-white;
}

View File

@ -73,11 +73,11 @@ const Workspace = {
.catch(() => false); .catch(() => false);
return result; return result;
}, },
streamChat: async function ({ slug }, message, mode = "query", handleChat) { streamChat: async function ({ slug }, message, handleChat) {
const ctrl = new AbortController(); const ctrl = new AbortController();
await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, { await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, {
method: "POST", method: "POST",
body: JSON.stringify({ message, mode }), body: JSON.stringify({ message }),
headers: baseHeaders(), headers: baseHeaders(),
signal: ctrl.signal, signal: ctrl.signal,
openWhenHidden: true, openWhenHidden: true,

View File

@ -77,7 +77,6 @@ const WorkspaceThread = {
streamChat: async function ( streamChat: async function (
{ workspaceSlug, threadSlug }, { workspaceSlug, threadSlug },
message, message,
mode = "query",
handleChat handleChat
) { ) {
const ctrl = new AbortController(); const ctrl = new AbortController();
@ -85,7 +84,7 @@ const WorkspaceThread = {
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`, `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ message, mode }), body: JSON.stringify({ message }),
headers: baseHeaders(), headers: baseHeaders(),
signal: ctrl.signal, signal: ctrl.signal,
openWhenHidden: true, openWhenHidden: true,

View File

@ -2,10 +2,7 @@ export default function ChatHistorySettings({ workspace, setHasChanges }) {
return ( return (
<div> <div>
<div className="flex flex-col gap-y-1 mb-4"> <div className="flex flex-col gap-y-1 mb-4">
<label <label htmlFor="name" className="block mb-2 input-label">
htmlFor="name"
className="block mb-2 text-sm font-medium text-white"
>
Chat History Chat History
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium"> <p className="text-white text-opacity-60 text-xs font-medium">

View File

@ -0,0 +1,57 @@
import { useState } from "react";
export default function ChatModeSelection({ workspace, setHasChanges }) {
const [chatMode, setChatMode] = useState(workspace?.chatMode || "chat");
return (
<div>
<div className="flex flex-col">
<label htmlFor="chatMode" className="block input-label">
Chat mode
</label>
</div>
<div className="flex flex-col gap-y-1 mt-2">
<div className="w-fit flex gap-x-1 items-center p-1 rounded-lg bg-zinc-800 ">
<input type="hidden" name="chatMode" value={chatMode} />
<button
type="button"
disabled={chatMode === "chat"}
onClick={() => {
setChatMode("chat");
setHasChanges(true);
}}
className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md"
>
Chat
</button>
<button
type="button"
disabled={chatMode === "query"}
onClick={() => {
setChatMode("query");
setHasChanges(true);
}}
className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md"
>
Query
</button>
</div>
<p className="text-sm text-white/60">
{chatMode === "chat" ? (
<>
<b>Chat</b> will provide answers with the LLM's general knowledge{" "}
<i className="font-semibold">and</i> document context that is
found.
</>
) : (
<>
<b>Query</b> will provide answers{" "}
<i className="font-semibold">only</i> if document context is
found.
</>
)}
</p>
</div>
</div>
);
}

View File

@ -16,10 +16,7 @@ export default function ChatModelSelection({
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label <label htmlFor="name" className="block input-label">
htmlFor="name"
className="block text-sm font-medium text-white"
>
Chat model Chat model
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
@ -44,7 +41,7 @@ export default function ChatModelSelection({
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
Chat model{" "} Chat model{" "}
<span className="font-normal">({settings?.LLMProvider})</span> <span className="font-normal">({settings?.LLMProvider})</span>
</label> </label>

View File

@ -4,7 +4,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
Prompt Prompt
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -16,7 +16,7 @@ export default function ChatTemperatureSettings({
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
LLM Temperature LLM Temperature
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -7,6 +7,7 @@ import ChatModelSelection from "./ChatModelSelection";
import ChatHistorySettings from "./ChatHistorySettings"; import ChatHistorySettings from "./ChatHistorySettings";
import ChatPromptSettings from "./ChatPromptSettings"; import ChatPromptSettings from "./ChatPromptSettings";
import ChatTemperatureSettings from "./ChatTemperatureSettings"; import ChatTemperatureSettings from "./ChatTemperatureSettings";
import ChatModeSelection from "./ChatModeSelection";
export default function ChatSettings({ workspace }) { export default function ChatSettings({ workspace }) {
const [settings, setSettings] = useState({}); const [settings, setSettings] = useState({});
@ -48,6 +49,7 @@ export default function ChatSettings({ workspace }) {
onSubmit={handleUpdate} onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6" className="w-1/2 flex flex-col gap-y-6"
> >
<ChatModeSelection workspace={workspace} setHasChanges={setHasChanges} />
<ChatModelSelection <ChatModelSelection
settings={settings} settings={settings}
workspace={workspace} workspace={workspace}

View File

@ -91,9 +91,7 @@ export default function SuggestedChatMessages({ slug }) {
if (loading) if (loading)
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block text-sm font-medium text-white"> <label className="block input-label">Suggested Chat Messages</label>
Suggested Chat Messages
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the messages that will be suggested to your workspace users. Customize the messages that will be suggested to your workspace users.
</p> </p>
@ -105,9 +103,7 @@ export default function SuggestedChatMessages({ slug }) {
return ( return (
<div className="w-screen"> <div className="w-screen">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block text-sm font-medium text-white"> <label className="block input-label">Suggested Chat Messages</label>
Suggested Chat Messages
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the messages that will be suggested to your workspace users. Customize the messages that will be suggested to your workspace users.
</p> </p>

View File

@ -16,7 +16,7 @@ export default function VectorCount({ reload, workspace }) {
if (totalVectors === null) if (totalVectors === null)
return ( return (
<div> <div>
<h3 className="text-white text-sm font-semibold">Number of vectors</h3> <h3 className="input-label">Number of vectors</h3>
<p className="text-white text-opacity-60 text-xs font-medium py-1"> <p className="text-white text-opacity-60 text-xs font-medium py-1">
Total number of vectors in your vector database. Total number of vectors in your vector database.
</p> </p>
@ -27,7 +27,7 @@ export default function VectorCount({ reload, workspace }) {
); );
return ( return (
<div> <div>
<h3 className="text-white text-sm font-semibold">Number of vectors</h3> <h3 className="input-label">Number of vectors</h3>
<p className="text-white text-opacity-60 text-xs font-medium py-1"> <p className="text-white text-opacity-60 text-xs font-medium py-1">
Total number of vectors in your vector database. Total number of vectors in your vector database.
</p> </p>

View File

@ -2,7 +2,7 @@ export default function WorkspaceName({ workspace, setHasChanges }) {
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
Workspace Name Workspace Name
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -5,7 +5,7 @@ export default function DocumentSimilarityThreshold({
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
Document similarity threshold Document similarity threshold
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -2,7 +2,7 @@ export default function MaxContextSnippets({ workspace, setHasChanges }) {
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white"> <label htmlFor="name" className="block input-label">
Max Context Snippets Max Context Snippets
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -1,13 +1,9 @@
export default function VectorDBIdentifier({ workspace }) { export default function VectorDBIdentifier({ workspace }) {
return ( return (
<div> <div>
<h3 className="text-white text-sm font-semibold"> <h3 className="input-label">Vector database identifier</h3>
Vector database identifier <p className="text-white/60 text-xs font-medium py-1"> </p>
</h3> <p className="text-white/60 text-sm">{workspace?.slug}</p>
<p className="text-white text-opacity-60 text-xs font-medium py-1"> </p>
<p className="text-white text-opacity-60 text-sm font-medium">
{workspace?.slug}
</p>
</div> </div>
); );
} }

View File

@ -1,14 +1,10 @@
const { v4: uuidv4 } = require("uuid"); const { v4: uuidv4 } = require("uuid");
const { reqBody, userFromSession, multiUserMode } = require("../utils/http"); const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
const { Workspace } = require("../models/workspace");
const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { WorkspaceChats } = require("../models/workspaceChats"); const { WorkspaceChats } = require("../models/workspaceChats");
const { SystemSettings } = require("../models/systemSettings"); const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry"); const { Telemetry } = require("../models/telemetry");
const { const { streamChatWithWorkspace } = require("../utils/chats/stream");
streamChatWithWorkspace,
VALID_CHAT_MODE,
} = require("../utils/chats/stream");
const { const {
ROLES, ROLES,
flexUserRoleValid, flexUserRoleValid,
@ -16,6 +12,7 @@ const {
const { EventLogs } = require("../models/eventLogs"); const { EventLogs } = require("../models/eventLogs");
const { const {
validWorkspaceAndThreadSlug, validWorkspaceAndThreadSlug,
validWorkspaceSlug,
} = require("../utils/middleware/validWorkspace"); } = require("../utils/middleware/validWorkspace");
const { writeResponseChunk } = require("../utils/helpers/chat/responses"); const { writeResponseChunk } = require("../utils/helpers/chat/responses");
@ -24,32 +21,21 @@ function chatEndpoints(app) {
app.post( app.post(
"/workspace/:slug/stream-chat", "/workspace/:slug/stream-chat",
[validatedRequest, flexUserRoleValid([ROLES.all])], [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
const { slug } = request.params; const { message } = reqBody(request);
const { message, mode = "query" } = reqBody(request); const workspace = response.locals.workspace;
const workspace = multiUserMode(response) if (!message?.length) {
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();
return;
}
if (!message?.length || !VALID_CHAT_MODE.includes(mode)) {
response.status(400).json({ response.status(400).json({
id: uuidv4(), id: uuidv4(),
type: "abort", type: "abort",
textResponse: null, textResponse: null,
sources: [], sources: [],
close: true, close: true,
error: !message?.length error: !message?.length ? "Message is empty." : null,
? "Message is empty."
: `${mode} is not a valid mode.`,
}); });
return; return;
} }
@ -95,7 +81,13 @@ function chatEndpoints(app) {
} }
} }
await streamChatWithWorkspace(response, workspace, message, mode, user); await streamChatWithWorkspace(
response,
workspace,
message,
workspace?.chatMode,
user
);
await Telemetry.sendTelemetry("sent_chat", { await Telemetry.sendTelemetry("sent_chat", {
multiUserMode: multiUserMode(response), multiUserMode: multiUserMode(response),
LLMSelection: process.env.LLM_PROVIDER || "openai", LLMSelection: process.env.LLM_PROVIDER || "openai",
@ -137,20 +129,18 @@ function chatEndpoints(app) {
async (request, response) => { async (request, response) => {
try { try {
const user = await userFromSession(request, response); const user = await userFromSession(request, response);
const { message, mode = "query" } = reqBody(request); const { message } = reqBody(request);
const workspace = response.locals.workspace; const workspace = response.locals.workspace;
const thread = response.locals.thread; const thread = response.locals.thread;
if (!message?.length || !VALID_CHAT_MODE.includes(mode)) { if (!message?.length) {
response.status(400).json({ response.status(400).json({
id: uuidv4(), id: uuidv4(),
type: "abort", type: "abort",
textResponse: null, textResponse: null,
sources: [], sources: [],
close: true, close: true,
error: !message?.length error: !message?.length ? "Message is empty." : null,
? "Message is empty."
: `${mode} is not a valid mode.`,
}); });
return; return;
} }
@ -202,7 +192,7 @@ function chatEndpoints(app) {
response, response,
workspace, workspace,
message, message,
mode, workspace?.chatMode,
user, user,
thread thread
); );

View File

@ -18,6 +18,7 @@ const Workspace = {
"similarityThreshold", "similarityThreshold",
"chatModel", "chatModel",
"topN", "topN",
"chatMode",
], ],
new: async function (name = null, creatorId = null) { new: async function (name = null, creatorId = null) {
@ -59,7 +60,7 @@ const Workspace = {
try { try {
const workspace = await prisma.workspaces.update({ const workspace = await prisma.workspaces.update({
where: { id }, where: { id },
data, data, // TODO: strict validation on writables here.
}); });
return { workspace, message: null }; return { workspace, message: null };
} catch (error) { } catch (error) {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "chatMode" TEXT DEFAULT 'chat';

View File

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