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 {
Chats,
CircleNotch,
Gear,
PaperPlaneRight,
Quotes,
} from "@phosphor-icons/react";
import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
import React, { useState, useRef } from "react";
import ManageWorkspace, {
useManageWorkspaceModal,
} from "../../../Modals/MangeWorkspace";
import useUser from "@/hooks/useUser";
import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
} from "./SlashCommands";
import { isMobile } from "react-device-detect";
import { Tooltip } from "react-tooltip";
export default function PromptInput({
workspace,
@ -27,10 +16,8 @@ export default function PromptInput({
sendCommand,
}) {
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const formRef = useRef(null);
const [_, setFocused] = useState(false);
const { user } = useUser();
const handleSubmit = (e) => {
setFocused(false);
@ -100,24 +87,6 @@ export default function PromptInput({
</div>
<div className="flex justify-between py-3.5">
<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
showing={showSlashCommand}
setShowSlashCommand={setShowSlashCommand}
@ -127,44 +96,6 @@ export default function PromptInput({
</div>
</div>
</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>
);
}

View File

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

View File

@ -439,3 +439,7 @@ dialog::backdrop {
.slide-up {
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);
return result;
},
streamChat: async function ({ slug }, message, mode = "query", handleChat) {
streamChat: async function ({ slug }, message, handleChat) {
const ctrl = new AbortController();
await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, {
method: "POST",
body: JSON.stringify({ message, mode }),
body: JSON.stringify({ message }),
headers: baseHeaders(),
signal: ctrl.signal,
openWhenHidden: true,

View File

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

View File

@ -2,10 +2,7 @@ export default function ChatHistorySettings({ workspace, setHasChanges }) {
return (
<div>
<div className="flex flex-col gap-y-1 mb-4">
<label
htmlFor="name"
className="block mb-2 text-sm font-medium text-white"
>
<label htmlFor="name" className="block mb-2 input-label">
Chat History
</label>
<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 (
<div>
<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
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
@ -44,7 +41,7 @@ export default function ChatModelSelection({
return (
<div>
<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{" "}
<span className="font-normal">({settings?.LLMProvider})</span>
</label>

View File

@ -4,7 +4,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
return (
<div>
<div className="flex flex-col">
<label htmlFor="name" className="block text-sm font-medium text-white">
<label htmlFor="name" className="block input-label">
Prompt
</label>
<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 (
<div>
<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
</label>
<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 ChatPromptSettings from "./ChatPromptSettings";
import ChatTemperatureSettings from "./ChatTemperatureSettings";
import ChatModeSelection from "./ChatModeSelection";
export default function ChatSettings({ workspace }) {
const [settings, setSettings] = useState({});
@ -48,6 +49,7 @@ export default function ChatSettings({ workspace }) {
onSubmit={handleUpdate}
className="w-1/2 flex flex-col gap-y-6"
>
<ChatModeSelection workspace={workspace} setHasChanges={setHasChanges} />
<ChatModelSelection
settings={settings}
workspace={workspace}

View File

@ -91,9 +91,7 @@ export default function SuggestedChatMessages({ slug }) {
if (loading)
return (
<div className="flex flex-col">
<label className="block text-sm font-medium text-white">
Suggested Chat Messages
</label>
<label className="block input-label">Suggested Chat Messages</label>
<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.
</p>
@ -105,9 +103,7 @@ export default function SuggestedChatMessages({ slug }) {
return (
<div className="w-screen">
<div className="flex flex-col">
<label className="block text-sm font-medium text-white">
Suggested Chat Messages
</label>
<label className="block input-label">Suggested Chat Messages</label>
<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.
</p>

View File

@ -16,7 +16,7 @@ export default function VectorCount({ reload, workspace }) {
if (totalVectors === null)
return (
<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">
Total number of vectors in your vector database.
</p>
@ -27,7 +27,7 @@ export default function VectorCount({ reload, workspace }) {
);
return (
<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">
Total number of vectors in your vector database.
</p>

View File

@ -2,7 +2,7 @@ export default function WorkspaceName({ workspace, setHasChanges }) {
return (
<div>
<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
</label>
<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 (
<div>
<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
</label>
<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 (
<div>
<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
</label>
<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 }) {
return (
<div>
<h3 className="text-white text-sm font-semibold">
Vector database identifier
</h3>
<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>
<h3 className="input-label">Vector database identifier</h3>
<p className="text-white/60 text-xs font-medium py-1"> </p>
<p className="text-white/60 text-sm">{workspace?.slug}</p>
</div>
);
}

View File

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

View File

@ -18,6 +18,7 @@ const Workspace = {
"similarityThreshold",
"chatModel",
"topN",
"chatMode",
],
new: async function (name = null, creatorId = null) {
@ -59,7 +60,7 @@ const Workspace = {
try {
const workspace = await prisma.workspaces.update({
where: { id },
data,
data, // TODO: strict validation on writables here.
});
return { workspace, message: null };
} 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)
chatModel String?
topN Int? @default(4)
chatMode String? @default("chat")
workspace_users workspace_users[]
documents workspace_documents[]
workspace_suggested_messages workspace_suggested_messages[]