diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83792da78..58c42b62d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,7 @@ // Terraform support "ghcr.io/devcontainers/features/terraform:1": {}, // Just a wrap to install needed packages - "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "ghcr.io/devcontainers-contrib/features/apt-packages:1": { // Dependencies copied from ../docker/Dockerfile plus some dev stuff "packages": [ "build-essential", diff --git a/.prettierignore b/.prettierignore index faedf3258..e3b0c14e0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,7 @@ frontend/bundleinspector.html #server server/swagger/openapi.json + +#embed +**/static/** +embed/src/utils/chat/hljs.js diff --git a/.prettierrc b/.prettierrc index 3574c1dfd..5e2bccfe4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,7 +17,7 @@ } }, { - "files": "*.config.js", + "files": ["*.config.js"], "options": { "semi": false, "parser": "flow", diff --git a/README.md b/README.md index dfedb4f95..bc3e9fdd8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

- English · 简体中文 + English · 简体中文 · 日本語

@@ -123,7 +123,7 @@ Some cool features of AnythingLLM - [Pinecone](https://pinecone.io) - [Chroma](https://trychroma.com) - [Weaviate](https://weaviate.io) -- [QDrant](https://qdrant.tech) +- [Qdrant](https://qdrant.tech) - [Milvus](https://milvus.io) - [Zilliz](https://zilliz.com) diff --git a/collector/package.json b/collector/package.json index 785604e38..938d65e15 100644 --- a/collector/package.json +++ b/collector/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "NODE_ENV=development nodemon --ignore hotdir --ignore storage --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./processSingleFile ./processLink ./utils index.js" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./processSingleFile ./processLink ./utils index.js" }, "dependencies": { "@googleapis/youtube": "^9.0.0", diff --git a/collector/utils/extensions/GithubRepo/RepoLoader/index.js b/collector/utils/extensions/GithubRepo/RepoLoader/index.js index dbe26fa29..c842f621b 100644 --- a/collector/utils/extensions/GithubRepo/RepoLoader/index.js +++ b/collector/utils/extensions/GithubRepo/RepoLoader/index.js @@ -14,7 +14,11 @@ class RepoLoader { #validGithubUrl() { const UrlPattern = require("url-pattern"); const pattern = new UrlPattern( - "https\\://github.com/(:author)/(:project(*))" + "https\\://github.com/(:author)/(:project(*))", + { + // fixes project names with special characters (.github) + segmentValueCharset: "a-zA-Z0-9-._~%/+", + } ); const match = pattern.match(this.repo); if (!match) return false; diff --git a/collector/utils/files/mime.js b/collector/utils/files/mime.js index 635a6aa32..6cd88f82e 100644 --- a/collector/utils/files/mime.js +++ b/collector/utils/files/mime.js @@ -23,6 +23,7 @@ class MimeDetector { { "text/plain": [ "ts", + "tsx", "py", "opts", "lock", @@ -35,6 +36,7 @@ class MimeDetector { "js", "lua", "pas", + "r", ], }, true diff --git a/docker/.env.example b/docker/.env.example index 23789af45..a38b4c5a2 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -128,6 +128,12 @@ GID='1000' # VOYAGEAI_API_KEY= # EMBEDDING_MODEL_PREF='voyage-large-2-instruct' +# EMBEDDING_ENGINE='litellm' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' +# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 +# LITE_LLM_BASE_PATH='http://127.0.0.1:4000' +# LITE_LLM_API_KEY='sk-123abc' + ########################################### ######## Vector Database Selection ######## ########################################### @@ -232,4 +238,10 @@ GID='1000' # AGENT_GSE_CTX= #------ Serper.dev ----------- https://serper.dev/ -# AGENT_SERPER_DEV_KEY= \ No newline at end of file +# AGENT_SERPER_DEV_KEY= + +#------ Bing Search ----------- https://portal.azure.com/ +# AGENT_BING_SEARCH_API_KEY= + +#------ Serply.io ----------- https://serply.io/ +# AGENT_SERPLY_API_KEY= diff --git a/docker/HOW_TO_USE_DOCKER.md b/docker/HOW_TO_USE_DOCKER.md index 19a0920ef..f570dce90 100644 --- a/docker/HOW_TO_USE_DOCKER.md +++ b/docker/HOW_TO_USE_DOCKER.md @@ -86,6 +86,49 @@ mintplexlabs/anythingllm; + + Docker Compose + + version: '3.8' + services: + anythingllm: + image: mintplexlabs/anythingllm + container_name: anythingllm + ports: + - "3001:3001" + cap_add: + - SYS_ADMIN + environment: + # Adjust for your environemnt + - STORAGE_DIR=/app/server/storage + - JWT_SECRET="make this a large list of random numbers and letters 20+" + - LLM_PROVIDER=ollama + - OLLAMA_BASE_PATH=http://127.0.0.1:11434 + - OLLAMA_MODEL_PREF=llama2 + - OLLAMA_MODEL_TOKEN_LIMIT=4096 + - EMBEDDING_ENGINE=ollama + - EMBEDDING_BASE_PATH=http://127.0.0.1:11434 + - EMBEDDING_MODEL_PREF=nomic-embed-text:latest + - EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192 + - VECTOR_DB=lancedb + - WHISPER_PROVIDER=local + - TTS_PROVIDER=native + - PASSWORDMINCHAR=8 + - AGENT_SERPER_DEV_KEY="SERPER DEV API KEY" + - AGENT_SERPLY_API_KEY="Serply.io API KEY" + volumes: + - anythingllm_storage:/app/server/storage + restart: always + + volumes: + anythingllm_storage: + driver: local + driver_opts: + type: none + o: bind + device: /path/on/local/disk + + Go to `http://localhost:3001` and you are now using AnythingLLM! All your data and progress will persist between diff --git a/embed/.prettierignore b/embed/.prettierignore deleted file mode 100644 index d90a3c089..000000000 --- a/embed/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -# defaults -**/.git -**/.svn -**/.hg -**/node_modules - -**/dist -**/static/** -src/utils/chat/hljs.js diff --git a/embed/jsconfig.json b/embed/jsconfig.json index c8cc81fdb..20cd368c0 100644 --- a/embed/jsconfig.json +++ b/embed/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], - } - } -} \ No newline at end of file + "@/*": ["./src/*"], + }, + }, +} diff --git a/embed/package.json b/embed/package.json index eb3999303..712af8e6c 100644 --- a/embed/package.json +++ b/embed/package.json @@ -1,6 +1,7 @@ { "name": "anythingllm-embedded-chat", "private": false, + "license": "MIT", "type": "module", "scripts": { "dev": "nodemon -e js,jsx,css --watch src --exec \"yarn run dev:preview\"", @@ -8,7 +9,7 @@ "dev:build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js", "build": "vite build && cat src/static/tailwind@3.4.1.js >> dist/anythingllm-chat-widget.js && npx terser --compress -o dist/anythingllm-chat-widget.min.js -- dist/anythingllm-chat-widget.js", "build:publish": "yarn build && mkdir -p ../frontend/public/embed && cp -r dist/anythingllm-chat-widget.min.js ../frontend/public/embed/anythingllm-chat-widget.min.js", - "lint": "yarn prettier --write ./src" + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src" }, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", diff --git a/embed/vite.config.js b/embed/vite.config.js index 215064221..9e23c70d2 100644 --- a/embed/vite.config.js +++ b/embed/vite.config.js @@ -38,7 +38,7 @@ export default defineConfig({ rollupOptions: { external: [ // Reduces transformation time by 50% and we don't even use this variant, so we can ignore. - /@phosphor-icons\/react\/dist\/ssr/, + /@phosphor-icons\/react\/dist\/ssr/ ] }, commonjsOptions: { @@ -51,7 +51,7 @@ export default defineConfig({ emptyOutDir: true, inlineDynamicImports: true, assetsDir: "", - sourcemap: 'inline', + sourcemap: "inline" }, optimizeDeps: { esbuildOptions: { @@ -60,5 +60,5 @@ export default defineConfig({ }, plugins: [] } - }, + } }) diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index c8cc81fdb..e21fc3764 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -4,9 +4,7 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": [ - "./src/*" - ], + "@/*": ["./src/*"] } } -} \ No newline at end of file +} diff --git a/frontend/package.json b/frontend/package.json index 11e612fcd..84c27166a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "start": "vite --open", "dev": "NODE_ENV=development vite --debug --host=0.0.0.0", "build": "vite build", - "lint": "yarn prettier --write ./src", + "lint": "yarn prettier --ignore-path ../.prettierignore --write ./src", "preview": "vite preview" }, "dependencies": { @@ -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", diff --git a/frontend/src/LogoContext.jsx b/frontend/src/LogoContext.jsx index 6818967b8..014c6a6be 100644 --- a/frontend/src/LogoContext.jsx +++ b/frontend/src/LogoContext.jsx @@ -1,27 +1,41 @@ import { createContext, useEffect, useState } from "react"; import AnythingLLM from "./media/logo/anything-llm.png"; +import DefaultLoginLogo from "./media/illustrations/login-logo.svg"; import System from "./models/system"; export const LogoContext = createContext(); export function LogoProvider({ children }) { const [logo, setLogo] = useState(""); + const [loginLogo, setLoginLogo] = useState(""); + const [isCustomLogo, setIsCustomLogo] = useState(false); useEffect(() => { async function fetchInstanceLogo() { try { - const logoURL = await System.fetchLogo(); - logoURL ? setLogo(logoURL) : setLogo(AnythingLLM); + const { isCustomLogo, logoURL } = await System.fetchLogo(); + if (logoURL) { + setLogo(logoURL); + setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo); + setIsCustomLogo(isCustomLogo); + } else { + setLogo(AnythingLLM); + setLoginLogo(DefaultLoginLogo); + setIsCustomLogo(false); + } } catch (err) { setLogo(AnythingLLM); + setLoginLogo(DefaultLoginLogo); + setIsCustomLogo(false); console.error("Failed to fetch logo:", err); } } + fetchInstanceLogo(); }, []); return ( - + {children} ); diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx index 8d3118838..c5a1f1907 100644 --- a/frontend/src/components/ChatBubble/index.jsx +++ b/frontend/src/components/ChatBubble/index.jsx @@ -1,5 +1,5 @@ import React from "react"; -import Jazzicon from "../UserIcon"; +import UserIcon from "../UserIcon"; import { userFromStorage } from "@/utils/request"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -11,8 +11,7 @@ export default function ChatBubble({ message, type, popMsg }) {

- diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 43ae6e7a6..ae52a0d2b 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -13,7 +13,7 @@ import { isMobile } from "react-device-detect"; import { SidebarMobileHeader } from "../Sidebar"; import ChatBubble from "../ChatBubble"; import System from "@/models/system"; -import Jazzicon from "../UserIcon"; +import UserIcon from "../UserIcon"; import { userFromStorage } from "@/utils/request"; import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; import useUser from "@/hooks/useUser"; @@ -46,7 +46,7 @@ export default function DefaultChatContainer() { className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- +
- +
- @@ -151,7 +150,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- @@ -213,7 +211,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
- @@ -275,7 +272,7 @@ export default function DefaultChatContainer() { className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`} >
- +
+
+
+ + setBasePathValue(e.target.value)} + onBlur={() => setBasePath(basePathValue)} + /> +
+ +
+ + e.target.blur()} + defaultValue={settings?.EmbeddingModelMaxChunkLength} + required={false} + autoComplete="off" + /> +
+
+
+
+
+ +
+ setApiKeyValue(e.target.value)} + onBlur={() => setApiKey(apiKeyValue)} + /> +
+
+
+ ); +} + +function LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) { + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!basePath) { + setCustomModels([]); + setLoading(false); + return; + } + setLoading(true); + const { models } = await System.customModels( + "litellm", + typeof apiKey === "boolean" ? null : apiKey, + basePath + ); + setCustomModels(models || []); + setLoading(false); + } + findCustomModels(); + }, [basePath, apiKey]); + + if (loading || customModels.length == 0) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ + +
+ +
+ ); +} + +function EmbeddingModelTooltip() { + return ( +
+ + +

+ Be sure to select a valid embedding model. Chat models are not + embedding models. See{" "} + + this page + {" "} + for more information. +

+
+
+ ); +} diff --git a/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx index 87e058827..cc25ae958 100644 --- a/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx +++ b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx @@ -32,6 +32,7 @@ export default function GeminiLLMOptions({ settings }) { > {[ "gemini-pro", + "gemini-1.0-pro", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", ].map((model) => { diff --git a/frontend/src/components/ModalWrapper/index.jsx b/frontend/src/components/ModalWrapper/index.jsx index 37041f634..bd3995ce4 100644 --- a/frontend/src/components/ModalWrapper/index.jsx +++ b/frontend/src/components/ModalWrapper/index.jsx @@ -1,9 +1,12 @@ +import { createPortal } from "react-dom"; + export default function ModalWrapper({ children, isOpen }) { if (!isOpen) return null; - return ( + return createPortal(
{children} -
+
, + document.getElementById("root") ); } diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/ConnectorOption/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/ConnectorOption/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/ConnectorOption/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/ConnectorOption/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Confluence/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Confluence/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Confluence/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Github/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Github/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Github/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/WebsiteDepth/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/WebsiteDepth/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/WebsiteDepth/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/WebsiteDepth/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Youtube/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Youtube/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/Connectors/Youtube/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Youtube/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx b/frontend/src/components/Modals/ManageWorkspace/DataConnectors/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/DataConnectors/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/DataConnectors/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderRow/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderRow/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/MoveToFolderIcon.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/Directory/MoveToFolderIcon.jsx 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 000000000..47ad85b05 --- /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 +

+ +
+
+
+
+
+ + setFolderName(e.target.value)} + /> +
+ {error &&

Error: {error}

} +
+
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx similarity index 81% rename from frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx index d479a6cce..10d3ca87b 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx @@ -3,11 +3,15 @@ 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 NewFolderModal from "./NewFolderModal"; +import debounce from "lodash.debounce"; +import { filterFileSearchResults } from "./utils"; function Directory({ files, @@ -24,9 +28,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 +129,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 +165,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 +215,8 @@ function Directory({ {loadingMessage}

- ) : files.items ? ( - files.items.map( + ) : filteredFiles.length > 0 ? ( + filteredFiles.map( (item, index) => item.type === "folder" && ( )}
+
+ {isFolderModalOpen && ( +
+ +
+ )}
); } 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 000000000..1bea2615a --- /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/components/Modals/MangeWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/Documents/index.jsx diff --git a/frontend/src/components/Modals/MangeWorkspace/index.jsx b/frontend/src/components/Modals/ManageWorkspace/index.jsx similarity index 100% rename from frontend/src/components/Modals/MangeWorkspace/index.jsx rename to frontend/src/components/Modals/ManageWorkspace/index.jsx diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx index e4de5e67e..04625b950 100644 --- a/frontend/src/components/Modals/Password/MultiUserAuth.jsx +++ b/frontend/src/components/Modals/Password/MultiUserAuth.jsx @@ -168,6 +168,7 @@ export default function MultiUserAuth() { const [token, setToken] = useState(null); const [showRecoveryForm, setShowRecoveryForm] = useState(false); const [showResetPasswordForm, setShowResetPasswordForm] = useState(false); + const [customAppName, setCustomAppName] = useState(null); const { isOpen: isRecoveryCodeModalOpen, @@ -250,6 +251,15 @@ export default function MultiUserAuth() { } }, [downloadComplete, user, token]); + useEffect(() => { + const fetchCustomAppName = async () => { + const { appName } = await System.fetchCustomAppName(); + setCustomAppName(appName || ""); + setLoading(false); + }; + fetchCustomAppName(); + }, []); + if (showRecoveryForm) { return (

- AnythingLLM + {customAppName || "AnythingLLM"}

- Sign in to your AnythingLLM account. + Sign in to your {customAppName || "AnythingLLM"} account.

diff --git a/frontend/src/components/Modals/Password/SingleUserAuth.jsx b/frontend/src/components/Modals/Password/SingleUserAuth.jsx index c1f328ba2..541d2db52 100644 --- a/frontend/src/components/Modals/Password/SingleUserAuth.jsx +++ b/frontend/src/components/Modals/Password/SingleUserAuth.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import System from "../../../models/system"; import { AUTH_TOKEN } from "../../../utils/constants"; -import useLogo from "../../../hooks/useLogo"; import paths from "../../../utils/paths"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; @@ -10,10 +9,10 @@ import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; export default function SingleUserAuth() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const { logo: _initLogo } = useLogo(); const [recoveryCodes, setRecoveryCodes] = useState([]); const [downloadComplete, setDownloadComplete] = useState(false); const [token, setToken] = useState(null); + const [customAppName, setCustomAppName] = useState(null); const { isOpen: isRecoveryCodeModalOpen, @@ -57,6 +56,15 @@ export default function SingleUserAuth() { } }, [downloadComplete, token]); + useEffect(() => { + const fetchCustomAppName = async () => { + const { appName } = await System.fetchCustomAppName(); + setCustomAppName(appName || ""); + setLoading(false); + }; + fetchCustomAppName(); + }, []); + return ( <>
@@ -68,11 +76,11 @@ export default function SingleUserAuth() { Welcome to

- AnythingLLM + {customAppName || "AnythingLLM"}

- Sign in to your AnythingLLM instance. + Sign in to your {customAppName || "AnythingLLM"} instance.

diff --git a/frontend/src/components/Modals/Password/index.jsx b/frontend/src/components/Modals/Password/index.jsx index 9305d032e..7010fc660 100644 --- a/frontend/src/components/Modals/Password/index.jsx +++ b/frontend/src/components/Modals/Password/index.jsx @@ -9,10 +9,9 @@ import { } from "../../../utils/constants"; import useLogo from "../../../hooks/useLogo"; import illustration from "@/media/illustrations/login-illustration.svg"; -import loginLogo from "@/media/illustrations/login-logo.svg"; export default function PasswordModal({ mode = "single" }) { - const { logo: _initLogo } = useLogo(); + const { loginLogo } = useLogo(); return (
-
+
logo {mode === "single" ? : }
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 2d59d0ff9..41fa60ea4 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -85,7 +85,7 @@ export default function SettingsSidebar() { />
{/* Header Information */} @@ -109,12 +109,14 @@ export default function SettingsSidebar() {
{/* Primary Body */} -
+
-
+
+
+
@@ -139,22 +141,21 @@ export default function SettingsSidebar() {
Instance Settings
-
+
-
+
-
-
-
+
+
+
diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx index 87fd55587..c4062102e 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx @@ -27,7 +27,6 @@ export default function ThreadItem({ const { slug } = useParams(); const optionsContainer = useRef(null); const [showOptions, setShowOptions] = useState(false); - const [name, setName] = useState(thread.name); const linkTo = !thread.slug ? paths.workspace.chat(slug) : paths.workspace.thread(slug, thread.slug); @@ -97,7 +96,7 @@ export default function ThreadItem({ isActive ? "font-medium text-white" : "text-slate-400" }`} > - {truncate(name, 25)} + {truncate(thread.name, 25)}

)} @@ -133,7 +132,6 @@ export default function ThreadItem({ workspace={workspace} thread={thread} onRemove={onRemove} - onRename={setName} close={() => setShowOptions(false)} /> )} @@ -144,14 +142,7 @@ export default function ThreadItem({ ); } -function OptionsMenu({ - containerRef, - workspace, - thread, - onRename, - onRemove, - close, -}) { +function OptionsMenu({ containerRef, workspace, thread, onRemove, close }) { const menuRef = useRef(null); // Ref menu options @@ -208,7 +199,7 @@ function OptionsMenu({ return; } - onRename(name); + thread.name = name; close(); }; diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index f3c0ac2a1..63cbef7b8 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -5,6 +5,7 @@ import { Plus, CircleNotch, Trash } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; import ThreadItem from "./ThreadItem"; import { useParams } from "react-router-dom"; +export const THREAD_RENAME_EVENT = "renameThread"; export default function ThreadContainer({ workspace }) { const { threadSlug = null } = useParams(); @@ -12,6 +13,26 @@ export default function ThreadContainer({ workspace }) { const [loading, setLoading] = useState(true); const [ctrlPressed, setCtrlPressed] = useState(false); + useEffect(() => { + const chatHandler = (event) => { + const { threadSlug, newName } = event.detail; + setThreads((prevThreads) => + prevThreads.map((thread) => { + if (thread.slug === threadSlug) { + return { ...thread, name: newName }; + } + return thread; + }) + ); + }; + + window.addEventListener(THREAD_RENAME_EVENT, chatHandler); + + return () => { + window.removeEventListener(THREAD_RENAME_EVENT, chatHandler); + }; + }, []); + useEffect(() => { async function fetchThreads() { if (!workspace.slug) return; @@ -22,11 +43,17 @@ export default function ThreadContainer({ workspace }) { fetchThreads(); }, [workspace.slug]); - // Enable toggling of meta-key (ctrl on win and cmd/fn on others) + // Enable toggling of bulk-deletion by holding meta-key (ctrl on win and cmd/fn on others) useEffect(() => { const handleKeyDown = (event) => { if (["Control", "Meta"].includes(event.key)) { - setCtrlPressed((prev) => !prev); + setCtrlPressed(true); + } + }; + + const handleKeyUp = (event) => { + if (["Control", "Meta"].includes(event.key)) { + setCtrlPressed(false); // when toggling, unset bulk progress so // previously marked threads that were never deleted // come back to life. @@ -37,9 +64,13 @@ export default function ThreadContainer({ workspace }) { ); } }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); }; }, []); @@ -56,7 +87,6 @@ export default function ThreadContainer({ workspace }) { const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug); await Workspace.threads.deleteBulk(workspace.slug, slugs); setThreads((prev) => prev.filter((t) => !t.deleted)); - setCtrlPressed(false); }; function removeThread(threadId) { @@ -89,6 +119,7 @@ export default function ThreadContainer({ workspace }) { ) ? threads.findIndex((thread) => thread?.slug === threadSlug) + 1 : 0; + return (
-
-
- {(!user || user?.role !== "default") && ( - - )} +
+
+
+ {(!user || user?.role !== "default") && ( + + )} +
+
-
-
+
@@ -156,12 +156,9 @@ export function SidebarMobileHeader() {
{/* Primary Body */} -
+
-
+
{(!user || user?.role !== "default") && (
-
+
diff --git a/frontend/src/components/UserIcon/index.jsx b/frontend/src/components/UserIcon/index.jsx index 6cc9b57d0..7fc6b8df6 100644 --- a/frontend/src/components/UserIcon/index.jsx +++ b/frontend/src/components/UserIcon/index.jsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from "react"; import JAZZ from "@metamask/jazzicon"; import usePfp from "../../hooks/usePfp"; -export default function Jazzicon({ size = 10, user, role }) { +export default function UserIcon({ size = 36, user, role }) { const { pfp } = usePfp(); const divRef = useRef(null); const seed = user?.uid diff --git a/frontend/src/components/UserIcon/workspace.png b/frontend/src/components/UserIcon/workspace.png new file mode 100644 index 000000000..537d583c5 Binary files /dev/null and b/frontend/src/components/UserIcon/workspace.png differ diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx new file mode 100644 index 000000000..f9346b26a --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx @@ -0,0 +1,126 @@ +import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; +import { Pencil } from "@phosphor-icons/react"; +import { useState, useEffect, useRef } from "react"; +import { Tooltip } from "react-tooltip"; +const EDIT_EVENT = "toggle-message-edit"; + +export function useEditMessage({ chatId, role }) { + const [isEditing, setIsEditing] = useState(false); + + function onEditEvent(e) { + if (e.detail.chatId !== chatId || e.detail.role !== role) { + setIsEditing(false); + return false; + } + setIsEditing((prev) => !prev); + } + + useEffect(() => { + function listenForEdits() { + if (!chatId || !role) return; + window.addEventListener(EDIT_EVENT, onEditEvent); + } + listenForEdits(); + return () => { + window.removeEventListener(EDIT_EVENT, onEditEvent); + }; + }, [chatId, role]); + + return { isEditing, setIsEditing }; +} + +export function EditMessageAction({ chatId = null, role, isEditing }) { + function handleEditClick() { + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + } + + if (!chatId || isEditing) return null; + return ( +
+ + +
+ ); +} + +export function EditMessageForm({ + role, + chatId, + message, + adjustTextArea, + saveChanges, +}) { + const formRef = useRef(null); + function handleSaveMessage(e) { + e.preventDefault(); + const form = new FormData(e.target); + const editedMessage = form.get("editedMessage"); + saveChanges({ editedMessage, chatId, role }); + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + } + + function cancelEdits() { + window.dispatchEvent( + new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) + ); + return false; + } + + useEffect(() => { + if (!formRef || !formRef.current) return; + formRef.current.focus(); + adjustTextArea({ target: formRef.current }); + }, [formRef]); + + return ( + +