Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
timothycarambat 2024-06-26 21:02:14 -07:00
commit c341f7a298
22 changed files with 637 additions and 277 deletions
collector/utils
extensions/Confluence
files
frontend/src
components
EmbeddingSelection
LMStudioOptions
OllamaOptions
LLMSelection
LMStudioOptions
OllamaLLMOptions
hooks
locales
models
pages
Admin/System
WorkspaceSettings/GeneralAppearance/DeleteWorkspace
utils
server

View file

@ -3,7 +3,7 @@ const path = require("path");
const { default: slugify } = require("slugify");
const { v4 } = require("uuid");
const UrlPattern = require("url-pattern");
const { writeToServerDocuments } = require("../../files");
const { writeToServerDocuments, sanitizeFileName } = require("../../files");
const { tokenizeString } = require("../../tokenizer");
const {
ConfluencePagesLoader,
@ -98,11 +98,11 @@ async function loadConfluence({ pageUrl, username, accessToken }, response) {
console.log(
`[Confluence Loader]: Saving ${doc.metadata.title} to ${outFolder}`
);
writeToServerDocuments(
data,
`${slugify(doc.metadata.title)}-${data.id}`,
outFolderPath
const fileName = sanitizeFileName(
`${slugify(doc.metadata.title)}-${data.id}`
);
writeToServerDocuments(data, fileName, outFolderPath);
});
return {

View file

@ -129,6 +129,11 @@ function normalizePath(filepath = "") {
return result;
}
function sanitizeFileName(fileName) {
if (!fileName) return fileName;
return fileName.replace(/[<>:"\/\\|?*]/g, "");
}
module.exports = {
trashFile,
isTextType,
@ -137,4 +142,5 @@ module.exports = {
wipeCollectorStorage,
normalizePath,
isWithin,
sanitizeFileName,
};

View file

@ -1,48 +1,112 @@
import React, { useEffect, useState } from "react";
import System from "@/models/system";
import PreLoader from "@/components/Preloader";
import { LMSTUDIO_COMMON_URLS } from "@/utils/constants";
import { CaretDown, CaretUp } from "@phosphor-icons/react";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
export default function LMStudioEmbeddingOptions({ settings }) {
const [basePathValue, setBasePathValue] = useState(
settings?.EmbeddingBasePath
const {
autoDetecting: loading,
basePath,
basePathValue,
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "lmstudio",
initialBasePath: settings?.EmbeddingBasePath,
ENDPOINTS: LMSTUDIO_COMMON_URLS,
});
const [maxChunkLength, setMaxChunkLength] = useState(
settings?.EmbeddingModelMaxChunkLength || 8192
);
const [basePath, setBasePath] = useState(settings?.EmbeddingBasePath);
const handleMaxChunkLengthChange = (e) => {
setMaxChunkLength(Number(e.target.value));
};
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="w-full flex items-start gap-4">
<LMStudioModelSelection settings={settings} basePath={basePath.value} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
LMStudio Base URL
</label>
<input
type="url"
name="EmbeddingBasePath"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:1234/v1"
defaultValue={settings?.EmbeddingBasePath}
onChange={(e) => setBasePathValue(e.target.value)}
onBlur={() => setBasePath(basePathValue)}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<LMStudioModelSelection settings={settings} basePath={basePath} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Max embedding chunk length
<label className="text-white text-sm font-semibold block mb-2">
Max Embedding Chunk Length
</label>
<input
type="number"
name="EmbeddingModelMaxChunkLength"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="8192"
min={1}
value={maxChunkLength}
onChange={handleMaxChunkLengthChange}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.EmbeddingModelMaxChunkLength}
required={false}
required={true}
autoComplete="off"
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Maximum length of text chunks for embedding.
</p>
</div>
</div>
<div className="flex justify-start mt-4">
<button
onClick={(e) => {
e.preventDefault();
setShowAdvancedControls(!showAdvancedControls);
}}
className="text-white hover:text-white/70 flex items-center text-sm"
>
{showAdvancedControls ? "Hide" : "Show"} Manual Endpoint Input
{showAdvancedControls ? (
<CaretUp size={14} className="ml-1" />
) : (
<CaretDown size={14} className="ml-1" />
)}
</button>
</div>
<div hidden={!showAdvancedControls}>
<div className="w-full flex items-start gap-4">
<div className="flex flex-col w-60">
<div className="flex justify-between items-center mb-2">
<label className="text-white text-sm font-semibold">
LM Studio Base URL
</label>
{loading ? (
<PreLoader size="6" />
) : (
<>
{!basePathValue.value && (
<button
onClick={handleAutoDetectClick}
className="bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Auto-Detect
</button>
)}
</>
)}
</div>
<input
type="url"
name="EmbeddingBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:1234/v1"
value={basePathValue.value}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter the URL where LM Studio is running.
</p>
</div>
</div>
</div>
</div>
@ -55,14 +119,23 @@ function LMStudioModelSelection({ settings, basePath = null }) {
useEffect(() => {
async function findCustomModels() {
if (!basePath || !basePath.includes("/v1")) {
if (!basePath) {
setCustomModels([]);
setLoading(false);
return;
}
setLoading(true);
const { models } = await System.customModels("lmstudio", null, basePath);
setCustomModels(models || []);
try {
const { models } = await System.customModels(
"lmstudio",
null,
basePath
);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
}
setLoading(false);
}
findCustomModels();
@ -71,8 +144,8 @@ function LMStudioModelSelection({ settings, basePath = null }) {
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
LM Studio Embedding Model
</label>
<select
name="EmbeddingModelPref"
@ -80,19 +153,23 @@ function LMStudioModelSelection({ settings, basePath = null }) {
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
{basePath?.includes("/v1")
? "-- loading available models --"
: "-- waiting for URL --"}
{!!basePath
? "--loading available models--"
: "Enter LM Studio URL first"}
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the LM Studio model for embeddings. Models will load after
entering a valid LM Studio URL.
</p>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
LM Studio Embedding Model
</label>
<select
name="EmbeddingModelPref"
@ -115,6 +192,9 @@ function LMStudioModelSelection({ settings, basePath = null }) {
</optgroup>
)}
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Choose the LM Studio model you want to use for generating embeddings.
</p>
</div>
);
}

View file

@ -1,55 +1,122 @@
import React, { useEffect, useState } from "react";
import System from "@/models/system";
import PreLoader from "@/components/Preloader";
import { OLLAMA_COMMON_URLS } from "@/utils/constants";
import { CaretDown, CaretUp } from "@phosphor-icons/react";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
export default function OllamaEmbeddingOptions({ settings }) {
const [basePathValue, setBasePathValue] = useState(
settings?.EmbeddingBasePath
const {
autoDetecting: loading,
basePath,
basePathValue,
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "ollama",
initialBasePath: settings?.EmbeddingBasePath,
ENDPOINTS: OLLAMA_COMMON_URLS,
});
const [maxChunkLength, setMaxChunkLength] = useState(
settings?.EmbeddingModelMaxChunkLength || 8192
);
const [basePath, setBasePath] = useState(settings?.EmbeddingBasePath);
const handleMaxChunkLengthChange = (e) => {
setMaxChunkLength(Number(e.target.value));
};
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="w-full flex items-start gap-4">
<OllamaEmbeddingModelSelection
settings={settings}
basePath={basePath.value}
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Ollama Base URL
</label>
<input
type="url"
name="EmbeddingBasePath"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://127.0.0.1:11434"
defaultValue={settings?.EmbeddingBasePath}
onChange={(e) => setBasePathValue(e.target.value)}
onBlur={() => setBasePath(basePathValue)}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<OllamaLLMModelSelection settings={settings} basePath={basePath} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Max embedding chunk length
<label className="text-white text-sm font-semibold block mb-2">
Max Embedding Chunk Length
</label>
<input
type="number"
name="EmbeddingModelMaxChunkLength"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="8192"
min={1}
value={maxChunkLength}
onChange={handleMaxChunkLengthChange}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.EmbeddingModelMaxChunkLength}
required={false}
required={true}
autoComplete="off"
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Maximum length of text chunks for embedding.
</p>
</div>
</div>
<div className="flex justify-start mt-4">
<button
onClick={(e) => {
e.preventDefault();
setShowAdvancedControls(!showAdvancedControls);
}}
className="text-white hover:text-white/70 flex items-center text-sm"
>
{showAdvancedControls ? "Hide" : "Show"} Manual Endpoint Input
{showAdvancedControls ? (
<CaretUp size={14} className="ml-1" />
) : (
<CaretDown size={14} className="ml-1" />
)}
</button>
</div>
<div hidden={!showAdvancedControls}>
<div className="w-full flex items-start gap-4">
<div className="flex flex-col w-60">
<div className="flex justify-between items-center mb-2">
<label className="text-white text-sm font-semibold">
Ollama Base URL
</label>
{loading ? (
<PreLoader size="6" />
) : (
<>
{!basePathValue.value && (
<button
onClick={handleAutoDetectClick}
className="bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Auto-Detect
</button>
)}
</>
)}
</div>
<input
type="url"
name="EmbeddingBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://127.0.0.1:11434"
value={basePathValue.value}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter the URL where Ollama is running.
</p>
</div>
</div>
</div>
</div>
);
}
function OllamaLLMModelSelection({ settings, basePath = null }) {
function OllamaEmbeddingModelSelection({ settings, basePath = null }) {
const [customModels, setCustomModels] = useState([]);
const [loading, setLoading] = useState(true);
@ -61,8 +128,13 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
return;
}
setLoading(true);
const { models } = await System.customModels("ollama", null, basePath);
setCustomModels(models || []);
try {
const { models } = await System.customModels("ollama", null, basePath);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
}
setLoading(false);
}
findCustomModels();
@ -71,33 +143,37 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Embedding Model Selection
<label className="text-white text-sm font-semibold block mb-2">
Ollama Embedding Model
</label>
<select
name="EmbeddingModelPref"
disabled={true}
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
{!!basePath
? "-- loading available models --"
: "-- waiting for URL --"}
? "--loading available models--"
: "Enter Ollama URL first"}
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the Ollama model for embeddings. Models will load after
entering a valid Ollama URL.
</p>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Embedding Model Selection
<label className="text-white text-sm font-semibold block mb-2">
Ollama Embedding Model
</label>
<select
name="EmbeddingModelPref"
required={true}
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{customModels.length > 0 && (
<optgroup label="Your loaded models">
@ -115,6 +191,9 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
</optgroup>
)}
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Choose the Ollama model you want to use for generating embeddings.
</p>
</div>
);
}

View file

@ -1,13 +1,32 @@
import { useEffect, useState } from "react";
import { Info } from "@phosphor-icons/react";
import { Info, CaretDown, CaretUp } from "@phosphor-icons/react";
import paths from "@/utils/paths";
import System from "@/models/system";
import PreLoader from "@/components/Preloader";
import { LMSTUDIO_COMMON_URLS } from "@/utils/constants";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
export default function LMStudioOptions({ settings, showAlert = false }) {
const [basePathValue, setBasePathValue] = useState(
settings?.LMStudioBasePath
const {
autoDetecting: loading,
basePath,
basePathValue,
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "lmstudio",
initialBasePath: settings?.LMStudioBasePath,
ENDPOINTS: LMSTUDIO_COMMON_URLS,
});
const [maxTokens, setMaxTokens] = useState(
settings?.LMStudioTokenLimit || 4096
);
const [basePath, setBasePath] = useState(settings?.LMStudioBasePath);
const handleMaxTokensChange = (e) => {
setMaxTokens(Number(e.target.value));
};
return (
<div className="w-full flex flex-col">
@ -28,45 +47,86 @@ export default function LMStudioOptions({ settings, showAlert = false }) {
</a>
</div>
)}
<div className="w-full flex items-center gap-4">
<div className="w-full flex items-start gap-4">
<LMStudioModelSelection settings={settings} basePath={basePath.value} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
LMStudio Base URL
<label className="text-white text-sm font-semibold block mb-2">
Max Tokens
</label>
<input
type="url"
name="LMStudioBasePath"
type="number"
name="LMStudioTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:1234/v1"
defaultValue={settings?.LMStudioBasePath}
placeholder="4096"
defaultChecked="4096"
min={1}
value={maxTokens}
onChange={handleMaxTokensChange}
onScroll={(e) => e.target.blur()}
required={true}
autoComplete="off"
spellCheck={false}
onChange={(e) => setBasePathValue(e.target.value)}
onBlur={() => setBasePath(basePathValue)}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Maximum number of tokens for context and response.
</p>
</div>
{!settings?.credentialsOnly && (
<>
<LMStudioModelSelection settings={settings} basePath={basePath} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Token context window
</div>
<div className="flex justify-start mt-4">
<button
onClick={(e) => {
e.preventDefault();
setShowAdvancedControls(!showAdvancedControls);
}}
className="text-white hover:text-white/70 flex items-center text-sm"
>
{showAdvancedControls ? "Hide" : "Show"} Manual Endpoint Input
{showAdvancedControls ? (
<CaretUp size={14} className="ml-1" />
) : (
<CaretDown size={14} className="ml-1" />
)}
</button>
</div>
<div hidden={!showAdvancedControls}>
<div className="w-full flex items-start gap-4 mt-4">
<div className="flex flex-col w-60">
<div className="flex justify-between items-center mb-2">
<label className="text-white text-sm font-semibold">
LM Studio Base URL
</label>
<input
type="number"
name="LMStudioTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="4096"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.LMStudioTokenLimit}
required={true}
autoComplete="off"
/>
{loading ? (
<PreLoader size="6" />
) : (
<>
{!basePathValue.value && (
<button
onClick={handleAutoDetectClick}
className="bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Auto-Detect
</button>
)}
</>
)}
</div>
</>
)}
<input
type="url"
name="LMStudioBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://localhost:1234/v1"
value={basePathValue.value}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter the URL where LM Studio is running.
</p>
</div>
</div>
</div>
</div>
);
@ -78,14 +138,23 @@ function LMStudioModelSelection({ settings, basePath = null }) {
useEffect(() => {
async function findCustomModels() {
if (!basePath || !basePath.includes("/v1")) {
if (!basePath) {
setCustomModels([]);
setLoading(false);
return;
}
setLoading(true);
const { models } = await System.customModels("lmstudio", null, basePath);
setCustomModels(models || []);
try {
const { models } = await System.customModels(
"lmstudio",
null,
basePath
);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
}
setLoading(false);
}
findCustomModels();
@ -94,8 +163,8 @@ function LMStudioModelSelection({ settings, basePath = null }) {
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
LM Studio Model
</label>
<select
name="LMStudioModelPref"
@ -103,19 +172,23 @@ function LMStudioModelSelection({ settings, basePath = null }) {
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
{basePath?.includes("/v1")
? "-- loading available models --"
: "-- waiting for URL --"}
{!!basePath
? "--loading available models--"
: "Enter LM Studio URL first"}
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the LM Studio model you want to use. Models will load after
entering a valid LM Studio URL.
</p>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
LM Studio Model
</label>
<select
name="LMStudioModelPref"
@ -138,6 +211,9 @@ function LMStudioModelSelection({ settings, basePath = null }) {
</optgroup>
)}
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Choose the LM Studio model you want to use for your conversations.
</p>
</div>
);
}

View file

@ -1,53 +1,117 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import System from "@/models/system";
import PreLoader from "@/components/Preloader";
import { OLLAMA_COMMON_URLS } from "@/utils/constants";
import { CaretDown, CaretUp } from "@phosphor-icons/react";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
export default function OllamaLLMOptions({ settings }) {
const [basePathValue, setBasePathValue] = useState(
settings?.OllamaLLMBasePath
const {
autoDetecting: loading,
basePath,
basePathValue,
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "ollama",
initialBasePath: settings?.OllamaLLMBasePath,
ENDPOINTS: OLLAMA_COMMON_URLS,
});
const [maxTokens, setMaxTokens] = useState(
settings?.OllamaLLMTokenLimit || 4096
);
const [basePath, setBasePath] = useState(settings?.OllamaLLMBasePath);
const handleMaxTokensChange = (e) => {
setMaxTokens(Number(e.target.value));
};
return (
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex items-center gap-4">
<div className="w-full flex items-start gap-4">
<OllamaLLMModelSelection
settings={settings}
basePath={basePath.value}
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Ollama Base URL
<label className="text-white text-sm font-semibold block mb-2">
Max Tokens
</label>
<input
type="url"
name="OllamaLLMBasePath"
type="number"
name="OllamaLLMTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://127.0.0.1:11434"
defaultValue={settings?.OllamaLLMBasePath}
placeholder="4096"
defaultChecked="4096"
min={1}
value={maxTokens}
onChange={handleMaxTokensChange}
onScroll={(e) => e.target.blur()}
required={true}
autoComplete="off"
spellCheck={false}
onChange={(e) => setBasePathValue(e.target.value)}
onBlur={() => setBasePath(basePathValue)}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Maximum number of tokens for context and response.
</p>
</div>
{!settings?.credentialsOnly && (
<>
<OllamaLLMModelSelection settings={settings} basePath={basePath} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Token context window
</div>
<div className="flex justify-start mt-4">
<button
onClick={(e) => {
e.preventDefault();
setShowAdvancedControls(!showAdvancedControls);
}}
className="text-white hover:text-white/70 flex items-center text-sm"
>
{showAdvancedControls ? "Hide" : "Show"} Manual Endpoint Input
{showAdvancedControls ? (
<CaretUp size={14} className="ml-1" />
) : (
<CaretDown size={14} className="ml-1" />
)}
</button>
</div>
<div hidden={!showAdvancedControls}>
<div className="w-full flex items-start gap-4">
<div className="flex flex-col w-60">
<div className="flex justify-between items-center mb-2">
<label className="text-white text-sm font-semibold">
Ollama Base URL
</label>
<input
type="number"
name="OllamaLLMTokenLimit"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="4096"
min={1}
onScroll={(e) => e.target.blur()}
defaultValue={settings?.OllamaLLMTokenLimit}
required={true}
autoComplete="off"
/>
{loading ? (
<PreLoader size="6" />
) : (
<>
{!basePathValue.value && (
<button
onClick={handleAutoDetectClick}
className="bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Auto-Detect
</button>
)}
</>
)}
</div>
</>
)}
<input
type="url"
name="OllamaLLMBasePath"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="http://127.0.0.1:11434"
value={basePathValue.value}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter the URL where Ollama is running.
</p>
</div>
</div>
</div>
</div>
);
@ -65,8 +129,13 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
return;
}
setLoading(true);
const { models } = await System.customModels("ollama", null, basePath);
setCustomModels(models || []);
try {
const { models } = await System.customModels("ollama", null, basePath);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
}
setLoading(false);
}
findCustomModels();
@ -75,8 +144,8 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
Ollama Model
</label>
<select
name="OllamaLLMModelPref"
@ -85,18 +154,22 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
>
<option disabled={true} selected={true}>
{!!basePath
? "-- loading available models --"
: "-- waiting for URL --"}
? "--loading available models--"
: "Enter Ollama URL first"}
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the Ollama model you want to use. Models will load after
entering a valid Ollama URL.
</p>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
<label className="text-white text-sm font-semibold block mb-2">
Ollama Model
</label>
<select
name="OllamaLLMModelPref"
@ -119,6 +192,9 @@ function OllamaLLMModelSelection({ settings, basePath = null }) {
</optgroup>
)}
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Choose the Ollama model you want to use for your conversations.
</p>
</div>
);
}

View file

@ -0,0 +1,99 @@
import { useEffect, useState } from "react";
import System from "@/models/system";
import showToast from "@/utils/toast";
export default function useProviderEndpointAutoDiscovery({
provider = null,
initialBasePath = "",
ENDPOINTS = [],
}) {
const [loading, setLoading] = useState(false);
const [basePath, setBasePath] = useState(initialBasePath);
const [basePathValue, setBasePathValue] = useState(initialBasePath);
const [autoDetectAttempted, setAutoDetectAttempted] = useState(false);
const [showAdvancedControls, setShowAdvancedControls] = useState(true);
async function autoDetect(isInitialAttempt = false) {
setLoading(true);
setAutoDetectAttempted(true);
const possibleEndpoints = [];
ENDPOINTS.forEach((endpoint) => {
possibleEndpoints.push(
new Promise((resolve, reject) => {
System.customModels(provider, null, endpoint, 2_000)
.then((results) => {
if (!results?.models || results.models.length === 0)
throw new Error("No models");
resolve({ endpoint, models: results.models });
})
.catch(() => {
reject(`${provider} @ ${endpoint} did not resolve.`);
});
})
);
});
const { endpoint, models } = await Promise.any(possibleEndpoints)
.then((resolved) => resolved)
.catch(() => {
console.error("All endpoints failed to resolve.");
return { endpoint: null, models: null };
});
if (models !== null) {
setBasePath(endpoint);
setBasePathValue(endpoint);
setLoading(false);
showToast("Provider endpoint discovered automatically.", "success", {
clear: true,
});
setShowAdvancedControls(false);
return;
}
setLoading(false);
setShowAdvancedControls(true);
showToast(
"Couldn't automatically discover the provider endpoint. Please enter it manually.",
"info",
{ clear: true }
);
}
function handleAutoDetectClick(e) {
e.preventDefault();
autoDetect();
}
function handleBasePathChange(e) {
const value = e.target.value;
setBasePathValue(value);
}
function handleBasePathBlur() {
setBasePath(basePathValue);
}
useEffect(() => {
if (!initialBasePath && !autoDetectAttempted) autoDetect(true);
}, [initialBasePath, autoDetectAttempted]);
return {
autoDetecting: loading,
autoDetectAttempted,
showAdvancedControls,
setShowAdvancedControls,
basePath: {
value: basePath,
set: setBasePathValue,
onChange: handleBasePathChange,
onBlur: handleBasePathBlur,
},
basePathValue: {
value: basePathValue,
set: setBasePathValue,
},
handleAutoDetectClick,
runAutoDetect: autoDetect,
};
}

View file

@ -85,6 +85,9 @@ const TRANSLATIONS = {
remove: "Remove Workspace Image",
},
delete: {
title: "Delete Workspace",
description:
"Delete this workspace and all of its data. This will delete the workspace for all users.",
delete: "Delete Workspace",
deleting: "Deleting Workspace...",
"confirm-start": "You are about to delete your entire",

View file

@ -82,6 +82,9 @@ const TRANSLATIONS = {
remove: "Eliminar imagen del espacio de trabajo",
},
delete: {
title: "Eliminar Espacio de Trabajo",
description:
"Eliminar este espacio de trabajo y todos sus datos. Esto eliminará el espacio de trabajo para todos los usuarios.",
delete: "Eliminar espacio de trabajo",
deleting: "Eliminando espacio de trabajo...",
"confirm-start": "Estás a punto de eliminar tu",

View file

@ -87,6 +87,9 @@ const TRANSLATIONS = {
remove: "Supprimer l'image de l'espace de travail",
},
delete: {
title: "Supprimer l'Espace de Travail",
description:
"Supprimer cet espace de travail et toutes ses données. Cela supprimera l'espace de travail pour tous les utilisateurs.",
delete: "Supprimer l'espace de travail",
deleting: "Suppression de l'espace de travail...",
"confirm-start": "Vous êtes sur le point de supprimer votre",

View file

@ -78,6 +78,9 @@ const TRANSLATIONS = {
remove: "Удалить изображение рабочего пространства",
},
delete: {
title: "Удалить Рабочее Пространство",
description:
"Удалите это рабочее пространство и все его данные. Это удалит рабочее пространство для всех пользователей.",
delete: "Удалить рабочее пространство",
deleting: "Удаление рабочего пространства...",
"confirm-start": "Вы собираетесь удалить весь ваш",

View file

@ -84,6 +84,8 @@ const TRANSLATIONS = {
remove: "移除工作区图像",
},
delete: {
title: "删除工作区",
description: "删除此工作区及其所有数据。这将删除所有用户的工作区。",
delete: "删除工作区",
deleting: "正在删除工作区...",
"confirm-start": "您即将删除整个",

View file

@ -420,22 +420,6 @@ const System = {
return { success: false, error: e.message };
});
},
getCanDeleteWorkspaces: async function () {
return await fetch(`${API_BASE}/system/can-delete-workspaces`, {
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not fetch can delete workspaces.");
return res.json();
})
.then((res) => res?.canDelete)
.catch((e) => {
console.error(e);
return false;
});
},
getWelcomeMessages: async function () {
return await fetch(`${API_BASE}/system/welcome-messages`, {
method: "GET",
@ -512,10 +496,23 @@ const System = {
return false;
});
},
customModels: async function (provider, apiKey = null, basePath = null) {
customModels: async function (
provider,
apiKey = null,
basePath = null,
timeout = null
) {
const controller = new AbortController();
if (!!timeout) {
setTimeout(() => {
controller.abort("Request timed out.");
}, timeout);
}
return fetch(`${API_BASE}/system/custom-models`, {
method: "POST",
headers: baseHeaders(),
signal: controller.signal,
body: JSON.stringify({
provider,
apiKey,

View file

@ -8,7 +8,6 @@ import CTAButton from "@/components/lib/CTAButton";
export default function AdminSystem() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [canDelete, setCanDelete] = useState(false);
const [messageLimit, setMessageLimit] = useState({
enabled: false,
limit: 10,
@ -18,7 +17,6 @@ export default function AdminSystem() {
e.preventDefault();
setSaving(true);
await Admin.updateSystemPreferences({
users_can_delete_workspaces: canDelete,
limit_user_messages: messageLimit.enabled,
message_limit: messageLimit.limit,
});
@ -31,7 +29,6 @@ export default function AdminSystem() {
async function fetchSettings() {
const settings = (await Admin.systemPreferences())?.settings;
if (!settings) return;
setCanDelete(settings?.users_can_delete_workspaces);
setMessageLimit({
enabled: settings.limit_user_messages,
limit: settings.message_limit,
@ -71,29 +68,6 @@ export default function AdminSystem() {
</div>
)}
<div className="mt-4 mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Users can delete workspaces
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Allow non-admin users to delete workspaces that they are a part
of. This would delete the workspace for everyone.
</p>
<label className="relative inline-flex cursor-pointer items-center mt-2">
<input
type="checkbox"
name="users_can_delete_workspaces"
checked={canDelete}
onChange={(e) => setCanDelete(e.target.checked)}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
</div>
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Limit messages per user per day

View file

@ -1,22 +1,14 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import Workspace from "@/models/workspace";
import paths from "@/utils/paths";
import System from "@/models/system";
import { useTranslation } from "react-i18next";
import showToast from "@/utils/toast";
export default function DeleteWorkspace({ workspace }) {
const { slug } = useParams();
const [deleting, setDeleting] = useState(false);
const [canDelete, setCanDelete] = useState(false);
const { t } = useTranslation();
useEffect(() => {
async function fetchKeys() {
const canDelete = await System.getCanDeleteWorkspaces();
setCanDelete(canDelete);
}
fetchKeys();
}, [workspace?.slug]);
const deleteWorkspace = async () => {
if (
@ -40,16 +32,20 @@ export default function DeleteWorkspace({ workspace }) {
? (window.location = paths.home())
: window.location.reload();
};
if (!canDelete) return null;
return (
<button
disabled={deleting}
onClick={deleteWorkspace}
type="button"
className="w-60 mt-[40px] transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
>
{deleting ? t("general.delete.deleting") : t("general.delete.delete")}
</button>
<div className="flex flex-col mt-10">
<label className="block input-label">{t("general.delete.title")}</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t("general.delete.description")}
</p>
<button
disabled={deleting}
onClick={deleteWorkspace}
type="button"
className="w-60 mt-4 transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
>
{deleting ? t("general.delete.deleting") : t("general.delete.delete")}
</button>
</div>
);
}

View file

@ -10,6 +10,19 @@ export const SEEN_WATCH_ALERT = "anythingllm_watched_document_alert";
export const USER_BACKGROUND_COLOR = "bg-historical-msg-user";
export const AI_BACKGROUND_COLOR = "bg-historical-msg-system";
export const OLLAMA_COMMON_URLS = [
"http://127.0.0.1:11434",
"http://host.docker.internal:11434",
"http://172.17.0.1:11434",
];
export const LMSTUDIO_COMMON_URLS = [
"http://localhost:1234/v1",
"http://127.0.0.1:1234/v1",
"http://host.docker.internal:1234/v1",
"http://172.17.0.1:1234/v1",
];
export function fullApiUrl() {
if (API_BASE !== "/api") return API_BASE;
return `${window.location.origin}/api`;

View file

@ -319,9 +319,6 @@ function adminEndpoints(app) {
try {
const embedder = getEmbeddingEngineSelection();
const settings = {
users_can_delete_workspaces:
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
?.value === "true",
limit_user_messages:
(await SystemSettings.get({ label: "limit_user_messages" }))
?.value === "true",

View file

@ -616,7 +616,6 @@ function apiAdminEndpoints(app) {
type: 'object',
example: {
settings: {
users_can_delete_workspaces: true,
limit_user_messages: false,
message_limit: 10,
}
@ -641,9 +640,6 @@ function apiAdminEndpoints(app) {
}
const settings = {
users_can_delete_workspaces:
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
?.value === "true",
limit_user_messages:
(await SystemSettings.get({ label: "limit_user_messages" }))
?.value === "true",
@ -673,7 +669,6 @@ function apiAdminEndpoints(app) {
content: {
"application/json": {
example: {
users_can_delete_workspaces: false,
limit_user_messages: true,
message_limit: 5,
}

View file

@ -479,7 +479,6 @@ function systemEndpoints(app) {
});
await SystemSettings._updateSettings({
multi_user_mode: true,
users_can_delete_workspaces: false,
limit_user_messages: false,
message_limit: 25,
});
@ -776,33 +775,6 @@ function systemEndpoints(app) {
}
);
app.get(
"/system/can-delete-workspaces",
[validatedRequest],
async function (request, response) {
try {
if (!response.locals.multiUserMode) {
return response.status(200).json({ canDelete: true });
}
const user = await userFromSession(request, response);
if ([ROLES.admin, ROLES.manager].includes(user?.role)) {
return response.status(200).json({ canDelete: true });
}
const canDelete = await SystemSettings.canDeleteWorkspaces();
response.status(200).json({ canDelete });
} catch (error) {
console.error("Error fetching can delete workspaces:", error);
response.status(500).json({
success: false,
message: "Internal server error",
canDelete: false,
});
}
}
);
app.get(
"/system/welcome-messages",
[validatedRequest, flexUserRoleValid([ROLES.all])],

View file

@ -15,7 +15,6 @@ function isNullOrNaN(value) {
const SystemSettings = {
protectedFields: ["multi_user_mode"],
supportedFields: [
"users_can_delete_workspaces",
"limit_user_messages",
"message_limit",
"logo_filename",
@ -302,16 +301,6 @@ const SystemSettings = {
}
},
canDeleteWorkspaces: async function () {
try {
const setting = await this.get({ label: "users_can_delete_workspaces" });
return setting?.value === "true";
} catch (error) {
console.error(error.message);
return false;
}
},
hasEmbeddings: async function () {
try {
const { Document } = require("./documents");

View file

@ -4,7 +4,6 @@ const prisma = new PrismaClient();
async function main() {
const settings = [
{ label: "multi_user_mode", value: "false" },
{ label: "users_can_delete_workspaces", value: "false" },
{ label: "limit_user_messages", value: "false" },
{ label: "message_limit", value: "25" },
{ label: "logo_filename", value: "anything-llm.png" },

View file

@ -710,7 +710,6 @@
"type": "object",
"example": {
"settings": {
"users_can_delete_workspaces": true,
"limit_user_messages": false,
"message_limit": 10
}
@ -792,7 +791,6 @@
"content": {
"application/json": {
"example": {
"users_can_delete_workspaces": false,
"limit_user_messages": true,
"message_limit": 5
}