mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 20:50:09 +01:00
[FEAT] New Workspace Settings Layout (#718)
* WIP new settings layout * add suggested messages to general & appearance and clean up/make more user friendly * lazy load workspace settings pages * css fix on X button for document picker where button is barely clickable * remove additional workspace settings page * fix thread selection action when on thread * refactor inputs into sub-components remove unused paths --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
dbb865f6f3
commit
858b2fcedb
@ -57,6 +57,10 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/workspace/:slug/settings/:tab"
|
||||||
|
element={<PrivateRoute Component={WorkspaceSettings} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/workspace/:slug"
|
path="/workspace/:slug"
|
||||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||||
@ -68,10 +72,6 @@ export default function App() {
|
|||||||
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
||||||
|
|
||||||
{/* Admin */}
|
{/* Admin */}
|
||||||
<Route
|
|
||||||
path="/workspace/:slug/settings"
|
|
||||||
element={<PrivateRoute Component={WorkspaceSettings} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/llm-preference"
|
path="/settings/llm-preference"
|
||||||
element={<AdminRoute Component={GeneralLLMPreference} />}
|
element={<AdminRoute Component={GeneralLLMPreference} />}
|
||||||
|
@ -195,7 +195,7 @@ export default function DocumentSettings({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-x-6 justify-center">
|
<div className="flex gap-x-6 justify-center -mt-6 z-10 relative">
|
||||||
<Directory
|
<Directory
|
||||||
files={availableDocs}
|
files={availableDocs}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
import useGetProviderModels, {
|
|
||||||
DISABLED_PROVIDERS,
|
|
||||||
} from "./useGetProviderModels";
|
|
||||||
|
|
||||||
export default function ChatModelSelection({
|
|
||||||
settings,
|
|
||||||
workspace,
|
|
||||||
setHasChanges,
|
|
||||||
}) {
|
|
||||||
const { defaultModels, customModels, loading } = useGetProviderModels(
|
|
||||||
settings?.LLMProvider
|
|
||||||
);
|
|
||||||
if (DISABLED_PROVIDERS.includes(settings?.LLMProvider)) return null;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Chat model
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
The specific chat model that will be used for this workspace. If
|
|
||||||
empty, will use the system LLM preference.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
name="chatModel"
|
|
||||||
required={true}
|
|
||||||
disabled={true}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
>
|
|
||||||
<option disabled={true} selected={true}>
|
|
||||||
-- waiting for models --
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-white">
|
|
||||||
Chat model{" "}
|
|
||||||
<span className="font-normal">({settings?.LLMProvider})</span>
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
The specific chat model that will be used for this workspace. If
|
|
||||||
empty, will use the system LLM preference.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select
|
|
||||||
name="chatModel"
|
|
||||||
required={true}
|
|
||||||
onChange={() => {
|
|
||||||
setHasChanges(true);
|
|
||||||
}}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
>
|
|
||||||
<option disabled={true} selected={workspace?.chatModel === null}>
|
|
||||||
System default
|
|
||||||
</option>
|
|
||||||
{defaultModels.length > 0 && (
|
|
||||||
<optgroup label="General models">
|
|
||||||
{defaultModels.map((model) => {
|
|
||||||
return (
|
|
||||||
<option
|
|
||||||
key={model}
|
|
||||||
value={model}
|
|
||||||
selected={workspace?.chatModel === model}
|
|
||||||
>
|
|
||||||
{model}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</optgroup>
|
|
||||||
)}
|
|
||||||
{Array.isArray(customModels) && customModels.length > 0 && (
|
|
||||||
<optgroup label="Custom models">
|
|
||||||
{customModels.map((model) => {
|
|
||||||
return (
|
|
||||||
<option
|
|
||||||
key={model.id}
|
|
||||||
value={model.id}
|
|
||||||
selected={workspace?.chatModel === model.id}
|
|
||||||
>
|
|
||||||
{model.id}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</optgroup>
|
|
||||||
)}
|
|
||||||
{/* For providers like TogetherAi where we partition model by creator entity. */}
|
|
||||||
{!Array.isArray(customModels) &&
|
|
||||||
Object.keys(customModels).length > 0 && (
|
|
||||||
<>
|
|
||||||
{Object.entries(customModels).map(([organization, models]) => (
|
|
||||||
<optgroup key={organization} label={organization}>
|
|
||||||
{models.map((model) => (
|
|
||||||
<option
|
|
||||||
key={model.id}
|
|
||||||
value={model.id}
|
|
||||||
selected={workspace?.chatModel === model.id}
|
|
||||||
>
|
|
||||||
{model.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</optgroup>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,387 +0,0 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
|
||||||
import Workspace from "../../../../models/workspace";
|
|
||||||
import paths from "../../../../utils/paths";
|
|
||||||
import { chatPrompt } from "../../../../utils/chat";
|
|
||||||
import System from "../../../../models/system";
|
|
||||||
import PreLoader from "../../../Preloader";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import showToast from "../../../../utils/toast";
|
|
||||||
import ChatModelPreference from "./ChatModelPreference";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
// Ensure that a type is correct before sending the body
|
|
||||||
// to the backend.
|
|
||||||
function castToType(key, value) {
|
|
||||||
const definitions = {
|
|
||||||
openAiTemp: {
|
|
||||||
cast: (value) => Number(value),
|
|
||||||
},
|
|
||||||
openAiHistory: {
|
|
||||||
cast: (value) => Number(value),
|
|
||||||
},
|
|
||||||
similarityThreshold: {
|
|
||||||
cast: (value) => parseFloat(value),
|
|
||||||
},
|
|
||||||
topN: {
|
|
||||||
cast: (value) => Number(value),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!definitions.hasOwnProperty(key)) return value;
|
|
||||||
return definitions[key].cast(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function recommendedSettings(provider = null) {
|
|
||||||
switch (provider) {
|
|
||||||
case "mistral":
|
|
||||||
return { temp: 0 };
|
|
||||||
default:
|
|
||||||
return { temp: 0.7 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WorkspaceSettings({ active, workspace, settings }) {
|
|
||||||
const { slug } = useParams();
|
|
||||||
const formEl = useRef(null);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
const defaults = recommendedSettings(settings?.LLMProvider);
|
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
|
||||||
setSaving(true);
|
|
||||||
e.preventDefault();
|
|
||||||
const data = {};
|
|
||||||
const form = new FormData(formEl.current);
|
|
||||||
for (var [key, value] of form.entries()) data[key] = castToType(key, value);
|
|
||||||
const { workspace: updatedWorkspace, message } = await Workspace.update(
|
|
||||||
workspace.slug,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
if (!!updatedWorkspace) {
|
|
||||||
showToast("Workspace updated!", "success", { clear: true });
|
|
||||||
} else {
|
|
||||||
showToast(`Error: ${message}`, "error", { clear: true });
|
|
||||||
}
|
|
||||||
setSaving(false);
|
|
||||||
setHasChanges(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteWorkspace = async () => {
|
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
`You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
setDeleting(true);
|
|
||||||
const success = await Workspace.delete(workspace.slug);
|
|
||||||
if (!success) {
|
|
||||||
showToast("Workspace could not be deleted!", "error", { clear: true });
|
|
||||||
setDeleting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.slug === slug
|
|
||||||
? (window.location = paths.home())
|
|
||||||
: window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form ref={formEl} onSubmit={handleUpdate}>
|
|
||||||
<div className="-mt-12 px-12 pb-6 flex flex-col h-full w-full max-h-[80vh] overflow-y-scroll">
|
|
||||||
<div className="flex flex-col gap-y-1 min-w-[900px]">
|
|
||||||
<div className="text-white text-opacity-60 text-sm font-bold uppercase py-6 border-b-2 border-white/10">
|
|
||||||
Workspace Settings
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row w-full py-6 border-b-2 border-white/10">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<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.5">
|
|
||||||
{" "}
|
|
||||||
</p>
|
|
||||||
<p className="text-white text-opacity-60 text-sm font-medium">
|
|
||||||
{workspace?.slug}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-1/2">
|
|
||||||
<h3 className="text-white text-sm font-semibold">
|
|
||||||
Number of vectors
|
|
||||||
</h3>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium my-[2px]">
|
|
||||||
Total number of vectors in your vector database.
|
|
||||||
</p>
|
|
||||||
<VectorCount reload={active} workspace={workspace} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-y-1 w-full mt-7">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex flex-col gap-y-4 w-1/2">
|
|
||||||
<div className="w-3/4 flex flex-col gap-y-4">
|
|
||||||
<ChatModelPreference
|
|
||||||
settings={settings}
|
|
||||||
workspace={workspace}
|
|
||||||
setHasChanges={setHasChanges}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Workspace Name
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
This will only change the display name of your workspace.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
minLength={2}
|
|
||||||
maxLength={80}
|
|
||||||
defaultValue={workspace?.name}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="My Workspace"
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
LLM Temperature
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
This setting controls how "random" or dynamic your chat
|
|
||||||
responses will be.
|
|
||||||
<br />
|
|
||||||
The higher the number (1.0 maximum) the more random and
|
|
||||||
incoherent.
|
|
||||||
<br />
|
|
||||||
<i>Recommended: {defaults.temp}</i>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
name="openAiTemp"
|
|
||||||
type="number"
|
|
||||||
min={0.0}
|
|
||||||
max={1.0}
|
|
||||||
step={0.1}
|
|
||||||
onWheel={(e) => e.target.blur()}
|
|
||||||
defaultValue={workspace?.openAiTemp ?? defaults.temp}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="0.7"
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col gap-y-1 mb-4">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block mb-2 text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Chat History
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium">
|
|
||||||
The number of previous chats that will be included in the
|
|
||||||
response's short-term memory.
|
|
||||||
<i>Recommend 20. </i>
|
|
||||||
Anything more than 45 is likely to lead to continuous chat
|
|
||||||
failures depending on message size.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
name="openAiHistory"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={45}
|
|
||||||
step={1}
|
|
||||||
onWheel={(e) => e.target.blur()}
|
|
||||||
defaultValue={workspace?.openAiHistory ?? 20}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="20"
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-1/2">
|
|
||||||
<div className="w-3/4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Prompt
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
The prompt that will be used on this workspace. Define the
|
|
||||||
context and instructions for the AI to generate a response.
|
|
||||||
You should to provide a carefully crafted prompt so the AI
|
|
||||||
can generate a relevant and accurate response.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
name="openAiPrompt"
|
|
||||||
rows={5}
|
|
||||||
defaultValue={chatPrompt(workspace)}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
|
|
||||||
required={true}
|
|
||||||
wrap="soft"
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Max Context Snippets
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
This setting controls the maximum amount of context
|
|
||||||
snippets the will be sent to the LLM for per chat or
|
|
||||||
query.
|
|
||||||
<br />
|
|
||||||
<i>Recommended: 4</i>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
name="topN"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={12}
|
|
||||||
step={1}
|
|
||||||
onWheel={(e) => e.target.blur()}
|
|
||||||
defaultValue={workspace?.topN ?? 4}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="4"
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Document similarity threshold
|
|
||||||
</label>
|
|
||||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
|
||||||
The minimum similarity score required for a source to be
|
|
||||||
considered related to the chat. The higher the number, the
|
|
||||||
more similar the source must be to the chat.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
name="similarityThreshold"
|
|
||||||
defaultValue={workspace?.similarityThreshold ?? 0.25}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
required={true}
|
|
||||||
>
|
|
||||||
<option value={0.0}>No restriction</option>
|
|
||||||
<option value={0.25}>
|
|
||||||
Low (similarity score ≥ .25)
|
|
||||||
</option>
|
|
||||||
<option value={0.5}>
|
|
||||||
Medium (similarity score ≥ .50)
|
|
||||||
</option>
|
|
||||||
<option value={0.75}>
|
|
||||||
High (similarity score ≥ .75)
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 w-full flex justify-start">
|
|
||||||
<Link to={paths.workspace.additionalSettings(workspace.slug)}>
|
|
||||||
<a className="underline text-white/60 text-sm font-medium hover:text-sky-600">
|
|
||||||
View additional settings
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between p-2 md:p-6 space-x-2 border-t rounded-b border-gray-600">
|
|
||||||
<DeleteWorkspace
|
|
||||||
deleting={deleting}
|
|
||||||
workspace={workspace}
|
|
||||||
onClick={deleteWorkspace}
|
|
||||||
/>
|
|
||||||
{hasChanges && (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
|
||||||
>
|
|
||||||
{saving ? "Updating..." : "Update workspace"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteWorkspace({ deleting, workspace, onClick }) {
|
|
||||||
const [canDelete, setCanDelete] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchKeys() {
|
|
||||||
const canDelete = await System.getCanDeleteWorkspaces();
|
|
||||||
setCanDelete(canDelete);
|
|
||||||
}
|
|
||||||
fetchKeys();
|
|
||||||
}, [workspace?.slug]);
|
|
||||||
|
|
||||||
if (!canDelete) return null;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
disabled={deleting}
|
|
||||||
onClick={onClick}
|
|
||||||
type="button"
|
|
||||||
className="transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-transparent text-white hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
|
|
||||||
>
|
|
||||||
{deleting ? "Deleting Workspace..." : "Delete Workspace"}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VectorCount({ reload, workspace }) {
|
|
||||||
const [totalVectors, setTotalVectors] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchVectorCount() {
|
|
||||||
const totalVectors = await System.totalIndexes(workspace.slug);
|
|
||||||
setTotalVectors(totalVectors);
|
|
||||||
}
|
|
||||||
fetchVectorCount();
|
|
||||||
}, [workspace?.slug, reload]);
|
|
||||||
|
|
||||||
if (totalVectors === null) return <PreLoader size="4" />;
|
|
||||||
return (
|
|
||||||
<p className="text-white text-opacity-60 text-sm font-medium">
|
|
||||||
{totalVectors}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,18 +1,15 @@
|
|||||||
import React, { useState, useEffect, lazy, memo } from "react";
|
import React, { useState, useEffect, memo } from "react";
|
||||||
import { X } from "@phosphor-icons/react";
|
import { X } from "@phosphor-icons/react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Workspace from "../../../models/workspace";
|
import Workspace from "../../../models/workspace";
|
||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import useUser from "../../../hooks/useUser";
|
import useUser from "../../../hooks/useUser";
|
||||||
|
import DocumentSettings from "./Documents";
|
||||||
const DocumentSettings = lazy(() => import("./Documents"));
|
|
||||||
const WorkspaceSettings = lazy(() => import("./Settings"));
|
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [selectedTab, setSelectedTab] = useState("documents");
|
|
||||||
const [workspace, setWorkspace] = useState(null);
|
const [workspace, setWorkspace] = useState(null);
|
||||||
const [fileTypes, setFileTypes] = useState(null);
|
const [fileTypes, setFileTypes] = useState(null);
|
||||||
const [settings, setSettings] = useState({});
|
const [settings, setSettings] = useState({});
|
||||||
@ -72,31 +69,7 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
|||||||
<div className="backdrop h-full w-full absolute top-0 z-10" />
|
<div className="backdrop h-full w-full absolute top-0 z-10" />
|
||||||
<div className={`absolute max-h-full w-fit transition duration-300 z-20`}>
|
<div className={`absolute max-h-full w-fit transition duration-300 z-20`}>
|
||||||
<div className="relative bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
|
<div className="relative bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
|
||||||
<div className="absolute top-[-18px] left-1/2 transform -translate-x-1/2 bg-sidebar-button p-1 rounded-xl shadow border-2 border-slate-300/10">
|
<div className="flex items-start justify-between p-2 rounded-t border-gray-500/50 z-40 relative">
|
||||||
<div className="flex gap-x-1">
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedTab("documents")}
|
|
||||||
className={`px-4 py-2 rounded-[8px] font-semibold text-white hover:bg-switch-selected hover:bg-opacity-60 ${
|
|
||||||
selectedTab === "documents"
|
|
||||||
? "bg-switch-selected shadow-md"
|
|
||||||
: "bg-sidebar-button"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Documents
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedTab("settings")}
|
|
||||||
className={`px-4 py-2 rounded-[8px] font-semibold text-white hover:bg-switch-selected hover:bg-opacity-60 ${
|
|
||||||
selectedTab === "settings"
|
|
||||||
? "bg-switch-selected shadow-md"
|
|
||||||
: "bg-sidebar-button"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start justify-between p-2 rounded-t border-gray-500/50">
|
|
||||||
<button
|
<button
|
||||||
onClick={hideModal}
|
onClick={hideModal}
|
||||||
type="button"
|
type="button"
|
||||||
@ -105,20 +78,11 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
|
|||||||
<X className="text-gray-300 text-lg" />
|
<X className="text-gray-300 text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={selectedTab === "documents" ? "" : "hidden"}>
|
<DocumentSettings
|
||||||
<DocumentSettings
|
workspace={workspace}
|
||||||
workspace={workspace}
|
fileTypes={fileTypes}
|
||||||
fileTypes={fileTypes}
|
systemSettings={settings}
|
||||||
systemSettings={settings}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={selectedTab === "settings" ? "" : "hidden"}>
|
|
||||||
<WorkspaceSettings
|
|
||||||
active={selectedTab === "settings"} // To force reload live sub-components like VectorCount
|
|
||||||
workspace={workspace}
|
|
||||||
settings={settings}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,11 +16,10 @@ export default function ThreadItem({
|
|||||||
onRemove,
|
onRemove,
|
||||||
hasNext,
|
hasNext,
|
||||||
}) {
|
}) {
|
||||||
const optionsContainer = useRef(null);
|
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
|
const optionsContainer = useRef(null);
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
const [name, setName] = useState(thread.name);
|
const [name, setName] = useState(thread.name);
|
||||||
|
|
||||||
const linkTo = !thread.slug
|
const linkTo = !thread.slug
|
||||||
? paths.workspace.chat(slug)
|
? paths.workspace.chat(slug)
|
||||||
: paths.workspace.thread(slug, thread.slug);
|
: paths.workspace.thread(slug, thread.slug);
|
||||||
@ -61,7 +60,10 @@ export default function ThreadItem({
|
|||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<a href={isActive ? "#" : linkTo} className="w-full">
|
<a
|
||||||
|
href={window.location.pathname === linkTo ? "#" : linkTo}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
className={`text-left text-sm ${
|
className={`text-left text-sm ${
|
||||||
isActive ? "font-medium text-white" : "text-slate-400"
|
isActive ? "font-medium text-white" : "text-slate-400"
|
||||||
|
@ -7,18 +7,20 @@ import ManageWorkspace, {
|
|||||||
} from "../../Modals/MangeWorkspace";
|
} from "../../Modals/MangeWorkspace";
|
||||||
import paths from "@/utils/paths";
|
import paths from "@/utils/paths";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { GearSix, SquaresFour } from "@phosphor-icons/react";
|
import { GearSix, SquaresFour, UploadSimple } from "@phosphor-icons/react";
|
||||||
import truncate from "truncate";
|
import truncate from "truncate";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import ThreadContainer from "./ThreadContainer";
|
import ThreadContainer from "./ThreadContainer";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export default function ActiveWorkspaces() {
|
export default function ActiveWorkspaces() {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [settingHover, setSettingHover] = useState({});
|
|
||||||
const [workspaces, setWorkspaces] = useState([]);
|
const [workspaces, setWorkspaces] = useState([]);
|
||||||
const [selectedWs, setSelectedWs] = useState(null);
|
const [selectedWs, setSelectedWs] = useState(null);
|
||||||
const [hoverStates, setHoverStates] = useState({});
|
const [hoverStates, setHoverStates] = useState({});
|
||||||
|
const [gearHover, setGearHover] = useState({});
|
||||||
|
const [uploadHover, setUploadHover] = useState({});
|
||||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
@ -30,7 +32,6 @@ export default function ActiveWorkspaces() {
|
|||||||
}
|
}
|
||||||
getWorkspaces();
|
getWorkspaces();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseEnter = useCallback((workspaceId) => {
|
const handleMouseEnter = useCallback((workspaceId) => {
|
||||||
setHoverStates((prev) => ({ ...prev, [workspaceId]: true }));
|
setHoverStates((prev) => ({ ...prev, [workspaceId]: true }));
|
||||||
}, []);
|
}, []);
|
||||||
@ -38,13 +39,20 @@ export default function ActiveWorkspaces() {
|
|||||||
const handleMouseLeave = useCallback((workspaceId) => {
|
const handleMouseLeave = useCallback((workspaceId) => {
|
||||||
setHoverStates((prev) => ({ ...prev, [workspaceId]: false }));
|
setHoverStates((prev) => ({ ...prev, [workspaceId]: false }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleGearMouseEnter = useCallback((workspaceId) => {
|
const handleGearMouseEnter = useCallback((workspaceId) => {
|
||||||
setSettingHover((prev) => ({ ...prev, [workspaceId]: true }));
|
setGearHover((prev) => ({ ...prev, [workspaceId]: true }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleGearMouseLeave = useCallback((workspaceId) => {
|
const handleGearMouseLeave = useCallback((workspaceId) => {
|
||||||
setSettingHover((prev) => ({ ...prev, [workspaceId]: false }));
|
setGearHover((prev) => ({ ...prev, [workspaceId]: false }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUploadMouseEnter = useCallback((workspaceId) => {
|
||||||
|
setUploadHover((prev) => ({ ...prev, [workspaceId]: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUploadMouseLeave = useCallback((workspaceId) => {
|
||||||
|
setUploadHover((prev) => ({ ...prev, [workspaceId]: false }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -67,14 +75,16 @@ export default function ActiveWorkspaces() {
|
|||||||
{workspaces.map((workspace) => {
|
{workspaces.map((workspace) => {
|
||||||
const isActive = workspace.slug === slug;
|
const isActive = workspace.slug === slug;
|
||||||
const isHovered = hoverStates[workspace.id];
|
const isHovered = hoverStates[workspace.id];
|
||||||
const isGearHovered = settingHover[workspace.id];
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div
|
||||||
|
className="flex flex-col w-full"
|
||||||
|
onMouseEnter={() => handleMouseEnter(workspace.id)}
|
||||||
|
onMouseLeave={() => handleMouseLeave(workspace.id)}
|
||||||
|
key={workspace.id}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
className="flex gap-x-2 items-center justify-between"
|
className="flex gap-x-2 items-center justify-between"
|
||||||
onMouseEnter={() => handleMouseEnter(workspace.id)}
|
|
||||||
onMouseLeave={() => handleMouseLeave(workspace.id)}
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
||||||
@ -99,30 +109,55 @@ export default function ActiveWorkspaces() {
|
|||||||
isActive ? "" : "text-opacity-80"
|
isActive ? "" : "text-opacity-80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isActive
|
{isActive || isHovered
|
||||||
? truncate(workspace.name, 17)
|
? truncate(workspace.name, 17)
|
||||||
: truncate(workspace.name, 20)}
|
: truncate(workspace.name, 20)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{isActive ||
|
||||||
type="button"
|
isHovered ||
|
||||||
onClick={(e) => {
|
gearHover[workspace.id] ||
|
||||||
e.preventDefault();
|
user?.role === "default" ? (
|
||||||
setSelectedWs(workspace);
|
<div className="flex items-center gap-x-2">
|
||||||
showModal();
|
<button
|
||||||
}}
|
type="button"
|
||||||
onMouseEnter={() => handleGearMouseEnter(workspace.id)}
|
onClick={(e) => {
|
||||||
onMouseLeave={() => handleGearMouseLeave(workspace.id)}
|
e.preventDefault();
|
||||||
className="rounded-md flex items-center justify-center text-white ml-auto"
|
setSelectedWs(workspace);
|
||||||
>
|
showModal();
|
||||||
<GearSix
|
}}
|
||||||
weight={isGearHovered ? "fill" : "regular"}
|
onMouseEnter={() =>
|
||||||
hidden={
|
handleUploadMouseEnter(workspace.id)
|
||||||
(!isActive && !isHovered) || user?.role === "default"
|
}
|
||||||
}
|
onMouseLeave={() =>
|
||||||
className="h-[20px] w-[20px] transition-all duration-300"
|
handleUploadMouseLeave(workspace.id)
|
||||||
/>
|
}
|
||||||
</button>
|
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||||
|
>
|
||||||
|
<UploadSimple
|
||||||
|
weight={
|
||||||
|
uploadHover[workspace.id] ? "fill" : "regular"
|
||||||
|
}
|
||||||
|
className="h-[20px] w-[20px] transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
type="button"
|
||||||
|
to={paths.workspace.settings.generalAppearance(
|
||||||
|
workspace.slug
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => handleGearMouseEnter(workspace.id)}
|
||||||
|
onMouseLeave={() => handleGearMouseLeave(workspace.id)}
|
||||||
|
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||||
|
>
|
||||||
|
<GearSix
|
||||||
|
weight={gearHover[workspace.id] ? "fill" : "regular"}
|
||||||
|
className="h-[20px] w-[20px] transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
76
frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx
Normal file
76
frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import System from "@/models/system";
|
||||||
|
import Workspace from "@/models/workspace";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { castToType } from "@/utils/types";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import ChatModelSelection from "./ChatModelSelection";
|
||||||
|
import ChatHistorySettings from "./ChatHistorySettings";
|
||||||
|
import ChatPromptSettings from "./ChatPromptSettings";
|
||||||
|
import ChatTemperatureSettings from "./ChatTemperatureSettings";
|
||||||
|
|
||||||
|
export default function ChatSettings({ workspace }) {
|
||||||
|
const [settings, setSettings] = useState({});
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const formEl = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchSettings() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings ?? {});
|
||||||
|
}
|
||||||
|
fetchSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpdate = async (e) => {
|
||||||
|
setSaving(true);
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(formEl.current);
|
||||||
|
for (var [key, value] of form.entries()) data[key] = castToType(key, value);
|
||||||
|
const { workspace: updatedWorkspace, message } = await Workspace.update(
|
||||||
|
workspace.slug,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (!!updatedWorkspace) {
|
||||||
|
showToast("Workspace updated!", "success", { clear: true });
|
||||||
|
} else {
|
||||||
|
showToast(`Error: ${message}`, "error", { clear: true });
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!workspace) return null;
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={formEl}
|
||||||
|
onSubmit={handleUpdate}
|
||||||
|
className="w-1/2 flex flex-col gap-y-6"
|
||||||
|
>
|
||||||
|
<ChatModelSelection
|
||||||
|
settings={settings}
|
||||||
|
workspace={workspace}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
<ChatHistorySettings
|
||||||
|
workspace={workspace}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
<ChatPromptSettings workspace={workspace} setHasChanges={setHasChanges} />
|
||||||
|
<ChatTemperatureSettings
|
||||||
|
settings={settings}
|
||||||
|
workspace={workspace}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
|
>
|
||||||
|
{saving ? "Updating..." : "Update workspace"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import Workspace from "@/models/workspace";
|
||||||
|
import { castToType } from "@/utils/types";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import VectorCount from "./VectorCount";
|
||||||
|
import WorkspaceName from "./WorkspaceName";
|
||||||
|
import SuggestedChatMessages from "./SuggestedChatMessages";
|
||||||
|
|
||||||
|
export default function GeneralInfo({ slug }) {
|
||||||
|
const [workspace, setWorkspace] = useState(null);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const formEl = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchWorkspace() {
|
||||||
|
const workspace = await Workspace.bySlug(slug);
|
||||||
|
setWorkspace(workspace);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchWorkspace();
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
const handleUpdate = async (e) => {
|
||||||
|
setSaving(true);
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(formEl.current);
|
||||||
|
for (var [key, value] of form.entries()) data[key] = castToType(key, value);
|
||||||
|
const { workspace: updatedWorkspace, message } = await Workspace.update(
|
||||||
|
workspace.slug,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (!!updatedWorkspace) {
|
||||||
|
showToast("Workspace updated!", "success", { clear: true });
|
||||||
|
setTimeout(() => window.location.reload(), 1_500);
|
||||||
|
} else {
|
||||||
|
showToast(`Error: ${message}`, "error", { clear: true });
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!workspace || loading) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
ref={formEl}
|
||||||
|
onSubmit={handleUpdate}
|
||||||
|
className="w-1/2 flex flex-col gap-y-6"
|
||||||
|
>
|
||||||
|
<VectorCount reload={true} workspace={workspace} />
|
||||||
|
<WorkspaceName
|
||||||
|
key={workspace.slug}
|
||||||
|
workspace={workspace}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="transition-all w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
|
>
|
||||||
|
{saving ? "Updating..." : "Update workspace"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
<div className="mt-6">
|
||||||
|
<SuggestedChatMessages slug={workspace.slug} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
import Workspace from "@/models/workspace";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { castToType } from "@/utils/types";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import VectorDBIdentifier from "./VectorDBIdentifier";
|
||||||
|
import MaxContextSnippets from "./MaxContextSnippets";
|
||||||
|
import DocumentSimilarityThreshold from "./DocumentSimilarityThreshold";
|
||||||
|
|
||||||
|
export default function VectorDatabase({ workspace }) {
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const formEl = useRef(null);
|
||||||
|
|
||||||
|
const handleUpdate = async (e) => {
|
||||||
|
setSaving(true);
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(formEl.current);
|
||||||
|
for (var [key, value] of form.entries()) data[key] = castToType(key, value);
|
||||||
|
const { workspace: updatedWorkspace, message } = await Workspace.update(
|
||||||
|
workspace.slug,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (!!updatedWorkspace) {
|
||||||
|
showToast("Workspace updated!", "success", { clear: true });
|
||||||
|
} else {
|
||||||
|
showToast(`Error: ${message}`, "error", { clear: true });
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!workspace) return null;
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={formEl}
|
||||||
|
onSubmit={handleUpdate}
|
||||||
|
className="w-1/2 flex flex-col gap-y-6"
|
||||||
|
>
|
||||||
|
<VectorDBIdentifier workspace={workspace} />
|
||||||
|
<MaxContextSnippets workspace={workspace} setHasChanges={setHasChanges} />
|
||||||
|
<DocumentSimilarityThreshold
|
||||||
|
workspace={workspace}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||||
|
>
|
||||||
|
{saving ? "Updating..." : "Update workspace"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -1,208 +1,119 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { isMobile } from "react-device-detect";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import showToast from "@/utils/toast";
|
|
||||||
import { ArrowUUpLeft, Plus, X } from "@phosphor-icons/react";
|
|
||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
|
import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { FullScreenLoader } from "@/components/Preloader";
|
||||||
|
import {
|
||||||
|
ArrowUUpLeft,
|
||||||
|
ChatText,
|
||||||
|
Database,
|
||||||
|
Wrench,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
import paths from "@/utils/paths";
|
import paths from "@/utils/paths";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import GeneralAppearance from "./GeneralAppearance";
|
||||||
|
import ChatSettings from "./ChatSettings";
|
||||||
|
import VectorDatabase from "./VectorDatabase";
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
"general-appearance": GeneralAppearance,
|
||||||
|
"chat-settings": ChatSettings,
|
||||||
|
"vector-database": VectorDatabase,
|
||||||
|
};
|
||||||
|
|
||||||
export default function WorkspaceSettings() {
|
export default function WorkspaceSettings() {
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const { loading, requiresAuth, mode } = usePasswordModal();
|
||||||
|
|
||||||
|
if (loading) return <FullScreenLoader />;
|
||||||
|
if (requiresAuth !== false) {
|
||||||
|
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ShowWorkspaceChat />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowWorkspaceChat() {
|
||||||
|
const { slug, tab } = useParams();
|
||||||
const [workspace, setWorkspace] = useState(null);
|
const [workspace, setWorkspace] = useState(null);
|
||||||
const [suggestedMessages, setSuggestedMessages] = useState([]);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editingIndex, setEditingIndex] = useState(-1);
|
|
||||||
const [newMessage, setNewMessage] = useState({ heading: "", message: "" });
|
|
||||||
const { slug } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchWorkspace() {
|
async function getWorkspace() {
|
||||||
if (!slug) return;
|
if (!slug) return;
|
||||||
const workspace = await Workspace.bySlug(slug);
|
const _workspace = await Workspace.bySlug(slug);
|
||||||
|
if (!_workspace) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
|
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
|
||||||
setWorkspace(workspace);
|
setWorkspace({
|
||||||
setSuggestedMessages(suggestedMessages);
|
..._workspace,
|
||||||
|
suggestedMessages,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
fetchWorkspace();
|
getWorkspace();
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
const handleSaveSuggestedMessages = async () => {
|
if (loading) return <FullScreenLoader />;
|
||||||
const validMessages = suggestedMessages.filter(
|
|
||||||
(msg) =>
|
|
||||||
msg?.heading?.trim()?.length > 0 || msg?.message?.trim()?.length > 0
|
|
||||||
);
|
|
||||||
const { success, error } = await Workspace.setSuggestedMessages(
|
|
||||||
slug,
|
|
||||||
validMessages
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
showToast(`Failed to update welcome messages: ${error}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showToast("Successfully updated welcome messages.", "success");
|
|
||||||
setHasChanges(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMessage = () => {
|
|
||||||
setEditingIndex(-1);
|
|
||||||
if (suggestedMessages.length >= 4) {
|
|
||||||
showToast("Maximum of 4 messages allowed.", "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const defaultMessage = {
|
|
||||||
heading: "Explain to me",
|
|
||||||
message: "the benefits of AnythingLLM",
|
|
||||||
};
|
|
||||||
setNewMessage(defaultMessage);
|
|
||||||
setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);
|
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeMessage = (index) => {
|
|
||||||
const messages = [...suggestedMessages];
|
|
||||||
messages.splice(index, 1);
|
|
||||||
setSuggestedMessages(messages);
|
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startEditing = (index) => {
|
|
||||||
setEditingIndex(index);
|
|
||||||
setNewMessage({ ...suggestedMessages[index] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveMessage = (index) => {
|
|
||||||
removeMessage(index);
|
|
||||||
setEditingIndex(-1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEditChange = (e) => {
|
|
||||||
const updatedNewMessage = {
|
|
||||||
...newMessage,
|
|
||||||
[e.target.name]: e.target.value,
|
|
||||||
};
|
|
||||||
setNewMessage(updatedNewMessage);
|
|
||||||
const updatedMessages = suggestedMessages.map((message, index) => {
|
|
||||||
if (index === editingIndex) {
|
|
||||||
return { ...message, [e.target.name]: e.target.value };
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
|
|
||||||
setSuggestedMessages(updatedMessages);
|
|
||||||
setHasChanges(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const TabContent = TABS[tab];
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
<a
|
{!isMobile && <Sidebar />}
|
||||||
href={paths.workspace.chat(slug)}
|
|
||||||
className="absolute top-2 left-2 md:top-16 md:left-10 transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border z-10"
|
|
||||||
>
|
|
||||||
<ArrowUUpLeft className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
className="transition-all duration-500 relative md:ml-[16px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
|
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
<div className="flex gap-x-10 pt-6 pb-4 ml-16 mr-8 border-b-2 border-white border-opacity-10">
|
||||||
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
<Link
|
||||||
<div className="items-center flex gap-x-4">
|
to={paths.workspace.chat(slug)}
|
||||||
<p className="text-2xl font-semibold text-white">
|
className="absolute top-2 left-2 md:top-4 md:left-4 transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border z-10"
|
||||||
Workspace Settings ({workspace?.name})
|
>
|
||||||
</p>
|
<ArrowUUpLeft className="h-4 w-4" />
|
||||||
</div>
|
</Link>
|
||||||
<p className="text-sm font-base text-white text-opacity-60">
|
<TabItem
|
||||||
Customize your workspace.
|
title="General Settings"
|
||||||
</p>
|
icon={<Wrench className="h-6 w-6" />}
|
||||||
</div>
|
to={paths.workspace.settings.generalAppearance(slug)}
|
||||||
<div className="my-6">
|
/>
|
||||||
<div className="flex flex-col gap-y-2">
|
<TabItem
|
||||||
<h2 className="leading-tight font-medium text-white">
|
title="Chat Settings"
|
||||||
Suggested Chat Messages
|
icon={<ChatText className="h-6 w-6" />}
|
||||||
</h2>
|
to={paths.workspace.settings.chatSettings(slug)}
|
||||||
<p className="text-sm font-base text-white/60">
|
/>
|
||||||
Customize the messages that will be suggested to your workspace
|
<TabItem
|
||||||
users.
|
title="Vector Database"
|
||||||
</p>
|
icon={<Database className="h-6 w-6" />}
|
||||||
</div>
|
to={paths.workspace.settings.vectorDatabase(slug)}
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-white/60 text-xs mt-6 w-full justify-center max-w-[600px]">
|
</div>
|
||||||
{suggestedMessages.map((suggestion, index) => (
|
<div className="px-16 py-6">
|
||||||
<div key={index} className="relative w-full">
|
<TabContent slug={slug} workspace={workspace} />
|
||||||
<button
|
|
||||||
className="transition-all duration-300 absolute z-10 text-neutral-700 bg-white rounded-full hover:bg-zinc-600 hover:border-zinc-600 hover:text-white border-transparent border shadow-lg ml-2"
|
|
||||||
style={{
|
|
||||||
top: -8,
|
|
||||||
left: 265,
|
|
||||||
}}
|
|
||||||
onClick={() => handleRemoveMessage(index)}
|
|
||||||
>
|
|
||||||
<X className="m-[1px]" size={20} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => startEditing(index)}
|
|
||||||
className={`text-left p-2.5 border rounded-xl w-full border-white/20 bg-sidebar hover:bg-workspace-item-selected-gradient ${
|
|
||||||
editingIndex === index ? "border-sky-400" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="font-semibold">{suggestion.heading}</p>
|
|
||||||
<p>{suggestion.message}</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{editingIndex >= 0 && (
|
|
||||||
<div className="flex flex-col gap-y-4 mr-2 mt-8">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-2">
|
|
||||||
Heading
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
placeholder="Message heading"
|
|
||||||
className=" bg-sidebar text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
value={newMessage.heading}
|
|
||||||
name="heading"
|
|
||||||
onChange={onEditChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-2">
|
|
||||||
Message
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
placeholder="Message"
|
|
||||||
className="bg-sidebar text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
value={newMessage.message}
|
|
||||||
name="message"
|
|
||||||
onChange={onEditChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{suggestedMessages.length < 4 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addMessage}
|
|
||||||
className="flex gap-x-2 items-center justify-center mt-6 text-white text-sm hover:text-sky-400 transition-all duration-300"
|
|
||||||
>
|
|
||||||
Add new message <Plus className="" size={24} weight="fill" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasChanges && (
|
|
||||||
<div className="flex justify-center py-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
|
||||||
onClick={handleSaveSuggestedMessages}
|
|
||||||
>
|
|
||||||
Save Messages
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TabItem({ title, icon, to }) {
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={to}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`${
|
||||||
|
isActive
|
||||||
|
? "text-sky-400 pb-4 border-b-[4px] -mb-[19px] border-sky-400"
|
||||||
|
: "text-white/60 hover:text-sky-400"
|
||||||
|
} ` + " flex gap-x-2 items-center font-medium"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<div>{title}</div>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -55,8 +55,16 @@ export default {
|
|||||||
chat: (slug) => {
|
chat: (slug) => {
|
||||||
return `/workspace/${slug}`;
|
return `/workspace/${slug}`;
|
||||||
},
|
},
|
||||||
additionalSettings: (slug) => {
|
settings: {
|
||||||
return `/workspace/${slug}/settings`;
|
generalAppearance: (slug) => {
|
||||||
|
return `/workspace/${slug}/settings/general-appearance`;
|
||||||
|
},
|
||||||
|
chatSettings: (slug) => {
|
||||||
|
return `/workspace/${slug}/settings/chat-settings`;
|
||||||
|
},
|
||||||
|
vectorDatabase: (slug) => {
|
||||||
|
return `/workspace/${slug}/settings/vector-database`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
thread: (wsSlug, threadSlug) => {
|
thread: (wsSlug, threadSlug) => {
|
||||||
return `/workspace/${wsSlug}/t/${threadSlug}`;
|
return `/workspace/${wsSlug}/t/${threadSlug}`;
|
||||||
|
19
frontend/src/utils/types.js
Normal file
19
frontend/src/utils/types.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export function castToType(key, value) {
|
||||||
|
const definitions = {
|
||||||
|
openAiTemp: {
|
||||||
|
cast: (value) => Number(value),
|
||||||
|
},
|
||||||
|
openAiHistory: {
|
||||||
|
cast: (value) => Number(value),
|
||||||
|
},
|
||||||
|
similarityThreshold: {
|
||||||
|
cast: (value) => parseFloat(value),
|
||||||
|
},
|
||||||
|
topN: {
|
||||||
|
cast: (value) => Number(value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!definitions.hasOwnProperty(key)) return value;
|
||||||
|
return definitions[key].cast(value);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user