diff --git a/frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/CannotRemoveModal/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/CannotRemoveModal/index.jsx rename to frontend/src/components/Modals/MangeWorkspace/Documents/CannotRemoveModal/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx similarity index 98% rename from frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx rename to frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx index bec3beec832b1ec2a6f18b42be270a94175740da..b5d00a219701864034f568f386d23ca5e65f29bf 100644 --- a/frontend/src/components/Modals/MangeWorkspace/ConfirmationModal/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/ConfirmationModal/index.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { dollarFormat } from "../../../../utils/numbers"; +import { dollarFormat } from "../../../../../utils/numbers"; export default function ConfirmationModal({ directories, diff --git a/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx similarity index 98% rename from frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx rename to frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx index b444f979327caa90675976df5e2bf847b4afb507..b332011de4c7c14292679b8352d564356d10c68c 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Directory/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx @@ -7,7 +7,7 @@ import { FolderPlus, Zap, } from "react-feather"; -import { nFormatter } from "../../../../utils/numbers"; +import { nFormatter } from "../../../../../utils/numbers"; export default function Directory({ files, diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b6ce6924ac55569094b71ab3859f4521513bb8cc --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from "react"; +import System from "../../../../models/system"; +import Workspace from "../../../../models/workspace"; +import paths from "../../../../utils/paths"; +import { useParams } from "react-router-dom"; +import Directory from "./Directory"; +import ConfirmationModal from "./ConfirmationModal"; +import CannotRemoveModal from "./CannotRemoveModal"; + +export default function DocumentSettings({ workspace }) { + const { slug } = useParams(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); + const [directories, setDirectories] = useState(null); + const [originalDocuments, setOriginalDocuments] = useState([]); + const [selectedFiles, setSelectFiles] = useState([]); + const [vectordb, setVectorDB] = useState(null); + const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false); + + useEffect(() => { + async function fetchKeys() { + const localFiles = await System.localFiles(); + const settings = await System.keys(); + const originalDocs = workspace.documents.map((doc) => doc.docpath) || []; + setDirectories(localFiles); + setOriginalDocuments([...originalDocs]); + setSelectFiles([...originalDocs]); + setVectorDB(settings?.VectorDB); + setLoading(false); + } + fetchKeys(); + }, []); + + 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; + await Workspace.delete(workspace.slug); + workspace.slug === slug + ? (window.location = paths.home()) + : window.location.reload(); + }; + + const docChanges = () => { + const changes = { + adds: [], + deletes: [], + }; + + selectedFiles.map((doc) => { + const inOriginal = !!originalDocuments.find((oDoc) => oDoc === doc); + if (!inOriginal) { + changes.adds.push(doc); + } + }); + + originalDocuments.map((doc) => { + const selected = !!selectedFiles.find((oDoc) => oDoc === doc); + if (!selected) { + changes.deletes.push(doc); + } + }); + + return changes; + }; + + const confirmChanges = (e) => { + e.preventDefault(); + const changes = docChanges(); + changes.adds.length > 0 ? setShowConfirmation(true) : updateWorkspace(e); + }; + + const updateWorkspace = async (e) => { + e.preventDefault(); + setSaving(true); + setShowConfirmation(false); + const changes = docChanges(); + await Workspace.modifyEmbeddings(workspace.slug, changes); + setSaving(false); + window.location.reload(); + }; + + const isSelected = (filepath) => { + const isFolder = !filepath.includes("/"); + return isFolder + ? selectedFiles.some((doc) => doc.includes(filepath.split("/")[0])) + : selectedFiles.some((doc) => doc.includes(filepath)); + }; + + const isOriginalDoc = (filepath) => { + const isFolder = !filepath.includes("/"); + return isFolder + ? originalDocuments.some((doc) => doc.includes(filepath.split("/")[0])) + : originalDocuments.some((doc) => doc.includes(filepath)); + }; + + const toggleSelection = (filepath) => { + const isFolder = !filepath.includes("/"); + const parent = isFolder ? filepath : filepath.split("/")[0]; + + if (isSelected(filepath)) { + // Certain vector DBs do not contain the ability to delete vectors + // so we cannot remove from these. The user will have to clear the entire workspace. + if (["lancedb"].includes(vectordb) && isOriginalDoc(filepath)) { + setShowingNoRemovalModal(true); + return false; + } + + const updatedDocs = isFolder + ? selectedFiles.filter((doc) => !doc.includes(parent)) + : selectedFiles.filter((doc) => !doc.includes(filepath)); + setSelectFiles([...new Set(updatedDocs)]); + } else { + var newDocs = []; + var parentDirs = directories.items.find((item) => item.name === parent); + if (isFolder && parentDirs) { + const folderItems = parentDirs.items; + newDocs = folderItems.map((item) => parent + "/" + item.name); + } else { + newDocs = [filepath]; + } + + const combined = [...selectedFiles, ...newDocs]; + setSelectFiles([...new Set(combined)]); + } + }; + + if (loading) { + return ( + <> + <div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll"> + <div className="flex flex-col gap-y-1 w-full"> + <p className="text-slate-200 dark:text-stone-300 text-center"> + loading workspace files + </p> + </div> + </div> + <div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"></div> + </> + ); + } + + return ( + <> + {showConfirmation && ( + <ConfirmationModal + directories={directories} + hideConfirm={() => setShowConfirmation(false)} + additions={docChanges().adds} + updateWorkspace={updateWorkspace} + /> + )} + {showingNoRemovalModal && ( + <CannotRemoveModal + hideModal={() => setShowingNoRemovalModal(false)} + vectordb={vectordb} + /> + )} + <div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll"> + <div className="flex flex-col gap-y-1 w-full"> + <div className="flex flex-col mb-2"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Select folders to add or remove from workspace. + </p> + <p className="text-gray-800 dark:text-stone-400 text-xs italic"> + {selectedFiles.length} documents in workspace selected. + </p> + </div> + <div className="w-full h-auto border border-slate-200 dark:border-stone-600 rounded-lg px-4 py-2"> + {!!directories && ( + <Directory + files={directories} + toggleSelection={toggleSelection} + isSelected={isSelected} + /> + )} + </div> + </div> + </div> + <div className="flex items-center justify-between p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + <button + onClick={deleteWorkspace} + type="button" + className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600" + > + Delete Workspace + </button> + <div className="flex items-center"> + <button + disabled={saving} + onClick={confirmChanges} + type="submit" + className="text-slate-200 bg-black-900 px-4 py-2 rounded-lg hover:bg-gray-900" + > + {saving ? "Saving..." : "Confirm Changes"} + </button> + </div> + </div> + </> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8e65298698b461ee558746c0a1e840e2ace51efb --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Settings/index.jsx @@ -0,0 +1,176 @@ +import React, { useState, useRef, useEffect } from "react"; +import Workspace from "../../../../models/workspace"; +import paths from "../../../../utils/paths"; + +export default function WorkspaceSettings({ workspace }) { + const formEl = useRef(null); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + useEffect(() => { + function setTimer() { + if (success !== null) { + setTimeout(() => { + setSuccess(null); + }, 3_000); + } + + if (error !== null) { + setTimeout(() => { + setError(null); + }, 3_000); + } + } + setTimer(); + }, [success, error]); + + const handleUpdate = async (e) => { + setError(null); + setSuccess(null); + setSaving(true); + e.preventDefault(); + const data = {}; + const form = new FormData(formEl.current); + for (var [key, value] of form.entries()) data[key] = value; + const { workspace: updatedWorkspace, message } = await Workspace.update( + workspace.slug, + data + ); + if (!!updatedWorkspace) { + setSuccess("Workspace updated!"); + } else { + setError(message); + } + setSaving(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; + await Workspace.delete(workspace.slug); + workspace.slug === slug + ? (window.location = paths.home()) + : window.location.reload(); + }; + + return ( + <form ref={formEl} onSubmit={handleUpdate}> + <div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll"> + <div className="flex flex-col gap-y-1 w-full"> + <div className="flex flex-col mb-2"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Edit your workspace's settings + </p> + </div> + + <div className="w-full flex flex-col gap-y-4"> + <div> + <input + type="text" + disabled={true} + defaultValue={workspace?.slug} + className="bg-gray-50 border disabled:bg-gray-400 disabled:text-gray-700 disabled:border-gray-400 disabled:dark:bg-stone-800 disabled:dark:border-stone-900 disabled:dark:text-stone-600 disabled:cursor-not-allowed border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + required={true} + autoComplete="off" + /> + </div> + + <div> + <div className="flex flex-col gap-y-1 mb-4"> + <label + htmlFor="name" + className="block text-sm font-medium text-gray-900 dark:text-white" + > + Workspace Name + </label> + <p className="text-xs text-gray-600 dark:text-stone-400"> + 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-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="My Workspace" + required={true} + autoComplete="off" + onChange={() => setHasChanges(true)} + /> + </div> + + <div> + <div className="flex flex-col gap-y-1 mb-4"> + <label + htmlFor="name" + className="block text-sm font-medium text-gray-900 dark:text-white" + > + LLM Temperature + </label> + <p className="text-xs text-gray-600 dark:text-stone-400"> + This setting controls how "random" or dynamic your chat + responses will be. + <br /> + The higher the number (2.0 maximum) the more random and + incoherent. + <br /> + Recommended: 0.7 + </p> + </div> + <input + name="openAiTemp" + type="number" + min={0.0} + max={2.0} + step={0.1} + onWheel={(e) => e.target.blur()} + defaultValue={workspace?.openAiTemp ?? 0.7} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="0.7" + required={true} + autoComplete="off" + onChange={() => setHasChanges(true)} + /> + </div> + </div> + + {error && ( + <p className="text-red-600 dark:text-red-400 text-sm"> + Error: {error} + </p> + )} + {success && ( + <p className="text-green-600 dark:text-green-400 text-sm"> + Success: {success} + </p> + )} + </div> + </div> + <div className="flex items-center justify-between p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + <button + onClick={deleteWorkspace} + type="button" + className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600" + > + Delete Workspace + </button> + {hasChanges && ( + <button + type="submit" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800" + > + {saving ? "Updating..." : "Update workspace"} + </button> + )} + </div> + </form> + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/index.jsx b/frontend/src/components/Modals/MangeWorkspace/index.jsx index a97a614d1dd3513c167ae6bc520dab0f58403655..0fe25f0b4088a1378ccaad04b8d8349982c3c404 100644 --- a/frontend/src/components/Modals/MangeWorkspace/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/index.jsx @@ -1,149 +1,47 @@ import React, { useState, useEffect } from "react"; -import { X } from "react-feather"; -import System from "../../../models/system"; -import Workspace from "../../../models/workspace"; -import paths from "../../../utils/paths"; +import { Archive, Sliders, X } from "react-feather"; +import DocumentSettings from "./Documents"; +import WorkspaceSettings from "./Settings"; import { useParams } from "react-router-dom"; -import Directory from "./Directory"; -import ConfirmationModal from "./ConfirmationModal"; -import CannotRemoveModal from "./CannotRemoveModal"; +import Workspace from "../../../models/workspace"; + +const TABS = { + documents: DocumentSettings, + settings: WorkspaceSettings, +}; const noop = () => false; -export default function ManageWorkspace({ hideModal = noop, workspace }) { +export default function ManageWorkspace({ + hideModal = noop, + providedSlug = null, +}) { const { slug } = useParams(); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [showConfirmation, setShowConfirmation] = useState(false); - const [directories, setDirectories] = useState(null); - const [originalDocuments, setOriginalDocuments] = useState([]); - const [selectedFiles, setSelectFiles] = useState([]); - const [vectordb, setVectorDB] = useState(null); - const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false); + const [selectedTab, setSelectedTab] = useState("documents"); + const [workspace, setWorkspace] = useState(null); useEffect(() => { - async function fetchKeys() { - const _workspace = await Workspace.bySlug(workspace.slug); - const localFiles = await System.localFiles(); - const settings = await System.keys(); - const originalDocs = _workspace.documents.map((doc) => doc.docpath) || []; - setDirectories(localFiles); - setOriginalDocuments([...originalDocs]); - setSelectFiles([...originalDocs]); - setVectorDB(settings?.VectorDB); - setLoading(false); + async function fetchWorkspace() { + const workspace = await Workspace.bySlug(providedSlug ?? slug); + setWorkspace(workspace); } - fetchKeys(); - }, []); - - 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; - await Workspace.delete(workspace.slug); - workspace.slug === slug - ? (window.location = paths.home()) - : window.location.reload(); - }; - - const docChanges = () => { - const changes = { - adds: [], - deletes: [], - }; - - selectedFiles.map((doc) => { - const inOriginal = !!originalDocuments.find((oDoc) => oDoc === doc); - if (!inOriginal) { - changes.adds.push(doc); - } - }); - - originalDocuments.map((doc) => { - const selected = !!selectedFiles.find((oDoc) => oDoc === doc); - if (!selected) { - changes.deletes.push(doc); - } - }); - - return changes; - }; - - const confirmChanges = (e) => { - e.preventDefault(); - const changes = docChanges(); - changes.adds.length > 0 ? setShowConfirmation(true) : updateWorkspace(e); - }; - - const updateWorkspace = async (e) => { - e.preventDefault(); - setSaving(true); - setShowConfirmation(false); - const changes = docChanges(); - await Workspace.modifyEmbeddings(workspace.slug, changes); - setSaving(false); - window.location.reload(); - }; - - const isSelected = (filepath) => { - const isFolder = !filepath.includes("/"); - return isFolder - ? selectedFiles.some((doc) => doc.includes(filepath.split("/")[0])) - : selectedFiles.some((doc) => doc.includes(filepath)); - }; - - const isOriginalDoc = (filepath) => { - const isFolder = !filepath.includes("/"); - return isFolder - ? originalDocuments.some((doc) => doc.includes(filepath.split("/")[0])) - : originalDocuments.some((doc) => doc.includes(filepath)); - }; - - const toggleSelection = (filepath) => { - const isFolder = !filepath.includes("/"); - const parent = isFolder ? filepath : filepath.split("/")[0]; - - if (isSelected(filepath)) { - // Certain vector DBs do not contain the ability to delete vectors - // so we cannot remove from these. The user will have to clear the entire workspace. - if (["lancedb"].includes(vectordb) && isOriginalDoc(filepath)) { - setShowingNoRemovalModal(true); - return false; - } + fetchWorkspace(); + }, [selectedTab, slug]); - const updatedDocs = isFolder - ? selectedFiles.filter((doc) => !doc.includes(parent)) - : selectedFiles.filter((doc) => !doc.includes(filepath)); - setSelectFiles([...new Set(updatedDocs)]); - } else { - var newDocs = []; - var parentDirs = directories.items.find((item) => item.name === parent); - if (isFolder && parentDirs) { - const folderItems = parentDirs.items; - newDocs = folderItems.map((item) => parent + "/" + item.name); - } else { - newDocs = [filepath]; - } + if (!workspace) return null; - const combined = [...selectedFiles, ...newDocs]; - setSelectFiles([...new Set(combined)]); - } - }; - - if (loading) { - return ( - <div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> - <div - className="flex fixed top-0 left-0 right-0 w-full h-full" - onClick={hideModal} - /> - <div className="relative w-full max-w-2xl max-h-full"> - <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> - <div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> + const Component = TABS[selectedTab || "documents"]; + return ( + <div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> + <div + className="flex fixed top-0 left-0 right-0 w-full h-full" + onClick={hideModal} + /> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex flex-col gap-y-1 border-b dark:border-gray-600 px-4 pt-4 "> + <div className="flex items-start justify-between rounded-t "> <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> - {workspace.name} Settings + Update "{workspace.name}" </h3> <button onClick={hideModal} @@ -154,101 +52,64 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) { <X className="text-gray-300 text-lg" /> </button> </div> - <div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll"> - <div className="flex flex-col gap-y-1 w-full"> - <p className="text-slate-200 dark:text-stone-300 text-center"> - loading workspace files - </p> - </div> - </div> - <div className="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"></div> + <WorkspaceSettingTabs + selectedTab={selectedTab} + changeTab={setSelectedTab} + /> </div> + <Component hideModal={hideModal} workspace={workspace} /> </div> </div> - ); - } + </div> + ); +} +function WorkspaceSettingTabs({ selectedTab, changeTab }) { return ( - <> - {showConfirmation && ( - <ConfirmationModal - directories={directories} - hideConfirm={() => setShowConfirmation(false)} - additions={docChanges().adds} - updateWorkspace={updateWorkspace} + <div> + <ul className="flex flex-wrap -mb-px text-sm gap-x-2 font-medium text-center text-gray-500 dark:text-gray-400"> + <WorkspaceTab + active={selectedTab === "documents"} + displayName="Documents" + tabName="documents" + icon={<Archive className="h-4 w-4" />} + onClick={changeTab} /> - )} - {showingNoRemovalModal && ( - <CannotRemoveModal - hideModal={() => setShowingNoRemovalModal(false)} - vectordb={vectordb} + <WorkspaceTab + active={selectedTab === "settings"} + displayName="Settings" + tabName="settings" + icon={<Sliders className="h-4 w-4" />} + onClick={changeTab} /> - )} - <div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> - <div - className="flex fixed top-0 left-0 right-0 w-full h-full" - onClick={hideModal} - /> - <div className="relative w-full max-w-2xl max-h-full"> - <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> - <div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> - <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> - "{workspace.name}" workspace settings - </h3> - <button - onClick={hideModal} - type="button" - className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" - data-modal-hide="staticModal" - > - <X className="text-gray-300 text-lg" /> - </button> - </div> - <div className="p-6 flex h-full w-full max-h-[80vh] overflow-y-scroll"> - <div className="flex flex-col gap-y-1 w-full"> - <div className="flex flex-col mb-2"> - <p className="text-gray-800 dark:text-stone-200 text-base "> - Select folders to add or remove from workspace. - </p> - <p className="text-gray-800 dark:text-stone-400 text-xs italic"> - {selectedFiles.length} documents in workspace selected. - </p> - </div> - <div className="w-full h-auto border border-slate-200 dark:border-stone-600 rounded-lg px-4 py-2"> - {!!directories && ( - <Directory - files={directories} - toggleSelection={toggleSelection} - isSelected={isSelected} - /> - )} - </div> - </div> - </div> + </ul> + </div> + ); +} - <div className="flex items-center justify-between p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> - <button - onClick={deleteWorkspace} - type="button" - className="border border-transparent text-gray-500 bg-white hover:bg-red-100 rounded-lg text-sm font-medium px-5 py-2.5 hover:text-red-900 focus:z-10 dark:bg-transparent dark:text-gray-300 dark:hover:text-white dark:hover:bg-red-600" - > - Delete Workspace - </button> - <div className="flex items-center"> - <button - disabled={saving} - onClick={confirmChanges} - type="submit" - className="text-slate-200 bg-black-900 px-4 py-2 rounded-lg hover:bg-gray-900" - > - {saving ? "Saving..." : "Confirm Changes"} - </button> - </div> - </div> - </div> - </div> - </div> - </> +function WorkspaceTab({ + active = false, + displayName, + tabName, + icon = "", + onClick, +}) { + const classes = active + ? "text-blue-600 border-blue-600 active dark:text-blue-400 dark:border-blue-400 bg-blue-500 bg-opacity-5" + : "border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"; + return ( + <li className="mr-2"> + <button + disabled={active} + onClick={() => onClick(tabName)} + className={ + "flex items-center gap-x-1 p-4 border-b-2 rounded-t-lg group " + + classes + } + > + {icon} {displayName} + </button> + </li> ); } diff --git a/frontend/src/components/Modals/NewWorkspace.jsx b/frontend/src/components/Modals/NewWorkspace.jsx index 44aed7d99b066633e0e102102717124043aa2775..81a87903be4aac2faaf0bd9f4be3f59a1b483aa1 100644 --- a/frontend/src/components/Modals/NewWorkspace.jsx +++ b/frontend/src/components/Modals/NewWorkspace.jsx @@ -18,33 +18,33 @@ export default function NewWorkspaceModal({ hideModal = noop }) { }; return ( - <div class="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> + <div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> <div className="flex fixed top-0 left-0 right-0 w-full h-full" onClick={hideModal} /> - <div class="relative w-full max-w-2xl max-h-full"> - <div class="relative bg-white rounded-lg shadow dark:bg-stone-700"> - <div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> - <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> + <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> Create a New Workspace </h3> <button onClick={hideModal} type="button" - class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" + className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="staticModal" > <X className="text-gray-300 text-lg" /> </button> </div> <form ref={formEl} onSubmit={handleCreate}> - <div class="p-6 space-y-6 flex h-full w-full"> + <div className="p-6 space-y-6 flex h-full w-full"> <div className="w-full flex flex-col gap-y-4"> <div> <label htmlFor="name" - class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" + className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Workspace Name </label> @@ -52,7 +52,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) { name="name" type="text" id="name" - class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="My Workspace" required={true} autoComplete="off" @@ -69,7 +69,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) { </p> </div> </div> - <div class="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> <button onClick={hideModal} type="button" @@ -79,7 +79,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) { </button> <button type="submit" - class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800" > Create Workspace </button> diff --git a/frontend/src/components/Modals/Password.jsx b/frontend/src/components/Modals/Password.jsx index 1a0fb1966668c9315442951be0e6384fd0b6ab2f..280572feb3b04966227091c5e186ddef7cc7395b 100644 --- a/frontend/src/components/Modals/Password.jsx +++ b/frontend/src/components/Modals/Password.jsx @@ -25,22 +25,22 @@ export default function PasswordModal() { }; return ( - <div class="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> + <div className="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center"> <div className="flex fixed top-0 left-0 right-0 w-full h-full" /> - <div class="relative w-full max-w-2xl max-h-full"> + <div className="relative w-full max-w-2xl max-h-full"> <form ref={formEl} onSubmit={handleLogin}> - <div class="relative bg-white rounded-lg shadow dark:bg-stone-700"> - <div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> - <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> + <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> This workspace is password protected. </h3> </div> - <div class="p-6 space-y-6 flex h-full w-full"> + <div className="p-6 space-y-6 flex h-full w-full"> <div className="w-full flex flex-col gap-y-4"> <div> <label htmlFor="password" - class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" + className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Workspace Password </label> @@ -48,7 +48,7 @@ export default function PasswordModal() { name="password" type="password" id="password" - class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required={true} autoComplete="off" /> @@ -64,11 +64,11 @@ export default function PasswordModal() { </p> </div> </div> - <div class="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + <div className="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> <button disabled={loading} type="submit" - class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600" > {loading ? "Validating..." : "Submit"} </button> diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx index 9a3cdc586597a18be4bfe65c3c2ce598301e7c23..549d3f34646864c0e5afbb2a89008c0ba5cccf3d 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx @@ -75,7 +75,7 @@ export default function ActiveWorkspaces() { ); })} {showing && !!selectedWs && ( - <ManageWorkspace hideModal={hideModal} workspace={selectedWs} /> + <ManageWorkspace hideModal={hideModal} providedSlug={selectedWs.slug} /> )} </> ); diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index 11f97437dd10d0b4c30bafa3356e087db5e1dcb3..cf0a6120cd83788148618c2ca92d7c7e63fca4a3 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -15,6 +15,22 @@ const Workspace = { return { workspace, message }; }, + update: async function (slug, data = {}) { + const { workspace, message } = await fetch( + `${API_BASE}/workspace/${slug}/update`, + { + method: "POST", + body: JSON.stringify(data), + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .catch((e) => { + return { workspace: null, message: e.message }; + }); + + return { workspace, message }; + }, modifyEmbeddings: async function (slug, changes = {}) { const { workspace, message } = await fetch( `${API_BASE}/workspace/${slug}/update-embeddings`, diff --git a/frontend/src/utils/chat/markdown.js b/frontend/src/utils/chat/markdown.js index ea8d794d07e20feff5e91197f4ca323bb14e1b41..7c6ccd6e1f41388d2b312b27cf156c65e1bbbc2d 100644 --- a/frontend/src/utils/chat/markdown.js +++ b/frontend/src/utils/chat/markdown.js @@ -31,6 +31,6 @@ window.copySnippet = function () { }, 5000); }; -export default function renderMarkdown(text) { +export default function renderMarkdown(text = "") { return markdown.render(text); } diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index d37b1ef613c41a1802944e741e3ce0b22c856f6d..73c9e17a20a5c899c22e7dec7d5aac608d7cc422 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -20,6 +20,28 @@ function workspaceEndpoints(app) { } }); + app.post("/workspace/:slug/update", async (request, response) => { + try { + const { slug = null } = request.params; + const data = reqBody(request); + const currWorkspace = await Workspace.get(`slug = '${slug}'`); + + if (!currWorkspace) { + response.sendStatus(400).end(); + return; + } + + const { workspace, message } = await Workspace.update( + currWorkspace.id, + data + ); + response.status(200).json({ workspace, message }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + app.post("/workspace/:slug/update-embeddings", async (request, response) => { try { const { slug = null } = request.params; diff --git a/server/index.js b/server/index.js index 5a506ae66a3c80aacfd9068fba0148ab4de477e4..e96deda48206a0940357f8cf5554ad6b6d33eaa4 100644 --- a/server/index.js +++ b/server/index.js @@ -12,6 +12,7 @@ const { systemEndpoints } = require("./endpoints/system"); const { workspaceEndpoints } = require("./endpoints/workspaces"); const { chatEndpoints } = require("./endpoints/chat"); const { getVectorDbClass } = require("./utils/helpers"); +const { validateTablePragmas } = require("./utils/database"); const app = express(); const apiRouter = express.Router(); @@ -25,8 +26,9 @@ app.use( ); apiRouter.use("/system/*", validatedRequest); -apiRouter.use("/workspace/*", validatedRequest); systemEndpoints(apiRouter); + +apiRouter.use("/workspace/*", validatedRequest); workspaceEndpoints(apiRouter); chatEndpoints(apiRouter); @@ -75,7 +77,8 @@ app.all("*", function (_, response) { }); app - .listen(process.env.SERVER_PORT || 3001, () => { + .listen(process.env.SERVER_PORT || 3001, async () => { + await validateTablePragmas(); console.log( `Example app listening on port ${process.env.SERVER_PORT || 3001}` ); diff --git a/server/models/documents.js b/server/models/documents.js index 732771195e8bda9c56e4e55b0ee115f3837b2537..777bd7175acb43cc1838697867b2f1ef499d44a0 100644 --- a/server/models/documents.js +++ b/server/models/documents.js @@ -1,6 +1,7 @@ const { fileData } = require("../utils/files"); const { v4: uuidv4 } = require("uuid"); const { getVectorDbClass } = require("../utils/helpers"); +const { checkForMigrations } = require("../utils/database"); const Document = { tablename: "workspace_documents", @@ -14,7 +15,15 @@ const Document = { createdAt TEXT DEFAULT CURRENT_TIMESTAMP, lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP `, - db: async function () { + migrateTable: async function () { + console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + const db = await this.db(false); + await checkForMigrations(this, db); + }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { const sqlite3 = require("sqlite3").verbose(); const { open } = require("sqlite"); @@ -28,7 +37,8 @@ const Document = { await db.exec( `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` ); - db.on("trace", (sql) => console.log(sql)); + + if (tracing) db.on("trace", (sql) => console.log(sql)); return db; }, forWorkspace: async function (workspaceId = null) { diff --git a/server/models/vectors.js b/server/models/vectors.js index 776179d02c07662dd9eef70458e295d684733d2e..9e1a8dd4253707bb0c946d25ee7b7e35ba08f932 100644 --- a/server/models/vectors.js +++ b/server/models/vectors.js @@ -1,8 +1,8 @@ +const { checkForMigrations } = require("../utils/database"); const { Document } = require("./documents"); // TODO: Do we want to store entire vectorized chunks in here // so that we can easily spin up temp-namespace clones for threading -// const DocumentVectors = { tablename: "document_vectors", colsInit: ` @@ -12,7 +12,17 @@ const DocumentVectors = { createdAt TEXT DEFAULT CURRENT_TIMESTAMP, lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP `, - db: async function () { + migrateTable: async function () { + console.log( + `\x1b[34m[MIGRATING]\x1b[0m Checking for DocumentVector migrations` + ); + const db = await this.db(false); + await checkForMigrations(this, db); + }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { const sqlite3 = require("sqlite3").verbose(); const { open } = require("sqlite"); @@ -26,7 +36,8 @@ const DocumentVectors = { await db.exec( `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` ); - db.on("trace", (sql) => console.log(sql)); + + if (tracing) db.on("trace", (sql) => console.log(sql)); return db; }, bulkInsert: async function (vectorRecords = []) { diff --git a/server/models/workspace.js b/server/models/workspace.js index 6472f4779f926f257dc665b7c24868491b67b7b3..09c3712cebd68a3d95a0be0b53d3ec4147b91518 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -1,17 +1,50 @@ const slugify = require("slugify"); const { Document } = require("./documents"); +const { checkForMigrations } = require("../utils/database"); const Workspace = { tablename: "workspaces", + writable: [ + // Used for generic updates so we can validate keys in request body + "name", + "slug", + "vectorTag", + "openAiTemp", + "lastUpdatedAt", + ], colsInit: ` id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, vectorTag TEXT DEFAULT NULL, createdAt TEXT DEFAULT CURRENT_TIMESTAMP, + openAiTemp REAL DEFAULT NULL, lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP `, - db: async function () { + migrateTable: async function () { + console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Workspace migrations`); + const db = await this.db(false); + await checkForMigrations(this, db); + }, + migrations: function () { + return [ + { + colName: "openAiTemp", + execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN openAiTemp REAL DEFAULT NULL`, + doif: false, + }, + { + colName: "id", + execCmd: `CREATE TRIGGER IF NOT EXISTS Trg_LastUpdated AFTER UPDATE ON ${this.tablename} + FOR EACH ROW + BEGIN + UPDATE ${this.tablename} SET lastUpdatedAt = CURRENT_TIMESTAMP WHERE id = old.id; + END`, + doif: true, + }, + ]; + }, + db: async function (tracing = true) { const sqlite3 = require("sqlite3").verbose(); const { open } = require("sqlite"); @@ -25,17 +58,25 @@ const Workspace = { await db.exec( `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` ); - db.on("trace", (sql) => console.log(sql)); + + if (tracing) db.on("trace", (sql) => console.log(sql)); return db; }, new: async function (name = null) { if (!name) return { result: null, message: "name cannot be null" }; + var slug = slugify(name, { lower: true }); + + const existingBySlug = await this.get(`slug = '${slug}'`); + if (existingBySlug !== null) { + const slugSeed = Math.floor(10000000 + Math.random() * 90000000); + slug = slugify(`${name}-${slugSeed}`, { lower: true }); + } const db = await this.db(); const { id, success, message } = await db .run(`INSERT INTO ${this.tablename} (name, slug) VALUES (?, ?)`, [ name, - slugify(name, { lower: true }), + slug, ]) .then((res) => { return { id: res.lastID, success: true, message: null }; @@ -43,19 +84,57 @@ const Workspace = { .catch((error) => { return { id: null, success: false, message: error.message }; }); - if (!success) return { workspace: null, message }; + + if (!success) { + db.close(); + return { workspace: null, message }; + } const workspace = await db.get( `SELECT * FROM ${this.tablename} WHERE id = ${id}` ); + db.close(); + return { workspace, message: null }; }, + update: async function (id = null, data = {}) { + if (!id) throw new Error("No workspace id provided for update"); + + const validKeys = Object.keys(data).filter((key) => + this.writable.includes(key) + ); + const values = Object.values(data); + if (validKeys.length === 0 || validKeys.length !== values.length) + return { workspace: { id }, message: "No valid fields to update!" }; + + const template = `UPDATE ${this.tablename} SET ${validKeys.map((key) => { + return `${key}=?`; + })} WHERE id = ?`; + const db = await this.db(); + const { success, message } = await db + .run(template, [...values, id]) + .then(() => { + return { success: true, message: null }; + }) + .catch((error) => { + return { success: false, message: error.message }; + }); + + db.close(); + if (!success) { + return { workspace: null, message }; + } + + const updatedWorkspace = await this.get(`id = ${id}`); + return { workspace: updatedWorkspace, message: null }; + }, get: async function (clause = "") { const db = await this.db(); const result = await db .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) .then((res) => res || null); if (!result) return null; + db.close(); const documents = await Document.forWorkspace(result.id); return { ...result, documents }; @@ -63,6 +142,8 @@ const Workspace = { delete: async function (clause = "") { const db = await this.db(); await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + db.close(); + return true; }, where: async function (clause = "", limit = null) { @@ -72,6 +153,8 @@ const Workspace = { !!limit ? `LIMIT ${limit}` : "" }` ); + db.close(); + return results; }, }; diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index 2ded62b4274c2703d4c6888069e62cd3cb901e95..3b90cc6143dac14df396a8737b1bd02f9664d538 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -1,3 +1,5 @@ +const { checkForMigrations } = require("../utils/database"); + const WorkspaceChats = { tablename: "workspace_chats", colsInit: ` @@ -9,7 +11,17 @@ const WorkspaceChats = { createdAt TEXT DEFAULT CURRENT_TIMESTAMP, lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP `, - db: async function () { + migrateTable: async function () { + console.log( + `\x1b[34m[MIGRATING]\x1b[0m Checking for WorkspaceChats migrations` + ); + const db = await this.db(false); + await checkForMigrations(this, db); + }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { const sqlite3 = require("sqlite3").verbose(); const { open } = require("sqlite"); @@ -23,7 +35,8 @@ const WorkspaceChats = { await db.exec( `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` ); - db.on("trace", (sql) => console.log(sql)); + + if (tracing) db.on("trace", (sql) => console.log(sql)); return db; }, new: async function ({ workspaceId, prompt, response = {} }) { @@ -39,11 +52,16 @@ const WorkspaceChats = { .catch((error) => { return { id: null, success: false, message: error.message }; }); - if (!success) return { chat: null, message }; + if (!success) { + db.close(); + return { chat: null, message }; + } const chat = await db.get( `SELECT * FROM ${this.tablename} WHERE id = ${id}` ); + db.close(); + return { chat, message: null }; }, forWorkspace: async function (workspaceId = null) { @@ -61,6 +79,8 @@ const WorkspaceChats = { `UPDATE ${this.tablename} SET include = false WHERE workspaceId = ?`, [workspaceId] ); + db.close(); + return; }, get: async function (clause = "") { @@ -68,12 +88,16 @@ const WorkspaceChats = { const result = await db .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) .then((res) => res || null); + db.close(); + if (!result) return null; return result; }, delete: async function (clause = "") { const db = await this.db(); await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + db.close(); + return true; }, where: async function (clause = "", limit = null, order = null) { @@ -83,6 +107,8 @@ const WorkspaceChats = { !!limit ? `LIMIT ${limit}` : "" } ${!!order ? order : ""}` ); + db.close(); + return results; }, }; diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 7459e37e26798e3cbc339fecca18b4d176e4bba2..9be40b695a3103d1c3ebe231512524b71861b310 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -87,7 +87,7 @@ async function chatWithWorkspace(workspace, message, chatMode = "query") { if (!hasVectorizedSpace) { const rawHistory = await WorkspaceChats.forWorkspace(workspace.id); const chatHistory = convertToPromptHistory(rawHistory); - const response = await openai.sendChat(chatHistory, message); + const response = await openai.sendChat(chatHistory, message, workspace); const data = { text: response, sources: [], type: "chat" }; await WorkspaceChats.new({ @@ -108,7 +108,11 @@ async function chatWithWorkspace(workspace, message, chatMode = "query") { response, sources, message: error, - } = await VectorDb[chatMode]({ namespace: workspace.slug, input: message }); + } = await VectorDb[chatMode]({ + namespace: workspace.slug, + input: message, + workspace, + }); if (!response) { return { id: uuid, diff --git a/server/utils/database/index.js b/server/utils/database/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f240d63b1e4dd9cbe2bc7551bbcfb5c2df515978 --- /dev/null +++ b/server/utils/database/index.js @@ -0,0 +1,54 @@ +function checkColumnTemplate(tablename = null, column = null) { + if (!tablename || !column) + throw new Error(`Migration Error`, { tablename, column }); + return `SELECT COUNT(*) AS _exists FROM pragma_table_info('${tablename}') WHERE name='${column}'`; +} + +// Note (tcarambat): Since there is no good way to track migrations in Node/SQLite we use this simple system +// Each model has a `migrations` method that will return an array like... +// { colName: 'stringColName', execCmd: `SQL Command to run when`, doif: boolean }, +// colName = name of column +// execCmd = Command to run when doif matches the state of the DB +// doif = condition to match that determines if execCmd will run. +// eg: Table workspace has slug column. +// execCmd: ALTER TABLE DROP COLUMN slug; +// doif: true +// => Will drop the slug column if the workspace table has a column named 'slug' otherwise nothing happens. +// If you are adding a new table column if needs to exist in the Models `colsInit` and as a migration. +// So both new and existing DBs will get the column when code is pulled in. + +async function checkForMigrations(model, db) { + if (model.migrations().length === 0) return; + const toMigrate = []; + for (const { colName, execCmd, doif } of model.migrations()) { + const { _exists } = await db.get( + checkColumnTemplate(model.tablename, colName) + ); + const colExists = _exists !== 0; + if (colExists !== doif) continue; + + toMigrate.push(execCmd); + } + + if (toMigrate.length === 0) return; + + console.log(`Running ${toMigrate.length} migrations`, toMigrate); + await db.exec(toMigrate.join(";\n")); + return; +} + +async function validateTablePragmas() { + const { Workspace } = require("../../models/workspace"); + const { Document } = require("../../models/documents"); + const { DocumentVectors } = require("../../models/vectors"); + const { WorkspaceChats } = require("../../models/workspaceChats"); + await Workspace.migrateTable(); + await Document.migrateTable(); + await DocumentVectors.migrateTable(); + await WorkspaceChats.migrateTable(); +} + +module.exports = { + checkForMigrations, + validateTablePragmas, +}; diff --git a/server/utils/openAi/index.js b/server/utils/openAi/index.js index 72742fb7434eca663c6ee885bbf847913fec160e..00ec1326b7a5c479e727eb79af635c1f863406eb 100644 --- a/server/utils/openAi/index.js +++ b/server/utils/openAi/index.js @@ -40,7 +40,7 @@ class OpenAi { return { safe: false, reasons }; } - async sendChat(chatHistory = [], prompt) { + async sendChat(chatHistory = [], prompt, workspace = {}) { const model = process.env.OPEN_MODEL_PREF; if (!this.isValidChatModel(model)) throw new Error( @@ -50,7 +50,7 @@ class OpenAi { const textResponse = await this.openai .createChatCompletion({ model, - temperature: 0.7, + temperature: Number(workspace?.openAiTemp ?? 0.7), n: 1, messages: [ { role: "system", content: "" }, diff --git a/server/utils/vectorDbProviders/chroma/index.js b/server/utils/vectorDbProviders/chroma/index.js index fd08c1e351a1989b83074173ba16a37078121caa..bd1c6058ee599c0e4d8a4234efea2b0f54e1a4ff 100644 --- a/server/utils/vectorDbProviders/chroma/index.js +++ b/server/utils/vectorDbProviders/chroma/index.js @@ -56,12 +56,12 @@ const Chroma = { const openai = new OpenAIApi(config); return openai; }, - llm: function () { + llm: function ({ temperature = 0.7 }) { const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; return new OpenAI({ openAIApiKey: process.env.OPEN_AI_KEY, - temperature: 0.7, modelName: model, + temperature, }); }, embedChunk: async function (openai, textChunk) { @@ -253,7 +253,7 @@ const Chroma = { return true; }, query: async function (reqBody = {}) { - const { namespace = null, input } = reqBody; + const { namespace = null, input, workspace = {} } = reqBody; if (!namespace || !input) throw new Error("Invalid request body"); const { client } = await this.connect(); @@ -269,7 +269,10 @@ const Chroma = { this.embedder(), { collectionName: namespace, url: process.env.CHROMA_ENDPOINT } ); - const model = this.llm(); + const model = this.llm({ + temperature: workspace?.openAiTemp, + }); + const chain = VectorDBQAChain.fromLLM(model, vectorStore, { k: 5, returnSourceDocuments: true, diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js index f4cc189853f093d0bc5c230bb6fa3fd019b74c57..d6aced156f3da0513c9a27752062e24e692df3a5 100644 --- a/server/utils/vectorDbProviders/lance/index.js +++ b/server/utils/vectorDbProviders/lance/index.js @@ -69,11 +69,16 @@ const LanceDb = { ? data[0].embedding : null; }, - getChatCompletion: async function (openai, messages = []) { + getChatCompletion: async function ( + openai, + messages = [], + { temperature = 0.7 } + ) { const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; const { data } = await openai.createChatCompletion({ model, messages, + temperature, }); if (!data.hasOwnProperty("choices")) return null; @@ -213,7 +218,7 @@ const LanceDb = { } }, query: async function (reqBody = {}) { - const { namespace = null, input } = reqBody; + const { namespace = null, input, workspace = {} } = reqBody; if (!namespace || !input) throw new Error("Invalid request body"); const { client } = await this.connect(); @@ -242,7 +247,9 @@ const LanceDb = { }, { role: "user", content: input }, ]; - const responseText = await this.getChatCompletion(this.openai(), messages); + const responseText = await this.getChatCompletion(this.openai(), messages, { + temperature: workspace?.openAiTemp, + }); return { response: responseText, diff --git a/server/utils/vectorDbProviders/pinecone/index.js b/server/utils/vectorDbProviders/pinecone/index.js index 9167b790ecef6933e8ce440b5d64bc33d421de8e..2dcf2b526bb75b8747258283e7ba201a7ab7d1d5 100644 --- a/server/utils/vectorDbProviders/pinecone/index.js +++ b/server/utils/vectorDbProviders/pinecone/index.js @@ -1,7 +1,6 @@ const { PineconeClient } = require("@pinecone-database/pinecone"); const { PineconeStore } = require("langchain/vectorstores/pinecone"); const { OpenAI } = require("langchain/llms/openai"); -const { ChatOpenAI } = require("langchain/chat_models/openai"); const { VectorDBQAChain, LLMChain } = require("langchain/chains"); const { OpenAIEmbeddings } = require("langchain/embeddings/openai"); const { VectorStoreRetrieverMemory } = require("langchain/memory"); @@ -50,20 +49,12 @@ const Pinecone = { ? data[0].embedding : null; }, - llm: function () { + llm: function ({ temperature = 0.7 }) { const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; return new OpenAI({ openAIApiKey: process.env.OPEN_AI_KEY, - temperature: 0.7, - modelName: model, - }); - }, - chatLLM: function () { - const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; - return new ChatOpenAI({ - openAIApiKey: process.env.OPEN_AI_KEY, - temperature: 0.7, modelName: model, + temperature, }); }, totalIndicies: async function () { @@ -233,7 +224,7 @@ const Pinecone = { }; }, query: async function (reqBody = {}) { - const { namespace = null, input } = reqBody; + const { namespace = null, input, workspace = {} } = reqBody; if (!namespace || !input) throw new Error("Invalid request body"); const { pineconeIndex } = await this.connect(); @@ -250,7 +241,9 @@ const Pinecone = { namespace, }); - const model = this.llm(); + const model = this.llm({ + temperature: workspace?.openAiTemp, + }); const chain = VectorDBQAChain.fromLLM(model, vectorStore, { k: 5, returnSourceDocuments: true, @@ -265,7 +258,7 @@ const Pinecone = { // This implementation of chat also expands the memory of the chat itself // and adds more tokens to the PineconeDB instance namespace chat: async function (reqBody = {}) { - const { namespace = null, input } = reqBody; + const { namespace = null, input, workspace = {} } = reqBody; if (!namespace || !input) throw new Error("Invalid request body"); const { pineconeIndex } = await this.connect(); @@ -284,7 +277,9 @@ const Pinecone = { memoryKey: "history", }); - const model = this.llm(); + const model = this.llm({ + temperature: workspace?.openAiTemp, + }); const prompt = PromptTemplate.fromTemplate(`The following is a friendly conversation between a human and an AI. The AI is very casual and talkative and responds with a friendly tone. If the AI does not know the answer to a question, it truthfully says it does not know. Relevant pieces of previous conversation: