Render v1.10.0 (#4892)

* Migrate to `bcryptjs` (#4767)

* Replace bcrypt with bcryptjs across multiple files

* dev build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Refactor frontend legacy JSON.parse with safeJsonParse (#4759)

* replace all frontend legacy JSON.parse with safeJsonParse

* default collapsed sidebar menu on failed parse

* remove extra check on conditional render

* undo singular json parse

* add guard clause and return null for `userFromStorage`

* patch domainList

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Fix pagination bug in paperless-ngx data connector (#4757)

* iterate over all pages in paperless-ngx data connector

* add error handling and data validation

* refactor to handle edge cases and null values

* catch edge case to prevent infinite loop

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Fix Stale User Session with Proper `fetch` Error Handling (#4770)

* add refresh user functionality

* prettier

* add eslint disable comment for exhaustive-deps warning in AuthContext to stop nagging about navigate func

* remove unused imports and fix typo

* handle unsafe parse of undefined for in-session user deleted

* Refactor refreshUser function to handle errors and return structured response. Update AuthProvider to manage user data based on success status.

* Remove console error logging from promise catch in System model for cleaner error handling.

* change status from 404 to 400 and valid to success

* Refactor error handling in AuthProvider's refreshUser logic to remove redundant catch block and streamline user session management on failure.

* prettier

* reorder clauses - return errors

* refactor
account for all user modes
dev build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Add Auth Token to Ollama Embedding Client (#4766)

* Enhance OllamaEmbedder to support authentication by adding an authorization token in headers for client initialization.

* Add optional Auth Token input for Ollama embedding options

* move info elements

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Upgrade to Multer 2.0.0 (#4768)

* upgrade to multer 2.0.0

* bump dev

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Implement Global Error Boundary (#4765)

* Implement global error boundary

* add 404 page for generic path catching

* devbuild

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Feat/cohere agent implementation (#4703)

* implement cohere agent support

* run yarn lint

* moderize Cohere
add supported langchain method
redo streaming since it was not working
looping of agent calls was not functioning

* change default model to real model tag
add case statement for model tag

* remove debug

* update default

* only whitelist known labels

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Upgrade MCP SDK to Latest (1.24.3) (#4773)

* upgrade mcp sdk to  latest (1.24.3)

* Upgrade MCP version floor in package.json to 1.24.3

* fix(devcontainer): forward ports 3000/3001 (#4779)

* 4601 log model on response (#4781)

* add model tag to chatCompletion

* add modelTag `model` to async streaming
keeps default arguments for prompt token calculation where applied via explict arg

* fix HF default arg

* render all performance metrics as available for backward compatibility
add `timestamp` to both sync/async chat methods

* extract metrics string to function

* Update Google Search Option Description To Reference Documentation For Rate Limits (#4789)

* Update Google Search description to reference documentation for rate limits

* remove

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Refactor `LLMPerformanceMonitor.measureStream()` to Use Options Object Pattern (#4786)

* Refactor LLMPerformanceMonitor to use options object for measureStream parameters

* Refactor invocations of `measureStream` to use options arguments

* Change invocation of `measureStream` in anthropic provider to use options argument

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* hanging lint

* fix unnecessary scrollbar in workspace general appearance settings tab (#4791)

* fixed SuggestedChatMessages width styling

* ran yarn lint

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Add Eslint Config in `/frontend` (#4785)

* Add local ESLint configuration and disable rules to allow for errorless state

* Remove unnecessary ESLint disable comments in AuthContext and usePromptInputStorage for cleaner code.

* Update eslint-plugin-react-hooks

* Configure prettier to work with eslint

* Removed trailing commas from eslint config

* Prettier to source code

* add a v2 lint script

* put back eslint-disable comments

* fix eslinter and prettier application
always apply --fix since we --write prettier, otherwise it fails

* precaution dev build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Refactor localWhisper to use custom FFMPEGWrapper class (#4775)

* refactor localWhisper to use new custom FFMPEGWrapper class

* stub tests in github actions

* add back wavefile conversion to 16khz 32f to fix docker builds

* use afterEach for cleanup in ffmpeg tests

* remove unused FFMPEG_PATH env check

* use spawnSync for ffmpeg to capture and log output

* lint

* revert removal of try/catch around validateAudioFile for more helpful error msgs

* use readFileSync instead of createReadStream for less overhead

* change import to require for fix-path and stub import in tests

* refactor to singleton to preserve ffmpeg path
dev build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Refactor Managed Services in "Data Handling & Privacy" Onboarding Step to Use Their Privacy Policy URL (#4790)

* Refactor non-local LLM Provider, Vector Database, and Embedding Engine privacy information to use their policy URLs instead of descriptions

* Update LLM Provider, Embedding Engine, and Vector Database sections to include privacy policy links

* fix broken links, lint

* Update AstraDB privacy policy URL in onboarding flow

* Refactor AnythingLLM Privacy & Data page to show managed provider privacy policy URLs

* Update Mistral privacy policy URLs in onboarding flow for consistency

* Abstract privacy policies of providers into a reusable component | Refactor Privacy & Data Handling Step of onboarding flow to focus on solely rendering that step | Move provider privacy policy maps into constants.js

* Remove commented-out code for third-party provider privacy policies in Privacy and Data Handling component

* Update privacy policy descriptions for consistency by adding periods at the end of sentences in ProviderPrivacy component and constants.js

* rescope constants for providers

* extract default to external function, add loading state

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* patch ESM import issue (#4819)

* Upgrade YT Scraper (#4820)

* Merge commit from fork

* Update Sponsors README

* fix: validate chat message input (#4811)

* fix: validate chat message input

* fix: align message validation for thread stream-chat endpoint

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* patch AWS credential issue in docker context (#4842)

path AWS credential issue in docker context

* support AWS bedrock agents with streaming (#4850)

* support AWS bedrock agents with streaming

* Add back error handlers from previous fix

* VectorDB class migration (#4787)

* Migrate Astra to class (#4722)

migrate astra to class

* Migrate LanceDB to class (#4721)

migrate lancedb to class

* Migrate Pinecone to class (#4726)

migrate pinecone to class

* Migrate Zilliz to class (#4729)

migrate zilliz to class

* Migrate Weaviate to class (#4728)

migrate weaviate to class

* Migrate Qdrant to class (#4727)

migrate qdrant to class

* Migrate Milvus to class (#4725)

migrate milvus to class

* Migrate Chroma to class (#4723)

migrate chroma to class

* Migrate Chroma Cloud to class (#4724)

* migrate chroma to class

* migrate chroma cloud to class

* move limits to class field

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Migrate PGVector to class (#4730)

* migrate pgvector to class

* patch pgvector test

* convert connectionString, tableName, and validateConnection to static methods

* move instance properties to class fields

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Refactor Zilliz Cloud vector DB provider (#4749)

simplify zilliz implementation by using milvus as base class

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* VectorDatabase base class (#4738)

create generic VectorDatabase base class

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Extend VectorDatabase base class to all providers (#4755)

extend VectorDatabase base class to all providers

* patch lancedb import

* breakout name and add generic logger

* dev tag build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Make XLSX spreadsheets visible in chat by combining sheets (#4847)

* fix bug with xlsx files not being added as context

* lint

* fix console logs/warn/error

* abstract sheet processing to function + normalize error handling

* fix jsdoc

* patch xlsx filename to prevent orphaned doc

* reduce tokens

* correct pluralization

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Remove Workspace Creation Onboarding Page (#4823)

* remove create workspace step for onboarding

* remove unused image

* workspace creation into dedicated useEffect + use translated workspace name

* dev tag

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Improved DMR support (#4863)

* Improve DMR support
- Autodetect models installed
- Grab all models from hub.docker to show available
- UI to handle render,search, install, and management of models
- Support functionality for chat, stream, and agentic calls

* forgot files

* fix loader circle being too large
fix tooltip width command
adjust location of docker installer open for web platform

* adjust imports

* AnythingLLM Mobile live (#4864)

* remove new labels on landing

* minor DMR UI changes + dynamic tooltip for context management

* Adjust fix path to use ESM import (#4867)

* Adjust fix path to use ESM import

* normalize fix-path imports and usage across the app

* extract path fix logic to utils for server and collector

* add helpers

* repin strip-ansi in collector

* fix log for localWhisper
lint

* Add postsettled callers to updateENV

* minor refactor for context window finder

* Extract Model Table to component (#4871)

* Extract Model Table to component
Add provider icons to header rows and installed models
Light mode supported
Mapping for model name id hints to provider
Update DMR to filter chat models by ability since not available via hub API

* linting + dev

* fix incorrect import

* remove race condition regression for FoundryLocal provider

* remove duplicated steam method on cohere handler

* feat(i18n): add Czech (cs) language translation to AnythingLLM (#4874)

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Docker model runner download from UI (#4884)

* Enable downloads of DMR models from UI

* add utils + dev build

* linting

* add fallback key to mono model provider

* update announcements for 1.10.0

* bump versions to 1.10.0

---------

Co-authored-by: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com>
Co-authored-by: Sean Hatfield <seanhatfield5@gmail.com>
Co-authored-by: Colin Perry <55003831+17ColinMiPerry@users.noreply.github.com>
Co-authored-by: Irene Wang <lohas1107@gmail.com>
Co-authored-by: timothycarambat <16845892+timothycarambat@users.noreply.github.com>
Co-authored-by: Ocheretovich <ocheretovich@gmail.com>
Co-authored-by: Vladimir Vlach <vladaman@gmail.com>
This commit is contained in:
Timothy Carambat
2026-01-22 07:59:28 -08:00
committed by GitHub
parent baca02f8de
commit ed92637f66
184 changed files with 7965 additions and 2943 deletions

View File

@@ -13,6 +13,9 @@
// "containerUser": "anythingllm",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm",
"forwardPorts": [3001, 3000],
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
// Docker very useful linter

View File

@@ -65,9 +65,8 @@ jobs:
tags: |
type=raw,value=render
type=raw,value=railway
# Uncomment these if you want to publish specific version tags or doing a release
# type=raw,value=render-1.9.1
# type=raw,value=railway-1.9.1
type=raw,value=render-1.10.0
type=raw,value=railway-1.10.0
- name: Build and push multi-platform Docker image
uses: docker/build-push-action@v6

File diff suppressed because one or more lines are too long

View File

@@ -58,7 +58,7 @@ Notes:
```yaml
image:
repository: mintplexlabs/anythingllm
tag: "1.9.1"
tag: "1.10.0"
service:
type: ClusterIP
@@ -104,7 +104,7 @@ helm install my-anythingllm ./anythingllm -f values-secret.yaml
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"mintplexlabs/anythingllm"` | |
| image.tag | string | `"1.9.1"` | |
| image.tag | string | `"1.10.0"` | |
| imagePullSecrets | list | `[]` | |
| ingress.annotations | object | `{}` | |
| ingress.className | string | `""` | |

View File

@@ -69,7 +69,7 @@ Notes:
```yaml
image:
repository: mintplexlabs/anythingllm
tag: "1.9.1"
tag: "1.10.0"
service:
type: ClusterIP

View File

@@ -8,7 +8,7 @@ initContainers: []
image:
repository: mintplexlabs/anythingllm
pullPolicy: IfNotPresent
tag: "1.9.1"
tag: "1.10.0"
imagePullSecrets: []
nameOverride: ""

View File

@@ -0,0 +1,77 @@
process.env.STORAGE_DIR = "test-storage";
const fs = require("fs");
const path = require("path");
// Mock fix-path as a noop to prevent SIGSEGV (segfault)
// Returns ESM-style default export for dynamic import()
jest.mock("fix-path", () => ({ default: jest.fn() }));
const { FFMPEGWrapper } = require("../../../../utils/WhisperProviders/ffmpeg");
const describeRunner = process.env.GITHUB_ACTIONS ? describe.skip : describe;
describeRunner("FFMPEGWrapper", () => {
/** @type { import("../../../../utils/WhisperProviders/ffmpeg/index").FFMPEGWrapper } */
let ffmpeg;
const testDir = path.resolve(__dirname, "../../../../storage/tmp");
const inputPath = path.resolve(testDir, "test-input.wav");
const outputPath = path.resolve(testDir, "test-output.wav");
beforeEach(() => {
ffmpeg = new FFMPEGWrapper();
});
afterEach(() => {
if (fs.existsSync(inputPath)) fs.rmSync(inputPath);
if (fs.existsSync(outputPath)) fs.rmSync(outputPath);
});
it("should find ffmpeg executable", async () => {
const knownPath = await ffmpeg.ffmpegPath();
expect(knownPath).toBeDefined();
expect(typeof knownPath).toBe("string");
expect(knownPath.length).toBeGreaterThan(0);
});
it("should validate ffmpeg executable", async () => {
const knownPath = await ffmpeg.ffmpegPath();
expect(ffmpeg.isValidFFMPEG(knownPath)).toBe(true);
});
it("should return false for invalid ffmpeg path", () => {
expect(ffmpeg.isValidFFMPEG("/invalid/path/to/ffmpeg")).toBe(false);
});
it("should convert audio file to wav format", async () => {
if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
const sampleUrl =
"https://github.com/ringcentral/ringcentral-api-docs/blob/main/resources/sample1.wav?raw=true";
const response = await fetch(sampleUrl);
if (!response.ok)
throw new Error(
`Failed to download sample file: ${response.statusText}`
);
const buffer = await response.arrayBuffer();
fs.writeFileSync(inputPath, Buffer.from(buffer));
const result = await ffmpeg.convertAudioToWav(inputPath, outputPath);
expect(result).toBe(true);
expect(fs.existsSync(outputPath)).toBe(true);
const stats = fs.statSync(outputPath);
expect(stats.size).toBeGreaterThan(0);
}, 30000);
it("should throw error when conversion fails", () => {
const nonExistentFile = path.resolve(testDir, "non-existent-file.wav");
const outputPath = path.resolve(testDir, "test-output-fail.wav");
expect(async () => {
return await ffmpeg.convertAudioToWav(nonExistentFile, outputPath);
}).rejects.toThrow(`Input file ${nonExistentFile} does not exist.`);
});
});

View File

@@ -1,33 +0,0 @@
process.env.STORAGE_DIR = "test-storage"; // needed for tests to run
const { YoutubeTranscript } = require("../../../../../utils/extensions/YoutubeTranscript/YoutubeLoader/youtube-transcript.js");
describe("YoutubeTranscript", () => {
if (process.env.GITHUB_ACTIONS) {
console.log("Skipping YoutubeTranscript test in GitHub Actions as the URLs will not resolve.");
it('is stubbed in GitHub Actions', () => expect(true).toBe(true));
} else {
it("should fetch transcript from YouTube video", async () => {
const videoId = "BJjsfNO5JTo";
const transcript = await YoutubeTranscript.fetchTranscript(videoId, {
lang: "en",
});
expect(transcript).toBeDefined();
expect(typeof transcript).toBe("string");
expect(transcript.length).toBeGreaterThan(0);
console.log("First 200 characters:", transcript.substring(0, 200) + "...");
}, 30000);
it("should fetch non asr transcript from YouTube video", async () => {
const videoId = "D111ao6wWH0";
const transcript = await YoutubeTranscript.fetchTranscript(videoId, {
lang: "zh-HK",
});
expect(transcript).toBeDefined();
expect(typeof transcript).toBe("string");
expect(transcript.length).toBeGreaterThan(0);
console.log("First 200 characters:", transcript.substring(0, 200) + "...");
}, 30000);
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "anything-llm-document-collector",
"version": "1.9.1",
"version": "1.10.0",
"description": "Document collector server endpoints",
"main": "index.js",
"author": "Timothy Carambat (Mintplex Labs)",
@@ -22,7 +22,7 @@
"dotenv": "^16.0.3",
"epub2": "git+https://github.com/Mintplex-Labs/epub2-static.git#main",
"express": "^4.21.2",
"fluent-ffmpeg": "^2.1.2",
"fix-path": "^4.0.0",
"html-to-text": "^9.0.5",
"ignore": "^5.3.0",
"js-tiktoken": "^1.0.8",
@@ -39,16 +39,22 @@
"puppeteer": "~21.5.2",
"sharp": "^0.33.5",
"slugify": "^1.6.6",
"strip-ansi": "^7.1.2",
"tesseract.js": "^6.0.0",
"url-pattern": "^1.0.3",
"uuid": "^9.0.0",
"wavefile": "^11.0.0",
"winston": "^3.13.0",
"youtube-transcript-plus": "^1.1.2",
"youtubei.js": "^9.1.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"nodemon": "^2.0.22",
"prettier": "^2.4.1"
},
"resolutions": {
"string-width": "^4.2.3",
"wrap-ansi": "^7.0.0"
}
}

View File

@@ -7,7 +7,6 @@ const {
trashFile,
writeToServerDocuments,
documentsFolder,
directUploadsFolder,
} = require("../../utils/files");
const { tokenizeString } = require("../../utils/tokenizer");
const { default: slugify } = require("slugify");
@@ -34,30 +33,83 @@ async function asXlsx({
metadata = {},
}) {
const documents = [];
const folderName = slugify(`${path.basename(filename)}-${v4().slice(0, 4)}`, {
lower: true,
trim: true,
});
const outFolderPath = options.parseOnly
? path.resolve(directUploadsFolder, folderName)
: path.resolve(documentsFolder, folderName);
try {
const workSheetsFromFile = xlsx.parse(fullFilePath);
if (!fs.existsSync(outFolderPath))
fs.mkdirSync(outFolderPath, { recursive: true });
for (const sheet of workSheetsFromFile) {
try {
const { name, data } = sheet;
const content = convertToCSV(data);
if (options.parseOnly) {
const allSheetContents = [];
let totalWordCount = 0;
const sheetNames = [];
if (!content?.length) {
console.warn(`Sheet "${name}" is empty. Skipping.`);
continue;
for (const sheet of workSheetsFromFile) {
const processed = processSheet(sheet);
if (!processed) continue;
const { name, content, wordCount } = processed;
sheetNames.push(name);
allSheetContents.push(`\nSheet: ${name}\n${content}`);
totalWordCount += wordCount;
}
if (allSheetContents.length === 0) {
console.log(`No valid sheets found in ${filename}.`);
return {
success: false,
reason: `No valid sheets found in ${filename}.`,
documents: [],
};
}
const combinedContent = allSheetContents.join("\n");
const sheetListText =
sheetNames.length > 1
? ` (Sheets: ${sheetNames.join(", ")})`
: ` (Sheet: ${sheetNames[0]})`;
const combinedData = {
id: v4(),
url: `file://${fullFilePath}`,
title: metadata.title || `${filename}${sheetListText}`,
docAuthor: metadata.docAuthor || "Unknown",
description:
metadata.description ||
`Spreadsheet data from ${filename} containing ${sheetNames.length} ${
sheetNames.length === 1 ? "sheet" : "sheets"
}`,
docSource: metadata.docSource || "an xlsx file uploaded by the user.",
chunkSource: metadata.chunkSource || "",
published: createdDate(fullFilePath),
wordCount: totalWordCount,
pageContent: combinedContent,
token_count_estimate: tokenizeString(combinedContent),
};
const document = writeToServerDocuments({
data: combinedData,
filename: `${slugify(path.basename(filename))}-${combinedData.id}`,
destinationOverride: null,
options: { parseOnly: true },
});
documents.push(document);
console.log(`[SUCCESS]: ${filename} converted & ready for embedding.`);
} else {
const folderName = slugify(
`${path.basename(filename)}-${v4().slice(0, 4)}`,
{
lower: true,
trim: true,
}
);
const outFolderPath = path.resolve(documentsFolder, folderName);
if (!fs.existsSync(outFolderPath))
fs.mkdirSync(outFolderPath, { recursive: true });
console.log(`-- Processing sheet: ${name} --`);
for (const sheet of workSheetsFromFile) {
const processed = processSheet(sheet);
if (!processed) continue;
const { name, content, wordCount } = processed;
const sheetData = {
id: v4(),
url: `file://${path.join(outFolderPath, `${slugify(name)}.csv`)}`,
@@ -68,7 +120,7 @@ async function asXlsx({
docSource: metadata.docSource || "an xlsx file uploaded by the user.",
chunkSource: metadata.chunkSource || "",
published: createdDate(fullFilePath),
wordCount: content.split(/\s+/).length,
wordCount: wordCount,
pageContent: content,
token_count_estimate: tokenizeString(content),
};
@@ -83,9 +135,6 @@ async function asXlsx({
console.log(
`[SUCCESS]: Sheet "${name}" converted & ready for embedding.`
);
} catch (err) {
console.error(`Error processing sheet "${name}":`, err);
continue;
}
}
} catch (err) {
@@ -114,4 +163,31 @@ async function asXlsx({
return { success: true, reason: null, documents };
}
/**
* Processes a single sheet and returns its content and metadata
* @param {{name: string, data: Array<Array<string|number|null|undefined>>}} sheet - Parsed sheet with name and 2D array of cell values
* @returns {{name: string, content: string, wordCount: number}|null} - Object with name, CSV content, and word count, or null if sheet is empty
*/
function processSheet(sheet) {
try {
const { name, data } = sheet;
const content = convertToCSV(data);
if (!content?.length) {
console.log(`Sheet "${name}" is empty. Skipping.`);
return null;
}
console.log(`-- Processing sheet: ${name} --`);
return {
name,
content,
wordCount: content.split(/\s+/).length,
};
} catch (err) {
console.error(`Error processing sheet "${sheet.name}":`, err);
return null;
}
}
module.exports = asXlsx;

View File

@@ -0,0 +1,114 @@
const fs = require("fs");
const path = require("path");
const { execSync, spawnSync } = require("child_process");
const { patchShellEnvironmentPath } = require("../../shell");
/**
* Custom FFMPEG wrapper class for audio file conversion.
* Replaces deprecated fluent-ffmpeg package.
* Locates ffmpeg binary and converts audio files to required
* WAV format (16k hz mono 32f) for Whisper transcription.
*
* @class FFMPEGWrapper
*/
class FFMPEGWrapper {
static _instance;
constructor() {
if (FFMPEGWrapper._instance) return FFMPEGWrapper._instance;
FFMPEGWrapper._instance = this;
this._ffmpegPath = null;
}
log(text, ...args) {
console.log(`\x1b[35m[FFMPEG]\x1b[0m ${text}`, ...args);
}
/**
* Locates ffmpeg binary.
* Uses fix-path on non-Windows platforms to ensure we can find ffmpeg.
*
* @returns {Promise<string>} Path to ffmpeg binary
* @throws {Error}
*/
async ffmpegPath() {
if (this._ffmpegPath) return this._ffmpegPath;
await patchShellEnvironmentPath();
try {
const which = process.platform === "win32" ? "where" : "which";
const result = execSync(`${which} ffmpeg`, { encoding: "utf8" }).trim();
const candidatePath = result?.split("\n")?.[0]?.trim();
if (!candidatePath) throw new Error("FFMPEG candidate path not found.");
if (!this.isValidFFMPEG(candidatePath))
throw new Error("FFMPEG candidate path is not valid ffmpeg binary.");
this.log(`Found FFMPEG binary at ${candidatePath}`);
this._ffmpegPath = candidatePath;
return this._ffmpegPath;
} catch (error) {
this.log(error.message);
}
throw new Error("FFMPEG binary not found.");
}
/**
* Validates that path points to a valid ffmpeg binary.
* Runs ffmpeg -version command.
*
* @param {string} pathToTest - Path of ffmpeg binary
* @returns {boolean}
*/
isValidFFMPEG(pathToTest) {
try {
if (!pathToTest || !fs.existsSync(pathToTest)) return false;
execSync(`"${pathToTest}" -version`, { encoding: "utf8", stdio: "pipe" });
return true;
} catch {
return false;
}
}
/**
* Converts audio file to WAV format with required parameters for Whisper.
* Output: 16k hz, mono, 32bit float.
*
* @param {string} inputPath - Input path for audio file (any format supported by ffmpeg)
* @param {string} outputPath - Output path for converted file
* @returns {Promise<boolean>}
* @throws {Error} If ffmpeg binary cannot be found or conversion fails
*/
async convertAudioToWav(inputPath, outputPath) {
if (!fs.existsSync(inputPath))
throw new Error(`Input file ${inputPath} does not exist.`);
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
this.log(`Converting ${path.basename(inputPath)} to WAV format...`);
// Convert to 16k hz mono 32f
const result = spawnSync(
await this.ffmpegPath(),
[
"-i",
inputPath,
"-ar",
"16000",
"-ac",
"1",
"-acodec",
"pcm_f32le",
"-y",
outputPath,
],
{ encoding: "utf8" }
);
// ffmpeg writes progress to stderr
if (result.stderr) this.log(result.stderr.trim());
if (result.status !== 0) throw new Error(`FFMPEG conversion failed`);
this.log(`Conversion complete: ${path.basename(outputPath)}`);
return true;
}
}
module.exports = { FFMPEGWrapper };

View File

@@ -64,52 +64,21 @@ class LocalWhisper {
try {
let buffer;
const wavefile = require("wavefile");
const ffmpeg = require("fluent-ffmpeg");
const { FFMPEGWrapper } = require("./ffmpeg");
const ffmpeg = new FFMPEGWrapper();
const outFolder = path.resolve(__dirname, `../../storage/tmp`);
if (!fs.existsSync(outFolder))
fs.mkdirSync(outFolder, { recursive: true });
const fileExtension = path.extname(sourcePath).toLowerCase();
if (fileExtension !== ".wav") {
this.#log(
`File conversion required! ${fileExtension} file detected - converting to .wav`
const outputFile = path.resolve(outFolder, `${v4()}.wav`);
const success = await ffmpeg.convertAudioToWav(sourcePath, outputFile);
if (!success)
throw new Error(
"[Conversion Failed]: Could not convert file to .wav format!"
);
const outputFile = path.resolve(outFolder, `${v4()}.wav`);
const convert = new Promise((resolve) => {
ffmpeg(sourcePath)
.toFormat("wav")
.on("error", (error) => {
this.#log(`Conversion Error! ${error.message}`);
resolve(false);
})
.on("progress", (progress) =>
this.#log(
`Conversion Processing! ${progress.targetSize}KB converted`
)
)
.on("end", () => {
this.#log(`Conversion Complete! File converted to .wav!`);
resolve(true);
})
.save(outputFile);
});
const success = await convert;
if (!success)
throw new Error(
"[Conversion Failed]: Could not convert file to .wav format!"
);
const chunks = [];
const stream = fs.createReadStream(outputFile);
for await (let chunk of stream) chunks.push(chunk);
buffer = Buffer.concat(chunks);
fs.rmSync(outputFile);
} else {
const chunks = [];
const stream = fs.createReadStream(sourcePath);
for await (let chunk of stream) chunks.push(chunk);
buffer = Buffer.concat(chunks);
}
buffer = fs.readFileSync(outputFile);
fs.rmSync(outputFile);
const wavFile = new wavefile.WaveFile(buffer);
try {
@@ -119,6 +88,9 @@ class LocalWhisper {
throw new Error(`Invalid audio file: ${error.message}`);
}
// Although we use ffmpeg to convert to the correct format (16k hz 32f),
// different versions of ffmpeg produce different results based on the
// environment. To ensure consistency, we convert to the correct format again.
wavFile.toBitDepth("32f");
wavFile.toSampleRate(16000);
@@ -164,7 +136,7 @@ class LocalWhisper {
progress_callback: (data) => {
if (!data.hasOwnProperty("progress")) return;
console.log(
`\x1b[34m[Embedding - Downloading Model Files]\x1b[0m ${
`\x1b[34m[ONNXWhisper - Downloading Model Files]\x1b[0m ${
data.file
} ${~~data?.progress}%`
);

View File

@@ -26,19 +26,48 @@ class PaperlessNgxLoader {
*/
async fetchAllDocuments() {
try {
const documents = await fetch(`${this.baseUrl}/api/documents/`, {
headers: {
"Content-Type": "application/json",
...this.baseHeaders,
},
})
.then((res) => res.json())
.then((data) => data.results || [])
.catch((error) => {
throw new Error(
`Failed to fetch documents from Paperless-ngx: ${error.message}`
const documents = [];
let nextUrl = `${this.baseUrl}/api/documents/`;
let page = 1;
while (nextUrl) {
console.log(`Fetching documents page ${page} from Paperless-ngx`);
try {
const data = await fetch(nextUrl, {
headers: {
"Content-Type": "application/json",
...this.baseHeaders,
},
}).then((res) => {
if (!res.ok)
throw new Error(
`Failed to fetch documents from Paperless-ngx: ${res.status}`
);
return res.json();
});
const validResults = data.results.filter((doc) => doc?.id);
if (!validResults.length) break;
documents.push(...validResults);
if (data.next === nextUrl) break;
nextUrl = data.next || null;
page++;
} catch (error) {
console.error(
`Error fetching page ${page} from Paperless-ngx:`,
error
);
});
break;
}
}
console.log(
`Fetched ${documents.length} documents from Paperless-ngx (Pages: ${
page - 1
})`
);
const documentsWithContent = await Promise.all(
documents.map(async (doc) => {

View File

@@ -54,13 +54,15 @@ class YoutubeLoader {
source: this.#videoId,
};
try {
const { YoutubeTranscript } = require("./youtube-transcript");
transcript = await YoutubeTranscript.fetchTranscript(this.#videoId, {
const fetchTranscript = await import("youtube-transcript-plus").then(
(module) => module.fetchTranscript
);
const transcriptSegments = await fetchTranscript(this.#videoId, {
lang: this.#language,
});
if (!transcript) {
if (!transcriptSegments || transcriptSegments.length === 0)
throw new Error("Transcription not found");
}
transcript = this.#convertTranscriptSegmentsToText(transcriptSegments);
if (this.#addVideoInfo) {
const { Innertube } = require("youtubei.js");
const youtube = await Innertube.create();
@@ -82,6 +84,16 @@ class YoutubeLoader {
},
];
}
#convertTranscriptSegmentsToText(transcriptSegments) {
return transcriptSegments
.map((segment) =>
typeof segment === "string" ? segment : segment.text || ""
)
.join(" ")
.replace(/\s+/g, " ")
.trim();
}
}
module.exports.YoutubeLoader = YoutubeLoader;

25
collector/utils/shell.js Normal file
View File

@@ -0,0 +1,25 @@
/**
* Patch the shell environment path to ensure the PATH is properly set for the current platform.
* On Docker, we are on Node v18 and cannot support fix-path v5.
* So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5.
* https://github.com/sindresorhus/fix-path/issues/6
* @returns {Promise<{[key: string]: string}>} - Environment variables from shell
*/
async function patchShellEnvironmentPath() {
try {
if (process.platform === "win32") return process.env;
const { default: fixPath } = await import("fix-path");
const { default: stripAnsi } = await import("strip-ansi");
fixPath();
if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);
console.log("Shell environment path patched successfully.");
return process.env;
} catch (error) {
console.error("Failed to patch shell environment path:", error);
return process.env;
}
}
module.exports = {
patchShellEnvironmentPath,
};

View File

@@ -521,11 +521,6 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.1.0:
version "6.2.3"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041"
integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
@@ -566,11 +561,6 @@ ast-types@^0.13.4:
dependencies:
tslib "^2.0.1"
async@^0.2.9:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==
async@^3.2.3:
version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
@@ -1007,7 +997,7 @@ cross-fetch@4.0.0:
dependencies:
node-fetch "^2.6.12"
cross-spawn@^7.0.1, cross-spawn@^7.0.6:
cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -1152,6 +1142,11 @@ deepmerge@^4.3.1:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
default-shell@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-2.2.0.tgz#31481c19747bfe59319b486591643eaf115a1864"
integrity sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
@@ -1259,11 +1254,6 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -1274,11 +1264,6 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
enabled@2.0.x:
version "2.0.0"
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
@@ -1419,6 +1404,21 @@ events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
human-signals "^2.1.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^4.0.1"
onetime "^5.1.2"
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
@@ -1538,6 +1538,13 @@ finalhandler@~1.3.1:
statuses "~2.0.2"
unpipe "~1.0.0"
fix-path@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fix-path/-/fix-path-4.0.0.tgz#bc1d14f038edb734ac46944a45454106952ca429"
integrity sha512-g31GX207Tt+psI53ZSaB1egprYbEN0ZYl90aKcO22A2LmCNnFsSq3b5YpoKp3E/QEiWByTXGJOkFQG4S07Bc1A==
dependencies:
shell-path "^3.0.0"
flat@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
@@ -1548,14 +1555,6 @@ flatbuffers@^1.12.0:
resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.12.0.tgz#72e87d1726cb1b216e839ef02658aa87dcef68aa"
integrity sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==
fluent-ffmpeg@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz#d6846be257777844249a4adeb320f25326d239f3"
integrity sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==
dependencies:
async "^0.2.9"
which "^1.1.1"
fn.name@1.x.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
@@ -1669,6 +1668,11 @@ get-stream@^5.1.0:
dependencies:
pump "^3.0.0"
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-uri@^6.0.1:
version "6.0.5"
resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16"
@@ -1812,6 +1816,11 @@ https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.6:
agent-base "^7.1.2"
debug "4"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
humanize-duration@^3.25.1:
version "3.33.2"
resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.33.2.tgz#2e41986eabb00cb5ad0eef616a78233099dbdac4"
@@ -2272,6 +2281,11 @@ merge-descriptors@1.0.3:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -2299,6 +2313,11 @@ mime@^3.0.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@@ -2484,6 +2503,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
@@ -2538,6 +2564,13 @@ one-time@^1.0.0:
dependencies:
fn.name "1.x.x"
onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
dependencies:
mimic-fn "^2.1.0"
onnx-proto@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/onnx-proto/-/onnx-proto-4.0.4.tgz#2431a25bee25148e915906dda0687aafe3b9e044"
@@ -2705,7 +2738,7 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.1.0:
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
@@ -3200,6 +3233,22 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-env@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-4.0.1.tgz#883302d9426095d398a39b102a851adb306b8cb8"
integrity sha512-w3oeZ9qg/P6Lu6qqwavvMnB/bwfsz67gPB3WXmLd/n6zuh7TWQZtGa3iMEdmua0kj8rivkwl+vUjgLWlqZOMPw==
dependencies:
default-shell "^2.0.0"
execa "^5.1.1"
strip-ansi "^7.0.1"
shell-path@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/shell-path/-/shell-path-3.1.0.tgz#950671fe15de70fb4d984b886d55e8a2f10bfe33"
integrity sha512-s/9q9PEtcRmDTz69+cJ3yYBAe9yGrL7e46gm2bU4pQ9N48ecPK9QrGFnLwYgb4smOHskx4PL7wCNMktW2AoD+g==
dependencies:
shell-env "^4.0.1"
side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
@@ -3240,6 +3289,11 @@ side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signal-exit@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
@@ -3343,7 +3397,7 @@ streamx@^2.15.0, streamx@^2.21.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.1.2:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -3352,15 +3406,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -3389,7 +3434,7 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
strip-ansi@^7.0.1, strip-ansi@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==
@@ -3403,6 +3448,11 @@ strip-dirs@^2.0.0:
dependencies:
is-natural-number "^4.0.1"
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -3746,13 +3796,6 @@ which-typed-array@^1.1.16:
gopd "^1.2.0"
has-tostringtag "^1.0.2"
which@^1.1.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -3795,7 +3838,7 @@ winston@^3.13.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
wrap-ansi@^7.0.0, wrap-ansi@^8.1.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -3804,15 +3847,6 @@ wrap-ansi@^7.0.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -3886,6 +3920,11 @@ yauzl@^2.10.0, yauzl@^2.4.2:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
youtube-transcript-plus@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/youtube-transcript-plus/-/youtube-transcript-plus-1.1.2.tgz#f86851852a056088c11f4f6523ab0f8dba7d9711"
integrity sha512-bLlqkA6gVVUorZpcc+THuECXyAwOpnHqW2lOav9g6gGovxAP3FCD8s9GBFVjmSl3cWWwwPPXtG/zY1nD+GvQ7A==
youtubei.js@^9.1.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.4.0.tgz#ccccaf4a295b96e3e17134a66730bbc82461594b"

View File

@@ -162,6 +162,11 @@ GID='1000'
# GITEE_AI_MODEL_PREF=
# GITEE_AI_MODEL_TOKEN_LIMIT=
# LLM_PROVIDER='docker-model-runner'
# DOCKER_MODEL_RUNNER_BASE_PATH='http://127.0.0.1:12434'
# DOCKER_MODEL_RUNNER_LLM_MODEL_PREF='phi-3.5-mini'
# DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT=4096
###########################################
######## Embedding API SElECTION ##########
###########################################

View File

@@ -171,7 +171,7 @@ COPY --chown=anythingllm:anythingllm --from=frontend-build /app/frontend/dist /a
# Setup the environment
ENV NODE_ENV=production
ENV ANYTHING_LLM_RUNTIME=docker
ENV DEPLOYMENT_VERSION=1.9.1
ENV DEPLOYMENT_VERSION=1.10.0
# Setup the healthcheck
HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \

View File

@@ -93,7 +93,7 @@ ENV PUPPETEER_EXECUTABLE_PATH=/app/.cache/puppeteer/chrome/linux-119.0.6045.105/
ENV NODE_ENV=production
ENV ANYTHING_LLM_RUNTIME=docker
ENV STORAGE_DIR=$STORAGE_DIR
ENV DEPLOYMENT_VERSION=1.9.1
ENV DEPLOYMENT_VERSION=1.10.0
# Expose the server port
EXPOSE 3001

View File

@@ -0,0 +1,25 @@
[
{
"thumbnail_url": "https://cdn.anythingllm.com/support/announcements/assets/meeting-assistant.png",
"title": "Meeting Assistant",
"short_description": "Transcribe meetings and generate meeting notes entirely on device.",
"goto": "https://docs.anythingllm.com/meeting-assistant/introduction",
"author": "AnythingLLM",
"date": "January 21, 2026"
},
{
"thumbnail_url": "https://cdn.anythingllm.com/support/announcements/assets/mobile.png",
"title": "AnythingLLM Mobile",
"short_description": "AnythingLLM Mobile is now available on the Google Play Store.",
"goto": "https://play.google.com/store/apps/details?id=com.anythingllm",
"author": "AnythingLLM",
"date": "January 5, 2026"
},
{
"title": "50K Stars on Github",
"short_description": "AnythingLLM broke 50K stars on Github!",
"goto": "https://github.com/mintplex-labs/anything-llm",
"author": "AnythingLLM",
"date": "October 21, 2025"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -1,2 +1,3 @@
2026-01-12.json
2025-07-08.json
2025-04-08.json

46
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,46 @@
import js from "@eslint/js"
import globals from "globals"
import pluginReact from "eslint-plugin-react"
import pluginReactHooks from "eslint-plugin-react-hooks"
import pluginPrettier from "eslint-plugin-prettier"
import configPrettier from "eslint-config-prettier"
import { defineConfig } from "eslint/config"
export default defineConfig([
{
ignores: ["**/*.min.js", "src/media/**/*"]
},
{
files: ["src/**/*.{js,jsx}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser }
},
{
files: ["src/**/*.{js,jsx}"],
...pluginReact.configs.flat.recommended,
plugins: {
"react-hooks": pluginReactHooks,
prettier: pluginPrettier
},
settings: {
react: {
version: "detect"
}
},
rules: {
...configPrettier.rules,
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"no-extra-boolean-cast": "off",
"no-prototype-builtins": "off",
"no-unused-vars": "off",
"no-empty": "off",
"no-useless-escape": "off",
"no-undef": "off",
"no-unsafe-optional-chaining": "off",
"no-constant-binary-expression": "off"
}
}
])

View File

@@ -7,10 +7,12 @@
"start": "vite --open",
"dev": "cross-env NODE_ENV=development vite --debug --host=0.0.0.0",
"build": "vite build && node scripts/postbuild.js",
"lint": "yarn prettier --ignore-path ../.prettierignore --write ./src",
"lint:check": "eslint src",
"lint": "eslint --fix src",
"preview": "vite preview"
},
"dependencies": {
"@lobehub/icons": "^4.0.3",
"@microsoft/fetch-event-source": "^2.0.1",
"@mintplex-labs/piper-tts-web": "^1.0.4",
"@phosphor-icons/react": "^2.1.7",
@@ -35,6 +37,7 @@
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^6.0.0",
"react-highlight-words": "^0.21.0",
"react-i18next": "^14.1.1",
"react-loading-skeleton": "^3.1.0",
@@ -51,6 +54,7 @@
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@eslint/js": "^9.39.2",
"@types/react": "^18.2.23",
"@types/react-dom": "^18.2.8",
"@types/react-router-dom": "^5.3.3",
@@ -58,16 +62,16 @@
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"cross-env": "^7.0.3",
"eslint": "^8.50.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-ft-flow": "^3.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.3",
"flow-bin": "^0.217.0",
"flow-remove-types": "^2.217.1",
"globals": "^13.21.0",
"globals": "^16.5.0",
"hermes-eslint": "^0.15.0",
"postcss": "^8.4.23",
"prettier": "^3.0.3",
@@ -75,4 +79,4 @@
"tailwindcss": "^3.3.1",
"vite": "^4.3.0"
}
}
}

View File

@@ -1,5 +1,5 @@
import React, { Suspense } from "react";
import { Outlet } from "react-router-dom";
import { Outlet, useLocation } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { AuthProvider } from "@/AuthContext";
import { ToastContainer } from "react-toastify";
@@ -12,25 +12,34 @@ import { FullScreenLoader } from "./components/Preloader";
import { ThemeProvider } from "./ThemeContext";
import { PWAModeProvider } from "./PWAContext";
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
import { ErrorBoundary } from "react-error-boundary";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
export default function App() {
const location = useLocation();
return (
<ThemeProvider>
<PWAModeProvider>
<Suspense fallback={<FullScreenLoader />}>
<AuthProvider>
<LogoProvider>
<PfpProvider>
<I18nextProvider i18n={i18n}>
<Outlet />
<ToastContainer />
<KeyboardShortcutsHelp />
</I18nextProvider>
</PfpProvider>
</LogoProvider>
</AuthProvider>
</Suspense>
</PWAModeProvider>
</ThemeProvider>
<ErrorBoundary
FallbackComponent={ErrorBoundaryFallback}
onError={console.error}
resetKeys={[location.pathname]}
>
<ThemeProvider>
<PWAModeProvider>
<Suspense fallback={<FullScreenLoader />}>
<AuthProvider>
<LogoProvider>
<PfpProvider>
<I18nextProvider i18n={i18n}>
<Outlet />
<ToastContainer />
<KeyboardShortcutsHelp />
</I18nextProvider>
</PfpProvider>
</LogoProvider>
</AuthProvider>
</Suspense>
</PWAModeProvider>
</ThemeProvider>
</ErrorBoundary>
);
}

View File

@@ -1,20 +1,31 @@
import React, { useState, createContext } from "react";
import React, { useState, createContext, useEffect } from "react";
import {
AUTH_TIMESTAMP,
AUTH_TOKEN,
AUTH_USER,
USER_PROMPT_INPUT_MAP,
} from "@/utils/constants";
import System from "./models/system";
import { useNavigate } from "react-router-dom";
import { safeJsonParse } from "@/utils/request";
export const AuthContext = createContext(null);
export function AuthProvider(props) {
const localUser = localStorage.getItem(AUTH_USER);
const localAuthToken = localStorage.getItem(AUTH_TOKEN);
const [store, setStore] = useState({
user: localUser ? JSON.parse(localUser) : null,
user: localUser ? safeJsonParse(localUser, null) : null,
authToken: localAuthToken ? localAuthToken : null,
});
const navigate = useNavigate();
/* NOTE:
* 1. There's no reason for these helper functions to be stateful. They could
* just be regular funcs or methods on a basic object.
* 2. These actions are not being invoked anywhere in the
* codebase, dead code.
*/
const [actions] = useState({
updateUser: (user, authToken = "") => {
localStorage.setItem(AUTH_USER, JSON.stringify(user));
@@ -30,6 +41,36 @@ export function AuthProvider(props) {
},
});
/*
* On initial mount and whenever the token changes, fetch a new user object
* If the user is suspended, (success === false and data === null) logout the user and redirect to the login page
* If success is true and data is not null, update the user object in the store (multi-user mode only)
* If success is true and data is null, do nothing (single-user mode only) with or without password protection
*/
useEffect(() => {
async function refreshUser() {
const { success, user: refreshedUser } = await System.refreshUser();
if (success && refreshedUser === null) return;
if (!success) {
localStorage.removeItem(AUTH_USER);
localStorage.removeItem(AUTH_TOKEN);
localStorage.removeItem(AUTH_TIMESTAMP);
localStorage.removeItem(USER_PROMPT_INPUT_MAP);
setStore({ user: null, authToken: null });
navigate("/login");
return;
}
localStorage.setItem(AUTH_USER, JSON.stringify(refreshedUser));
setStore((prev) => ({
...prev,
user: refreshedUser,
}));
}
if (store.authToken) refreshUser();
}, [store.authToken]);
return (
<AuthContext.Provider value={{ store, actions }}>
{props.children}

View File

@@ -14,6 +14,8 @@ export default function OllamaEmbeddingOptions({ settings }) {
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
authToken,
authTokenValue,
} = useProviderEndpointAutoDiscovery({
provider: "ollama",
initialBasePath: settings?.EmbeddingBasePath,
@@ -48,13 +50,13 @@ export default function OllamaEmbeddingOptions({ settings }) {
data-tooltip-id="max-embedding-chunk-length-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Max embedding chunk length
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<label className="text-white text-sm font-semibold block">
Max embedding chunk length
</label>
<Tooltip id="max-embedding-chunk-length-tooltip">
Maximum length of text chunks, in characters, for embedding.
</Tooltip>
@@ -134,13 +136,13 @@ export default function OllamaEmbeddingOptions({ settings }) {
data-tooltip-id="ollama-batch-size-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Embedding batch size
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<label className="text-white text-sm font-semibold block">
Embedding batch size
</label>
<Tooltip id="ollama-batch-size-tooltip">
Number of text chunks to embed in parallel. Higher values
improve speed but use more memory. Default is 1.
@@ -163,6 +165,31 @@ export default function OllamaEmbeddingOptions({ settings }) {
faster embedding.
</p>
</div>
<div>
<label className="text-white font-semibold block mb-3 text-sm">
Auth Token (optional)
</label>
<input
type="password"
name="OllamaLLMAuthToken"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Enter your Auth Token"
defaultValue={settings?.OllamaLLMAuthToken ? "*".repeat(20) : ""}
value={authTokenValue.value}
onChange={authToken.onChange}
onBlur={authToken.onBlur}
required={false}
autoComplete="off"
spellCheck={false}
/>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Enter a <code>Bearer</code> Auth Token for interacting with your
Ollama server.
<br />
Used <b>only</b> if running Ollama behind an authentication
server.
</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,93 @@
import { NavLink } from "react-router-dom";
import { House, ArrowClockwise, Copy, Check } from "@phosphor-icons/react";
import { useState } from "react";
export default function ErrorBoundaryFallback({ error, resetErrorBoundary }) {
const [copied, setCopied] = useState(false);
const copyErrorDetails = async () => {
const details = {
url: window.location.href,
error: error?.name || "Unknown Error",
message: error?.message || "No message available",
stack: error?.stack || "No stack trace available",
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
const formattedDetails = `
Error Report
============
Timestamp: ${details.timestamp}
URL: ${details.url}
User Agent: ${details.userAgent}
Error: ${details.error}
Message: ${details.message}
Stack Trace:
${details.stack}
`.trim();
try {
await navigator.clipboard.writeText(formattedDetails);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy error details:", err);
}
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full">
<h1 className="text-xl md:text-2xl font-bold text-center">
An error occurred.
</h1>
<p className="text-theme-text-secondary text-center px-4">
{error?.message}
</p>
{import.meta.env.DEV && (
<div className="w-full max-w-4xl">
<div className="flex justify-end mb-2">
<button
onClick={copyErrorDetails}
className="flex items-center gap-2 px-3 py-1.5 bg-theme-bg-secondary text-theme-text-primary rounded hover:bg-theme-sidebar-item-hover transition-all duration-200 text-xs font-medium"
title="Copy error details"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5" weight="bold" />
Copied!
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
Copy Details
</>
)}
</button>
</div>
<pre className="w-full text-xs md:text-sm text-theme-text-secondary bg-theme-bg-secondary p-4 md:p-6 rounded-lg overflow-x-auto overflow-y-auto max-h-[60vh] md:max-h-[70vh] whitespace-pre-wrap break-words font-mono border border-theme-border shadow-sm">
{error?.stack}
</pre>
</div>
)}
<div className="flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto">
<button
onClick={resetErrorBoundary}
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
<ArrowClockwise className="w-4 h-4" />
Reset
</button>
<NavLink
to="/"
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
<House className="w-4 h-4" />
Home
</NavLink>
</div>
</div>
);
}

View File

@@ -33,7 +33,7 @@ export default function DellProAIStudioOptions({
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-2">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -0,0 +1,386 @@
import { useState, useEffect } from "react";
import System from "@/models/system";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
import { CircleNotch, Info } from "@phosphor-icons/react";
import strDistance from "js-levenshtein";
import { LLM_PREFERENCE_CHANGED_EVENT } from "@/pages/GeneralSettings/LLMPreference";
import { DOCKER_MODEL_RUNNER_COMMON_URLS } from "@/utils/constants";
import { Tooltip } from "react-tooltip";
import { Link } from "react-router-dom";
import ModelTable from "@/components/lib/ModelTable";
import ModelTableLayout from "@/components/lib/ModelTable/layout";
import ModelTableLoadingSkeleton from "@/components/lib/ModelTable/loading";
import DMRUtils from "@/models/utils/dmrUtils";
import showToast from "@/utils/toast";
export default function DockerModelRunnerOptions({ settings }) {
const {
autoDetecting: loading,
basePath,
basePathValue,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "docker-model-runner",
initialBasePath: settings?.DockerModelRunnerBasePath,
ENDPOINTS: DOCKER_MODEL_RUNNER_COMMON_URLS,
});
const [selectedModelId, setSelectedModelId] = useState(
settings?.DockerModelRunnerModelPref
);
const [maxTokens, setMaxTokens] = useState(
settings?.DockerModelRunnerModelTokenLimit || 4096
);
return (
<div className="w-full flex flex-col gap-y-7">
<div className="flex gap-[36px] mt-1.5 flex-wrap">
<div className="flex flex-col w-60">
<div className="flex items-center gap-1 mb-3">
<div className="flex justify-between items-center gap-x-2">
<label className="text-white text-sm font-semibold">
Base URL
</label>
{loading ? (
<CircleNotch className="w-4 h-4 text-theme-text-secondary animate-spin" />
) : (
<>
{!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>
<Tooltip
id="docker-model-runner-base-url"
place="top"
delayShow={300}
delayHide={800}
clickable={true}
className="tooltip !text-xs !opacity-100 z-99"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
Enter the URL where the Docker Model Runner is running.
<br />
<br />
You <b>must</b> have enabled the Docker Model Runner TCP support
for this to work.
<br />
<br />
<Link
to="https://docs.docker.com/ai/model-runner/get-started/#docker-desktop"
target="_blank"
className="text-blue-500 hover:underline"
>
Learn more &rarr;
</Link>
</Tooltip>
<div
className="text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full"
data-tooltip-id="docker-model-runner-base-url"
data-tooltip-place="top"
data-tooltip-delay-hide={800}
>
<Info size={18} className="text-theme-text-secondary" />
</div>
</div>
<input
type="url"
name="DockerModelRunnerBasePath"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="http://localhost:12434/engines/llama.cpp/v1"
value={basePathValue.value}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
</div>
<div className="flex flex-col w-60">
<div className="flex items-center gap-1 mb-3">
<label className="text-white text-sm font-semibold block">
Model context window
</label>
<Tooltip
id="docker-model-runner-model-context-window"
place="top"
delayShow={300}
delayHide={800}
clickable={true}
className="tooltip !text-xs !opacity-100 z-99"
style={{
maxWidth: "350px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
The maximum number of tokens that can be used for a model context
window.
<br />
<br />
To set the context window limit for a model, you can use the{" "}
<code>docker run</code> command with the{" "}
<code>--context-window</code> parameter.
<br />
<br />
<code>
docker model configure --context-size {maxTokens || 8192}{" "}
{selectedModelId ?? "ai/qwen3:latest"}
</code>
<br />
<br />
<Link
to="https://docs.docker.com/ai/model-runner/#context-size"
target="_blank"
className="text-blue-500 hover:underline"
>
Learn more &rarr;
</Link>
</Tooltip>
<div
className="text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full"
data-tooltip-id="docker-model-runner-model-context-window"
data-tooltip-place="top"
data-tooltip-delay-hide={800}
>
<Info size={18} className="text-theme-text-secondary" />
</div>
</div>
<input
type="number"
name="DockerModelRunnerModelTokenLimit"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="4096"
min={1}
value={maxTokens}
onChange={(e) => setMaxTokens(Number(e.target.value))}
onScroll={(e) => e.target.blur()}
required={true}
autoComplete="off"
/>
</div>
<DockerModelRunnerModelSelection
selectedModelId={selectedModelId}
setSelectedModelId={setSelectedModelId}
basePath={basePathValue.value}
/>
</div>
</div>
);
}
function DockerModelRunnerModelSelection({
selectedModelId,
setSelectedModelId,
basePath = null,
}) {
const [customModels, setCustomModels] = useState([]);
const [filteredModels, setFilteredModels] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
async function fetchModels() {
if (!basePath) {
setCustomModels([]);
setFilteredModels([]);
setLoading(false);
setSearchQuery("");
return;
}
setLoading(true);
const { models } = await System.customModels(
"docker-model-runner",
null,
basePath
);
setCustomModels(models || []);
setFilteredModels(models || []);
setSearchQuery("");
setLoading(false);
}
useEffect(() => {
fetchModels();
}, [basePath]);
useEffect(() => {
if (!searchQuery || !customModels.length) {
setFilteredModels(customModels || []);
return;
}
const normalizedSearchQuery = searchQuery.toLowerCase().trim();
const filteredModels = new Map();
customModels.forEach((model) => {
const modelNameNormalized = model.name.toLowerCase();
const modelOrganizationNormalized = model.organization.toLowerCase();
if (modelNameNormalized.startsWith(normalizedSearchQuery))
filteredModels.set(model.id, model);
if (modelOrganizationNormalized.startsWith(normalizedSearchQuery))
filteredModels.set(model.id, model);
if (strDistance(modelNameNormalized, normalizedSearchQuery) <= 2)
filteredModels.set(model.id, model);
if (strDistance(modelOrganizationNormalized, normalizedSearchQuery) <= 2)
filteredModels.set(model.id, model);
});
setFilteredModels(Array.from(filteredModels.values()));
}, [searchQuery]);
async function downloadModel(modelId, fileSize, progressCallback) {
try {
if (
!window.confirm(
`Are you sure you want to download this model? It is ${fileSize} in size and may take a while to download.`
)
)
return;
const { success, error } = await DMRUtils.downloadModel(
modelId,
basePath,
progressCallback
);
if (!success)
throw new Error(
error || "An error occurred while downloading the model"
);
progressCallback(100);
handleSetActiveModel(modelId);
const existingModels = [...customModels];
const newModel = existingModels.find((model) => model.id === modelId);
if (newModel) {
newModel.downloaded = true;
setCustomModels(existingModels);
setFilteredModels(existingModels);
setSearchQuery("");
}
} catch (e) {
console.error("Error downloading model:", e);
showToast(
e.message || "An error occurred while downloading the model",
"error",
{ clear: true }
);
} finally {
setLoading(false);
}
}
function groupModelsByAlias(models) {
const mapping = new Map();
mapping.set("installed", new Map());
mapping.set("not installed", new Map());
const groupedModels = models.reduce((acc, model) => {
acc[model.organization] = acc[model.organization] || [];
acc[model.organization].push(model);
return acc;
}, {});
Object.entries(groupedModels).forEach(([organization, models]) => {
const hasInstalled = models.some((model) => model.downloaded);
if (hasInstalled) {
const installedModels = models.filter((model) => model.downloaded);
mapping
.get("installed")
.set("Downloaded Models", [
...(mapping.get("installed").get("Downloaded Models") || []),
...installedModels,
]);
}
const tags = models.map((model) => ({
...model,
name: model.name.split(":")[1],
}));
mapping.get("not installed").set(organization, tags);
});
const orderedMap = new Map();
const installedMap = new Map();
mapping
.get("installed")
.entries()
.forEach(([organization, models]) =>
installedMap.set(organization, models)
);
mapping
.get("not installed")
.entries()
.forEach(([organization, models]) =>
orderedMap.set(organization, models)
);
// Sort the models by organization/creator name alphabetically but keep the installed models at the top
return Object.fromEntries(
Array.from(installedMap.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.concat(
Array.from(orderedMap.entries()).sort((a, b) =>
a[0].localeCompare(b[0])
)
)
);
}
function handleSetActiveModel(modelId) {
if (modelId === selectedModelId) return;
setSelectedModelId(modelId);
window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT));
}
const groupedModels = groupModelsByAlias(filteredModels);
return (
<ModelTableLayout
fetchModels={fetchModels}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
loading={loading}
>
<Tooltip
id="docker-model-runner-install-model-tooltip"
place="top"
className="tooltip !text-xs !opacity-100 z-99"
/>
<input
type="hidden"
name="DockerModelRunnerModelPref"
id="DockerModelRunnerModelPref"
value={selectedModelId}
/>
{loading ? (
<ModelTableLoadingSkeleton />
) : filteredModels.length === 0 ? (
<div className="flex flex-col w-full gap-y-2 mt-4">
<p className="text-theme-text-secondary text-sm">No models found!</p>
</div>
) : (
Object.entries(groupedModels).map(([alias, models]) => (
<ModelTable
key={alias}
alias={alias}
models={models}
setActiveModel={handleSetActiveModel}
downloadModel={downloadModel}
selectedModelId={selectedModelId}
ui={{
showRuntime: false,
}}
/>
))
)}
</ModelTableLayout>
);
}

View File

@@ -92,7 +92,7 @@ export default function FoundryOptions({ settings }) {
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Token Context Window
Model context window
</label>
<input
type="number"

View File

@@ -50,7 +50,7 @@ export default function GenericOpenAiOptions({ settings }) {
<div className="flex gap-[36px] flex-wrap">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -24,7 +24,7 @@ export default function GiteeAIOptions({ settings }) {
<GiteeAIModelSelection settings={settings} />
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-2">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -43,7 +43,7 @@ export default function KoboldCPPOptions({ settings }) {
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-2">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -34,7 +34,7 @@ export default function LiteLLMOptions({ settings }) {
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -51,7 +51,7 @@ export default function LocalAiOptions({ settings, showAlert = false }) {
/>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-2">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -18,7 +18,7 @@ export default function TextGenWebUIOptions({ settings }) {
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Token context window
Model context window
</label>
<input
type="number"

View File

@@ -0,0 +1,384 @@
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import OpenAiLogo from "@/media/llmprovider/openai.png";
import GenericOpenAiLogo from "@/media/llmprovider/generic-openai.png";
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
import AnthropicLogo from "@/media/llmprovider/anthropic.png";
import GeminiLogo from "@/media/llmprovider/gemini.png";
import OllamaLogo from "@/media/llmprovider/ollama.png";
import TogetherAILogo from "@/media/llmprovider/togetherai.png";
import FireworksAILogo from "@/media/llmprovider/fireworksai.jpeg";
import NvidiaNimLogo from "@/media/llmprovider/nvidia-nim.png";
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
import LocalAiLogo from "@/media/llmprovider/localai.png";
import MistralLogo from "@/media/llmprovider/mistral.jpeg";
import HuggingFaceLogo from "@/media/llmprovider/huggingface.png";
import PerplexityLogo from "@/media/llmprovider/perplexity.png";
import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
import NovitaLogo from "@/media/llmprovider/novita.png";
import GroqLogo from "@/media/llmprovider/groq.png";
import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png";
import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
import APIPieLogo from "@/media/llmprovider/apipie.png";
import XAILogo from "@/media/llmprovider/xai.png";
import ZAiLogo from "@/media/llmprovider/zai.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import ChromaLogo from "@/media/vectordbs/chroma.png";
import PineconeLogo from "@/media/vectordbs/pinecone.png";
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
import QDrantLogo from "@/media/vectordbs/qdrant.png";
import MilvusLogo from "@/media/vectordbs/milvus.png";
import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
import PPIOLogo from "@/media/llmprovider/ppio.png";
import PGVectorLogo from "@/media/vectordbs/pgvector.png";
import DPAISLogo from "@/media/llmprovider/dpais.png";
import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png";
import CometApiLogo from "@/media/llmprovider/cometapi.png";
import FoundryLogo from "@/media/llmprovider/foundry-local.png";
import GiteeAILogo from "@/media/llmprovider/giteeai.png";
import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
const LLM_PROVIDER_PRIVACY_MAP = {
openai: {
name: "OpenAI",
policyUrl: "https://openai.com/policies/privacy-policy/",
logo: OpenAiLogo,
},
azure: {
name: "Azure OpenAI",
policyUrl: "https://privacy.microsoft.com/privacystatement",
logo: AzureOpenAiLogo,
},
anthropic: {
name: "Anthropic",
policyUrl: "https://www.anthropic.com/privacy",
logo: AnthropicLogo,
},
gemini: {
name: "Google Gemini",
policyUrl: "https://policies.google.com/privacy",
logo: GeminiLogo,
},
"nvidia-nim": {
name: "NVIDIA NIM",
description: [
"Your model and chats are only accessible on the machine running the NVIDIA NIM.",
],
logo: NvidiaNimLogo,
},
lmstudio: {
name: "LMStudio",
description: [
"Your model and chats are only accessible on the server running LMStudio.",
],
logo: LMStudioLogo,
},
localai: {
name: "LocalAI",
description: [
"Your model and chats are only accessible on the server running LocalAI.",
],
logo: LocalAiLogo,
},
ollama: {
name: "Ollama",
description: [
"Your model and chats are only accessible on the machine running Ollama models.",
],
logo: OllamaLogo,
},
togetherai: {
name: "TogetherAI",
policyUrl: "https://www.together.ai/privacy",
logo: TogetherAILogo,
},
fireworksai: {
name: "FireworksAI",
policyUrl: "https://fireworks.ai/privacy-policy",
logo: FireworksAILogo,
},
mistral: {
name: "Mistral",
policyUrl: "https://legal.mistral.ai/terms/privacy-policy",
logo: MistralLogo,
},
huggingface: {
name: "HuggingFace",
policyUrl: "https://huggingface.co/privacy",
logo: HuggingFaceLogo,
},
perplexity: {
name: "Perplexity AI",
policyUrl: "https://www.perplexity.ai/privacy",
logo: PerplexityLogo,
},
openrouter: {
name: "OpenRouter",
policyUrl: "https://openrouter.ai/privacy",
logo: OpenRouterLogo,
},
novita: {
name: "Novita AI",
policyUrl: "https://novita.ai/legal/privacy-policy",
logo: NovitaLogo,
},
groq: {
name: "Groq",
policyUrl: "https://groq.com/privacy-policy/",
logo: GroqLogo,
},
koboldcpp: {
name: "KoboldCPP",
description: [
"Your model and chats are only accessible on the server running KoboldCPP",
],
logo: KoboldCPPLogo,
},
textgenwebui: {
name: "Oobabooga Web UI",
description: [
"Your model and chats are only accessible on the server running the Oobabooga Text Generation Web UI",
],
logo: TextGenWebUILogo,
},
"generic-openai": {
name: "Generic OpenAI compatible service",
description: [
"Data is shared according to the terms of service applicable with your generic endpoint provider.",
],
logo: GenericOpenAiLogo,
},
cohere: {
name: "Cohere",
policyUrl: "https://cohere.com/privacy",
logo: CohereLogo,
},
litellm: {
name: "LiteLLM",
description: [
"Your model and chats are only accessible on the server running LiteLLM",
],
logo: LiteLLMLogo,
},
bedrock: {
name: "AWS Bedrock",
policyUrl: "https://aws.amazon.com/bedrock/security-compliance/",
logo: AWSBedrockLogo,
},
deepseek: {
name: "DeepSeek",
policyUrl:
"https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html",
logo: DeepSeekLogo,
},
apipie: {
name: "APIpie.AI",
policyUrl: "https://apipie.ai/docs/Terms/privacy",
logo: APIPieLogo,
},
xai: {
name: "xAI",
policyUrl: "https://x.ai/legal/privacy-policy",
logo: XAILogo,
},
zai: {
name: "Z.AI",
policyUrl: "https://docs.z.ai/legal-agreement/privacy-policy",
logo: ZAiLogo,
},
ppio: {
name: "PPIO",
policyUrl: "https://www.pipio.ai/privacy-policy",
logo: PPIOLogo,
},
dpais: {
name: "Dell Pro AI Studio",
description: [
"Your model and chat contents are only accessible on the computer running Dell Pro AI Studio.",
],
logo: DPAISLogo,
},
moonshotai: {
name: "Moonshot AI",
policyUrl: "https://platform.moonshot.ai/docs/agreement/userprivacy",
logo: MoonshotAiLogo,
},
cometapi: {
name: "CometAPI",
policyUrl: "https://apidoc.cometapi.com/privacy-policy-873819m0",
logo: CometApiLogo,
},
foundry: {
name: "Microsoft Foundry Local",
description: [
"Your model and chats are only accessible on the machine running Foundry Local.",
],
logo: FoundryLogo,
},
giteeai: {
name: "GiteeAI",
policyUrl: "https://ai.gitee.com/docs/appendix/privacy",
logo: GiteeAILogo,
},
"docker-model-runner": {
name: "Docker Model Runner",
description: [
"Your model and chats are only accessible on the machine running Docker Model Runner.",
],
logo: DockerModelRunnerLogo,
},
};
const VECTOR_DB_PROVIDER_PRIVACY_MAP = {
pgvector: {
name: "PGVector",
description: [
"Your vectors and document text are stored on your PostgreSQL instance.",
"Access to your instance is managed by you.",
],
logo: PGVectorLogo,
},
chroma: {
name: "Chroma",
description: [
"Your vectors and document text are stored on your Chroma instance.",
"Access to your instance is managed by you.",
],
logo: ChromaLogo,
},
chromacloud: {
name: "Chroma Cloud",
policyUrl: "https://www.trychroma.com/privacy",
logo: ChromaLogo,
},
pinecone: {
name: "Pinecone",
policyUrl: "https://www.pinecone.io/privacy/",
logo: PineconeLogo,
},
qdrant: {
name: "Qdrant",
policyUrl: "https://qdrant.tech/legal/privacy-policy/",
logo: QDrantLogo,
},
weaviate: {
name: "Weaviate",
policyUrl: "https://weaviate.io/privacy",
logo: WeaviateLogo,
},
milvus: {
name: "Milvus",
description: [
"Your vectors and document text are stored on your Milvus instance (cloud or self-hosted).",
],
logo: MilvusLogo,
},
zilliz: {
name: "Zilliz Cloud",
policyUrl: "https://zilliz.com/privacy-policy",
logo: ZillizLogo,
},
astra: {
name: "AstraDB",
policyUrl: "https://www.ibm.com/us-en/privacy",
logo: AstraDBLogo,
},
lancedb: {
name: "LanceDB",
description: [
"Your vectors and document text are stored privately on this instance of AnythingLLM.",
],
logo: LanceDbLogo,
},
};
const EMBEDDING_ENGINE_PROVIDER_PRIVACY_MAP = {
native: {
name: "AnythingLLM Embedder",
description: [
"Your document text is embedded privately on this instance of AnythingLLM.",
],
logo: AnythingLLMIcon,
},
openai: {
name: "OpenAI",
policyUrl: "https://openai.com/policies/privacy-policy/",
logo: OpenAiLogo,
},
azure: {
name: "Azure OpenAI",
policyUrl: "https://privacy.microsoft.com/privacystatement",
logo: AzureOpenAiLogo,
},
localai: {
name: "LocalAI",
description: [
"Your document text is embedded privately on the server running LocalAI.",
],
logo: LocalAiLogo,
},
ollama: {
name: "Ollama",
description: [
"Your document text is embedded privately on the server running Ollama.",
],
logo: OllamaLogo,
},
lmstudio: {
name: "LMStudio",
description: [
"Your document text is embedded privately on the server running LMStudio.",
],
logo: LMStudioLogo,
},
openrouter: {
name: "OpenRouter",
policyUrl: "https://openrouter.ai/privacy",
logo: OpenRouterLogo,
},
cohere: {
name: "Cohere",
policyUrl: "https://cohere.com/privacy",
logo: CohereLogo,
},
voyageai: {
name: "Voyage AI",
policyUrl: "https://www.voyageai.com/privacy",
logo: VoyageAiLogo,
},
mistral: {
name: "Mistral AI",
policyUrl: "https://legal.mistral.ai/terms/privacy-policy",
logo: MistralLogo,
},
litellm: {
name: "LiteLLM",
description: [
"Your document text is only accessible on the server running LiteLLM and to the providers you configured in LiteLLM.",
],
logo: LiteLLMLogo,
},
"generic-openai": {
name: "Generic OpenAI compatible service",
description: [
"Data is shared according to the terms of service applicable with your generic endpoint provider.",
],
logo: GenericOpenAiLogo,
},
gemini: {
name: "Google Gemini",
policyUrl: "https://policies.google.com/privacy",
logo: GeminiLogo,
},
};
export const PROVIDER_PRIVACY_MAP = {
llm: LLM_PROVIDER_PRIVACY_MAP,
embeddingEngine: EMBEDDING_ENGINE_PROVIDER_PRIVACY_MAP,
vectorDb: VECTOR_DB_PROVIDER_PRIVACY_MAP,
};

View File

@@ -0,0 +1,119 @@
import { useState, useEffect } from "react";
import System from "@/models/system";
import { PROVIDER_PRIVACY_MAP } from "./constants";
import { ArrowSquareOut } from "@phosphor-icons/react";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import { Link } from "react-router-dom";
import { titleCase, sentenceCase } from "text-case";
function defaultProvider(providerString) {
return {
name: providerString
? titleCase(sentenceCase(String(providerString)))
: "Unknown",
description: [
`"${providerString}" has no known data handling policy defined in AnythingLLM.`,
],
logo: AnythingLLMIcon,
};
}
export default function ProviderPrivacy() {
const [loading, setLoading] = useState(true);
const [providers, setProviders] = useState({
llmProvider: null,
embeddingEngine: null,
vectorDb: null,
});
useEffect(() => {
async function fetchProviders() {
const _settings = await System.keys();
const providerDefinition =
PROVIDER_PRIVACY_MAP.llm[_settings?.LLMProvider] ||
defaultProvider(_settings?.LLMProvider);
const embeddingEngineDefinition =
PROVIDER_PRIVACY_MAP.embeddingEngine[_settings?.EmbeddingEngine] ||
defaultProvider(_settings?.EmbeddingEngine);
const vectorDbDefinition =
PROVIDER_PRIVACY_MAP.vectorDb[_settings?.VectorDB] ||
defaultProvider(_settings?.VectorDB);
setProviders({
llmProvider: providerDefinition,
embeddingEngine: embeddingEngineDefinition,
vectorDb: vectorDbDefinition,
});
setLoading(false);
}
fetchProviders();
}, []);
if (loading) return null;
return (
<div className="flex flex-col gap-8 w-full max-w-2xl">
<ProviderPrivacyItem
title="LLM Provider"
provider={providers.llmProvider}
altText="LLM Logo"
/>
<ProviderPrivacyItem
title="Embedding Preference"
provider={providers.embeddingEngine}
altText="Embedding Logo"
/>
<ProviderPrivacyItem
title="Vector Database"
provider={providers.vectorDb}
altText="Vector DB Logo"
/>
</div>
);
}
function ProviderPrivacyItem({ title, provider, altText }) {
return (
<div className="flex flex-col items-start gap-y-3 pb-4 border-b border-theme-sidebar-border">
<div className="text-theme-text-primary text-base font-bold">{title}</div>
<div className="flex items-start gap-3">
<img
src={provider.logo}
alt={altText}
className="w-8 h-8 rounded flex-shrink-0 mt-0.5"
/>
<div className="flex flex-col gap-2 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-theme-text-primary text-sm font-semibold">
{provider.name}
</span>
</div>
{provider.policyUrl ? (
<div className="text-theme-text-secondary text-sm">
Your usage, chats, and data are subject to the service&apos;s{" "}
<Link
className="text-theme-text-secondary hover:text-theme-text-primary text-sm font-medium underline transition-colors inline-flex items-center gap-1"
to={provider.policyUrl}
target="_blank"
rel="noopener noreferrer"
>
privacy policy
<ArrowSquareOut size={12} />
</Link>
.
</div>
) : (
provider.description && (
<ul className="flex flex-col list-none gap-1">
{provider.description.map((desc, idx) => (
<li key={idx} className="text-theme-text-secondary text-sm">
{desc}
</li>
))}
</ul>
)
)}
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { CaretRight } from "@phosphor-icons/react";
import { Link, useLocation } from "react-router-dom";
import { safeJsonParse } from "@/utils/request";
export default function MenuOption({
btnText,
@@ -130,7 +131,7 @@ function useIsExpanded({
if (hasVisibleChildren) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== null) {
return JSON.parse(storedValue);
return safeJsonParse(storedValue, false);
}
return childOptions.some((child) => child.href === location);
}

View File

@@ -386,6 +386,12 @@ const SidebarOptions = ({ user = null, t }) => (
flex: true,
roles: ["admin", "manager"],
},
{
btnText: t("settings.mobile-app"),
href: paths.settings.mobile(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option

View File

@@ -10,6 +10,7 @@ import { useTheme } from "@/hooks/useTheme";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { Tooltip } from "react-tooltip";
import { safeJsonParse } from "@/utils/request";
export default function AccountModal({ user, hideModal }) {
const { pfp, setPfp } = usePfp();
@@ -54,7 +55,7 @@ export default function AccountModal({ user, hideModal }) {
const { success, error } = await System.updateUser(data);
if (success) {
let storedUser = JSON.parse(localStorage.getItem(AUTH_USER));
let storedUser = safeJsonParse(localStorage.getItem(AUTH_USER), null);
if (storedUser) {
storedUser.username = data.username;
storedUser.bio = data.bio;

View File

@@ -1,3 +1,4 @@
import { formatDateTimeAsMoment } from "@/utils/directories";
import { numberWithCommas } from "@/utils/numbers";
import React, { useEffect, useState, useContext } from "react";
const MetricsContext = React.createContext();
@@ -41,6 +42,26 @@ function getAutoShowMetrics() {
return window?.localStorage?.getItem(SHOW_METRICS_KEY) === "true";
}
/**
* Build the metrics string for a given metrics object
* - Model name
* - Duration and output TPS
* - Timestamp
* @param {metrics: {duration:number, outputTps: number, model?: string, timestamp?: number}} metrics
* @returns {string}
*/
function buildMetricsString(metrics = {}) {
return [
metrics?.model ? metrics.model : "",
`${formatDuration(metrics.duration)} (${formatTps(metrics.outputTps)} tok/s)`,
metrics?.timestamp
? formatDateTimeAsMoment(metrics.timestamp, "MMM D, h:mm A")
: "",
]
.filter(Boolean)
.join(" · ");
}
/**
* Toggle the show metrics setting in localStorage `anythingllm_show_chat_metrics` key
* @returns {void}
@@ -88,7 +109,7 @@ export function MetricsProvider({ children }) {
/**
* Render the metrics for a given chat, if available
* @param {metrics: {duration:number, outputTps: number}} props
* @param {metrics: {duration:number, outputTps: number, model: string, timestamp: number}} props
* @returns
*/
export default function RenderMetrics({ metrics = {} }) {
@@ -110,8 +131,7 @@ export default function RenderMetrics({ metrics = {} }) {
className={`border-none flex justify-end items-center gap-x-[8px] ${showMetricsAutomatically ? "opacity-100" : "opacity-0"} md:group-hover:opacity-100 transition-all duration-300`}
>
<p className="cursor-pointer text-xs font-mono text-theme-text-secondary opacity-50">
{formatDuration(metrics.duration)} ({formatTps(metrics.outputTps)}{" "}
tok/s)
{buildMetricsString(metrics)}
</p>
</button>
);

View File

@@ -0,0 +1,359 @@
import { useRef, useState, useEffect } from "react";
import {
CaretDown,
CaretRight,
Cpu,
Circle,
DotsThreeVertical,
CloudArrowDown,
CircleNotch,
} from "@phosphor-icons/react";
import pluralize from "pluralize";
import { titleCase } from "text-case";
import { humanFileSize } from "@/utils/numbers";
import MonoProviderIcon from "../MonoProviderIcon";
/**
* @typedef {Object} ModelDefinition
* @property {string} id - The ID of the model.
* @property {'CPU' | 'GPU' | 'NPU'} deviceType - The device type of the model.
* @property {number} modelSize - The size of the model in megabytes.
* @property {boolean} downloaded - Whether the model is downloaded.
*/
/**
* @param {object} props - The props of the component.
* @param {string} props.alias - The alias of the model.
* @param {Array<ModelDefinition>} props.models - The models to display.
* @param {(model: string, progressCallback: (percentage: number) => void) => void} props.downloadModel - The function to download the model.
* @param {(model: string) => void} props.uninstallModel - The function to uninstall the model.
* @param {(model: string) => void} props.setActiveModel - The function to set the active model.
* @param {string} props.selectedModelId - The ID of the selected model.
* @param {object} props.ui - The UI configuration.
* @param {boolean} props.ui.showRuntime - Whether to show the runtime.
* @returns {React.ReactNode}
*/
export default function ModelTable({
alias = "",
models = [],
downloadModel = null,
uninstallModel = null,
setActiveModel = () => {},
selectedModelId = "",
ui = {
showRuntime: true,
},
}) {
const [showAll, setShowAll] = useState(
models.some((model) => model.downloaded)
);
const totalModels = models.length;
return (
<div className="flex flex-col w-full border-b border-theme-modal-border py-[18px]">
<button
type="button"
onClick={() => setShowAll(!showAll)}
className="border-none text-theme-text-secondary text-sm font-medium hover:underline flex items-center gap-x-[8px]"
>
{showAll ? (
<CaretDown
size={16}
weight="bold"
className="text-theme-text-secondary"
/>
) : (
<CaretRight
size={16}
weight="bold"
className="text-theme-text-secondary"
/>
)}
<div className="flex items-center gap-x-[4px]">
<MonoProviderIcon
provider={alias}
match="pattern"
size={16}
className="text-theme-text-primary"
/>
<p className="flex items-center gap-x-1 text-theme-text-primary text-base font-bold">
{titleCase(alias)}
<span className="text-theme-text-secondary font-normal text-sm">
({totalModels} {pluralize("Model", totalModels)})
</span>
</p>
</div>
</button>
<div hidden={!showAll} className="mt-[16px]">
<div className="w-full flex flex-col gap-y-[8px]">
{models.map((model) => (
<ModelRow
key={model.id}
alias={alias}
model={model}
downloadModel={downloadModel}
uninstallModel={uninstallModel}
setActiveModel={setActiveModel}
selectedModelId={selectedModelId}
ui={ui}
/>
))}
</div>
</div>
</div>
);
}
/**
* @param {{deviceType: ModelDefinition["deviceType"]}} deviceType
* @returns {React.ReactNode}
*/
function DeviceTypeTag({ deviceType }) {
const Wrapper = ({ text, bgClass, textClass }) => {
return (
<div
className={
bgClass + " px-1.5 py-1 rounded-full flex items-center gap-x-1 w-fit"
}
>
<Cpu size={14} weight="bold" className={textClass} />
<p className={textClass + " text-xs"}>{text}</p>
</div>
);
};
switch (deviceType?.toLowerCase()) {
case "cpu":
return (
<Wrapper
text="CPU"
bgClass="bg-zinc-800 light:bg-zinc-200"
textClass="text-theme-text-primary"
/>
);
case "gpu":
return (
<Wrapper
text="GPU"
bgClass="bg-green-800 light:bg-green-200"
textClass="text-theme-text-primary"
/>
);
case "npu":
return (
<Wrapper
text="NPU"
bgClass="bg-indigo-800 light:bg-indigo-200"
textClass="text-theme-text-primary"
/>
);
default:
return (
<Wrapper
text="CPU"
bgClass="bg-zinc-800 light:bg-zinc-200"
textClass="text-theme-text-primary"
/>
);
}
}
/**
* @param {object} props - The props of the component.
* @param {ModelDefinition} props.model - The model to display.
* @param {(model: string, progressCallback: (percentage: number) => void) => Promise<void>} props.downloadModel - The function to download the model.
* @param {(model: string) => Promise<void>} props.uninstallModel - The function to uninstall the model.
* @param {(model: string) => void} props.setActiveModel - The function to set the active model.
* @param {string} props.selectedModelId - The ID of the selected model.
* @param {object} props.ui - The UI configuration.
* @param {boolean} props.ui.showRuntime - Whether to show the runtime.
* @returns {React.ReactNode}
*/
function ModelRow({
alias,
model,
downloadModel = null,
uninstallModel = null,
setActiveModel,
selectedModelId,
ui = {
showRuntime: true,
},
}) {
const modelRowRef = useRef(null);
const [showOptions, setShowOptions] = useState(false);
const [processing, setProcessing] = useState(false);
const [downloadPercentage, setDownloadPercentage] = useState(0);
const fileSize =
typeof model.size === "number"
? humanFileSize(model.size * 1e6, true, 2)
: (model.size ?? "Unknown size");
const [isActiveModel, setIsActiveModel] = useState(
selectedModelId === model.id
);
async function handleSetActiveModel() {
setDownloadPercentage(0);
if (model.downloaded) setActiveModel(model.id);
else {
try {
if (!downloadModel) return;
setProcessing(true);
await downloadModel(model.id, fileSize, (percentage) => {
setDownloadPercentage(percentage);
});
} catch {
} finally {
setProcessing(false);
}
}
}
async function handleUninstallModel() {
if (!uninstallModel) return;
try {
setProcessing(true);
await uninstallModel(model.id);
} catch {
} finally {
setProcessing(false);
}
}
useEffect(() => {
if (selectedModelId === model.id) {
setIsActiveModel(true);
modelRowRef.current.classList.add("!bg-gray-200/10");
setTimeout(
() => modelRowRef.current.classList.remove("!bg-gray-200/10"),
800
);
} else {
setIsActiveModel(false);
}
}, [selectedModelId]);
return (
<div
ref={modelRowRef}
className="w-full grid grid-cols-[1fr_auto_1fr] items-center gap-x-4 transition-all duration-300 rounded-lg"
>
<button
type="button"
className="border-none flex items-center gap-x-[8px] whitespace-nowrap py-[8px]"
disabled={processing}
onClick={handleSetActiveModel}
>
{ui.showRuntime && <DeviceTypeTag deviceType={model.deviceType} />}
{!ui.showRuntime &&
model.downloaded &&
alias === "Downloaded Models" && (
<MonoProviderIcon
provider={model.organization}
match="pattern"
size={16}
className="text-theme-text-primary"
/>
)}
<p className="text-theme-text-primary text-base">{model.name}</p>
<p className="text-theme-text-secondary opacity-70 text-base">
{fileSize}
</p>
</button>
<div className="justify-self-start">
<RenderStatus model={model} isActiveModel={isActiveModel} />
</div>
<div className="relative justify-self-end">
{uninstallModel && model.downloaded ? (
<>
<button
type="button"
className="border-none hover:bg-white/20 rounded-lg p-1"
onClick={() => setShowOptions(!showOptions)}
>
<DotsThreeVertical
size={22}
weight="bold"
className="text-theme-text-primary cursor-pointer"
/>
</button>
{showOptions && (
<div className="absolute top-[20px] right-[20px] bg-theme-action-menu-bg border border-theme-modal-border rounded-lg py-2 px-4 shadow-lg">
<button
type="button"
className="border-none font-medium group"
onClick={handleUninstallModel}
>
<p className="text-sm text-theme-text-primary group-hover:underline group-hover:text-theme-text-secondary">
Uninstall
</p>
</button>
</div>
)}
</>
) : null}
{!model.downloaded && !processing && (
<button
type="button"
data-tooltip-id="docker-model-runner-install-model-tooltip"
data-tooltip-place="top"
data-tooltip-delay-show={300}
data-tooltip-content={`Install ${model.organization}:${model.name}`}
className="border-none hover:bg-white/20 light:hover:bg-black/5 rounded-lg p-2 flex items-center gap-x-1 cursor-pointer"
onClick={handleSetActiveModel}
>
<CloudArrowDown
size={20}
weight="bold"
className="text-theme-text-primary"
/>
</button>
)}
{!model.downloaded && processing && (
<div className="flex items-center justify-center gap-x-[10px] whitespace-nowrap">
{!downloadPercentage && (
<CircleNotch
size={16}
weight="bold"
className="text-theme-text-primary animate-spin"
/>
)}
<p className="text-theme-text-secondary text-sm">
{downloadPercentage}%
</p>
</div>
)}
</div>
</div>
);
}
function RenderStatus({ model, isActiveModel }) {
if (isActiveModel) {
return (
<div className="flex items-center justify-center gap-x-[10px] whitespace-nowrap">
<Circle size={8} weight="fill" className="text-green-500" />
<p className="text-theme-text-primary text-sm">Active</p>
</div>
);
}
if (!isActiveModel && model.downloaded) {
return (
<p className="text-theme-text-secondary text-sm italic whitespace-nowrap">
Installed
</p>
);
}
if (!model.downloaded) {
return (
<p className="text-theme-text-secondary text-sm italic whitespace-nowrap">
Not Installed
</p>
);
}
return null;
}

View File

@@ -0,0 +1,80 @@
import { useState } from "react";
import {
ArrowClockwise,
CircleNotch,
MagnifyingGlass,
} from "@phosphor-icons/react";
export default function ModelTableLayout({
children,
fetchModels = null,
searchQuery = "",
setSearchQuery = () => {},
loading = false,
}) {
const [isRefreshing, setIsRefreshing] = useState(false);
async function refreshModels() {
setIsRefreshing(true);
try {
await fetchModels?.();
} catch {
} finally {
setIsRefreshing(false);
}
}
return (
<div className="flex flex-col w-full">
<div className="flex gap-x-2 items-center pb-[8px]">
<label className="text-theme-text-primary text-base font-semibold">
Available Models
</label>
</div>
<div className="flex w-full items-center gap-x-[16px]">
<div className="relative flex-1 flex-grow">
<MagnifyingGlass
size={16}
weight="bold"
color="var(--theme-text-primary)"
className="absolute left-[9px] top-[10px] text-theme-settings-input-placeholder peer-focus:invisible"
/>
<input
type="search"
placeholder="Search models"
value={searchQuery}
disabled={loading}
className="min-h-[32px] border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pl-[30px] py-2 search-input disabled:opacity-50 disabled:cursor-not-allowed"
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
setSearchQuery(e.target.value);
}}
/>
</div>
{!!fetchModels && (
<button
type="button"
onClick={refreshModels}
disabled={isRefreshing || loading}
className="border-none text-theme-text-secondary text-sm font-medium hover:bg-white/10 light:hover:bg-black/5 rounded-lg px-2 h-full flex items-center gap-x-1 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRefreshing ? (
<CircleNotch className="w-4 h-4 text-theme-text-secondary animate-spin" />
) : (
<ArrowClockwise
weight="bold"
className="w-4 h-4 text-theme-text-secondary"
/>
)}
<span
className={`text-sm font-medium ${isRefreshing ? "hidden" : "text-theme-text-secondary"}`}
>
Refresh Models
</span>
</button>
)}
</div>
{children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
export default function ModelTableLoadingSkeleton() {
return (
<div className="flex flex-col w-full gap-y-4 pt-4">
<Skeleton.default
height={100}
width="100%"
count={7}
highlightColor="var(--theme-settings-input-active)"
baseColor="var(--theme-settings-input-bg)"
enableAnimation={true}
containerClassName="w-fill flex gap-[8px] flex-col p-0"
/>
</div>
);
}

View File

@@ -0,0 +1,103 @@
// https://lobehub.com/icons for all the icons
import OpenAI from "@lobehub/icons/es/OpenAI/components/Mono";
import Anthropic from "@lobehub/icons/es/Anthropic/components/Mono";
import Google from "@lobehub/icons/es/Google/components/Mono";
import Gemma from "@lobehub/icons/es/Gemma/components/Mono";
import Gemini from "@lobehub/icons/es/Gemini/components/Mono";
import Microsoft from "@lobehub/icons/es/Microsoft/components/Mono";
import Meta from "@lobehub/icons/es/Meta/components/Mono";
import Mistral from "@lobehub/icons/es/Mistral/components/Mono";
import Azure from "@lobehub/icons/es/Azure/components/Mono";
import AzureAI from "@lobehub/icons/es/AzureAI/components/Mono";
import DeepSeek from "@lobehub/icons/es/DeepSeek/components/Mono";
import HuggingFace from "@lobehub/icons/es/HuggingFace/components/Mono";
import Qwen from "@lobehub/icons/es/Qwen/components/Mono";
import IBM from "@lobehub/icons/es/IBM/components/Mono";
import Bytedance from "@lobehub/icons/es/ByteDance/components/Mono";
import Kimi from "@lobehub/icons/es/Kimi/components/Mono";
import Snowflake from "@lobehub/icons/es/Snowflake/components/Mono";
// Direct provider key -> icon mapping for exact matches
const providerIcons = {
openai: OpenAI,
anthropic: Anthropic,
google: Google,
microsoft: Microsoft,
gemma: Gemma,
gemini: Gemini,
meta: Meta,
mistral: Mistral,
azure: Azure,
azureai: AzureAI,
deepseek: DeepSeek,
huggingface: HuggingFace,
qwen: Qwen,
qwq: Qwen,
ibm: IBM,
bytedance: Bytedance,
kimi: Kimi,
};
// Pattern matching rules: regex pattern -> icon component
// These are checked in order, first match wins
const modelPatterns = [
{ pattern: /^gpt/i, icon: OpenAI },
{ pattern: /^o\d+/i, icon: OpenAI }, // o1, o3, etc.
{ pattern: /^claude-/i, icon: Anthropic },
{ pattern: /^gemini-/i, icon: Gemini },
{ pattern: /gemma/i, icon: Gemma },
{ pattern: /llama/i, icon: Meta },
{ pattern: /^meta/i, icon: Meta },
{
pattern: /^(mistral|devstral|mixtral|magistral|codestral|ministral)/i,
icon: Mistral,
},
{ pattern: /^deepseek/i, icon: DeepSeek },
{ pattern: /^qwen/i, icon: Qwen },
{ pattern: /^qwq/i, icon: Qwen },
{ pattern: /^phi/i, icon: Microsoft },
{ pattern: /^granite/i, icon: IBM },
{ pattern: /^doubao/i, icon: Bytedance },
{ pattern: /^moonshot/i, icon: Kimi },
{ pattern: /^smol/i, icon: HuggingFace },
{ pattern: /^seed/i, icon: Bytedance },
{ pattern: /^kimi/i, icon: Kimi },
{ pattern: /^snowflake/i, icon: Snowflake },
];
/**
* Find icon by matching model name against known patterns
* @param {string} modelName - The model name to match
* @returns {React.ComponentType|null}
*/
function findIconByModelName(modelName) {
if (!modelName) return null;
const match = modelPatterns.find(({ pattern }) => pattern.test(modelName));
return match?.icon || null;
}
/**
* @param {object} props - The props of the component.
* @param {string} props.provider - The provider key (for exact match) or model name (for pattern match).
* @param {('exact'|'pattern')} props.match - Match mode: 'exact' for provider key, 'pattern' for model name matching.
* @param {number} props.size - The size of the icon.
* @param {string} props.className - The class name of the icon.
* @param {string} props.fallbackIconKey - The key of the fallback icon to use if no icon is found.
* @returns {React.ReactNode}
*/
export default function MonoProviderIcon({
provider,
match = "exact",
size = 24,
className = "",
fallbackIconKey = null,
}) {
let Icon = null;
if (match === "exact") Icon = providerIcons[provider?.toLowerCase()];
else if (match === "pattern") Icon = findIconByModelName(provider);
if (!Icon && fallbackIconKey && providerIcons[fallbackIconKey])
Icon = providerIcons[fallbackIconKey];
if (!Icon) return null;
return <Icon size={size} className={className} />;
}

View File

@@ -52,6 +52,7 @@ const groupedProviders = [
"novita",
"openrouter",
"ppio",
"docker-model-runner",
];
export default function useGetProviderModels(provider = null) {
const [defaultModels, setDefaultModels] = useState([]);

View File

@@ -43,7 +43,6 @@ export default function usePromptInputStorage({
// Notify parent component so message state is synchronized
onChange({ target: { value: userPromptInputValue } });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const debouncedWriteToStorage = useMemo(

View File

@@ -101,6 +101,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

File diff suppressed because it is too large Load Diff

View File

@@ -103,6 +103,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -103,6 +103,7 @@ const TRANSLATIONS = {
contact: "Support kontaktieren",
"browser-extension": "Browser-Extension",
"system-prompt-variables": "Systempromptvariablen",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -112,6 +112,7 @@ const TRANSLATIONS = {
"experimental-features": "Experimental Features",
contact: "Contact Support",
"browser-extension": "Browser Extension",
"mobile-app": "AnythingLLM Mobile",
},
// Page Definitions
@@ -715,7 +716,7 @@ const TRANSLATIONS = {
title: "Privacy & Data-Handling",
description:
"This is your configuration for how connected third party providers and AnythingLLM handle your data.",
llm: "LLM Selection",
llm: "LLM Provider",
embedding: "Embedding Preference",
vector: "Vector Database",
anonymous: "Anonymous Telemetry Enabled",

View File

@@ -103,6 +103,7 @@ const TRANSLATIONS = {
"experimental-features": "Funciones experimentales",
contact: "Contactar con soporte",
"browser-extension": "Extensión del navegador",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -101,6 +101,7 @@ const TRANSLATIONS = {
"experimental-features": "Eksperimentaalsed funktsioonid",
contact: "Tugi",
"browser-extension": "Brauserilaiend",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -94,6 +94,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -102,6 +102,7 @@ const TRANSLATIONS = {
interface: "Interface",
branding: "Personnalisation",
chat: "Chat",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -100,6 +100,7 @@ const TRANSLATIONS = {
"experimental-features": "תכונות ניסיוניות",
contact: "צור קשר עם התמיכה",
"browser-extension": "תוסף דפדפן",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -94,6 +94,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -102,6 +102,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -101,6 +101,7 @@ const TRANSLATIONS = {
interface: "UI 환경 설정",
branding: "브랜딩 및 화이트라벨링",
chat: "채팅",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -102,6 +102,7 @@ const TRANSLATIONS = {
"experimental-features": "Eksperimentālās funkcijas",
contact: "Sazināties ar atbalstu",
"browser-extension": "Pārlūka paplašinājums",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -94,6 +94,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -103,6 +103,7 @@ const TRANSLATIONS = {
"experimental-features": "Funkcje eksperymentalne",
contact: "Kontakt z pomocą techniczną",
"browser-extension": "Rozszerzenie przeglądarki",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -101,6 +101,7 @@ const TRANSLATIONS = {
"experimental-features": "Recursos Experimentais",
contact: "Suporte",
"browser-extension": "Extensão de Navegador",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -36,6 +36,7 @@ import Japanese from "./ja/common.js";
import Lativian from "./lv/common.js";
import Polish from "./pl/common.js";
import Romanian from "./ro/common.js";
import Czech from "./cs/common.js";
export const defaultNS = "common";
export const resources = {
@@ -105,4 +106,7 @@ export const resources = {
ro: {
common: Romanian,
},
cs: {
common: Czech,
},
};

View File

@@ -103,6 +103,7 @@ const TRANSLATIONS = {
"experimental-features": "Funcții experimentale",
contact: "Contact suport",
"browser-extension": "Extensie browser",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -102,6 +102,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -94,6 +94,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -94,6 +94,7 @@ const TRANSLATIONS = {
interface: null,
branding: null,
chat: null,
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -98,6 +98,7 @@ const TRANSLATIONS = {
contact: "联系支持",
"browser-extension": "浏览器扩展",
"system-prompt-variables": "系统提示变量",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -98,6 +98,7 @@ const TRANSLATIONS = {
interface: "使用者介面偏好設定",
branding: "品牌與白標設定",
chat: "聊天室",
"mobile-app": null,
},
login: {
"multi-user": {

View File

@@ -372,6 +372,14 @@ const router = createBrowserRouter([
return { element: <ManagerRoute Component={MobileConnections} /> };
},
},
// Catch-all route for 404s
{
path: "*",
lazy: async () => {
const { default: NotFound } = await import("@/pages/404");
return { element: <NotFound /> };
},
},
],
},
]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,4 +1,5 @@
import { APPEARANCE_SETTINGS } from "@/utils/constants";
import { safeJsonParse } from "@/utils/request";
/**
* @typedef { 'showScrollbar' |
@@ -23,12 +24,8 @@ const Appearance = {
* @returns {{showScrollbar: boolean, autoSubmitSttInput: boolean, autoPlayAssistantTtsResponse: boolean, enableSpellCheck: boolean, renderHTML: boolean}}
*/
getSettings: () => {
try {
const settings = localStorage.getItem(APPEARANCE_SETTINGS);
return settings ? JSON.parse(settings) : Appearance.defaultSettings;
} catch (e) {
return Appearance.defaultSettings;
}
const settings = localStorage.getItem(APPEARANCE_SETTINGS);
return safeJsonParse(settings, Appearance.defaultSettings);
},
/**

View File

@@ -83,6 +83,22 @@ const System = {
return { valid: false, message: e.message };
});
},
/**
* Refreshes the user object from the session.
* @returns {Promise<{success: boolean, user: Object | null, message: string | null}>}
*/
refreshUser: () => {
return fetch(`${API_BASE}/system/refresh-user`, {
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not refresh user.");
return res.json();
})
.catch((e) => {
return { success: false, user: null, message: e.message };
});
},
recoverAccount: async function (username, recoveryCodes) {
return await fetch(`${API_BASE}/system/recover-account`, {
method: "POST",

View File

@@ -0,0 +1,77 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
import { safeJsonParse } from "@/utils/request";
const DMRUtils = {
/**
* Download a DMR model.
* @param {string} modelId - The ID of the model to download.
* @param {(percentage: number) => void} progressCallback - The callback to receive the progress percentage. If the model is already downloaded, it will be called once with 100.
* @returns {Promise<{success: boolean, error: string|null}>}
*/
downloadModel: async function (
modelId,
basePath = "",
progressCallback = () => {}
) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
try {
const response = await fetch(`${API_BASE}/utils/dmr/download-model`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ modelId, basePath }),
});
if (!response.ok)
throw new Error("Error downloading model: " + response.statusText);
const reader = response.body.getReader();
let done = false;
while (!done) {
const { value, done: readerDone } = await reader.read();
if (readerDone) {
done = true;
resolve({ success: true });
} else {
const chunk = new TextDecoder("utf-8").decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data:")) {
const data = safeJsonParse(line.slice(5));
switch (data?.type) {
case "success":
done = true;
resolve({ success: true });
break;
case "error":
done = true;
resolve({
success: false,
error: data?.error || data?.message,
});
break;
case "progress":
progressCallback(data?.percentage);
break;
default:
break;
}
}
}
}
}
} catch (error) {
console.error("Error downloading model:", error);
resolve({
success: false,
error:
error?.message || "An error occurred while downloading the model",
});
}
});
},
// Uninstall a DMR model is not supported via the API
};
export default DMRUtils;

View File

@@ -185,10 +185,8 @@ const Workspace = {
}
},
async onmessage(msg) {
try {
const chatResult = JSON.parse(msg.data);
handleChat(chatResult);
} catch {}
const chatResult = safeJsonParse(msg.data, null);
if (chatResult) handleChat(chatResult);
},
onerror(err) {
handleChat({

View File

@@ -1,6 +1,6 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
import { baseHeaders, safeJsonParse } from "@/utils/request";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { v4 } from "uuid";
@@ -144,10 +144,8 @@ const WorkspaceThread = {
}
},
async onmessage(msg) {
try {
const chatResult = JSON.parse(msg.data);
handleChat(chatResult);
} catch {}
const chatResult = safeJsonParse(msg.data, null);
if (chatResult) handleChat(chatResult);
},
onerror(err) {
handleChat({

View File

@@ -1,24 +1,25 @@
import Header from "../components/Header";
import Footer from "../components/Footer";
import { NavLink } from "react-router-dom";
import { House, MagnifyingGlass } from "@phosphor-icons/react";
export default function Contact() {
export default function NotFound() {
return (
<div className="text-black">
<Header />
<div className="flex flex-col justify-center mx-auto mt-52 text-center max-w-2x1">
<h1 className="text-3xl font-bold tracking-tight text-black md:text-5xl">
404 Unavailable
</h1>
<br />
<a
className="w-64 p-1 mx-auto font-bold text-center text-black border border-gray-500 rounded-lg sm:p-4"
href="/"
<div className="flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full">
<MagnifyingGlass className="w-16 h-16 text-theme-text-secondary" />
<h1 className="text-xl md:text-2xl font-bold text-center">
404 - Page Not Found
</h1>
<p className="text-theme-text-secondary text-center px-4">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto">
<NavLink
to="/"
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
Return Home
</a>
<House className="w-4 h-4" />
Go Home
</NavLink>
</div>
<div className="mt-64"></div>
<Footer />
</div>
);
}

View File

@@ -53,8 +53,7 @@ const SEARCH_PROVIDERS = [
value: "google-search-engine",
logo: GoogleSearchIcon,
options: (settings) => <GoogleSearchOptions settings={settings} />,
description:
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
description: "Web search powered by a custom Google Search Engine.",
},
{
name: "SerpApi",

View File

@@ -7,10 +7,4 @@ export const configurableFeatures = {
component: LiveSyncToggle,
key: "experimental_live_file_sync",
},
experimental_mobile_connections: {
title: "AnythingLLM Mobile",
href: paths.settings.mobileConnections(),
key: "experimental_mobile_connections",
autoEnabled: true,
},
};

View File

@@ -1,5 +1,6 @@
import { CaretDown, CaretUp } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { safeJsonParse } from "@/utils/request";
export default function LogRow({ log }) {
const [expanded, setExpanded] = useState(false);
@@ -8,11 +9,9 @@ export default function LogRow({ log }) {
useEffect(() => {
function parseAndSetMetadata() {
try {
let data = JSON.parse(log.metadata);
setHasMetadata(Object.keys(data)?.length > 0);
setMetadata(data);
} catch {}
const data = safeJsonParse(log.metadata, {});
setHasMetadata(Object.keys(data)?.length > 0);
setMetadata(data);
}
parseAndSetMetadata();
}, [log.metadata]);

View File

@@ -10,6 +10,7 @@ import {
} from "../../NewEmbedModal";
import Embed from "@/models/embed";
import showToast from "@/utils/toast";
import { safeJsonParse } from "@/utils/request";
export default function EditEmbedModal({ embed, closeModal }) {
const [error, setError] = useState(null);
@@ -53,9 +54,7 @@ export default function EditEmbedModal({ embed, closeModal }) {
<ChatModeSelection defaultValue={embed.chat_mode} />
<PermittedDomains
defaultValue={
embed.allowlist_domains
? JSON.parse(embed.allowlist_domains)
: []
safeJsonParse(embed.allowlist_domains, null) || []
}
/>
<NumberInput

View File

@@ -9,6 +9,7 @@ import { nFormatter } from "@/utils/numbers";
import EditEmbedModal from "./EditEmbedModal";
import CodeSnippetModal from "./CodeSnippetModal";
import moment from "moment";
import { safeJsonParse } from "@/utils/request";
export default function EmbedRow({ embed }) {
const rowRef = useRef(null);
@@ -140,21 +141,17 @@ export default function EmbedRow({ embed }) {
}
function ActiveDomains({ domainList }) {
if (!domainList) return <p>all</p>;
try {
const domains = JSON.parse(domainList);
return (
<div className="flex flex-col gap-y-2">
{domains.map((domain, index) => {
return (
<p key={index} className="font-mono !font-normal">
{domain}
</p>
);
})}
</div>
);
} catch {
return <p>all</p>;
}
const domains = safeJsonParse(domainList, []);
if (domains.length === 0) return <p>all</p>;
return (
<div className="flex flex-col gap-y-2">
{domains.map((domain, index) => {
return (
<p key={index} className="font-mono !font-normal">
{domain}
</p>
);
})}
</div>
);
}

View File

@@ -1,9 +1,10 @@
import { Link } from "react-router-dom";
import paths from "@/utils/paths";
import { VisibilityIcon } from "./generic";
import { safeJsonParse } from "@/utils/request";
export default function AgentFlowHubCard({ item }) {
const flow = JSON.parse(item.flow);
const flow = safeJsonParse(item.flow, { steps: [] });
return (
<Link
to={paths.communityHub.importItem(item.importId)}

View File

@@ -36,6 +36,7 @@ import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png";
import CometApiLogo from "@/media/llmprovider/cometapi.png";
import FoundryLogo from "@/media/llmprovider/foundry-local.png";
import GiteeAILogo from "@/media/llmprovider/giteeai.png";
import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
import PreLoader from "@/components/Preloader";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
@@ -70,6 +71,7 @@ import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions";
import MoonshotAiOptions from "@/components/LLMSelection/MoonshotAiOptions";
import FoundryOptions from "@/components/LLMSelection/FoundryOptions";
import GiteeAIOptions from "@/components/LLMSelection/GiteeAIOptions/index.jsx";
import DockerModelRunnerOptions from "@/components/LLMSelection/DockerModelRunnerOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
@@ -160,6 +162,18 @@ export const AVAILABLE_LLM_PROVIDERS = [
"Discover, download, and run thousands of cutting edge LLMs in a few clicks.",
requiredConfig: ["LMStudioBasePath"],
},
{
name: "Docker Model Runner",
value: "docker-model-runner",
logo: DockerModelRunnerLogo,
options: (settings) => <DockerModelRunnerOptions settings={settings} />,
description: "Run LLMs using Docker Model Runner.",
requiredConfig: [
"DockerModelRunnerBasePath",
"DockerModelRunnerModelPref",
"DockerModelRunnerModelTokenLimit",
],
},
{
name: "Local AI",
value: "localai",
@@ -370,6 +384,7 @@ export const AVAILABLE_LLM_PROVIDERS = [
},
];
export const LLM_PREFERENCE_CHANGED_EVENT = "llm-preference-changed";
export default function GeneralLLMPreference() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
@@ -427,6 +442,21 @@ export default function GeneralLLMPreference() {
fetchKeys();
}, []);
// Some more complex LLM options do not bubble up the change event, so we need to listen to the custom event
// we can emit from the LLM options component using window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT));
useEffect(() => {
function updateHasChanges() {
setHasChanges(true);
}
window.addEventListener(LLM_PREFERENCE_CHANGED_EVENT, updateHasChanges);
return () => {
window.removeEventListener(
LLM_PREFERENCE_CHANGED_EVENT,
updateHasChanges
);
};
}, []);
useEffect(() => {
const filtered = AVAILABLE_LLM_PROVIDERS.filter((llm) =>
llm.name.toLowerCase().includes(searchQuery.toLowerCase())

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="artwork" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 238.96 70.87">
<!-- Generator: Adobe Illustrator 29.8.3, SVG Export Plug-In . SVG Version: 2.1.1 Build 3) -->
<defs>
<style>
.st0 {
fill: #4285f4;
}
.st1 {
fill: #a6a6a6;
}
.st2 {
stroke: #fff;
stroke-miterlimit: 10;
stroke-width: .2px;
}
.st2, .st3 {
fill: #fff;
}
.st4 {
fill: #34a853;
}
.st5 {
fill: #fbbc04;
}
.st6 {
fill: #ea4335;
}
</style>
</defs>
<rect x="-.11" width="239.17" height="70.87" rx="8.86" ry="8.86"/>
<path class="st1" d="M230.21,1.42c4.1,0,7.44,3.34,7.44,7.44v53.15c0,4.1-3.34,7.44-7.44,7.44H8.75c-4.1,0-7.44-3.34-7.44-7.44V8.86C1.31,4.76,4.65,1.42,8.75,1.42h221.46M230.21,0H8.75C3.88,0-.11,3.99-.11,8.86v53.15c0,4.87,3.99,8.86,8.86,8.86h221.46c4.87,0,8.86-3.99,8.86-8.86V8.86c0-4.87-3.99-8.86-8.86-8.86h0Z"/>
<g>
<path class="st2" d="M83.9,18.15c0,1.48-.44,2.67-1.32,3.55-1,1.05-2.3,1.57-3.9,1.57s-2.84-.53-3.91-1.59c-1.07-1.06-1.61-2.38-1.61-3.96s.54-2.89,1.61-3.96c1.07-1.06,2.38-1.6,3.91-1.6.76,0,1.49.15,2.18.44.69.3,1.25.69,1.66,1.19l-.93.94c-.7-.84-1.67-1.26-2.91-1.26-1.12,0-2.09.39-2.9,1.18s-1.22,1.81-1.22,3.06.41,2.28,1.22,3.06c.82.79,1.78,1.18,2.9,1.18,1.19,0,2.18-.4,2.97-1.19.51-.52.81-1.23.89-2.15h-3.86v-1.28h5.15c.05.28.07.54.07.8Z"/>
<path class="st2" d="M92.07,13.71h-4.84v3.37h4.37v1.28h-4.37v3.37h4.84v1.31h-6.21v-10.63h6.21v1.31h0Z"/>
<path class="st2" d="M97.83,23.03h-1.37v-9.32h-2.97v-1.31h7.3v1.31h-2.97s0,9.32,0,9.32Z"/>
<path class="st2" d="M106.08,23.03v-10.63h1.36v10.63h-1.36Z"/>
<path class="st2" d="M113.51,23.03h-1.37v-9.32h-2.97v-1.31h7.3v1.31h-2.97s0,9.32,0,9.32Z"/>
<path class="st2" d="M130.3,21.66c-1.05,1.08-2.34,1.61-3.9,1.61s-2.85-.54-3.9-1.61c-1.05-1.07-1.57-2.39-1.57-3.94s.52-2.87,1.57-3.94c1.04-1.08,2.34-1.61,3.9-1.61s2.84.54,3.89,1.62c1.05,1.08,1.57,2.39,1.57,3.93s-.52,2.87-1.57,3.94ZM123.52,20.77c.79.8,1.75,1.19,2.89,1.19s2.1-.4,2.89-1.19c.79-.8,1.18-1.81,1.18-3.05s-.4-2.26-1.18-3.05c-.78-.8-1.75-1.19-2.89-1.19s-2.1.4-2.89,1.19c-.78.8-1.18,1.81-1.18,3.05s.4,2.26,1.18,3.05Z"/>
<path class="st2" d="M133.79,23.03v-10.63h1.66l5.17,8.27h.06l-.06-2.05v-6.22h1.37v10.63h-1.43l-5.41-8.67h-.06l.06,2.05v6.62h-1.37,0Z"/>
</g>
<path class="st3" d="M120.61,38.54c-4.17,0-7.56,3.17-7.56,7.53s3.4,7.53,7.56,7.53,7.56-3.2,7.56-7.53-3.4-7.53-7.56-7.53ZM120.61,50.64c-2.28,0-4.25-1.88-4.25-4.57s1.97-4.57,4.25-4.57,4.25,1.85,4.25,4.57-1.97,4.57-4.25,4.57ZM104.11,38.54c-4.17,0-7.56,3.17-7.56,7.53s3.4,7.53,7.56,7.53,7.56-3.2,7.56-7.53-3.4-7.53-7.56-7.53ZM104.11,50.64c-2.28,0-4.25-1.88-4.25-4.57s1.97-4.57,4.25-4.57,4.25,1.85,4.25,4.57-1.97,4.57-4.25,4.57ZM84.48,40.85v3.2h7.65c-.23,1.8-.83,3.11-1.74,4.03-1.11,1.11-2.85,2.34-5.91,2.34-4.71,0-8.39-3.8-8.39-8.51s3.68-8.51,8.39-8.51c2.54,0,4.4,1,5.76,2.28l2.26-2.26c-1.91-1.83-4.45-3.22-8.02-3.22-6.45,0-11.87,5.25-11.87,11.7s5.42,11.7,11.87,11.7c3.48,0,6.11-1.14,8.16-3.28,2.11-2.11,2.77-5.08,2.77-7.48,0-.74-.06-1.43-.17-2h-10.76,0ZM164.75,43.33c-.63-1.68-2.54-4.8-6.45-4.8s-7.11,3.05-7.11,7.53c0,4.22,3.2,7.53,7.48,7.53,3.45,0,5.45-2.11,6.28-3.34l-2.57-1.71c-.86,1.26-2.03,2.08-3.71,2.08s-2.88-.77-3.65-2.28l10.08-4.17-.34-.86h0ZM154.47,45.84c-.09-2.91,2.26-4.4,3.94-4.4,1.31,0,2.43.66,2.8,1.6l-6.74,2.8ZM146.28,53.15h3.31v-22.15h-3.31v22.15ZM140.86,40.22h-.11c-.74-.88-2.17-1.68-3.97-1.68-3.77,0-7.22,3.31-7.22,7.56s3.45,7.51,7.22,7.51c1.8,0,3.22-.8,3.97-1.71h.11v1.08c0,2.88-1.54,4.42-4.03,4.42-2.03,0-3.28-1.46-3.8-2.68l-2.88,1.2c.83,2,3.03,4.45,6.68,4.45,3.88,0,7.16-2.28,7.16-7.85v-13.53h-3.14v1.23ZM137.06,50.64c-2.28,0-4.2-1.91-4.2-4.54s1.91-4.6,4.2-4.6,4.03,1.94,4.03,4.6-1.77,4.54-4.03,4.54ZM180.26,31h-7.92v22.15h3.31v-8.39h4.62c3.66,0,7.27-2.65,7.27-6.88s-3.6-6.88-7.27-6.88ZM180.34,41.68h-4.7v-7.59h4.7c2.47,0,3.87,2.05,3.87,3.8s-1.4,3.8-3.87,3.8h0ZM200.77,38.5c-2.39,0-4.87,1.05-5.9,3.39l2.94,1.23c.63-1.23,1.79-1.62,3.02-1.62,1.71,0,3.45,1.03,3.48,2.85v.23c-.6-.34-1.88-.86-3.45-.86-3.16,0-6.38,1.74-6.38,4.99,0,2.96,2.59,4.87,5.5,4.87,2.22,0,3.45-1,4.22-2.16h.11v1.71h3.19v-8.49c0-3.93-2.94-6.13-6.73-6.13h0ZM200.38,50.63c-1.08,0-2.59-.54-2.59-1.88,0-1.71,1.88-2.37,3.5-2.37,1.45,0,2.14.31,3.02.74-.26,2.05-2.02,3.5-3.93,3.5ZM219.12,38.98l-3.79,9.6h-.11l-3.93-9.6h-3.56l5.9,13.42-3.36,7.47h3.45l9.09-20.89h-3.68ZM189.35,53.15h3.31v-22.15h-3.31v22.15Z"/>
<g>
<path class="st6" d="M36.6,34.41l-18.86,20.02s0,0,0,.01c.58,2.17,2.56,3.77,4.92,3.77.94,0,1.83-.26,2.58-.7l.06-.04,21.23-12.25-9.94-10.82Z"/>
<path class="st5" d="M55.68,31h-.02s-9.17-5.33-9.17-5.33l-10.33,9.19,10.36,10.36,9.12-5.26c1.6-.86,2.68-2.55,2.68-4.49s-1.07-3.61-2.65-4.47h0Z"/>
<path class="st0" d="M17.73,16.44c-.11.42-.17.86-.17,1.31v35.38c0,.45.06.89.17,1.31l19.51-19.51-19.51-18.49Z"/>
<path class="st4" d="M36.74,35.43l9.76-9.76-21.21-12.3c-.77-.46-1.67-.73-2.63-.73-2.36,0-4.34,1.6-4.92,3.78h0s19,19,19,19h0Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -8,6 +8,7 @@ import MobileConnection from "@/models/mobile";
import PreLoader from "@/components/Preloader";
import Logo from "@/media/logo/anything-llm-infinity.png";
import paths from "@/utils/paths";
import GetOnGooglePlay from "./gplay-badge.svg";
export default function MobileConnectModal({ isOpen, onClose }) {
return (
@@ -37,13 +38,23 @@ export default function MobileConnectModal({ isOpen, onClose }) {
Go mobile. Stay local. AnythingLLM Mobile.
</p>
<p className="text-[#FFF] text-lg">
AnythingLLM for mobile allows you to connect or clone your
workspace's chats, threads and documents for you to use on the go.
AnythingLLM for mobile allows you to connect to your workspace's
chats, threads, tools, and documents for you to use on the go.
<br />
<br />
Run with local models on your phone privately or relay chats
directly to this instance seamlessly.
</p>
<Link
to="https://play.google.com/store/apps/details?id=com.anythingllm"
target="_blank"
>
<img
src={GetOnGooglePlay}
alt="Get on Google Play"
className="w-[150px] h-auto"
/>
</Link>
</div>
{/* right column */}

View File

@@ -4,13 +4,8 @@ import { isMobile } from "react-device-detect";
import showToast from "@/utils/toast";
import System from "@/models/system";
import PreLoader from "@/components/Preloader";
import {
EMBEDDING_ENGINE_PRIVACY,
LLM_SELECTION_PRIVACY,
VECTOR_DB_PRIVACY,
FALLBACKS,
} from "@/pages/OnboardingFlow/Steps/DataHandling";
import { useTranslation } from "react-i18next";
import ProviderPrivacy from "@/components/ProviderPrivacy";
export default function PrivacyAndDataHandling() {
const [settings, setSettings] = useState({});
@@ -51,8 +46,8 @@ export default function PrivacyAndDataHandling() {
</div>
</div>
) : (
<div className="overflow-x-auto">
<ThirdParty settings={settings} />
<div className="overflow-x-auto flex flex-col gap-y-6 pt-6">
<ProviderPrivacy />
<TelemetryLogs settings={settings} />
</div>
)}
@@ -62,88 +57,6 @@ export default function PrivacyAndDataHandling() {
);
}
function ThirdParty({ settings }) {
const llmChoice = settings?.LLMProvider || "openai";
const embeddingEngine = settings?.EmbeddingEngine || "openai";
const vectorDb = settings?.VectorDB || "lancedb";
const { t } = useTranslation();
const LLMSelection =
LLM_SELECTION_PRIVACY?.[llmChoice] || FALLBACKS.LLM(llmChoice);
const EmbeddingEngine =
EMBEDDING_ENGINE_PRIVACY?.[embeddingEngine] ||
FALLBACKS.EMBEDDING(embeddingEngine);
const VectorDb = VECTOR_DB_PRIVACY?.[vectorDb] || FALLBACKS.VECTOR(vectorDb);
return (
<div className="py-8 w-full flex items-start justify-center flex-col gap-y-6 border-b-2 border-theme-sidebar-border">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
<div className="text-theme-text-primary text-base font-bold">
{t("privacy.llm")}
</div>
<div className="flex items-center gap-2.5">
<img
src={LLMSelection.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{LLMSelection.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{LLMSelection.description.map((desc) => (
<li className="text-theme-text-secondary text-sm">{desc}</li>
))}
</ul>
</div>
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
<div className="text-theme-text-primary text-base font-bold">
{t("privacy.embedding")}
</div>
<div className="flex items-center gap-2.5">
<img
src={EmbeddingEngine.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{EmbeddingEngine.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{EmbeddingEngine.description.map((desc) => (
<li className="text-theme-text-secondary text-sm">{desc}</li>
))}
</ul>
</div>
<div className="flex flex-col gap-y-2 pb-4">
<div className="text-theme-text-primary text-base font-bold">
{t("privacy.vector")}
</div>
<div className="flex items-center gap-2.5">
<img
src={VectorDb.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{VectorDb.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{VectorDb.description.map((desc) => (
<li className="text-theme-text-secondary text-sm">{desc}</li>
))}
</ul>
</div>
</div>
</div>
);
}
function TelemetryLogs({ settings }) {
const [telemetry, setTelemetry] = useState(
settings?.DisableTelemetry !== "true"
@@ -164,7 +77,6 @@ function TelemetryLogs({ settings }) {
return (
<div className="relative w-full max-h-full">
<div className="relative rounded-lg">
<div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div className="">

View File

@@ -17,7 +17,7 @@ export function ChecklistItem({ id, title, action, onAction, icon: Icon }) {
const shouldComplete = await onAction();
if (shouldComplete) {
const stored = window.localStorage.getItem(CHECKLIST_STORAGE_KEY);
const completedItems = stored ? JSON.parse(stored) : {};
const completedItems = safeJsonParse(stored, {});
completedItems[id] = true;
window.localStorage.setItem(
CHECKLIST_STORAGE_KEY,

View File

@@ -71,7 +71,6 @@ export default function ExploreFeatures() {
)}
onPrimaryAction={chatWithAgent}
onSecondaryAction={buildAgentFlow}
isNew={true}
/>
<FeatureCard
title={t("main-page.exploreMore.features.slashCommands.title")}
@@ -101,7 +100,6 @@ export default function ExploreFeatures() {
)}
onPrimaryAction={setSystemPrompt}
onSecondaryAction={managePromptVariables}
isNew={true}
/>
</div>
</div>

View File

@@ -1,95 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import illustration from "@/media/illustrations/create-workspace.png";
import paths from "@/utils/paths";
import showToast from "@/utils/toast";
import { useNavigate } from "react-router-dom";
import Workspace from "@/models/workspace";
import { useTranslation } from "react-i18next";
export default function CreateWorkspace({
setHeader,
setForwardBtn,
setBackBtn,
}) {
const { t } = useTranslation();
const [workspaceName, setWorkspaceName] = useState("");
const navigate = useNavigate();
const createWorkspaceRef = useRef();
const TITLE = t("onboarding.workspace.title");
const DESCRIPTION = t("onboarding.workspace.description");
useEffect(() => {
setHeader({ title: TITLE, description: DESCRIPTION });
setBackBtn({ showing: false, disabled: false, onClick: handleBack });
}, []);
useEffect(() => {
if (workspaceName.length > 0) {
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
} else {
setForwardBtn({ showing: true, disabled: true, onClick: handleForward });
}
}, [workspaceName]);
const handleCreate = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
const { workspace, error } = await Workspace.new({
name: form.get("name"),
onboardingComplete: true,
});
if (!!workspace) {
showToast(
"Workspace created successfully! Taking you to home...",
"success"
);
await new Promise((resolve) => setTimeout(resolve, 1000));
navigate(paths.home());
} else {
showToast(`Failed to create workspace: ${error}`, "error");
}
};
function handleForward() {
createWorkspaceRef.current.click();
}
function handleBack() {
navigate(paths.onboarding.survey());
}
return (
<form
onSubmit={handleCreate}
className="w-full flex items-center justify-center flex-col gap-y-2"
>
<img src={illustration} alt="Create workspace" />
<div className="flex flex-col gap-y-4 w-full max-w-[600px]">
{" "}
<div className="w-full mt-4">
<label
htmlFor="name"
className="block mb-3 text-sm font-medium text-white"
>
{t("common.workspaces-name")}
</label>
<input
name="name"
type="text"
className="border-none bg-theme-settings-input-bg text-white focus:outline-primary-button active:outline-primary-button placeholder:text-theme-settings-input-placeholder outline-none text-sm rounded-lg block w-full p-2.5"
placeholder="My Workspace"
required={true}
autoComplete="off"
onChange={(e) => setWorkspaceName(e.target.value)}
/>
</div>
</div>
<button
type="submit"
ref={createWorkspaceRef}
hidden
aria-hidden="true"
></button>
</form>
);
}

View File

@@ -1,499 +1,11 @@
import PreLoader from "@/components/Preloader";
import System from "@/models/system";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import OpenAiLogo from "@/media/llmprovider/openai.png";
import GenericOpenAiLogo from "@/media/llmprovider/generic-openai.png";
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
import AnthropicLogo from "@/media/llmprovider/anthropic.png";
import GeminiLogo from "@/media/llmprovider/gemini.png";
import OllamaLogo from "@/media/llmprovider/ollama.png";
import TogetherAILogo from "@/media/llmprovider/togetherai.png";
import FireworksAILogo from "@/media/llmprovider/fireworksai.jpeg";
import NvidiaNimLogo from "@/media/llmprovider/nvidia-nim.png";
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
import LocalAiLogo from "@/media/llmprovider/localai.png";
import MistralLogo from "@/media/llmprovider/mistral.jpeg";
import HuggingFaceLogo from "@/media/llmprovider/huggingface.png";
import PerplexityLogo from "@/media/llmprovider/perplexity.png";
import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
import NovitaLogo from "@/media/llmprovider/novita.png";
import GroqLogo from "@/media/llmprovider/groq.png";
import KoboldCPPLogo from "@/media/llmprovider/koboldcpp.png";
import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
import APIPieLogo from "@/media/llmprovider/apipie.png";
import XAILogo from "@/media/llmprovider/xai.png";
import ZAiLogo from "@/media/llmprovider/zai.png";
import CohereLogo from "@/media/llmprovider/cohere.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import ChromaLogo from "@/media/vectordbs/chroma.png";
import PineconeLogo from "@/media/vectordbs/pinecone.png";
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
import QDrantLogo from "@/media/vectordbs/qdrant.png";
import MilvusLogo from "@/media/vectordbs/milvus.png";
import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png";
import PPIOLogo from "@/media/llmprovider/ppio.png";
import PGVectorLogo from "@/media/vectordbs/pgvector.png";
import DPAISLogo from "@/media/llmprovider/dpais.png";
import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png";
import CometApiLogo from "@/media/llmprovider/cometapi.png";
import FoundryLogo from "@/media/llmprovider/foundry-local.png";
import GiteeAILogo from "@/media/llmprovider/giteeai.png";
import React, { useState, useEffect } from "react";
import { useEffect } from "react";
import paths from "@/utils/paths";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
export const LLM_SELECTION_PRIVACY = {
openai: {
name: "OpenAI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to OpenAI",
],
logo: OpenAiLogo,
},
azure: {
name: "Azure OpenAI",
description: [
"Your chats will not be used for training",
"Your text and embedding text are not visible to OpenAI or Microsoft",
],
logo: AzureOpenAiLogo,
},
anthropic: {
name: "Anthropic",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to Anthropic",
],
logo: AnthropicLogo,
},
gemini: {
name: "Google Gemini",
description: [
"Your chats are de-identified and used in training",
"Your prompts and document text used in response creation are visible to Google",
],
logo: GeminiLogo,
},
"nvidia-nim": {
name: "NVIDIA NIM",
description: [
"Your model and chats are only accessible on the machine running the NVIDIA NIM",
],
logo: NvidiaNimLogo,
},
lmstudio: {
name: "LMStudio",
description: [
"Your model and chats are only accessible on the server running LMStudio",
],
logo: LMStudioLogo,
},
localai: {
name: "LocalAI",
description: [
"Your model and chats are only accessible on the server running LocalAI",
],
logo: LocalAiLogo,
},
ollama: {
name: "Ollama",
description: [
"Your model and chats are only accessible on the machine running Ollama models",
],
logo: OllamaLogo,
},
togetherai: {
name: "TogetherAI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to TogetherAI",
],
logo: TogetherAILogo,
},
fireworksai: {
name: "FireworksAI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to Fireworks AI",
],
logo: FireworksAILogo,
},
mistral: {
name: "Mistral",
description: [
"Your prompts and document text used in response creation are visible to Mistral",
],
logo: MistralLogo,
},
huggingface: {
name: "HuggingFace",
description: [
"Your prompts and document text used in response are sent to your HuggingFace managed endpoint",
],
logo: HuggingFaceLogo,
},
perplexity: {
name: "Perplexity AI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to Perplexity AI",
],
logo: PerplexityLogo,
},
openrouter: {
name: "OpenRouter",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to OpenRouter",
],
logo: OpenRouterLogo,
},
novita: {
name: "Novita AI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to Novita AI",
],
logo: NovitaLogo,
},
groq: {
name: "Groq",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to Groq",
],
logo: GroqLogo,
},
koboldcpp: {
name: "KoboldCPP",
description: [
"Your model and chats are only accessible on the server running KoboldCPP",
],
logo: KoboldCPPLogo,
},
textgenwebui: {
name: "Oobabooga Web UI",
description: [
"Your model and chats are only accessible on the server running the Oobabooga Text Generation Web UI",
],
logo: TextGenWebUILogo,
},
"generic-openai": {
name: "Generic OpenAI compatible service",
description: [
"Data is shared according to the terms of service applicable with your generic endpoint provider.",
],
logo: GenericOpenAiLogo,
},
cohere: {
name: "Cohere",
description: [
"Data is shared according to the terms of service of cohere.com and your localities privacy laws.",
],
logo: CohereLogo,
},
litellm: {
name: "LiteLLM",
description: [
"Your model and chats are only accessible on the server running LiteLLM",
],
logo: LiteLLMLogo,
},
bedrock: {
name: "AWS Bedrock",
description: [
"You model and chat contents are subject to the agreed EULA for AWS and the model provider on aws.amazon.com",
],
logo: AWSBedrockLogo,
},
deepseek: {
name: "DeepSeek",
description: ["Your model and chat contents are visible to DeepSeek"],
logo: DeepSeekLogo,
},
apipie: {
name: "APIpie.AI",
description: [
"Your model and chat contents are visible to APIpie in accordance with their terms of service.",
],
logo: APIPieLogo,
},
xai: {
name: "xAI",
description: [
"Your model and chat contents are visible to xAI in accordance with their terms of service.",
],
logo: XAILogo,
},
zai: {
name: "Z.AI",
description: [
"Your content is processed in real-time and not stored on Z.AI servers",
"Your prompts and document text are visible to Z.AI during processing",
"Data is processed in accordance with Z.AI's API Services terms",
],
logo: ZAiLogo,
},
ppio: {
name: "PPIO",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to PPIO",
],
logo: PPIOLogo,
},
dpais: {
name: "Dell Pro AI Studio",
description: [
"Your model and chat contents are only accessible on the computer running Dell Pro AI Studio",
],
logo: DPAISLogo,
},
moonshotai: {
name: "Moonshot AI",
description: [
"Your chats may be used by Moonshot AI for training and model refinement",
"Your prompts and document text used in response creation are visible to Moonshot AI",
],
logo: MoonshotAiLogo,
},
cometapi: {
name: "CometAPI",
description: [
"Your chats will not be used for training",
"Your prompts and document text used in response creation are visible to CometAPI",
],
logo: CometApiLogo,
},
foundry: {
name: "Microsoft Foundry Local",
description: [
"Your model and chats are only accessible on the machine running Foundry Local",
],
logo: FoundryLogo,
},
giteeai: {
name: "GiteeAI",
description: [
"Your model and chat contents are visible to GiteeAI in accordance with their terms of service.",
],
logo: GiteeAILogo,
},
};
export const VECTOR_DB_PRIVACY = {
pgvector: {
name: "PGVector",
description: [
"Your vectors and document text are stored on your PostgreSQL instance",
"Access to your instance is managed by you",
],
logo: PGVectorLogo,
},
chroma: {
name: "Chroma",
description: [
"Your vectors and document text are stored on your Chroma instance",
"Access to your instance is managed by you",
],
logo: ChromaLogo,
},
chromacloud: {
name: "Chroma Cloud",
description: [
"Your vectors and document text are stored on Chroma's cloud service",
"Access to your data is managed by Chroma",
],
logo: ChromaLogo,
},
pinecone: {
name: "Pinecone",
description: [
"Your vectors and document text are stored on Pinecone's servers",
"Access to your data is managed by Pinecone",
],
logo: PineconeLogo,
},
qdrant: {
name: "Qdrant",
description: [
"Your vectors and document text are stored on your Qdrant instance (cloud or self-hosted)",
],
logo: QDrantLogo,
},
weaviate: {
name: "Weaviate",
description: [
"Your vectors and document text are stored on your Weaviate instance (cloud or self-hosted)",
],
logo: WeaviateLogo,
},
milvus: {
name: "Milvus",
description: [
"Your vectors and document text are stored on your Milvus instance (cloud or self-hosted)",
],
logo: MilvusLogo,
},
zilliz: {
name: "Zilliz Cloud",
description: [
"Your vectors and document text are stored on your Zilliz cloud cluster.",
],
logo: ZillizLogo,
},
astra: {
name: "AstraDB",
description: [
"Your vectors and document text are stored on your cloud AstraDB database.",
],
logo: AstraDBLogo,
},
lancedb: {
name: "LanceDB",
description: [
"Your vectors and document text are stored privately on this instance of AnythingLLM",
],
logo: LanceDbLogo,
},
};
export const EMBEDDING_ENGINE_PRIVACY = {
native: {
name: "AnythingLLM Embedder",
description: [
"Your document text is embedded privately on this instance of AnythingLLM",
],
logo: AnythingLLMIcon,
},
openai: {
name: "OpenAI",
description: [
"Your document text is sent to OpenAI servers",
"Your documents are not used for training",
],
logo: OpenAiLogo,
},
azure: {
name: "Azure OpenAI",
description: [
"Your document text is sent to your Microsoft Azure service",
"Your documents are not used for training",
],
logo: AzureOpenAiLogo,
},
localai: {
name: "LocalAI",
description: [
"Your document text is embedded privately on the server running LocalAI",
],
logo: LocalAiLogo,
},
ollama: {
name: "Ollama",
description: [
"Your document text is embedded privately on the server running Ollama",
],
logo: OllamaLogo,
},
lmstudio: {
name: "LMStudio",
description: [
"Your document text is embedded privately on the server running LMStudio",
],
logo: LMStudioLogo,
},
openrouter: {
name: "OpenRouter",
description: [
"Your document text is sent to OpenRouter's servers for processing",
"Your document text is stored or managed according to the terms of service of OpenRouter API Terms of Service",
],
logo: OpenRouterLogo,
},
cohere: {
name: "Cohere",
description: [
"Data is shared according to the terms of service of cohere.com and your localities privacy laws.",
],
logo: CohereLogo,
},
voyageai: {
name: "Voyage AI",
description: [
"Data sent to Voyage AI's servers is shared according to the terms of service of voyageai.com.",
],
logo: VoyageAiLogo,
},
mistral: {
name: "Mistral AI",
description: [
"Data sent to Mistral AI's servers is shared according to the terms of service of https://mistral.ai.",
],
logo: MistralLogo,
},
litellm: {
name: "LiteLLM",
description: [
"Your document text is only accessible on the server running LiteLLM and to the providers you configured in LiteLLM.",
],
logo: LiteLLMLogo,
},
"generic-openai": {
name: "Generic OpenAI compatible service",
description: [
"Data is shared according to the terms of service applicable with your generic endpoint provider.",
],
logo: GenericOpenAiLogo,
},
gemini: {
name: "Google Gemini",
description: [
"Your document text is sent to Google Gemini's servers for processing",
"Your document text is stored or managed according to the terms of service of Google Gemini API Terms of Service",
],
logo: GeminiLogo,
},
};
export const FALLBACKS = {
LLM: (provider) => ({
name: "Unknown",
description: [
`"${provider}" has no known data handling policy defined in AnythingLLM`,
],
logo: AnythingLLMIcon,
}),
EMBEDDING: (provider) => ({
name: "Unknown",
description: [
`"${provider}" has no known data handling policy defined in AnythingLLM`,
],
logo: AnythingLLMIcon,
}),
VECTOR: (provider) => ({
name: "Unknown",
description: [
`"${provider}" has no known data handling policy defined in AnythingLLM`,
],
logo: AnythingLLMIcon,
}),
};
import ProviderPrivacy from "@/components/ProviderPrivacy";
export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
const { t } = useTranslation();
const [llmChoice, setLLMChoice] = useState("openai");
const [loading, setLoading] = useState(true);
const [vectorDb, setVectorDb] = useState("pinecone");
const [embeddingEngine, setEmbeddingEngine] = useState("openai");
const navigate = useNavigate();
const TITLE = t("onboarding.data.title");
@@ -503,15 +15,6 @@ export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
setHeader({ title: TITLE, description: DESCRIPTION });
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
setBackBtn({ showing: false, disabled: false, onClick: handleBack });
async function fetchKeys() {
const _settings = await System.keys();
setLLMChoice(_settings?.LLMProvider || "openai");
setVectorDb(_settings?.VectorDB || "lancedb");
setEmbeddingEngine(_settings?.EmbeddingEngine || "openai");
setLoading(false);
}
fetchKeys();
}, []);
function handleForward() {
@@ -522,85 +25,9 @@ export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
navigate(paths.onboarding.userSetup());
}
if (loading)
return (
<div className="w-full h-full flex justify-center items-center p-20">
<PreLoader />
</div>
);
const LLMSelection =
LLM_SELECTION_PRIVACY?.[llmChoice] || FALLBACKS.LLM(llmChoice);
const EmbeddingEngine =
EMBEDDING_ENGINE_PRIVACY?.[embeddingEngine] ||
FALLBACKS.EMBEDDING(embeddingEngine);
const VectorDb = VECTOR_DB_PRIVACY?.[vectorDb] || FALLBACKS.VECTOR(vectorDb);
return (
<div className="w-full flex items-center justify-center flex-col gap-y-6">
<div className="p-8 flex flex-col gap-8">
<div className="flex flex-col gap-y-2 border-b border-theme-sidebar-border pb-4">
<div className="text-theme-text-primary text-base font-bold">
LLM Selection
</div>
<div className="flex items-center gap-2.5">
<img
src={LLMSelection.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{LLMSelection.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{LLMSelection.description.map((desc) => (
<li className="text-theme-text-primary text-sm">{desc}</li>
))}
</ul>
</div>
<div className="flex flex-col gap-y-2 border-b border-theme-sidebar-border pb-4">
<div className="text-theme-text-primary text-base font-bold">
Embedding Preference
</div>
<div className="flex items-center gap-2.5">
<img
src={EmbeddingEngine.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{EmbeddingEngine.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{EmbeddingEngine.description.map((desc) => (
<li className="text-theme-text-primary text-sm">{desc}</li>
))}
</ul>
</div>
<div className="flex flex-col gap-y-2 pb-4">
<div className="text-theme-text-primary text-base font-bold">
Vector Database
</div>
<div className="flex items-center gap-2.5">
<img
src={VectorDb.logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-theme-text-primary text-sm font-bold">
{VectorDb.name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{VectorDb.description.map((desc) => (
<li className="text-theme-text-primary text-sm">{desc}</li>
))}
</ul>
</div>
</div>
<ProviderPrivacy />
<p className="text-theme-text-secondary text-sm font-medium py-1">
{t("onboarding.data.settingsHint")}
</p>

View File

@@ -31,6 +31,7 @@ import DellProAiStudioLogo from "@/media/llmprovider/dpais.png";
import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png";
import CometApiLogo from "@/media/llmprovider/cometapi.png";
import GiteeAILogo from "@/media/llmprovider/giteeai.png";
import DockerModelRunnerLogo from "@/media/llmprovider/docker-model-runner.png";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions";
@@ -63,6 +64,7 @@ import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions";
import MoonshotAiOptions from "@/components/LLMSelection/MoonshotAiOptions";
import CometApiLLMOptions from "@/components/LLMSelection/CometApiLLMOptions";
import GiteeAiOptions from "@/components/LLMSelection/GiteeAIOptions";
import DockerModelRunnerOptions from "@/components/LLMSelection/DockerModelRunnerOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import System from "@/models/system";
@@ -139,6 +141,13 @@ const LLMS = [
description:
"Discover, download, and run thousands of cutting edge LLMs in a few clicks.",
},
{
name: "Docker Model Runner",
value: "docker-model-runner",
logo: DockerModelRunnerLogo,
options: (settings) => <DockerModelRunnerOptions settings={settings} />,
description: "Run LLMs using Docker Model Runner.",
},
{
name: "Local AI",
value: "localai",

View File

@@ -7,6 +7,7 @@ import { CheckCircle } from "@phosphor-icons/react";
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Workspace from "@/models/workspace";
async function sendQuestionnaire({ email, useCase, comment }) {
if (import.meta.env.DEV) {
@@ -53,7 +54,7 @@ export default function Survey({ setHeader, setForwardBtn, setBackBtn }) {
function handleForward() {
if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {
navigate(paths.onboarding.createWorkspace());
navigate(paths.home());
return;
}
@@ -78,7 +79,7 @@ export default function Survey({ setHeader, setForwardBtn, setBackBtn }) {
}
function skipSurvey() {
navigate(paths.onboarding.createWorkspace());
navigate(paths.home());
}
function handleBack() {
@@ -91,6 +92,19 @@ export default function Survey({ setHeader, setForwardBtn, setBackBtn }) {
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
}, []);
useEffect(() => {
async function createDefaultWorkspace() {
const workspaces = await Workspace.all();
if (workspaces.length === 0) {
await Workspace.new({
name: t("new-workspace.placeholder"),
onboardingComplete: true,
});
}
}
createDefaultWorkspace();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
const form = e.target;
@@ -102,7 +116,7 @@ export default function Survey({ setHeader, setForwardBtn, setBackBtn }) {
comment: formData.get("comment") || null,
});
navigate(paths.onboarding.createWorkspace());
navigate(paths.home());
};
if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {

View File

@@ -6,7 +6,6 @@ import LLMPreference from "./LLMPreference";
import UserSetup from "./UserSetup";
import DataHandling from "./DataHandling";
import Survey from "./Survey";
import CreateWorkspace from "./CreateWorkspace";
const OnboardingSteps = {
home: Home,
@@ -14,7 +13,6 @@ const OnboardingSteps = {
"user-setup": UserSetup,
"data-handling": DataHandling,
survey: Survey,
"create-workspace": CreateWorkspace,
};
export default OnboardingSteps;

View File

@@ -36,8 +36,9 @@ const ENABLED_PROVIDERS = [
"foundry",
"zai",
"giteeai",
"cohere",
"docker-model-runner",
// TODO: More agent support.
// "cohere", // Has tool calling and will need to build explicit support
// "huggingface" // Can be done but already has issues with no-chat templated. Needs to be tested.
];
const WARN_PERFORMANCE = [
@@ -46,6 +47,7 @@ const WARN_PERFORMANCE = [
"ollama",
"localai",
"textgenwebui",
"docker-model-runner",
];
const LLM_DEFAULT = {

Some files were not shown because too many files have changed in this diff Show More