diff --git a/collector/processSingleFile/convert/asPDF.js b/collector/processSingleFile/convert/asPDF.js index 560c4939f..e81fe6c76 100644 --- a/collector/processSingleFile/convert/asPDF.js +++ b/collector/processSingleFile/convert/asPDF.js @@ -40,9 +40,9 @@ async function asPDF({ fullFilePath = "", filename = "" }) { const data = { id: v4(), url: "file://" + fullFilePath, - title: docs[0]?.metadata?.pdf?.info?.Title || filename, + title: filename, docAuthor: docs[0]?.metadata?.pdf?.info?.Creator || "no author found", - description: "No description found.", + description: docs[0]?.metadata?.pdf?.info?.Title || "No description found.", docSource: "pdf file uploaded by the user.", chunkSource: "", published: createdDate(fullFilePath), diff --git a/docker/.env.example b/docker/.env.example index ed6fd3bce..5efb2c049 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -27,6 +27,7 @@ GID='1000' # LLM_PROVIDER='lmstudio' # LMSTUDIO_BASE_PATH='http://your-server:1234/v1' +# LMSTUDIO_MODEL_PREF='Loaded from Chat UI' # this is a bug in LMStudio 0.2.17 # LMSTUDIO_MODEL_TOKEN_LIMIT=4096 # LLM_PROVIDER='localai' diff --git a/docker/HOW_TO_USE_DOCKER.md b/docker/HOW_TO_USE_DOCKER.md index 9532fea0b..19a0920ef 100644 --- a/docker/HOW_TO_USE_DOCKER.md +++ b/docker/HOW_TO_USE_DOCKER.md @@ -109,29 +109,34 @@ container rebuilds or pulls from Docker Hub. Your docker host will show the image as online once the build process is completed. This will build the app to `http://localhost:3001`. -## ⚠️ Vector DB support ⚠️ +## Integrations and one-click setups -Out of the box, all vector databases are supported. Any vector databases requiring special configuration are listed below. +The integrations below are templates or tooling built by the community to make running the docker experience of AnythingLLM easier. -### Using local ChromaDB with Dockerized AnythingLLM +### Use the Midori AI Subsystem to Manage AnythingLLM -- Ensure in your `./docker/.env` file that you have +Follow the setup found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/) for your host OS +After setting that up install the AnythingLLM docker backend to the Midori AI Subsystem. -``` -#./docker/.env -...other configs - -VECTOR_DB="chroma" -CHROMA_ENDPOINT='http://host.docker.internal:8000' # Allow docker to look on host port, not container. -# CHROMA_API_HEADER="X-Api-Key" // If you have an Auth middleware on your instance. -# CHROMA_API_KEY="sk-123abc" - -...other configs - -``` +Once that is done, you are all set! ## Common questions and fixes +### Cannot connect to service running on localhost! + +If you are in docker and cannot connect to a service running on your host machine running on a local interface or loopback: + +- `localhost` +- `127.0.0.1` +- `0.0.0.0` + +> [!IMPORTANT] +> On linux `http://host.docker.internal:xxxx` does not work. +> Use `http://172.17.0.1:xxxx` instead to emulate this functionality. + +Then in docker you need to replace that localhost part with `host.docker.internal`. For example, if running Ollama on the host machine, bound to http://127.0.0.1:11434 you should put `http://host.docker.internal:11434` into the connection URL in AnythingLLM. + + ### API is not working, cannot login, LLM is "offline"? You are likely running the docker container on a remote machine like EC2 or some other instance where the reachable URL diff --git a/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx index fbba7666f..200c77a6e 100644 --- a/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx +++ b/frontend/src/components/LLMSelection/LMStudioOptions/index.jsx @@ -1,7 +1,14 @@ +import { useEffect, useState } from "react"; import { Info } from "@phosphor-icons/react"; import paths from "@/utils/paths"; +import System from "@/models/system"; export default function LMStudioOptions({ settings, showAlert = false }) { + const [basePathValue, setBasePathValue] = useState( + settings?.LMStudioBasePath + ); + const [basePath, setBasePath] = useState(settings?.LMStudioBasePath); + return (
{showAlert && ( @@ -35,8 +42,11 @@ export default function LMStudioOptions({ settings, showAlert = false }) { required={true} autoComplete="off" spellCheck={false} + onChange={(e) => setBasePathValue(e.target.value)} + onBlur={() => setBasePath(basePathValue)} />
+
); } + +function LMStudioModelSelection({ settings, basePath = null }) { + const [customModels, setCustomModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!basePath || !basePath.includes("/v1")) { + setCustomModels([]); + setLoading(false); + return; + } + setLoading(true); + const { models } = await System.customModels("lmstudio", null, basePath); + setCustomModels(models || []); + setLoading(false); + } + findCustomModels(); + }, [basePath]); + + if (loading || customModels.length == 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx index 976c65988..e679896fa 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx @@ -4,44 +4,12 @@ import { getFileExtension, middleTruncate, } from "@/utils/directories"; -import { File, Trash } from "@phosphor-icons/react"; -import System from "@/models/system"; +import { File } from "@phosphor-icons/react"; import debounce from "lodash.debounce"; -export default function FileRow({ - item, - folderName, - selected, - toggleSelection, - fetchKeys, - setLoading, - setLoadingMessage, -}) { +export default function FileRow({ item, selected, toggleSelection }) { const [showTooltip, setShowTooltip] = useState(false); - const onTrashClick = async (event) => { - event.stopPropagation(); - if ( - !window.confirm( - "Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible." - ) - ) { - return false; - } - - try { - setLoading(true); - setLoadingMessage("This may take a while for large documents"); - await System.deleteDocument(`${folderName}/${item.name}`); - await fetchKeys(true); - } catch (error) { - console.error("Failed to delete the document:", error); - } - - if (selected) toggleSelection(item); - setLoading(false); - }; - const handleShowTooltip = () => { setShowTooltip(true); }; @@ -56,11 +24,11 @@ export default function FileRow({ return ( toggleSelection(item)} - className={`transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-sky-500/20 cursor-pointer file-row ${ + className={`text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-sky-500/20 cursor-pointer file-row ${ selected ? "selected" : "" }`} > -
+
{getFileExtension(item.url)}

-
+
{item?.cached && (

Cached

)} -
); diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx index 48953ab1f..46c4b7fd0 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx @@ -1,8 +1,7 @@ import { useState } from "react"; import FileRow from "../FileRow"; -import { CaretDown, FolderNotch, Trash } from "@phosphor-icons/react"; +import { CaretDown, FolderNotch } from "@phosphor-icons/react"; import { middleTruncate } from "@/utils/directories"; -import System from "@/models/system"; export default function FolderRow({ item, @@ -10,36 +9,10 @@ export default function FolderRow({ onRowClick, toggleSelection, isSelected, - fetchKeys, - setLoading, - setLoadingMessage, autoExpanded = false, }) { const [expanded, setExpanded] = useState(autoExpanded); - const onTrashClick = async (event) => { - event.stopPropagation(); - if ( - !window.confirm( - "Are you sure you want to delete this folder?\nThis will require you to re-upload and re-embed it.\nAny documents in this folder will be removed from any workspace that is currently referencing it.\nThis action is not reversible." - ) - ) { - return false; - } - - try { - setLoading(true); - setLoadingMessage("This may take a while for large folders"); - await System.deleteFolder(item.name); - await fetchKeys(true); - } catch (error) { - console.error("Failed to delete the document:", error); - } - - if (selected) toggleSelection(item); - setLoading(false); - }; - const handleExpandClick = (event) => { event.stopPropagation(); setExpanded(!expanded); @@ -49,7 +22,7 @@ export default function FolderRow({ <> @@ -59,6 +32,10 @@ export default function FolderRow({ role="checkbox" aria-checked={selected} tabIndex={0} + onClick={(event) => { + event.stopPropagation(); + toggleSelection(item); + }} > {selected &&
}
@@ -75,35 +52,23 @@ export default function FolderRow({ weight="fill" />

- {middleTruncate(item.name, 40)} + {middleTruncate(item.name, 35)}

-

- {item.name !== "custom-documents" && ( - - )} -
{expanded && ( -
+ <> {item.items.map((fileItem) => ( ))} -
+ )} ); diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx new file mode 100644 index 000000000..2ebfcde2d --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx @@ -0,0 +1,24 @@ +import { middleTruncate } from "@/utils/directories"; + +export default function FolderSelectionPopup({ folders, onSelect, onClose }) { + const handleFolderSelect = (folder) => { + onSelect(folder); + onClose(); + }; + + return ( +
+
    + {folders.map((folder) => ( +
  • handleFolderSelect(folder)} + className="px-4 py-2 text-xs text-gray-700 hover:bg-gray-200 rounded-lg cursor-pointer whitespace-nowrap" + > + {middleTruncate(folder.name, 25)} +
  • + ))} +
+
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx new file mode 100644 index 000000000..3916fc771 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx @@ -0,0 +1,44 @@ +export default function MoveToFolderIcon({ + className, + width = 18, + height = 18, +}) { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx index 95a1ecd07..83544f72d 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx @@ -2,11 +2,16 @@ import UploadFile from "../UploadFile"; import PreLoader from "@/components/Preloader"; import { memo, useEffect, useState } from "react"; import FolderRow from "./FolderRow"; -import pluralize from "pluralize"; import System from "@/models/system"; +import { Plus, Trash } from "@phosphor-icons/react"; +import Document from "@/models/document"; +import showToast from "@/utils/toast"; +import FolderSelectionPopup from "./FolderSelectionPopup"; +import MoveToFolderIcon from "./MoveToFolderIcon"; function Directory({ files, + setFiles, loading, setLoading, workspace, @@ -19,12 +24,19 @@ function Directory({ loadingMessage, }) { const [amountSelected, setAmountSelected] = useState(0); + const [newFolderName, setNewFolderName] = useState(""); + const [showNewFolderInput, setShowNewFolderInput] = useState(false); + const [showFolderSelection, setShowFolderSelection] = useState(false); + + useEffect(() => { + setAmountSelected(Object.keys(selectedItems).length); + }, [selectedItems]); const deleteFiles = async (event) => { event.stopPropagation(); if ( !window.confirm( - "Are you sure you want to delete these files?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible." + "Are you sure you want to delete these files and folders?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible." ) ) { return false; @@ -32,6 +44,8 @@ function Directory({ try { const toRemove = []; + const foldersToRemove = []; + for (const itemId of Object.keys(selectedItems)) { for (const folder of files.items) { const foundItem = folder.items.find((file) => file.id === itemId); @@ -41,13 +55,29 @@ function Directory({ } } } + for (const folder of files.items) { + if (folder.name === "custom-documents") { + continue; + } + + if (isSelected(folder.id, folder)) { + foldersToRemove.push(folder.name); + } + } + setLoading(true); - setLoadingMessage(`Removing ${toRemove.length} documents. Please wait.`); + setLoadingMessage( + `Removing ${toRemove.length} documents and ${foldersToRemove.length} folders. Please wait.` + ); await System.deleteDocuments(toRemove); + for (const folderName of foldersToRemove) { + await System.deleteFolder(folderName); + } + await fetchKeys(true); setSelectedItems({}); } catch (error) { - console.error("Failed to delete the document:", error); + console.error("Failed to delete files and folders:", error); } finally { setLoading(false); setSelectedItems({}); @@ -57,15 +87,17 @@ function Directory({ const toggleSelection = (item) => { setSelectedItems((prevSelectedItems) => { const newSelectedItems = { ...prevSelectedItems }; - if (item.type === "folder") { - const isCurrentlySelected = isFolderCompletelySelected(item); - if (isCurrentlySelected) { + // select all files in the folder + if (newSelectedItems[item.name]) { + delete newSelectedItems[item.name]; item.items.forEach((file) => delete newSelectedItems[file.id]); } else { + newSelectedItems[item.name] = true; item.items.forEach((file) => (newSelectedItems[file.id] = true)); } } else { + // single file selections if (newSelectedItems[item.id]) { delete newSelectedItems[item.id]; } else { @@ -77,44 +109,124 @@ function Directory({ }); }; - const isFolderCompletelySelected = (folder) => { - if (folder.items.length === 0) { - return false; - } - return folder.items.every((file) => selectedItems[file.id]); - }; - + // check if item is selected based on selectedItems state const isSelected = (id, item) => { if (item && item.type === "folder") { - return isFolderCompletelySelected(item); + if (!selectedItems[item.name]) { + return false; + } + return item.items.every((file) => selectedItems[file.id]); } return !!selectedItems[id]; }; - useEffect(() => { - setAmountSelected(Object.keys(selectedItems).length); - }, [selectedItems]); + 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)) { + for (const currentFolder of files.items) { + const foundItem = currentFolder.items.find( + (file) => file.id === itemId + ); + if (foundItem) { + toMove.push({ ...foundItem, folderName: currentFolder.name }); + break; + } + } + } + setLoading(true); + setLoadingMessage(`Moving ${toMove.length} documents. Please wait.`); + const { success, message } = await Document.moveToFolder( + toMove, + folder.name + ); + if (!success) { + showToast(`Error moving files: ${message}`, "error"); + setLoading(false); + return; + } + + if (success && message) { + // show info if some files were not moved due to being embedded + showToast(message, "info"); + } else { + showToast(`Successfully moved ${toMove.length} documents.`, "success"); + } + await fetchKeys(true); + setSelectedItems({}); + setLoading(false); + }; 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]" + /> +
+ +
+
+ ) : ( + + )}
-
-
-

Name

+
+
+

Name

Date

Kind

-

Cached

-
+
{loading ? (
@@ -122,11 +234,10 @@ function Directory({ {loadingMessage}

- ) : !!files.items ? ( + ) : files.items ? ( files.items.map( (item, index) => - (item.name === "custom-documents" || - (item.type === "folder" && item.items.length > 0)) && ( + item.type === "folder" && ( toggleSelection(item)} toggleSelection={toggleSelection} isSelected={isSelected} - setLoading={setLoading} - setLoadingMessage={setLoadingMessage} autoExpanded={index === 0} /> ) @@ -152,26 +260,45 @@ function Directory({
)}
- {amountSelected !== 0 && ( -
-
- +
+
+
+ +
+ + {showFolderSelection && ( + item.type === "folder" + )} + onSelect={moveToFolder} + onClose={() => setShowFolderSelection(false)} + /> + )} +
+ +
-
)}
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx index b83aadc2f..acf319d92 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/UploadFile/index.jsx @@ -76,7 +76,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) { return (
{fetchingUrl ? "Fetching..." : "Fetch website"} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx index f73916290..f47325427 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx @@ -53,7 +53,7 @@ export default function WorkspaceFileRow({ const handleMouseLeave = debounce(handleHideTooltip, 500); return (
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx index 68a5005a4..4cfa55a5d 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/WorkspaceDirectory/index.jsx @@ -55,7 +55,7 @@ function WorkspaceDirectory({
@@ -96,7 +96,7 @@ function WorkspaceDirectory({
{hasChanges && ( -
+

{embeddingCosts === 0 @@ -114,7 +114,7 @@ function WorkspaceDirectory({ @@ -177,7 +177,7 @@ const PinAlert = memo(() => { diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx index 736a1476f..12784299e 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx @@ -191,9 +191,10 @@ export default function DocumentSettings({ workspace, systemSettings }) { }; return ( -

+
-
+
{ return (
-
+
-
+
+
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 52e870123..d424c906f 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import SlashCommandsButton, { SlashCommands, useSlashCommands, @@ -7,9 +7,7 @@ import { isMobile } from "react-device-detect"; import debounce from "lodash.debounce"; import { PaperPlaneRight } from "@phosphor-icons/react"; import StopGenerationButton from "./StopGenerationButton"; - export default function PromptInput({ - workspace, message, submit, onChange, @@ -19,13 +17,27 @@ export default function PromptInput({ }) { const { showSlashCommand, setShowSlashCommand } = useSlashCommands(); const formRef = useRef(null); + const textareaRef = useRef(null); const [_, setFocused] = useState(false); + useEffect(() => { + if (!inputDisabled && textareaRef.current) { + textareaRef.current.focus(); + } + resetTextAreaHeight(); + }, [inputDisabled]); + const handleSubmit = (e) => { setFocused(false); submit(e); }; + const resetTextAreaHeight = () => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + }; + const checkForSlash = (e) => { const input = e.target.value; if (input === "/") setShowSlashCommand(true); @@ -44,14 +56,12 @@ export default function PromptInput({ const adjustTextArea = (event) => { if (isMobile) return false; const element = event.target; - element.style.height = "1px"; - element.style.height = - event.target.value.length !== 0 - ? 25 + element.scrollHeight + "px" - : "1px"; + element.style.height = "auto"; + element.style.height = `${element.scrollHeight}px`; }; const watchForSlash = debounce(checkForSlash, 300); + return (