diff --git a/frontend/package.json b/frontend/package.json
index 8aa4dcfa..84c27166 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,6 +19,7 @@
"file-saver": "^2.0.5",
"he": "^1.2.0",
"highlight.js": "^11.9.0",
+ "js-levenshtein": "^1.1.6",
"lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1",
"pluralize": "^8.0.0",
@@ -63,4 +64,4 @@
"tailwindcss": "^3.3.1",
"vite": "^4.3.0"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/NewFolderModal/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/NewFolderModal/index.jsx
new file mode 100644
index 00000000..47ad85b0
--- /dev/null
+++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/NewFolderModal/index.jsx
@@ -0,0 +1,90 @@
+import React, { useState } from "react";
+import { X } from "@phosphor-icons/react";
+import Document from "@/models/document";
+
+export default function NewFolderModal({ closeModal, files, setFiles }) {
+ const [error, setError] = useState(null);
+ const [folderName, setFolderName] = useState("");
+
+ const handleCreate = async (e) => {
+ e.preventDefault();
+ setError(null);
+ if (folderName.trim() !== "") {
+ const newFolder = {
+ name: folderName,
+ type: "folder",
+ items: [],
+ };
+ const { success } = await Document.createFolder(folderName);
+ if (success) {
+ setFiles({
+ ...files,
+ items: [...files.items, newFolder],
+ });
+ closeModal();
+ } else {
+ setError("Failed to create folder");
+ }
+ }
+ };
+
+ return (
+
+
+
+
+ Create New Folder
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx
index d479a6cc..c7794a3f 100644
--- a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx
+++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx
@@ -3,11 +3,16 @@ import PreLoader from "@/components/Preloader";
import { memo, useEffect, useState } from "react";
import FolderRow from "./FolderRow";
import System from "@/models/system";
-import { Plus, Trash } from "@phosphor-icons/react";
+import { MagnifyingGlass, Plus, Trash } from "@phosphor-icons/react";
import Document from "@/models/document";
import showToast from "@/utils/toast";
import FolderSelectionPopup from "./FolderSelectionPopup";
import MoveToFolderIcon from "./MoveToFolderIcon";
+import { useModal } from "@/hooks/useModal";
+import ModalWrapper from "@/components/ModalWrapper";
+import NewFolderModal from "./NewFolderModal";
+import debounce from "lodash.debounce";
+import { filterFileSearchResults } from "./utils";
function Directory({
files,
@@ -24,9 +29,13 @@ function Directory({
loadingMessage,
}) {
const [amountSelected, setAmountSelected] = useState(0);
- const [newFolderName, setNewFolderName] = useState("");
- const [showNewFolderInput, setShowNewFolderInput] = useState(false);
const [showFolderSelection, setShowFolderSelection] = useState(false);
+ const [searchTerm, setSearchTerm] = useState("");
+ const {
+ isOpen: isFolderModalOpen,
+ openModal: openFolderModal,
+ closeModal: closeFolderModal,
+ } = useModal();
useEffect(() => {
setAmountSelected(Object.keys(selectedItems).length);
@@ -121,32 +130,6 @@ function Directory({
return !!selectedItems[id];
};
- const createNewFolder = () => {
- setShowNewFolderInput(true);
- };
-
- const confirmNewFolder = async () => {
- if (newFolderName.trim() !== "") {
- const newFolder = {
- name: newFolderName,
- type: "folder",
- items: [],
- };
-
- // If folder failed to create - silently fail.
- const { success } = await Document.createFolder(newFolderName);
- if (success) {
- setFiles({
- ...files,
- items: [...files.items, newFolder],
- });
- }
-
- setNewFolderName("");
- setShowNewFolderInput(false);
- }
- };
-
const moveToFolder = async (folder) => {
const toMove = [];
for (const itemId of Object.keys(selectedItems)) {
@@ -183,40 +166,39 @@ function Directory({
setLoading(false);
};
+ const handleSearch = debounce((e) => {
+ const searchValue = e.target.value;
+ setSearchTerm(searchValue);
+ }, 500);
+
+ const filteredFiles = filterFileSearchResults(files, searchTerm);
return (
My Documents
- {showNewFolderInput ? (
-
-
setNewFolderName(e.target.value)}
- className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[150px] h-[32px]"
- />
-
-
-
+
+
+
+
+
@@ -234,8 +216,8 @@ function Directory({
{loadingMessage}
- ) : files.items ? (
- files.items.map(
+ ) : filteredFiles.length > 0 ? (
+ filteredFiles.map(
(item, index) =>
item.type === "folder" && (
)}
+
+
+
+
+
);
}
diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/utils.js b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/utils.js
new file mode 100644
index 00000000..1bea2615
--- /dev/null
+++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/utils.js
@@ -0,0 +1,49 @@
+import strDistance from "js-levenshtein";
+
+const LEVENSHTEIN_MIN = 8;
+
+// Regular expression pattern to match the v4 UUID and the ending .json
+const uuidPattern =
+ /-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
+const jsonPattern = /\.json$/;
+
+// Function to strip UUID v4 and JSON from file names as that will impact search results.
+const stripUuidAndJsonFromString = (input = "") => {
+ return input
+ ?.replace(uuidPattern, "") // remove v4 uuid
+ ?.replace(jsonPattern, "") // remove trailing .json
+ ?.replace("-", " "); // turn slugged names into spaces
+};
+
+export function filterFileSearchResults(files = [], searchTerm = "") {
+ if (!searchTerm) return files?.items || [];
+
+ const searchResult = [];
+ for (const folder of files?.items) {
+ // If folder is a good match then add all its children
+ if (strDistance(folder.name, searchTerm) <= LEVENSHTEIN_MIN) {
+ searchResult.push(folder);
+ continue;
+ }
+
+ // Otherwise check children for good results
+ const fileSearchResults = [];
+ for (const file of folder?.items) {
+ if (
+ strDistance(stripUuidAndJsonFromString(file.name), searchTerm) <=
+ LEVENSHTEIN_MIN
+ ) {
+ fileSearchResults.push(file);
+ }
+ }
+
+ if (fileSearchResults.length > 0) {
+ searchResult.push({
+ ...folder,
+ items: fileSearchResults,
+ });
+ }
+ }
+
+ return searchResult;
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 35159b3f..f3fa95ea 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -742,3 +742,7 @@ does not extend the close button beyond the viewport. */
opacity: 0;
}
}
+
+.search-input::-webkit-search-cancel-button {
+ filter: grayscale(100%) invert(1) brightness(100) opacity(0.5);
+}
diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx
index 7a58da45..b2ff33bf 100644
--- a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx
+++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx
@@ -9,7 +9,7 @@ export const DB_LOGOS = {
"sql-server": MSSQLLogo,
};
-export default function DBConnection({ connection, onRemove }) {
+export default function DBConnection({ connection, onRemove, setHasChanges }) {
const { database_id, engine } = connection;
function removeConfirmation() {
if (
@@ -20,6 +20,7 @@ export default function DBConnection({ connection, onRemove }) {
return false;
}
onRemove(database_id);
+ setHasChanges(true);
}
return (
diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx
index 9feb4b8b..848d44ed 100644
--- a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx
@@ -9,6 +9,7 @@ export default function AgentSQLConnectorSelection({
settings,
toggleSkill,
enabled = false,
+ setHasChanges,
}) {
const { isOpen, openModal, closeModal } = useModal();
const [connections, setConnections] = useState(
@@ -72,6 +73,7 @@ export default function AgentSQLConnectorSelection({
})
);
}}
+ setHasChanges={setHasChanges}
/>
))}
{hasChanges && (
@@ -211,6 +217,7 @@ function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
settings={settings}
toggleSkill={toggleAgentSkill}
enabled={skills.includes("sql-agent")}
+ setHasChanges={setHasChanges}
/>
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 93bdc088..d5bdc0d6 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2260,6 +2260,11 @@ jiti@^1.19.1:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
+js-levenshtein@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+ integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js
index c8e5e06c..7fdb92cc 100644
--- a/server/endpoints/api/system/index.js
+++ b/server/endpoints/api/system/index.js
@@ -206,6 +206,72 @@ function apiSystemEndpoints(app) {
}
}
);
+ app.delete(
+ "/v1/system/remove-documents",
+ [validApiKey],
+ async (request, response) => {
+ /*
+ #swagger.tags = ['System Settings']
+ #swagger.description = 'Permanently remove documents from the system.'
+ #swagger.requestBody = {
+ description: 'Array of document names to be removed permanently.',
+ required: true,
+ content: {
+ "application/json": {
+ schema: {
+ type: 'object',
+ properties: {
+ names: {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ example: [
+ "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ #swagger.responses[200] = {
+ description: 'Documents removed successfully.',
+ content: {
+ "application/json": {
+ schema: {
+ type: 'object',
+ example: {
+ success: true,
+ message: 'Documents removed successfully'
+ }
+ }
+ }
+ }
+ }
+ #swagger.responses[403] = {
+ description: 'Forbidden',
+ schema: {
+ "$ref": "#/definitions/InvalidAPIKey"
+ }
+ }
+ #swagger.responses[500] = {
+ description: 'Internal Server Error'
+ }
+ */
+ try {
+ const { names } = reqBody(request);
+ for await (const name of names) await purgeDocument(name);
+ response
+ .status(200)
+ .json({ success: true, message: "Documents removed successfully" })
+ .end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
}
module.exports = { apiSystemEndpoints };
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index ed6f1533..230f0ce6 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -2241,6 +2241,71 @@
}
}
}
+ },
+ "/v1/system/remove-documents": {
+ "delete": {
+ "tags": [
+ "System Settings"
+ ],
+ "description": "Permanently remove documents from the system.",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "Documents removed successfully.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "example": {
+ "success": true,
+ "message": "Documents removed successfully"
+ }
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidAPIKey"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/InvalidAPIKey"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error"
+ }
+ },
+ "requestBody": {
+ "description": "Array of document names to be removed permanently.",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
},
"components": {