Merge branch 'Mintplex-Labs:master' into feature/devcontv2

This commit is contained in:
Francisco Bischoff 2024-03-21 22:50:01 +00:00 committed by GitHub
commit eee187ed00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 3519 additions and 1209 deletions

8
.hadolint.yaml Normal file
View File

@ -0,0 +1,8 @@
failure-threshold: warning
ignored:
- DL3008
- DL3013
format: tty
trustedRegistries:
- docker.io
- gcr.io

View File

@ -4,16 +4,20 @@
"Astra",
"Dockerized",
"Embeddable",
"GROQ",
"hljs",
"inferencing",
"Langchain",
"Milvus",
"Mintplex",
"Ollama",
"openai",
"openrouter",
"Qdrant",
"vectordbs",
"Weaviate",
"Zilliz"
],
"eslint.experimental.useFlatConfig": true
}
"eslint.experimental.useFlatConfig": true,
"docker.languageserver.formatter.ignoreMultilineInstructions": true
}

View File

@ -25,7 +25,7 @@ app.use(
);
app.post("/process", async function (request, response) {
const { filename } = reqBody(request);
const { filename, options = {} } = reqBody(request);
try {
const targetFilename = path
.normalize(filename)
@ -34,7 +34,7 @@ app.post("/process", async function (request, response) {
success,
reason,
documents = [],
} = await processSingleFile(targetFilename);
} = await processSingleFile(targetFilename, options);
response
.status(200)
.json({ filename: targetFilename, success, reason, documents });

View File

@ -33,6 +33,7 @@
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"officeparser": "^4.0.5",
"openai": "^3.2.1",
"pdf-parse": "^1.1.1",
"puppeteer": "~21.5.2",
"slugify": "^1.6.6",
@ -40,10 +41,10 @@
"uuid": "^9.0.0",
"wavefile": "^11.0.0",
"youtube-transcript": "^1.0.6",
"youtubei.js": "^8.0.0"
"youtubei.js": "^9.1.0"
},
"devDependencies": {
"nodemon": "^2.0.22",
"prettier": "^2.4.1"
}
}
}

View File

@ -1,5 +1,3 @@
const fs = require("fs");
const path = require("path");
const { v4 } = require("uuid");
const {
createdDate,
@ -9,39 +7,35 @@ const {
const { tokenizeString } = require("../../utils/tokenizer");
const { default: slugify } = require("slugify");
const { LocalWhisper } = require("../../utils/WhisperProviders/localWhisper");
const { OpenAiWhisper } = require("../../utils/WhisperProviders/OpenAiWhisper");
async function asAudio({ fullFilePath = "", filename = "" }) {
const whisper = new LocalWhisper();
const WHISPER_PROVIDERS = {
openai: OpenAiWhisper,
local: LocalWhisper,
};
async function asAudio({ fullFilePath = "", filename = "", options = {} }) {
const WhisperProvider = WHISPER_PROVIDERS.hasOwnProperty(
options?.whisperProvider
)
? WHISPER_PROVIDERS[options?.whisperProvider]
: WHISPER_PROVIDERS.local;
console.log(`-- Working ${filename} --`);
const transcriberPromise = new Promise((resolve) =>
whisper.client().then((client) => resolve(client))
);
const audioDataPromise = new Promise((resolve) =>
convertToWavAudioData(fullFilePath).then((audioData) => resolve(audioData))
);
const [audioData, transcriber] = await Promise.all([
audioDataPromise,
transcriberPromise,
]);
const whisper = new WhisperProvider({ options });
const { content, error } = await whisper.processFile(fullFilePath, filename);
if (!audioData) {
console.error(`Failed to parse content from ${filename}.`);
if (!!error) {
console.error(`Error encountered for parsing of ${filename}.`);
trashFile(fullFilePath);
return {
success: false,
reason: `Failed to parse content from ${filename}.`,
reason: error,
documents: [],
};
}
console.log(`[Model Working]: Transcribing audio data to text`);
const { text: content } = await transcriber(audioData, {
chunk_length_s: 30,
stride_length_s: 5,
});
if (!content.length) {
if (!content?.length) {
console.error(`Resulting text content was empty for ${filename}.`);
trashFile(fullFilePath);
return {
@ -76,79 +70,4 @@ async function asAudio({ fullFilePath = "", filename = "" }) {
return { success: true, reason: null, documents: [document] };
}
async function convertToWavAudioData(sourcePath) {
try {
let buffer;
const wavefile = require("wavefile");
const ffmpeg = require("fluent-ffmpeg");
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") {
console.log(
`[Conversion Required] ${fileExtension} file detected - converting to .wav`
);
const outputFile = path.resolve(outFolder, `${v4()}.wav`);
const convert = new Promise((resolve) => {
ffmpeg(sourcePath)
.toFormat("wav")
.on("error", (error) => {
console.error(`[Conversion Error] ${error.message}`);
resolve(false);
})
.on("progress", (progress) =>
console.log(
`[Conversion Processing]: ${progress.targetSize}KB converted`
)
)
.on("end", () => {
console.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);
}
const wavFile = new wavefile.WaveFile(buffer);
wavFile.toBitDepth("32f");
wavFile.toSampleRate(16000);
let audioData = wavFile.getSamples();
if (Array.isArray(audioData)) {
if (audioData.length > 1) {
const SCALING_FACTOR = Math.sqrt(2);
// Merge channels into first channel to save memory
for (let i = 0; i < audioData[0].length; ++i) {
audioData[0][i] =
(SCALING_FACTOR * (audioData[0][i] + audioData[1][i])) / 2;
}
}
audioData = audioData[0];
}
return audioData;
} catch (error) {
console.error(`convertToWavAudioData`, error);
return null;
}
}
module.exports = asAudio;

View File

@ -40,9 +40,9 @@ async function asPDF({ fullFilePath = "", filename = "" }) {
const data = {
id: v4(),
url: "file://" + fullFilePath,
title: docs[0]?.metadata?.pdf?.info?.Title || filename,
title: filename,
docAuthor: docs[0]?.metadata?.pdf?.info?.Creator || "no author found",
description: "No description found.",
description: docs[0]?.metadata?.pdf?.info?.Title || "No description found.",
docSource: "pdf file uploaded by the user.",
chunkSource: "",
published: createdDate(fullFilePath),

View File

@ -7,7 +7,7 @@ const {
const { trashFile, isTextType } = require("../utils/files");
const RESERVED_FILES = ["__HOTDIR__.md"];
async function processSingleFile(targetFilename) {
async function processSingleFile(targetFilename, options = {}) {
const fullFilePath = path.resolve(WATCH_DIRECTORY, targetFilename);
if (RESERVED_FILES.includes(targetFilename))
return {
@ -54,6 +54,7 @@ async function processSingleFile(targetFilename) {
return await FileTypeProcessor({
fullFilePath,
filename: targetFilename,
options,
});
}

View File

@ -0,0 +1,44 @@
const fs = require("fs");
class OpenAiWhisper {
constructor({ options }) {
const { Configuration, OpenAIApi } = require("openai");
if (!options.openAiKey) throw new Error("No OpenAI API key was set.");
const config = new Configuration({
apiKey: options.openAiKey,
});
this.openai = new OpenAIApi(config);
this.model = "whisper-1";
this.temperature = 0;
this.#log("Initialized.");
}
#log(text, ...args) {
console.log(`\x1b[32m[OpenAiWhisper]\x1b[0m ${text}`, ...args);
}
async processFile(fullFilePath) {
return await this.openai
.createTranscription(
fs.createReadStream(fullFilePath),
this.model,
undefined,
"text",
this.temperature
)
.then((res) => {
if (res.hasOwnProperty("data"))
return { content: res.data, error: null };
return { content: "", error: "No content was able to be transcribed." };
})
.catch((e) => {
this.#log(`Could not get any response from openai whisper`, e.message);
return { content: "", error: e.message };
});
}
}
module.exports = {
OpenAiWhisper,
};

View File

@ -1,5 +1,6 @@
const path = require("path");
const fs = require("fs");
const path = require("path");
const { v4 } = require("uuid");
class LocalWhisper {
constructor() {
@ -16,12 +17,94 @@ class LocalWhisper {
// Make directory when it does not exist in existing installations
if (!fs.existsSync(this.cacheDir))
fs.mkdirSync(this.cacheDir, { recursive: true });
this.#log("Initialized.");
}
#log(text, ...args) {
console.log(`\x1b[32m[LocalWhisper]\x1b[0m ${text}`, ...args);
}
async #convertToWavAudioData(sourcePath) {
try {
let buffer;
const wavefile = require("wavefile");
const ffmpeg = require("fluent-ffmpeg");
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 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);
}
const wavFile = new wavefile.WaveFile(buffer);
wavFile.toBitDepth("32f");
wavFile.toSampleRate(16000);
let audioData = wavFile.getSamples();
if (Array.isArray(audioData)) {
if (audioData.length > 1) {
const SCALING_FACTOR = Math.sqrt(2);
// Merge channels into first channel to save memory
for (let i = 0; i < audioData[0].length; ++i) {
audioData[0][i] =
(SCALING_FACTOR * (audioData[0][i] + audioData[1][i])) / 2;
}
}
audioData = audioData[0];
}
return audioData;
} catch (error) {
console.error(`convertToWavAudioData`, error);
return null;
}
}
async client() {
if (!fs.existsSync(this.modelPath)) {
console.log(
"\x1b[34m[INFO]\x1b[0m The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~250MB)\n\n"
this.#log(
`The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~250MB)`
);
}
@ -48,10 +131,45 @@ class LocalWhisper {
: {}),
});
} catch (error) {
console.error("Failed to load the native whisper model:", error);
this.#log("Failed to load the native whisper model:", error);
throw error;
}
}
async processFile(fullFilePath, filename) {
try {
const transcriberPromise = new Promise((resolve) =>
this.client().then((client) => resolve(client))
);
const audioDataPromise = new Promise((resolve) =>
this.#convertToWavAudioData(fullFilePath).then((audioData) =>
resolve(audioData)
)
);
const [audioData, transcriber] = await Promise.all([
audioDataPromise,
transcriberPromise,
]);
if (!audioData) {
this.#log(`Failed to parse content from ${filename}.`);
return {
content: null,
error: `Failed to parse content from ${filename}.`,
};
}
this.#log(`Transcribing audio data to text...`);
const { text } = await transcriber(audioData, {
chunk_length_s: 30,
stride_length_s: 5,
});
return { content: text, error: null };
} catch (error) {
return { content: null, error: error.message };
}
}
}
module.exports = {

View File

@ -40,9 +40,9 @@
js-tokens "^4.0.0"
"@fastify/busboy@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==
version "2.1.1"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
"@googleapis/youtube@^9.0.0":
version "9.0.0"
@ -258,9 +258,9 @@ accepts@~1.3.8:
negotiator "0.6.3"
acorn@^8.8.0:
version "8.11.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
version "8.11.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
agent-base@6:
version "6.0.2"
@ -372,6 +372,13 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^0.26.0:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
b4a@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9"
@ -1203,6 +1210,11 @@ fluent-ffmpeg@^2.1.2:
async ">=0.2.9"
which "^1.1.1"
follow-redirects@^1.14.8:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
form-data-encoder@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
@ -2304,6 +2316,14 @@ onnxruntime-web@1.14.0:
onnxruntime-common "~1.14.0"
platform "^1.3.6"
openai@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-3.3.0.tgz#a6408016ad0945738e1febf43f2fccca83a3f532"
integrity sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==
dependencies:
axios "^0.26.0"
form-data "^4.0.0"
openai@^4.19.0:
version "4.20.1"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.20.1.tgz#afa0d496d125b5a0f6cebcb4b9aeabf71e00214e"
@ -3152,9 +3172,9 @@ undici-types@~5.26.4:
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^5.19.1:
version "5.28.2"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91"
integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==
version "5.28.3"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b"
integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==
dependencies:
"@fastify/busboy" "^2.0.0"
@ -3322,10 +3342,10 @@ youtube-transcript@^1.0.6:
dependencies:
phin "^3.5.0"
youtubei.js@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-8.0.0.tgz#0fcbe332e263d9be6afe4e3d1917e9ddc1ffbed3"
integrity sha512-kUwHvqoB5vfaGaY1quAGcX5JPIyjr5fjj9Zj/ZwUDCrermz/r5uIkNiJ5cNHkmAJbZP9fdygzNMvGHd7fM445g==
youtubei.js@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.1.0.tgz#bcf154c9fa21d3c8c1d00a5e10360d0a065c660e"
integrity sha512-C5GBJ4LgnS6vGAUkdIdQNOFFb5EZ1p3xBvUELNXmIG3Idr6vxWrKNBNy8ClZT3SuDVXaAJqDgF9b5jvY8lNKcg==
dependencies:
jintr "^1.1.0"
tslib "^2.5.0"

View File

@ -61,6 +61,10 @@ GID='1000'
# HUGGING_FACE_LLM_API_KEY=hf_xxxxxx
# HUGGING_FACE_LLM_TOKEN_LIMIT=8000
# LLM_PROVIDER='groq'
# GROQ_API_KEY=gsk_abcxyz
# GROQ_MODEL_PREF=llama2-70b-4096
###########################################
######## Embedding API SElECTION ##########
###########################################
@ -127,6 +131,16 @@ GID='1000'
# ASTRA_DB_APPLICATION_TOKEN=
# ASTRA_DB_ENDPOINT=
###########################################
######## Audio Model Selection ############
###########################################
# (default) use built-in whisper-small model.
# WHISPER_PROVIDER="local"
# use openai hosted whisper model.
# WHISPER_PROVIDER="openai"
# OPEN_AI_KEY=sk-xxxxxxxx
# CLOUD DEPLOYMENT VARIRABLES ONLY
# AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
# DISABLE_TELEMETRY="false"

View File

@ -1,12 +1,17 @@
# Setup base image
FROM ubuntu:jammy-20230522 AS base
FROM ubuntu:jammy-20230916 AS base
# Build arguments
ARG ARG_UID=1000
ARG ARG_GID=1000
FROM base AS build-arm64
RUN echo "Preparing build of AnythingLLM image for arm64 architecture"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install system dependencies
# hadolint ignore=DL3008,DL3013
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
unzip curl gnupg libgfortran5 libgbm1 tzdata netcat \
@ -25,8 +30,8 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& rm yarn_1.22.19_all.deb
# Create a group and user with specific UID and GID
RUN groupadd -g $ARG_GID anythingllm && \
useradd -u $ARG_UID -m -d /app -s /bin/bash -g anythingllm anythingllm && \
RUN groupadd -g "$ARG_GID" anythingllm && \
useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g anythingllm anythingllm && \
mkdir -p /app/frontend/ /app/server/ /app/collector/ && chown -R anythingllm:anythingllm /app
# Copy docker helper scripts
@ -61,6 +66,10 @@ RUN echo "Done running arm64 specific installtion steps"
FROM base AS build-amd64
RUN echo "Preparing build of AnythingLLM image for non-ARM architecture"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install system dependencies
# hadolint ignore=DL3008,DL3013
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
curl gnupg libgfortran5 libgbm1 tzdata netcat \
@ -79,8 +88,8 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& rm yarn_1.22.19_all.deb
# Create a group and user with specific UID and GID
RUN groupadd -g $ARG_GID anythingllm && \
useradd -u $ARG_UID -m -d /app -s /bin/bash -g anythingllm anythingllm && \
RUN groupadd -g "$ARG_GID" anythingllm && \
useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g anythingllm anythingllm && \
mkdir -p /app/frontend/ /app/server/ /app/collector/ && chown -R anythingllm:anythingllm /app
# Copy docker helper scripts
@ -95,6 +104,8 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh && \
#############################################
# COMMON BUILD FLOW FOR ALL ARCHS
#############################################
# hadolint ignore=DL3006
FROM build-${TARGETARCH} AS build
RUN echo "Running common build flow of AnythingLLM image for all architectures"
@ -102,43 +113,54 @@ USER anythingllm
WORKDIR /app
# Install frontend dependencies
FROM build as frontend-deps
FROM build AS frontend-deps
COPY ./frontend/package.json ./frontend/yarn.lock ./frontend/
RUN cd ./frontend/ && yarn install --network-timeout 100000 && yarn cache clean
WORKDIR /app/frontend
RUN yarn install --network-timeout 100000 && yarn cache clean
WORKDIR /app
# Install server dependencies
FROM build as server-deps
FROM build AS server-deps
COPY ./server/package.json ./server/yarn.lock ./server/
RUN cd ./server/ && yarn install --production --network-timeout 100000 && yarn cache clean
WORKDIR /app/server
RUN yarn install --production --network-timeout 100000 && yarn cache clean
WORKDIR /app
# Compile Llama.cpp bindings for node-llama-cpp for this operating system.
USER root
RUN cd ./server && npx --no node-llama-cpp download
WORKDIR /app/server
RUN npx --no node-llama-cpp download
WORKDIR /app
USER anythingllm
# Build the frontend
FROM frontend-deps as build-stage
FROM frontend-deps AS build-stage
COPY ./frontend/ ./frontend/
RUN cd ./frontend/ && yarn build && yarn cache clean
WORKDIR /app/frontend
RUN yarn build && yarn cache clean
WORKDIR /app
# Setup the server
FROM server-deps as production-stage
FROM server-deps AS production-stage
COPY --chown=anythingllm:anythingllm ./server/ ./server/
# Copy built static frontend files to the server public directory
COPY --from=build-stage /app/frontend/dist ./server/public
COPY --chown=anythingllm:anythingllm --from=build-stage /app/frontend/dist ./server/public
# Copy the collector
COPY --chown=anythingllm:anythingllm ./collector/ ./collector/
# Install collector dependencies
WORKDIR /app/collector
ENV PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public
RUN cd /app/collector && yarn install --production --network-timeout 100000 && yarn cache clean
RUN yarn install --production --network-timeout 100000 && yarn cache clean
# Migrate and Run Prisma against known schema
RUN cd ./server && npx prisma generate --schema=./prisma/schema.prisma
RUN cd ./server && npx prisma migrate deploy --schema=./prisma/schema.prisma
WORKDIR /app/server
RUN npx prisma generate --schema=./prisma/schema.prisma && \
npx prisma migrate deploy --schema=./prisma/schema.prisma
WORKDIR /app
# Setup the environment
ENV NODE_ENV=production
@ -152,4 +174,4 @@ HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \
CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1
# Run the server
ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"]
ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"]

View File

@ -75,7 +75,7 @@ mintplexlabs/anythingllm
# Run this in powershell terminal
$env:STORAGE_LOCATION="$HOME\Documents\anythingllm"; `
If(!(Test-Path $env:STORAGE_LOCATION)) {New-Item $env:STORAGE_LOCATION -ItemType Directory}; `
If(!(Test-Path "$env:STORAGE_LOCATION\.env")) {New-Item "$env:STORAGE_LOCATION\.env"}; `
If(!(Test-Path "$env:STORAGE_LOCATION\.env")) {New-Item "$env:STORAGE_LOCATION\.env" -ItemType File}; `
docker run -d -p 3001:3001 `
--cap-add SYS_ADMIN `
-v "$env:STORAGE_LOCATION`:/app/server/storage" `
@ -109,29 +109,23 @@ container rebuilds or pulls from Docker Hub.
Your docker host will show the image as online once the build process is completed. This will build the app to `http://localhost:3001`.
## ⚠️ Vector DB support ⚠️
Out of the box, all vector databases are supported. Any vector databases requiring special configuration are listed below.
### Using local ChromaDB with Dockerized AnythingLLM
- Ensure in your `./docker/.env` file that you have
```
#./docker/.env
...other configs
VECTOR_DB="chroma"
CHROMA_ENDPOINT='http://host.docker.internal:8000' # Allow docker to look on host port, not container.
# CHROMA_API_HEADER="X-Api-Key" // If you have an Auth middleware on your instance.
# CHROMA_API_KEY="sk-123abc"
...other configs
```
## Common questions and fixes
### Cannot connect to service running on localhost!
If you are in docker and cannot connect to a service running on your host machine running on a local interface or loopback:
- `localhost`
- `127.0.0.1`
- `0.0.0.0`
> [!IMPORTANT]
> On linux `http://host.docker.internal:xxxx` does not work.
> Use `http://172.17.0.1:xxxx` instead to emulate this functionality.
Then in docker you need to replace that localhost part with `host.docker.internal`. For example, if running Ollama on the host machine, bound to http://127.0.0.1:11434 you should put `http://host.docker.internal:11434` into the connection URL in AnythingLLM.
### API is not working, cannot login, LLM is "offline"?
You are likely running the docker container on a remote machine like EC2 or some other instance where the reachable URL

View File

@ -1,9 +1,10 @@
#!/bin/bash
{ cd /app/server/ &&\
npx prisma generate --schema=./prisma/schema.prisma &&\
npx prisma migrate deploy --schema=./prisma/schema.prisma &&\
node /app/server/index.js
{
cd /app/server/ &&
npx prisma generate --schema=./prisma/schema.prisma &&
npx prisma migrate deploy --schema=./prisma/schema.prisma &&
node /app/server/index.js
} &
{ node /app/collector/index.js; } &
wait -n
exit $?
exit $?

View File

@ -4,10 +4,10 @@
response=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:3001/api/ping)
# If the HTTP response code is 200 (OK), the server is up
if [ $response -eq 200 ]; then
echo "Server is up"
exit 0
if [ "$response" -eq 200 ]; then
echo "Server is up"
exit 0
else
echo "Server is down"
exit 1
echo "Server is down"
exit 1
fi

View File

@ -29,6 +29,9 @@ const GeneralApiKeys = lazy(() => import("@/pages/GeneralSettings/ApiKeys"));
const GeneralLLMPreference = lazy(
() => import("@/pages/GeneralSettings/LLMPreference")
);
const GeneralTranscriptionPreference = lazy(
() => import("@/pages/GeneralSettings/TranscriptionPreference")
);
const GeneralEmbeddingPreference = lazy(
() => import("@/pages/GeneralSettings/EmbeddingPreference")
);
@ -47,6 +50,9 @@ const EmbedConfigSetup = lazy(
() => import("@/pages/GeneralSettings/EmbedConfigs")
);
const EmbedChats = lazy(() => import("@/pages/GeneralSettings/EmbedChats"));
const PrivacyAndData = lazy(
() => import("@/pages/GeneralSettings/PrivacyAndData")
);
export default function App() {
return (
@ -76,6 +82,12 @@ export default function App() {
path="/settings/llm-preference"
element={<AdminRoute Component={GeneralLLMPreference} />}
/>
<Route
path="/settings/transcription-preference"
element={
<AdminRoute Component={GeneralTranscriptionPreference} />
}
/>
<Route
path="/settings/embedding-preference"
element={<AdminRoute Component={GeneralEmbeddingPreference} />}
@ -101,6 +113,10 @@ export default function App() {
path="/settings/security"
element={<ManagerRoute Component={GeneralSecurity} />}
/>
<Route
path="/settings/privacy"
element={<AdminRoute Component={PrivacyAndData} />}
/>
<Route
path="/settings/appearance"
element={<ManagerRoute Component={GeneralAppearance} />}

View File

@ -335,7 +335,7 @@ export default function DefaultChatContainer() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
>
{isMobile && <SidebarMobileHeader />}
{fetchedMessages.length === 0

View File

@ -13,49 +13,52 @@ export default function EditingChatBubble({
const isUser = type === "user";
return (
<div
className={`relative flex w-full mt-2 items-start ${
isUser ? "justify-end" : "justify-start"
}`}
>
<button
className={`transition-all duration-300 absolute z-10 text-white bg-neutral-700 rounded-full hover:bg-selected-preference-gradient hover:border-white border-transparent border shadow-lg ${
isUser ? "right-0 mr-2" : "ml-2"
}`}
style={{ top: "-8px", [isUser ? "right" : "left"]: "255px" }}
onClick={() => removeMessage(index)}
>
<X className="m-0.5" size={20} />
</button>
<div>
<p className={`text-xs text-[#D3D4D4] ${isUser ? "text-right" : ""}`}>
{isUser ? "User" : "AnythingLLM Chat Assistant"}
</p>
<div
className={`p-4 max-w-full md:w-[290px] ${
isUser ? "bg-sky-400 text-black" : "bg-white text-black"
} ${
isUser
? "rounded-tr-[40px] rounded-tl-[40px] rounded-bl-[40px]"
: "rounded-br-[40px] rounded-tl-[40px] rounded-tr-[40px]"
}
className={`relative flex w-full mt-2 items-start ${
isUser ? "justify-end" : "justify-start"
}`}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
value={tempMessage}
onChange={(e) => setTempMessage(e.target.value)}
onBlur={() => {
handleMessageChange(index, type, tempMessage);
setIsEditing(false);
}}
autoFocus
className="w-full"
/>
) : (
tempMessage && (
<p className="text-black font-[500] md:font-semibold text-sm md:text-base break-words">
{tempMessage}
</p>
)
)}
<button
className={`transition-all duration-300 absolute z-10 text-white rounded-full hover:bg-neutral-700 hover:border-white border-transparent border shadow-lg ${
isUser ? "right-0 mr-2" : "ml-2"
}`}
style={{ top: "6px", [isUser ? "right" : "left"]: "290px" }}
onClick={() => removeMessage(index)}
>
<X className="m-0.5" size={20} />
</button>
<div
className={`p-2 max-w-full md:w-[290px] text-black rounded-[8px] ${
isUser ? "bg-[#41444C] text-white" : "bg-[#2E3036] text-white"
}
}`}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
value={tempMessage}
onChange={(e) => setTempMessage(e.target.value)}
onBlur={() => {
handleMessageChange(index, type, tempMessage);
setIsEditing(false);
}}
autoFocus
className={`w-full ${
isUser ? "bg-[#41444C] text-white" : "bg-[#2E3036] text-white"
}`}
/>
) : (
tempMessage && (
<p className=" font-[500] md:font-semibold text-sm md:text-base break-words">
{tempMessage}
</p>
)
)}
</div>
</div>
</div>
);

View File

@ -1,6 +1,5 @@
import System from "@/models/system";
import paths from "@/utils/paths";
import { safeJsonParse } from "@/utils/request";
import {
BookOpen,
DiscordLogo,
@ -13,6 +12,8 @@ import {
LinkSimple,
} from "@phosphor-icons/react";
import React, { useEffect, useState } from "react";
import SettingsButton from "../SettingsButton";
import { isMobile } from "react-device-detect";
export const MAX_ICONS = 3;
export const ICON_COMPONENTS = {
@ -44,11 +45,12 @@ export default function Footer() {
if (!Array.isArray(footerData) || footerData.length === 0) {
return (
<div className="flex justify-center mt-2">
<div className="flex justify-center mb-2">
<div className="flex space-x-4">
<a
href={paths.github()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<GithubLogo weight="fill" className="h-5 w-5 " />
@ -56,6 +58,7 @@ export default function Footer() {
<a
href={paths.docs()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<BookOpen weight="fill" className="h-5 w-5 " />
@ -63,6 +66,7 @@ export default function Footer() {
<a
href={paths.discord()}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<DiscordLogo
@ -70,19 +74,21 @@ export default function Footer() {
className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200"
/>
</a>
{!isMobile && <SettingsButton />}
</div>
</div>
);
}
return (
<div className="flex justify-center mt-2">
<div className="flex justify-center mb-2">
<div className="flex space-x-4">
{footerData.map((item, index) => (
<a
key={index}
href={item.url}
target="_blank"
rel="noreferrer"
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
{React.createElement(ICON_COMPONENTS[item.icon], {
@ -91,6 +97,7 @@ export default function Footer() {
})}
</a>
))}
{!isMobile && <SettingsButton />}
</div>
</div>
);

View File

@ -48,7 +48,14 @@ export default function AnthropicAiOptions({ settings, showAlert = false }) {
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{["claude-2", "claude-instant-1"].map((model) => {
{[
"claude-instant-1.2",
"claude-2.0",
"claude-2.1",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
].map((model) => {
return (
<option key={model} value={model}>
{model}

View File

@ -0,0 +1,41 @@
export default function GroqAiOptions({ settings }) {
return (
<div className="flex gap-x-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Groq API Key
</label>
<input
type="password"
name="GroqApiKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="Groq API Key"
defaultValue={settings?.GroqApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Chat Model Selection
</label>
<select
name="GroqModelPref"
defaultValue={settings?.GroqModelPref || "llama2-70b-4096"}
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{["llama2-70b-4096", "mixtral-8x7b-32768"].map((model) => {
return (
<option key={model} value={model}>
{model}
</option>
);
})}
</select>
</div>
</div>
);
}

View File

@ -4,45 +4,12 @@ import {
getFileExtension,
middleTruncate,
} from "@/utils/directories";
import { File, Trash } from "@phosphor-icons/react";
import System from "@/models/system";
import { File } from "@phosphor-icons/react";
import debounce from "lodash.debounce";
export default function FileRow({
item,
folderName,
selected,
toggleSelection,
expanded,
fetchKeys,
setLoading,
setLoadingMessage,
}) {
export default function FileRow({ item, selected, toggleSelection }) {
const [showTooltip, setShowTooltip] = useState(false);
const onTrashClick = async (event) => {
event.stopPropagation();
if (
!window.confirm(
"Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
)
) {
return false;
}
try {
setLoading(true);
setLoadingMessage("This may take a while for large documents");
await System.deleteDocument(`${folderName}/${item.name}`);
await fetchKeys(true);
} catch (error) {
console.error("Failed to delete the document:", error);
}
if (selected) toggleSelection(item);
setLoading(false);
};
const handleShowTooltip = () => {
setShowTooltip(true);
};
@ -53,14 +20,15 @@ export default function FileRow({
const handleMouseEnter = debounce(handleShowTooltip, 500);
const handleMouseLeave = debounce(handleHideTooltip, 500);
return (
<div
<tr
onClick={() => toggleSelection(item)}
className={`transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer ${`${
selected ? "bg-sky-500/20" : ""
} ${expanded ? "bg-sky-500/10" : ""}`}`}
className={`text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-sky-500/20 cursor-pointer file-row ${
selected ? "selected" : ""
}`}
>
<div className="pl-2 col-span-5 flex gap-x-[4px] items-center">
<div className="pl-2 col-span-6 flex gap-x-[4px] items-center">
<div
className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
role="checkbox"
@ -94,17 +62,13 @@ export default function FileRow({
<p className="col-span-2 pl-2 uppercase overflow-x-hidden">
{getFileExtension(item.url)}
</p>
<div className="col-span-2 flex justify-end items-center">
<div className="-col-span-2 flex justify-end items-center">
{item?.cached && (
<div className="bg-white/10 rounded-3xl">
<p className="text-xs px-2 py-0.5">Cached</p>
</div>
)}
<Trash
onClick={onTrashClick}
className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
/>
</div>
</div>
</tr>
);
}

View File

@ -1,8 +1,7 @@
import { useState } from "react";
import FileRow from "../FileRow";
import { CaretDown, FolderNotch, Trash } from "@phosphor-icons/react";
import { CaretDown, FolderNotch } from "@phosphor-icons/react";
import { middleTruncate } from "@/utils/directories";
import System from "@/models/system";
export default function FolderRow({
item,
@ -10,36 +9,10 @@ export default function FolderRow({
onRowClick,
toggleSelection,
isSelected,
fetchKeys,
setLoading,
setLoadingMessage,
autoExpanded = false,
}) {
const [expanded, setExpanded] = useState(autoExpanded);
const onTrashClick = async (event) => {
event.stopPropagation();
if (
!window.confirm(
"Are you sure you want to delete this folder?\nThis will require you to re-upload and re-embed it.\nAny documents in this folder will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
)
) {
return false;
}
try {
setLoading(true);
setLoadingMessage("This may take a while for large folders");
await System.deleteFolder(item.name);
await fetchKeys(true);
} catch (error) {
console.error("Failed to delete the document:", error);
}
if (selected) toggleSelection(item);
setLoading(false);
};
const handleExpandClick = (event) => {
event.stopPropagation();
setExpanded(!expanded);
@ -47,10 +20,10 @@ export default function FolderRow({
return (
<>
<div
<tr
onClick={onRowClick}
className={`transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer w-full ${
selected ? "bg-sky-500/20" : ""
className={`text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 bg-[#1C1E21] hover:bg-sky-500/20 cursor-pointer w-full file-row ${
selected ? "selected" : ""
}`}
>
<div className="col-span-6 flex gap-x-[4px] items-center">
@ -59,6 +32,10 @@ export default function FolderRow({
role="checkbox"
aria-checked={selected}
tabIndex={0}
onClick={(event) => {
event.stopPropagation();
toggleSelection(item);
}}
>
{selected && <div className="w-2 h-2 bg-white rounded-[2px]" />}
</div>
@ -75,36 +52,23 @@ export default function FolderRow({
weight="fill"
/>
<p className="whitespace-nowrap overflow-show">
{middleTruncate(item.name, 40)}
{middleTruncate(item.name, 35)}
</p>
</div>
<p className="col-span-2 pl-3.5" />
<p className="col-span-2 pl-2" />
<div className="col-span-2 flex justify-end items-center">
{item.name !== "custom-documents" && (
<Trash
onClick={onTrashClick}
className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
/>
)}
</div>
</div>
</tr>
{expanded && (
<div className="col-span-full">
<>
{item.items.map((fileItem) => (
<FileRow
key={fileItem.id}
item={fileItem}
folderName={item.name}
selected={isSelected(fileItem.id)}
expanded={expanded}
toggleSelection={toggleSelection}
fetchKeys={fetchKeys}
setLoading={setLoading}
setLoadingMessage={setLoadingMessage}
/>
))}
</div>
</>
)}
</>
);

View File

@ -0,0 +1,24 @@
import { middleTruncate } from "@/utils/directories";
export default function FolderSelectionPopup({ folders, onSelect, onClose }) {
const handleFolderSelect = (folder) => {
onSelect(folder);
onClose();
};
return (
<div className="absolute bottom-full left-0 mb-2 bg-white rounded-lg shadow-lg">
<ul>
{folders.map((folder) => (
<li
key={folder.name}
onClick={() => handleFolderSelect(folder)}
className="px-4 py-2 text-xs text-gray-700 hover:bg-gray-200 rounded-lg cursor-pointer whitespace-nowrap"
>
{middleTruncate(folder.name, 25)}
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,44 @@
export default function MoveToFolderIcon({
className,
width = 18,
height = 18,
}) {
return (
<svg
width={width}
height={height}
viewBox="0 0 17 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M1.46092 17.9754L3.5703 12.7019C3.61238 12.5979 3.68461 12.5088 3.7777 12.4462C3.8708 12.3836 3.98051 12.3502 4.09272 12.3504H7.47897C7.59001 12.3502 7.69855 12.3174 7.79116 12.2562L9.19741 11.3196C9.29001 11.2583 9.39855 11.2256 9.50959 11.2254H15.5234C15.6126 11.2254 15.7004 11.2465 15.7798 11.2872C15.8591 11.3278 15.9277 11.3867 15.9798 11.459C16.0319 11.5313 16.0661 11.6149 16.0795 11.703C16.093 11.7912 16.0853 11.8812 16.0571 11.9658L14.0532 17.9754H1.46092Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.25331 6.53891H2.02342C1.67533 6.53891 1.34149 6.67719 1.09534 6.92333C0.849204 7.16947 0.710922 7.50331 0.710922 7.85141V17.9764C0.710922 18.3906 1.04671 18.7264 1.46092 18.7264C1.87514 18.7264 2.21092 18.3906 2.21092 17.9764V8.03891H2.25331V6.53891ZM13.0859 9.98714V11.2264C13.0859 11.6406 13.4217 11.9764 13.8359 11.9764C14.2501 11.9764 14.5859 11.6406 14.5859 11.2264V9.53891C14.5859 9.19081 14.4476 8.85698 14.2015 8.61083C13.9554 8.36469 13.6215 8.22641 13.2734 8.22641H13.0863V9.98714H13.0859Z"
fill="currentColor"
/>
<path
d="M7.53416 1.62906L7.53416 7.70406"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.6411 5.21854L7.53456 7.70376L4.42803 5.21854"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@ -2,11 +2,16 @@ import UploadFile from "../UploadFile";
import PreLoader from "@/components/Preloader";
import { memo, useEffect, useState } from "react";
import FolderRow from "./FolderRow";
import pluralize from "pluralize";
import System from "@/models/system";
import { Plus, Trash } from "@phosphor-icons/react";
import Document from "@/models/document";
import showToast from "@/utils/toast";
import FolderSelectionPopup from "./FolderSelectionPopup";
import MoveToFolderIcon from "./MoveToFolderIcon";
function Directory({
files,
setFiles,
loading,
setLoading,
workspace,
@ -19,12 +24,19 @@ function Directory({
loadingMessage,
}) {
const [amountSelected, setAmountSelected] = useState(0);
const [newFolderName, setNewFolderName] = useState("");
const [showNewFolderInput, setShowNewFolderInput] = useState(false);
const [showFolderSelection, setShowFolderSelection] = useState(false);
useEffect(() => {
setAmountSelected(Object.keys(selectedItems).length);
}, [selectedItems]);
const deleteFiles = async (event) => {
event.stopPropagation();
if (
!window.confirm(
"Are you sure you want to delete these files?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible."
"Are you sure you want to delete these files and folders?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible."
)
) {
return false;
@ -32,6 +44,8 @@ function Directory({
try {
const toRemove = [];
const foldersToRemove = [];
for (const itemId of Object.keys(selectedItems)) {
for (const folder of files.items) {
const foundItem = folder.items.find((file) => file.id === itemId);
@ -41,13 +55,29 @@ function Directory({
}
}
}
for (const folder of files.items) {
if (folder.name === "custom-documents") {
continue;
}
if (isSelected(folder.id, folder)) {
foldersToRemove.push(folder.name);
}
}
setLoading(true);
setLoadingMessage(`Removing ${toRemove.length} documents. Please wait.`);
setLoadingMessage(
`Removing ${toRemove.length} documents and ${foldersToRemove.length} folders. Please wait.`
);
await System.deleteDocuments(toRemove);
for (const folderName of foldersToRemove) {
await System.deleteFolder(folderName);
}
await fetchKeys(true);
setSelectedItems({});
} catch (error) {
console.error("Failed to delete the document:", error);
console.error("Failed to delete files and folders:", error);
} finally {
setLoading(false);
setSelectedItems({});
@ -57,15 +87,17 @@ function Directory({
const toggleSelection = (item) => {
setSelectedItems((prevSelectedItems) => {
const newSelectedItems = { ...prevSelectedItems };
if (item.type === "folder") {
const isCurrentlySelected = isFolderCompletelySelected(item);
if (isCurrentlySelected) {
// select all files in the folder
if (newSelectedItems[item.name]) {
delete newSelectedItems[item.name];
item.items.forEach((file) => delete newSelectedItems[file.id]);
} else {
newSelectedItems[item.name] = true;
item.items.forEach((file) => (newSelectedItems[file.id] = true));
}
} else {
// single file selections
if (newSelectedItems[item.id]) {
delete newSelectedItems[item.id];
} else {
@ -77,44 +109,124 @@ function Directory({
});
};
const isFolderCompletelySelected = (folder) => {
if (folder.items.length === 0) {
return false;
}
return folder.items.every((file) => selectedItems[file.id]);
};
// check if item is selected based on selectedItems state
const isSelected = (id, item) => {
if (item && item.type === "folder") {
return isFolderCompletelySelected(item);
if (!selectedItems[item.name]) {
return false;
}
return item.items.every((file) => selectedItems[file.id]);
}
return !!selectedItems[id];
};
useEffect(() => {
setAmountSelected(Object.keys(selectedItems).length);
}, [selectedItems]);
const createNewFolder = () => {
setShowNewFolderInput(true);
};
const confirmNewFolder = async () => {
if (newFolderName.trim() !== "") {
const newFolder = {
name: newFolderName,
type: "folder",
items: [],
};
// If folder failed to create - silently fail.
const { success } = await Document.createFolder(newFolderName);
if (success) {
setFiles({
...files,
items: [...files.items, newFolder],
});
}
setNewFolderName("");
setShowNewFolderInput(false);
}
};
const moveToFolder = async (folder) => {
const toMove = [];
for (const itemId of Object.keys(selectedItems)) {
for (const currentFolder of files.items) {
const foundItem = currentFolder.items.find(
(file) => file.id === itemId
);
if (foundItem) {
toMove.push({ ...foundItem, folderName: currentFolder.name });
break;
}
}
}
setLoading(true);
setLoadingMessage(`Moving ${toMove.length} documents. Please wait.`);
const { success, message } = await Document.moveToFolder(
toMove,
folder.name
);
if (!success) {
showToast(`Error moving files: ${message}`, "error");
setLoading(false);
return;
}
if (success && message) {
// show info if some files were not moved due to being embedded
showToast(message, "info");
} else {
showToast(`Successfully moved ${toMove.length} documents.`, "success");
}
await fetchKeys(true);
setSelectedItems({});
setLoading(false);
};
return (
<div className="px-8 pb-8">
<div className="flex flex-col gap-y-6">
<div className="flex items-center justify-between w-[560px] px-5">
<div className="flex items-center justify-between w-[560px] px-5 relative">
<h3 className="text-white text-base font-bold">My Documents</h3>
{showNewFolderInput ? (
<div className="flex items-center gap-x-2 z-50">
<input
type="text"
placeholder="Folder name"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[150px] h-[32px]"
/>
<div className="flex gap-x-2">
<button
onClick={confirmNewFolder}
className="text-sky-400 rounded-md text-sm font-bold hover:text-sky-500"
>
Create
</button>
</div>
</div>
) : (
<button
className="flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-[#222628]/60"
onClick={createNewFolder}
>
<Plus size={18} weight="bold" color="#D3D4D4" />
<div className="text-[#D3D4D4] text-xs font-bold leading-[18px]">
New Folder
</div>
</button>
)}
</div>
<div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl">
<div className="rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900 sticky top-0 z-10">
<p className="col-span-5">Name</p>
<div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl overflow-hidden">
<div className="absolute top-0 left-0 right-0 z-10 rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900">
<p className="col-span-6">Name</p>
<p className="col-span-3">Date</p>
<p className="col-span-2">Kind</p>
<p className="col-span-2">Cached</p>
</div>
<div
className="overflow-y-auto pb-9"
style={{ height: "calc(100% - 40px)" }}
>
<div className="overflow-y-auto h-full pt-8">
{loading ? (
<div className="w-full h-full flex items-center justify-center flex-col gap-y-5">
<PreLoader />
@ -122,11 +234,10 @@ function Directory({
{loadingMessage}
</p>
</div>
) : !!files.items ? (
) : files.items ? (
files.items.map(
(item, index) =>
(item.name === "custom-documents" ||
(item.type === "folder" && item.items.length > 0)) && (
item.type === "folder" && (
<FolderRow
key={index}
item={item}
@ -134,12 +245,9 @@ function Directory({
item.id,
item.type === "folder" ? item : null
)}
fetchKeys={fetchKeys}
onRowClick={() => toggleSelection(item)}
toggleSelection={toggleSelection}
isSelected={isSelected}
setLoading={setLoading}
setLoadingMessage={setLoadingMessage}
autoExpanded={index === 0}
/>
)
@ -152,26 +260,45 @@ function Directory({
</div>
)}
</div>
{amountSelected !== 0 && (
<div className="absolute bottom-0 left-0 w-full flex justify-between items-center h-9 bg-white rounded-b-2xl">
<div className="flex gap-x-5 w-[80%] justify-center">
<button
onMouseEnter={() => setHighlightWorkspace(true)}
onMouseLeave={() => setHighlightWorkspace(false)}
onClick={moveToWorkspace}
className="border-none text-sm font-semibold h-7 px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80 flex items-center"
>
Move {amountSelected} {pluralize("file", amountSelected)} to
workspace
</button>
<div className="absolute bottom-[12px] left-0 right-0 flex justify-center">
<div className="mx-auto bg-white/40 rounded-lg py-1 px-2">
<div className="flex flex-row items-center gap-x-2">
<button
onClick={moveToWorkspace}
onMouseEnter={() => setHighlightWorkspace(true)}
onMouseLeave={() => setHighlightWorkspace(false)}
className="border-none text-sm font-semibold bg-white h-[30px] px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80"
>
Move to Workspace
</button>
<div className="relative">
<button
onClick={() =>
setShowFolderSelection(!showFolderSelection)
}
className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-[#222628] hover:bg-neutral-800/80 flex justify-center items-center group"
>
<MoveToFolderIcon className="text-[#222628] group-hover:text-white" />
</button>
{showFolderSelection && (
<FolderSelectionPopup
folders={files.items.filter(
(item) => item.type === "folder"
)}
onSelect={moveToFolder}
onClose={() => setShowFolderSelection(false)}
/>
)}
</div>
<button
onClick={deleteFiles}
className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-[#222628] hover:text-white hover:bg-neutral-800/80 flex justify-center items-center"
>
<Trash size={18} weight="bold" />
</button>
</div>
</div>
<button
onClick={deleteFiles}
className="border-none text-red-500/50 text-sm font-semibold h-7 px-2.5 rounded-lg hover:text-red-500/80 flex items-center"
>
Delete
</button>
</div>
)}
</div>

View File

@ -57,8 +57,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
reason: file.errors[0].code,
};
});
setFiles([...files, ...newAccepted, ...newRejected]);
setFiles([...newAccepted, ...newRejected]);
};
useEffect(() => {
@ -77,7 +76,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
return (
<div>
<div
className={`transition-all duration-300 w-[560px] border-2 border-dashed rounded-2xl bg-zinc-900/50 p-3 ${
className={`w-[560px] border-2 border-dashed rounded-2xl bg-zinc-900/50 p-3 ${
ready ? "cursor-pointer" : "cursor-not-allowed"
} hover:bg-zinc-900/90`}
{...getRootProps()}
@ -135,7 +134,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
<button
disabled={fetchingUrl}
type="submit"
className="disabled:bg-white/20 disabled:text-slate-300 disabled:border-slate-400 disabled:cursor-wait bg bg-transparent hover:bg-slate-200 hover:text-slate-800 w-auto border border-white text-sm text-white p-2.5 rounded-lg transition-all duration-300"
className="disabled:bg-white/20 disabled:text-slate-300 disabled:border-slate-400 disabled:cursor-wait bg bg-transparent hover:bg-slate-200 hover:text-slate-800 w-auto border border-white text-sm text-white p-2.5 rounded-lg"
>
{fetchingUrl ? "Fetching..." : "Fetch website"}
</button>

View File

@ -53,8 +53,8 @@ export default function WorkspaceFileRow({
const handleMouseLeave = debounce(handleHideTooltip, 500);
return (
<div
className={`items-center transition-all duration-200 text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 border-b border-white/20 hover:bg-sky-500/20 cursor-pointer
${isMovedItem ? "bg-green-800/40" : ""}`}
className={`items-center text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-sky-500/20 cursor-pointer
${isMovedItem ? "bg-green-800/40" : "file-row"}`}
>
<div className="col-span-5 flex gap-x-[4px] items-center">
<File

View File

@ -29,7 +29,7 @@ function WorkspaceDirectory({
</h3>
</div>
<div className="relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5">
<div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20">
<div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8">
<p className="col-span-5">Name</p>
<p className="col-span-3">Date</p>
<p className="col-span-2">Kind</p>
@ -55,7 +55,7 @@ function WorkspaceDirectory({
</h3>
</div>
<div
className={`relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5 overflow-y-auto border-4 transition-all duration-300 ${
className={`relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5 overflow-y-auto border-4 ${
highlightWorkspace ? "border-cyan-300/80" : "border-transparent"
}`}
>
@ -96,7 +96,7 @@ function WorkspaceDirectory({
</div>
</div>
{hasChanges && (
<div className="flex items-center justify-between py-6 transition-all duration-300">
<div className="flex items-center justify-between py-6">
<div className="text-white/80">
<p className="text-sm font-semibold">
{embeddingCosts === 0
@ -114,7 +114,7 @@ function WorkspaceDirectory({
<button
onClick={saveChanges}
className="transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
className="border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Save and Embed
</button>
@ -148,7 +148,7 @@ const PinAlert = memo(() => {
<ModalWrapper isOpen={showAlert}>
<div className="relative w-full max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<div className="flex items-start justify-between p-4 rounded-t border-gray-500/50">
<div className="flex items-center gap-2">
<PushPin className="text-red-600 text-lg w-6 h-6" weight="fill" />
<h3 className="text-xl font-semibold text-white">
@ -177,7 +177,7 @@ const PinAlert = memo(() => {
<button disabled={true} className="invisible" />
<button
onClick={dismissAlert}
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
className="border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Okay, got it
</button>

View File

@ -191,9 +191,10 @@ export default function DocumentSettings({ workspace, systemSettings }) {
};
return (
<div className="flex gap-x-6 justify-center -mt-6 z-10 relative">
<div className="flex upload-modal -mt-6 z-10 relative">
<Directory
files={availableDocs}
setFiles={setAvailableDocs}
loading={loading}
loadingMessage={loadingMessage}
setLoading={setLoading}
@ -207,7 +208,7 @@ export default function DocumentSettings({ workspace, systemSettings }) {
moveToWorkspace={moveSelectedItemsToWorkspace}
setLoadingMessage={setLoadingMessage}
/>
<div className="flex items-center">
<div className="upload-modal-arrow">
<ArrowsDownUp className="text-white text-base font-bold rotate-90 w-11 h-11" />
</div>
<WorkspaceDirectory

View File

@ -64,13 +64,14 @@ const ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {
return (
<div className="w-screen h-screen fixed top-0 left-0 flex justify-center items-center z-99">
<div className="backdrop h-full w-full absolute top-0 z-10" />
<div className={`absolute max-h-full w-fit transition duration-300 z-20`}>
<div className="absolute max-h-full w-fit transition duration-300 z-20 md:overflow-y-auto py-10">
<div className="relative bg-main-gradient rounded-[12px] shadow border-2 border-slate-300/10">
<div className="flex items-start justify-between p-2 rounded-t border-gray-500/50 z-40 relative">
<div className="flex items-start justify-between p-2 rounded-t border-gray-500/50 relative">
<div />
<button
onClick={hideModal}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
className="z-50 text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>

View File

@ -0,0 +1,31 @@
import useUser from "@/hooks/useUser";
import paths from "@/utils/paths";
import { ArrowUUpLeft, Wrench } from "@phosphor-icons/react";
import { Link } from "react-router-dom";
import { useMatch } from "react-router-dom";
export default function SettingsButton() {
const isInSettings = !!useMatch("/settings/*");
const { user } = useUser();
if (user && user?.role === "default") return null;
if (isInSettings)
return (
<Link
to={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<ArrowUUpLeft className="h-5 w-5" weight="fill" />
</Link>
);
return (
<Link
to={!!user?.role ? paths.settings.system() : paths.settings.appearance()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<Wrench className="h-5 w-5" weight="fill" />
</Link>
);
}

View File

@ -13,18 +13,20 @@ import {
Database,
Lock,
House,
X,
List,
FileCode,
Plugs,
Notepad,
CodeBlock,
Barcode,
ClosedCaptioning,
EyeSlash,
} from "@phosphor-icons/react";
import useUser from "@/hooks/useUser";
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
import { isMobile } from "react-device-detect";
import Footer from "../Footer";
import { Link } from "react-router-dom";
export default function SettingsSidebar() {
const { logo } = useLogo();
@ -112,9 +114,7 @@ export default function SettingsSidebar() {
<SidebarOptions user={user} />
</div>
</div>
<div>
<Footer />
</div>
<Footer />
</div>
</div>
</div>
@ -124,50 +124,40 @@ export default function SettingsSidebar() {
}
return (
<>
<div>
<Link
to={paths.home()}
className="flex shrink-0 max-w-[55%] items-center justify-start mx-[38px] my-[18px]"
>
<img
src={logo}
alt="Logo"
className="rounded max-h-[24px]"
style={{ objectFit: "contain" }}
/>
</Link>
<div
ref={sidebarRef}
style={{ height: "calc(100% - 32px)" }}
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-sidebar border-4 border-accent min-w-[250px] p-[18px]"
style={{ height: "calc(100% - 76px)" }}
className="transition-all duration-500 relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px]"
>
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<div className="flex shrink-0 max-w-[65%] items-center justify-start ml-2">
<img
src={logo}
alt="Logo"
className="rounded max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<a
href={paths.home()}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="h-4 w-4" />
</a>
</div>
<div className="w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]">
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-[4px] mb-0 ml-2">
Instance Settings
</div>
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-4 mb-0 ml-2">
Settings
</div>
{/* Primary Body */}
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll">
<div className="relative h-full flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
<div className="h-auto sidebar-items">
{/* Options */}
<div className="flex flex-col gap-y-2 h-full pb-8 overflow-y-scroll no-scroll">
<SidebarOptions user={user} />
</div>
</div>
<div>
<div className="mb-2">
<Footer />
</div>
</div>
</div>
</div>
</>
</div>
);
}
@ -195,24 +185,25 @@ const Option = ({
return (
<>
<div className="flex gap-x-2 items-center justify-between text-white">
<a
href={href}
<div className="flex gap-x-2 items-center justify-between">
<Link
to={href}
className={`
transition-all duration-[200ms]
flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 rounded justify-start items-center border
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] justify-start items-center
hover:bg-workspace-item-selected-gradient hover:text-white hover:font-medium
${
isActive
? "bg-menu-item-selected-gradient border-slate-100 border-opacity-50 font-medium"
: "hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent"
? "bg-menu-item-selected-gradient font-medium border-outline text-white"
: "hover:bg-menu-item-selected-gradient text-zinc-200"
}
`}
>
{React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })}
<p className="text-sm leading-loose text-opacity-60 whitespace-nowrap overflow-hidden ">
<p className="text-sm leading-loose whitespace-nowrap overflow-hidden ">
{btnText}
</p>
</a>
</Link>
</div>
{!!subOptions && (isActive || hasActiveChild) && (
<div
@ -289,9 +280,17 @@ const SidebarOptions = ({ user = null }) => (
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.transcriptionPreference()}
btnText="Transcription Model"
icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.embeddingPreference()}
btnText="Embedding Preference"
btnText="Embedding Model"
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
@ -351,5 +350,13 @@ const SidebarOptions = ({ user = null }) => (
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.privacy()}
btnText="Privacy & Data"
icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
</>
);

View File

@ -11,7 +11,7 @@ import { GearSix, SquaresFour, UploadSimple } from "@phosphor-icons/react";
import truncate from "truncate";
import useUser from "@/hooks/useUser";
import ThreadContainer from "./ThreadContainer";
import { Link } from "react-router-dom";
import { Link, useMatch } from "react-router-dom";
export default function ActiveWorkspaces() {
const { slug } = useParams();
@ -23,6 +23,7 @@ export default function ActiveWorkspaces() {
const [uploadHover, setUploadHover] = useState({});
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const { user } = useUser();
const isInWorkspaceSettings = !!useMatch("/workspace/:slug/settings/:tab");
useEffect(() => {
async function getWorkspaces() {
@ -90,55 +91,60 @@ export default function ActiveWorkspaces() {
href={isActive ? null : paths.workspace.chat(workspace.slug)}
className={`
transition-all duration-[200ms]
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-lg text-slate-200 justify-start items-center
hover:bg-workspace-item-selected-gradient
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] text-white justify-start items-center
hover:bg-workspace-item-selected-gradient hover:font-bold border-2 border-outline
${
isActive
? "border-2 bg-workspace-item-selected-gradient border-white"
: "border bg-workspace-item-gradient bg-opacity-60 border-transparent hover:border-slate-100 hover:border-opacity-50"
? "bg-workspace-item-selected-gradient font-bold"
: ""
}`}
>
<div className="flex flex-row justify-between w-full">
<div className="flex items-center space-x-2">
<SquaresFour
weight={isActive ? "fill" : "regular"}
className="h-5 w-5 flex-shrink-0"
className="flex-shrink-0"
size={24}
/>
<p
className={`text-white text-sm leading-loose font-medium whitespace-nowrap overflow-hidden ${
isActive ? "" : "text-opacity-80"
className={`text-[14px] leading-loose whitespace-nowrap overflow-hidden ${
isActive ? "text-white " : "text-zinc-200"
}`}
>
{isActive || isHovered
? truncate(workspace.name, 17)
? truncate(workspace.name, 15)
: truncate(workspace.name, 20)}
</p>
</div>
{(isActive || isHovered || gearHover[workspace.id]) &&
user?.role !== "default" ? (
<div className="flex items-center gap-x-2">
<button
type="button"
onClick={(e) => {
e.preventDefault();
setSelectedWs(workspace);
showModal();
}}
onMouseEnter={() =>
handleUploadMouseEnter(workspace.id)
}
onMouseLeave={() =>
handleUploadMouseLeave(workspace.id)
}
className="rounded-md flex items-center justify-center text-white ml-auto"
<div className="flex items-center gap-x-[2px]">
<div
className={`flex hover:bg-[#646768] p-[2px] rounded-[4px] text-[#A7A8A9] hover:text-white ${
uploadHover[workspace.id] ? "bg-[#646768]" : ""
}`}
>
<UploadSimple
weight={
uploadHover[workspace.id] ? "fill" : "regular"
<button
type="button"
onClick={(e) => {
e.preventDefault();
setSelectedWs(workspace);
showModal();
}}
onMouseEnter={() =>
handleUploadMouseEnter(workspace.id)
}
className="h-[20px] w-[20px] transition-all duration-300"
/>
</button>
onMouseLeave={() =>
handleUploadMouseLeave(workspace.id)
}
className="rounded-md flex items-center justify-center ml-auto"
>
<UploadSimple
className="h-[20px] w-[20px]"
weight="bold"
/>
</button>
</div>
<Link
type="button"
@ -147,12 +153,21 @@ export default function ActiveWorkspaces() {
)}
onMouseEnter={() => handleGearMouseEnter(workspace.id)}
onMouseLeave={() => handleGearMouseLeave(workspace.id)}
className="rounded-md flex items-center justify-center text-white ml-auto"
className="rounded-md flex items-center justify-center text-[#A7A8A9] hover:text-white ml-auto"
>
<GearSix
weight={gearHover[workspace.id] ? "fill" : "regular"}
className="h-[20px] w-[20px] transition-all duration-300"
/>
<div className="flex hover:bg-[#646768] p-[2px] rounded-[4px]">
<GearSix
color={
isInWorkspaceSettings && workspace.slug === slug
? "#46C8FF"
: gearHover[workspace.id]
? "#FFFFFF"
: "#A7A8A9"
}
weight="bold"
className="h-[20px] w-[20px]"
/>
</div>
</Link>
</div>
) : null}

View File

@ -1,14 +1,16 @@
import React, { useEffect, useRef, useState } from "react";
import { Wrench, Plus, List } from "@phosphor-icons/react";
import { Plus, List } from "@phosphor-icons/react";
import NewWorkspaceModal, {
useNewWorkspaceModal,
} from "../Modals/NewWorkspace";
import ActiveWorkspaces from "./ActiveWorkspaces";
import paths from "@/utils/paths";
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
import useLogo from "@/hooks/useLogo";
import useUser from "@/hooks/useUser";
import Footer from "../Footer";
import SettingsButton from "../SettingsButton";
import { Link } from "react-router-dom";
import paths from "@/utils/paths";
export default function Sidebar() {
const { user } = useUser();
@ -21,40 +23,33 @@ export default function Sidebar() {
} = useNewWorkspaceModal();
return (
<>
<div>
<Link
to={paths.home()}
className="flex shrink-0 max-w-[55%] items-center justify-start mx-[38px] my-[18px]"
>
<img
src={logo}
alt="Logo"
className="rounded max-h-[24px]"
style={{ objectFit: "contain" }}
/>
</Link>
<div
ref={sidebarRef}
style={{ height: "calc(100% - 32px)" }}
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-sidebar border-4 border-accent min-w-[250px] p-[18px]"
style={{ height: "calc(100% - 76px)" }}
className="relative m-[16px] rounded-[16px] bg-sidebar border-2 border-outline min-w-[250px] p-[10px]"
>
<div className="flex flex-col h-full overflow-x-hidden">
{/* Header Information */}
<div className="flex items-center justify-between mb-4">
<div className="flex shrink-0 max-w-[65%] items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
{(!user || user?.role !== "default") && (
<div className="flex gap-x-2 items-center text-slate-200">
<SettingsButton />
</div>
)}
</div>
{/* Primary Body */}
<div className="flex-grow flex flex-col">
<div className="flex-grow flex flex-col min-w-[235px]">
<div className="flex flex-col gap-y-2 pb-8 overflow-y-scroll no-scroll">
<div className="flex gap-x-2 items-center justify-between">
{(!user || user?.role !== "default") && (
<button
onClick={showNewWsModal}
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 mb-2 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
className="flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-2.5 mb-2 bg-white rounded-[8px] text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300"
>
<Plus className="h-5 w-5" />
<Plus size={18} weight="bold" />
<p className="text-sidebar text-sm font-semibold">
New Workspace
</p>
@ -70,7 +65,7 @@ export default function Sidebar() {
</div>
</div>
{showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}
</>
</div>
);
}
@ -190,17 +185,3 @@ export function SidebarMobileHeader() {
</>
);
}
function SettingsButton() {
const { user } = useUser();
return (
<a
href={
!!user?.role ? paths.settings.system() : paths.settings.appearance()
}
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<Wrench className="h-4 w-4" weight="fill" />
</a>
);
}

View File

@ -0,0 +1,38 @@
import { Gauge } from "@phosphor-icons/react";
export default function NativeTranscriptionOptions() {
return (
<div className="w-full flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Gauge size={25} />
<p className="text-sm">
Using the local whisper model on machines with limited RAM or CPU
can stall AnythingLLM when processing media files.
<br />
We recommend at least 2GB of RAM and upload files &lt;10Mb.
<br />
<br />
<i>
The built-in model will automatically download on the first use.
</i>
</p>
</div>
</div>
<div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Model Selection
</label>
<select
disabled={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
Xenova/whisper-small
</option>
</select>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,41 @@
import { useState } from "react";
export default function OpenAiWhisperOptions({ settings }) {
const [inputValue, setInputValue] = useState(settings?.OpenAiKey);
const [_openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey);
return (
<div className="flex gap-x-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
API Key
</label>
<input
type="password"
name="OpenAiKey"
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="OpenAI API Key"
defaultValue={settings?.OpenAiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
onChange={(e) => setInputValue(e.target.value)}
onBlur={() => setOpenAIKey(inputValue)}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4">
Whisper Model
</label>
<select
disabled={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
Whisper Large
</option>
</select>
</div>
</div>
);
}

View File

@ -61,7 +61,7 @@ export default function UserButton() {
type="button"
className="uppercase transition-all duration-300 w-[35px] h-[35px] text-base font-semibold rounded-full flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient justify-center text-white p-2 hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
{mode === "multi" ? userDisplay() : <Person size={14} />}
{mode === "multi" ? <UserDisplay /> : <Person size={14} />}
</button>
{showMenu && (
@ -109,7 +109,7 @@ export default function UserButton() {
);
}
function userDisplay() {
function UserDisplay() {
const { pfp } = usePfp();
const user = userFromStorage();

View File

@ -31,15 +31,7 @@ const HistoricalMessage = ({
className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col`}
>
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{
uid:
role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
<ProfileImage role={role} workspace={workspace} />
{error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className={`inline-block `}>
@ -76,4 +68,28 @@ const HistoricalMessage = ({
);
};
function ProfileImage({ role, workspace }) {
if (role === "assistant" && workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return (
<Jazzicon
size={36}
user={{
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
);
}
export default memo(HistoricalMessage);

View File

@ -14,7 +14,6 @@ const PromptReply = ({
closed = true,
}) => {
const assistantBackgroundColor = "bg-historical-msg-system";
if (!reply && sources.length === 0 && !pending && !error) return null;
if (pending) {
@ -24,11 +23,7 @@ const PromptReply = ({
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<WorkspaceProfileImage workspace={workspace} />
<div className="mt-3 ml-5 dot-falling"></div>
</div>
</div>
@ -43,11 +38,7 @@ const PromptReply = ({
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5">
<Jazzicon
size={36}
user={{ uid: workspace.slug }}
role="assistant"
/>
<WorkspaceProfileImage workspace={workspace} />
<span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
>
@ -68,7 +59,7 @@ const PromptReply = ({
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5">
<Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />
<WorkspaceProfileImage workspace={workspace} />
<span
className={`reply flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
@ -80,4 +71,20 @@ const PromptReply = ({
);
};
function WorkspaceProfileImage({ workspace }) {
if (!!workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return <Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />;
}
export default memo(PromptReply);

View File

@ -5,8 +5,10 @@ import { useManageWorkspaceModal } from "../../../Modals/MangeWorkspace";
import ManageWorkspace from "../../../Modals/MangeWorkspace";
import { ArrowDown } from "@phosphor-icons/react";
import debounce from "lodash.debounce";
import useUser from "@/hooks/useUser";
export default function ChatHistory({ history = [], workspace, sendCommand }) {
const { user } = useUser();
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const [isAtBottom, setIsAtBottom] = useState(true);
const chatHistoryRef = useRef(null);
@ -56,16 +58,22 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) {
<p className="text-white/60 text-lg font-base py-4">
Welcome to your new workspace.
</p>
<p className="w-full items-center text-white/60 text-lg font-base flex flex-col md:flex-row gap-x-1">
To get started either{" "}
<span
className="underline font-medium cursor-pointer"
onClick={showModal}
>
upload a document
</span>
or <b className="font-medium italic">send a chat.</b>
</p>
{!user || user.role !== "default" ? (
<p className="w-full items-center text-white/60 text-lg font-base flex flex-col md:flex-row gap-x-1">
To get started either{" "}
<span
className="underline font-medium cursor-pointer"
onClick={showModal}
>
upload a document
</span>
or <b className="font-medium italic">send a chat.</b>
</p>
) : (
<p className="w-full items-center text-white/60 text-lg font-base flex flex-col md:flex-row gap-x-1">
To get started <b className="font-medium italic">send a chat.</b>
</p>
)}
<WorkspaceChatSuggestions
suggestions={workspace?.suggestedMessages ?? []}
sendSuggestion={handleSendSuggestedMessage}

View File

@ -0,0 +1,50 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { Tooltip } from "react-tooltip";
export default function StopGenerationButton() {
function emitHaltEvent() {
window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));
}
return (
<>
<button
type="button"
onClick={emitHaltEvent}
data-tooltip-id="stop-generation-button"
data-tooltip-content="Stop generating response"
className="border-none text-white/60 cursor-pointer group"
>
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="group-hover:stroke-[#46C8FF] stroke-white"
cx="10"
cy="10.562"
r="9"
stroke-width="2"
/>
<rect
className="group-hover:fill-[#46C8FF] fill-white"
x="6.3999"
y="6.96204"
width="7.2"
height="7.2"
rx="2"
/>
</svg>
</button>
<Tooltip
id="stop-generation-button"
place="bottom"
delayShow={300}
className="tooltip !text-xs invert"
/>
</>
);
}

View File

@ -0,0 +1,4 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10.8984" cy="10.562" r="9" stroke="white" stroke-width="2"/>
<rect x="7.29846" y="6.96204" width="7.2" height="7.2" rx="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -1,14 +1,13 @@
import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
import React, { useState, useRef } from "react";
import React, { useState, useRef, useEffect } from "react";
import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
} from "./SlashCommands";
import { isMobile } from "react-device-detect";
import debounce from "lodash.debounce";
import { PaperPlaneRight } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton";
export default function PromptInput({
workspace,
message,
submit,
onChange,
@ -18,13 +17,27 @@ export default function PromptInput({
}) {
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const formRef = useRef(null);
const textareaRef = useRef(null);
const [_, setFocused] = useState(false);
useEffect(() => {
if (!inputDisabled && textareaRef.current) {
textareaRef.current.focus();
}
resetTextAreaHeight();
}, [inputDisabled]);
const handleSubmit = (e) => {
setFocused(false);
submit(e);
};
const resetTextAreaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
};
const checkForSlash = (e) => {
const input = e.target.value;
if (input === "/") setShowSlashCommand(true);
@ -43,14 +56,12 @@ export default function PromptInput({
const adjustTextArea = (event) => {
if (isMobile) return false;
const element = event.target;
element.style.height = "1px";
element.style.height =
event.target.value.length !== 0
? 25 + element.scrollHeight + "px"
: "1px";
element.style.height = "auto";
element.style.height = `${element.scrollHeight}px`;
};
const watchForSlash = debounce(checkForSlash, 300);
return (
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
<SlashCommands
@ -66,12 +77,13 @@ export default function PromptInput({
<div className="w-[600px] bg-main-gradient shadow-2xl border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
<div className="flex items-center w-full border-b-2 border-gray-500/50">
<textarea
onKeyUp={adjustTextArea}
onKeyDown={captureEnter}
ref={textareaRef}
onChange={(e) => {
onChange(e);
watchForSlash(e);
adjustTextArea(e);
}}
onKeyDown={captureEnter}
required={true}
disabled={inputDisabled}
onFocus={() => setFocused(true)}
@ -83,19 +95,18 @@ export default function PromptInput({
className="cursor-text max-h-[100px] md:min-h-[40px] mx-2 md:mx-0 py-2 w-full text-[16px] md:text-md text-white bg-transparent placeholder:text-white/60 resize-none active:outline-none focus:outline-none flex-grow"
placeholder={"Send a message"}
/>
<button
ref={formRef}
type="submit"
disabled={buttonDisabled}
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
>
{buttonDisabled ? (
<CircleNotch className="w-6 h-6 animate-spin" />
) : (
{buttonDisabled ? (
<StopGenerationButton />
) : (
<button
ref={formRef}
type="submit"
className="inline-flex justify-center rounded-2xl cursor-pointer text-white/60 hover:text-white group ml-4"
>
<PaperPlaneRight className="w-7 h-7 my-3" weight="fill" />
)}
<span className="sr-only">Send message</span>
</button>
<span className="sr-only">Send message</span>
</button>
)}
</div>
<div className="flex justify-between py-3.5">
<div className="flex gap-x-2">

View File

@ -68,11 +68,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];
var _chatHistory = [...remHistory];
if (!promptMessage || !promptMessage?.userMessage) {
setLoadingResponse(false);
return false;
}
if (!promptMessage || !promptMessage?.userMessage) return false;
if (!!threadSlug) {
await Workspace.threads.streamChat(
{ workspaceSlug: workspace.slug, threadSlug },
@ -108,7 +104,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
>
{isMobile && <SidebarMobileHeader />}
<div className="flex flex-col h-full w-full md:mt-0 mt-[40px]">
@ -118,7 +114,6 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
sendCommand={sendCommand}
/>
<PromptInput
workspace={workspace}
message={message}
submit={handleSubmit}
onChange={handleMessageChange}

View File

@ -13,12 +13,20 @@ const PROVIDER_DEFAULT_MODELS = {
"gpt-4-32k",
],
gemini: ["gemini-pro"],
anthropic: ["claude-2", "claude-instant-1"],
anthropic: [
"claude-instant-1.2",
"claude-2.0",
"claude-2.1",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
azure: [],
lmstudio: [],
localai: [],
ollama: [],
togetherai: [],
groq: ["llama2-70b-4096", "mixtral-8x7b-32768"],
native: [],
};

View File

@ -597,3 +597,39 @@ dialog::backdrop {
font-weight: 600;
color: #fff;
}
.file-row:nth-child(even) {
@apply bg-[#1C1E21];
}
.file-row:nth-child(odd) {
@apply bg-[#2C2C2C];
}
.file-row.selected:nth-child(even) {
@apply bg-sky-500/20;
}
.file-row.selected:nth-child(odd) {
@apply bg-sky-500/10;
}
/* Flex upload modal to be a column when on small screens so that the UI
does not extend the close button beyond the viewport. */
@media (max-width: 1330px) {
.upload-modal {
@apply !flex-col !items-center !py-4 no-scroll;
}
.upload-modal-arrow {
margin-top: 0px !important;
}
}
.upload-modal {
@apply flex-row items-start gap-x-6 justify-center;
}
.upload-modal-arrow {
margin-top: 25%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const Document = {
createFolder: async (name) => {
return await fetch(`${API_BASE}/document/create-folder`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ name }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
moveToFolder: async (files, folderName) => {
const data = {
files: files.map((file) => ({
from: file.folderName ? `${file.folderName}/${file.name}` : file.name,
to: `${folderName}/${file.name}`,
})),
};
return await fetch(`${API_BASE}/document/move-files`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify(data),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
};
export default Document;

View File

@ -3,6 +3,7 @@ import { baseHeaders } from "@/utils/request";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import WorkspaceThread from "@/models/workspaceThread";
import { v4 } from "uuid";
import { ABORT_STREAM_EVENT } from "@/utils/chat";
const Workspace = {
new: async function (data = {}) {
@ -75,6 +76,16 @@ const Workspace = {
},
streamChat: async function ({ slug }, message, handleChat) {
const ctrl = new AbortController();
// Listen for the ABORT_STREAM_EVENT key to be emitted by the client
// to early abort the streaming response. On abort we send a special `stopGeneration`
// event to be handled which resets the UI for us to be able to send another message.
// The backend response abort handling is done in each LLM's handleStreamResponse.
window.addEventListener(ABORT_STREAM_EVENT, () => {
ctrl.abort();
handleChat({ id: v4(), type: "stopGeneration" });
});
await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, {
method: "POST",
body: JSON.stringify({ message }),
@ -238,6 +249,54 @@ const Workspace = {
});
},
threads: WorkspaceThread,
uploadPfp: async function (formData, slug) {
return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, {
method: "POST",
body: formData,
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Error uploading pfp.");
return { success: true, error: null };
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
fetchPfp: async function (slug) {
return await fetch(`${API_BASE}/workspace/${slug}/pfp`, {
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
})
.then((res) => {
if (res.ok && res.status !== 204) return res.blob();
throw new Error("Failed to fetch pfp.");
})
.then((blob) => (blob ? URL.createObjectURL(blob) : null))
.catch((e) => {
console.log(e);
return null;
});
},
removePfp: async function (slug) {
return await fetch(`${API_BASE}/workspace/${slug}/remove-pfp`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => {
if (res.ok) return { success: true, error: null };
throw new Error("Failed to remove pfp.");
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
};
export default Workspace;

View File

@ -1,3 +1,4 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
import { fetchEventSource } from "@microsoft/fetch-event-source";
@ -80,6 +81,16 @@ const WorkspaceThread = {
handleChat
) {
const ctrl = new AbortController();
// Listen for the ABORT_STREAM_EVENT key to be emitted by the client
// to early abort the streaming response. On abort we send a special `stopGeneration`
// event to be handled which resets the UI for us to be able to send another message.
// The backend response abort handling is done in each LLM's handleStreamResponse.
window.addEventListener(ABORT_STREAM_EVENT, () => {
ctrl.abort();
handleChat({ id: v4(), type: "stopGeneration" });
});
await fetchEventSource(
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`,
{

View File

@ -13,25 +13,29 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminInvites() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Invitations</p>
<p className="text-lg leading-6 font-bold text-white">
Invitations
</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<EnvelopeSimple className="h-4 w-4" /> Create Invite Link
<EnvelopeSimple className="h-4 w-4" />
Create Invite Link
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Create invitation links for people in your organization to accept
and sign up with. Invitations can only be used by a single user.
</p>
@ -50,6 +54,7 @@ function InvitationsContainer() {
const darkMode = usePrefersDarkMode();
const [loading, setLoading] = useState(true);
const [invites, setInvites] = useState([]);
useEffect(() => {
async function fetchInvites() {
const _invites = await Admin.invites();
@ -74,13 +79,13 @@ function InvitationsContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3">
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Status
</th>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
<th scope="col" className="px-6 py-3">
Accepted By
</th>
<th scope="col" className="px-6 py-3">

View File

@ -30,20 +30,22 @@ export default function AdminLogs() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Event Logs</p>
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Event Logs
</p>
<button
onClick={handleResetLogs}
className="px-4 py-1 rounded-lg text-slate-200/50 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
Clear event logs
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
View all actions and events happening on this instance for
monitoring.
</p>
@ -95,10 +97,10 @@ function LogsContainer() {
return (
<>
<table className="md:w-5/6 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3">
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Event Type
</th>
<th scope="col" className="px-6 py-3">
@ -116,7 +118,7 @@ function LogsContainer() {
{!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<div className="flex w-full justify-between items-center mt-6">
<button
onClick={handlePrevious}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"

View File

@ -12,6 +12,7 @@ export default function AdminSystem() {
enabled: false,
limit: 10,
});
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
@ -43,46 +44,35 @@ export default function AdminSystem() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
onSubmit={handleSubmit}
onChange={() => setHasChanges(true)}
className="flex w-full"
className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
System Preferences
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
These are the overall settings and configurations of your
instance.
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
System Preferences
</p>
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the overall settings and configurations of your
instance.
</p>
</div>
<div className="my-5">
<div className="flex flex-col gap-y-2 mb-2.5">
<label className="leading-tight font-semibold text-white">
Users can delete workspaces
</label>
<p className="leading-tight text-sm text-white text-opacity-60 w-96">
Allow non-admin users to delete workspaces that they are a
part of. This would delete the workspace for everyone.
</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<div className="mt-6 mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Users can delete workspaces
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Allow non-admin users to delete workspaces that they are a part
of. This would delete the workspace for everyone.
</p>
<label className="relative inline-flex cursor-pointer items-center mt-2">
<input
type="checkbox"
name="users_can_delete_workspaces"
@ -94,42 +84,44 @@ export default function AdminSystem() {
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
</div>
<div className="my-4">
<div className="flex flex-col gap-y-2 mb-2.5">
<label className="leading-tight font-medium text-black dark:text-white">
Limit messages per user per day
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Limit messages per user per day
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Restrict non-admin users to a number of successful queries or
chats within a 24 hour window. Enable this to prevent users from
running up OpenAI costs.
</p>
<div className="mt-2">
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name="limit_user_messages"
value="yes"
checked={messageLimit.enabled}
onChange={(e) => {
setMessageLimit({
...messageLimit,
enabled: e.target.checked,
});
}}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
<p className="leading-tight text-sm text-white text-opacity-60 w-96">
Restrict non-admin users to a number of successful queries or
chats within a 24 hour window. Enable this to prevent users
from running up OpenAI costs.
</p>
</div>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name="limit_user_messages"
value="yes"
checked={messageLimit.enabled}
onChange={(e) => {
setMessageLimit({
...messageLimit,
enabled: e.target.checked,
});
}}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
{messageLimit.enabled && (
<div className="mb-4">
<label className=" block flex items-center gap-x-1 font-medium text-black dark:text-white">
<div className="mt-4">
<label className="block text-sm font-medium text-white">
Message limit per day
</label>
<div className="relative">
<div className="relative mt-2">
<input
type="number"
name="message_limit"
@ -143,12 +135,24 @@ export default function AdminSystem() {
value={messageLimit.limit}
min={1}
max={300}
className="w-1/3 my-2 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-gray-800 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-gray-800 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>
</div>
)}
</div>
{hasChanges && (
<div className="flex justify-start">
<button
type="submit"
disabled={saving}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
</div>
)}
</form>
</div>
</div>

View File

@ -13,25 +13,26 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminUsers() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Users</p>
<p className="text-lg leading-6 font-bold text-white">Users</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<UserPlus className="h-4 w-4" /> Add user
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the accounts which have an account on this instance.
Removing an account will instantly remove their access to this
instance.
@ -51,6 +52,7 @@ function UsersContainer() {
const { user: currUser } = useUser();
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const _users = await Admin.users();
@ -75,8 +77,8 @@ function UsersContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Username
@ -120,7 +122,7 @@ const ROLE_HINT = {
export function RoleHintDisplay({ role }) {
return (
<div className="flex flex-col gap-y-1 py-1 pb-4">
<p className="text-white/60 font-semibold text-sm">Permissions</p>
<p className="text-sm font-medium text-white">Permissions</p>
<ul className="flex flex-col gap-y-1 list-disc px-4">
{ROLE_HINT[role ?? "default"].map((hints, i) => {
return (

View File

@ -52,7 +52,7 @@ export default function EditWorkspaceUsersModal({
</div>
<form onSubmit={handleUpdate}>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div className="w-full flex flex-col gap-y-4 max-h-[350px] overflow-y-scroll">
{users
.filter((user) => user.role !== "admin")
.map((user) => {

View File

@ -13,27 +13,28 @@ import ModalWrapper from "@/components/ModalWrapper";
export default function AdminWorkspaces() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Instance workspaces
<p className="text-lg leading-6 font-bold text-white">
Instance Workspaces
</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<BookOpen className="h-4 w-4" /> New Workspace
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the workspaces that exist on this instance. Removing
a workspace will delete all of it's associated chats and settings.
</p>
@ -80,8 +81,8 @@ function WorkspacesContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Name

View File

@ -15,25 +15,26 @@ import { useModal } from "@/hooks/useModal";
export default function AdminApiKeys() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">API Keys</p>
<p className="text-lg leading-6 font-bold text-white">API Keys</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<PlusCircle className="h-4 w-4" /> Generate New API Key
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
API keys allow the holder to programmatically access and manage
this AnythingLLM instance.
</p>
@ -41,7 +42,7 @@ export default function AdminApiKeys() {
href={paths.apiDocs()}
target="_blank"
rel="noreferrer"
className="text-sm font-base text-blue-300 hover:underline"
className="text-xs leading-[18px] font-base text-blue-300 hover:underline"
>
Read the API documentation &rarr;
</a>
@ -59,11 +60,11 @@ export default function AdminApiKeys() {
function ApiKeysContainer() {
const [loading, setLoading] = useState(true);
const [apiKeys, setApiKeys] = useState([]);
useEffect(() => {
async function fetchExistingKeys() {
const user = userFromStorage();
const Model = !!user ? Admin : System;
const { apiKeys: foundKeys } = await Model.getApiKeys();
setApiKeys(foundKeys);
setLoading(false);
@ -86,8 +87,8 @@ function ApiKeysContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
API Key

View File

@ -1,7 +1,7 @@
import useLogo from "@/hooks/useLogo";
import System from "@/models/system";
import showToast from "@/utils/toast";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import AnythingLLM from "@/media/logo/anything-llm.png";
import { Plus } from "@phosphor-icons/react";
@ -9,6 +9,7 @@ export default function CustomLogo() {
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
const [logo, setLogo] = useState("");
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
const fileInputRef = useRef(null);
useEffect(() => {
async function logoInit() {
@ -62,61 +63,88 @@ export default function CustomLogo() {
showToast("Image successfully removed.", "success");
};
const triggerFileInputClick = () => {
fileInputRef.current?.click();
};
return (
<div className="my-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">Custom Logo</h2>
<p className="text-sm font-base text-white/60">
<div className="mt-6 mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Logo
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Upload your custom logo to make your chatbot yours.
</p>
</div>
<div className="flex md:flex-row flex-col items-center">
<img
src={logo}
alt="Uploaded Logo"
className="w-48 h-48 object-contain mr-6"
hidden={isDefaultLogo}
onError={(e) => (e.target.src = AnythingLLM)}
/>
<div className="flex flex-row gap-x-8">
<label
className="mt-5 transition-all duration-300 hover:opacity-60"
hidden={!isDefaultLogo}
>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
<div
className="w-80 py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
htmlFor="logo-upload"
{isDefaultLogo ? (
<div className="flex md:flex-row flex-col items-center">
<div className="flex flex-row gap-x-8">
<label
className="mt-3 transition-all duration-300 hover:opacity-60"
hidden={!isDefaultLogo}
>
<div className="flex flex-col items-center justify-center">
<div className="rounded-full bg-white/40">
<Plus className="w-6 h-6 text-black/80 m-2" />
</div>
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
Add a custom logo
</div>
<div className="text-white text-opacity-60 text-xs font-medium py-1">
Recommended size: 800 x 200
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
<div
className="w-80 py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
htmlFor="logo-upload"
>
<div className="flex flex-col items-center justify-center">
<div className="rounded-full bg-white/40">
<Plus className="w-6 h-6 text-black/80 m-2" />
</div>
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
Add a custom logo
</div>
<div className="text-white text-opacity-60 text-xs font-medium py-1">
Recommended size: 800 x 200
</div>
</div>
</div>
</div>
</label>
{!isDefaultLogo && (
<button
onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60"
>
Delete
</button>
)}
</label>
</div>
</div>
</div>
) : (
<div className="flex md:flex-row flex-col items-center relative">
<div className="group w-80 h-[130px] mt-3 overflow-hidden">
<img
src={logo}
alt="Uploaded Logo"
className="w-full h-full object-cover border-2 border-white/20 border-dashed p-1 rounded-2xl"
/>
<div className="absolute w-80 top-0 left-0 right-0 bottom-0 flex flex-col gap-y-3 justify-center items-center rounded-2xl mt-3 bg-black bg-opacity-80 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out border-2 border-transparent hover:border-white">
<button
onClick={triggerFileInputClick}
className="text-white text-base font-medium hover:text-opacity-60 mx-2"
>
Replace
</button>
<input
id="logo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
ref={fileInputRef}
/>
<button
onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60 mx-2"
>
Remove
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -53,16 +53,16 @@ export default function CustomMessages() {
};
return (
<div className="mb-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Messages
</h2>
<p className="text-sm font-base text-white/60">
<p className="text-xs leading-[18px] font-base text-white/60">
Customize the automatic messages displayed to your users.
</p>
</div>
<div className="mt-6 flex flex-col gap-y-6 bg-zinc-900 rounded-lg px-6 pt-4 max-w-[700px]">
<div className="mt-3 flex flex-col gap-y-6 bg-[#1C1E21] rounded-lg pr-[31px] pl-[12px] pt-4 max-w-[700px]">
{messages.map((message, index) => (
<div key={index} className="flex flex-col gap-y-2">
{message.user && (
@ -85,27 +85,34 @@ export default function CustomMessages() {
)}
</div>
))}
<div className="flex gap-4 mt-12 justify-between pb-7">
<div className="flex gap-4 mt-12 justify-between pb-[15px]">
<button
className="self-end text-white hover:text-white/60 transition"
onClick={() => addMessage("response")}
>
<div className="flex items-center justify-start">
<Plus className="w-5 h-5 m-2" weight="fill" /> New System Message
<div className="flex items-center justify-start text-sm font-normal -ml-2">
<Plus className="m-2" size={16} weight="bold" />
<span className="leading-5">
New <span className="font-bold italic mr-1">system</span>{" "}
message
</span>
</div>
</button>
<button
className="self-end text-sky-400 hover:text-sky-400/60 transition"
className="self-end text-white hover:text-white/60 transition"
onClick={() => addMessage("user")}
>
<div className="flex items-center">
<Plus className="w-5 h-5 m-2" weight="fill" /> New User Message
<div className="flex items-center justify-start text-sm font-normal">
<Plus className="m-2" size={16} weight="bold" />
<span className="leading-5">
New <span className="font-bold italic mr-1">user</span> message
</span>
</div>
</button>
</div>
</div>
{hasChanges && (
<div className="flex justify-center py-6">
<div className="flex justify-start pt-6">
<button
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
onClick={handleMessageSave}

View File

@ -1,11 +1,20 @@
import { ICON_COMPONENTS } from "@/components/Footer";
import React, { useEffect, useRef, useState } from "react";
import { Plus, X } from "@phosphor-icons/react";
export default function NewIconForm({ handleSubmit, showing }) {
const [selectedIcon, setSelectedIcon] = useState("Info");
export default function NewIconForm({ icon, url, onSave, onRemove }) {
const [selectedIcon, setSelectedIcon] = useState(icon || "Plus");
const [selectedUrl, setSelectedUrl] = useState(url || "");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isEdited, setIsEdited] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
setSelectedIcon(icon || "Plus");
setSelectedUrl(url || "");
setIsEdited(false);
}, [icon, url]);
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -17,82 +26,90 @@ export default function NewIconForm({ handleSubmit, showing }) {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [dropdownRef]);
if (!showing) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (selectedIcon !== "Plus" && selectedUrl) {
onSave(selectedIcon, selectedUrl);
setIsEdited(false);
}
};
const handleRemove = () => {
onRemove();
setSelectedIcon("Plus");
setSelectedUrl("");
setIsEdited(false);
};
const handleIconChange = (iconName) => {
setSelectedIcon(iconName);
setIsDropdownOpen(false);
setIsEdited(true);
};
const handleUrlChange = (e) => {
setSelectedUrl(e.target.value);
setIsEdited(true);
};
return (
<form onSubmit={handleSubmit} className="flex justify-start">
<div className="mt-6 mb-6 flex flex-col bg-zinc-900 rounded-lg px-6 py-4">
<div className="flex gap-x-4 items-center">
<div
className="relative flex flex-col items-center gap-y-4"
ref={dropdownRef}
>
<input type="hidden" name="icon" value={selectedIcon} />
<label className="text-sm font-medium text-white">Icon</label>
<form onSubmit={handleSubmit} className="flex items-center gap-x-1.5">
<div className="relative" ref={dropdownRef}>
<div
className="h-[34px] w-[34px] bg-[#1C1E21] rounded-full flex items-center justify-center cursor-pointer"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
{React.createElement(ICON_COMPONENTS[selectedIcon] || Plus, {
className: "h-5 w-5 text-white",
weight: selectedIcon === "Plus" ? "bold" : "fill",
})}
</div>
{isDropdownOpen && (
<div className="absolute z-10 grid grid-cols-4 bg-[#41444C] mt-2 rounded-md w-[150px] h-[78px] overflow-y-auto border border-white/20 shadow-lg">
{Object.keys(ICON_COMPONENTS).map((iconName) => (
<button
key={iconName}
type="button"
className="flex justify-center items-center border border-transparent hover:bg-[#1C1E21] hover:border-slate-100 rounded-full p-2"
onClick={() => handleIconChange(iconName)}
>
{React.createElement(ICON_COMPONENTS[iconName], {
className: "h-5 w-5 text-white",
weight: "fill",
})}
</button>
))}
</div>
)}
</div>
<input
type="url"
value={selectedUrl}
onChange={handleUrlChange}
placeholder="https://example.com"
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[300px] h-[32px]"
required
/>
{selectedIcon !== "Plus" && (
<>
{isEdited ? (
<button
type="submit"
className="text-sky-400 px-2 py-2 rounded-md text-sm font-bold hover:text-sky-500"
>
Save
</button>
) : (
<button
type="button"
className={`${
isDropdownOpen
? "bg-menu-item-selected-gradient border-slate-100/50"
: ""
}border-transparent transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border`}
onClick={(e) => {
e.preventDefault();
setIsDropdownOpen(!isDropdownOpen);
}}
onClick={handleRemove}
className="hover:text-red-500 text-white/80 px-2 py-2 rounded-md text-sm font-bold"
>
{React.createElement(ICON_COMPONENTS[selectedIcon], {
className: "h-5 w-5 text-white",
weight: "fill",
})}
<X size={20} />
</button>
{isDropdownOpen && (
<div className="absolute z-10 grid grid-cols-4 gap-4 bg-zinc-800 -mt-20 ml-44 p-1 rounded-md w-56 h-28 overflow-y-auto border border-slate-100/10">
{Object.keys(ICON_COMPONENTS).map((iconName) => (
<button
key={iconName}
type="button"
className="flex justify-center items-center border border-transparent hover:bg-menu-item-selected-gradient hover:border-slate-100 rounded-full"
onClick={() => {
setSelectedIcon(iconName);
setIsDropdownOpen(false);
}}
>
{React.createElement(ICON_COMPONENTS[iconName], {
className: "h-5 w-5 text-white m-2.5",
weight: "fill",
})}
</button>
))}
</div>
)}
</div>
<div className="flex flex-col gap-y-4">
<label className="text-sm font-medium text-white">Link</label>
<input
type="url"
name="url"
required={true}
placeholder="https://example.com"
className="bg-sidebar text-white placeholder:text-white/20 rounded-md p-2"
/>
</div>
{selectedIcon !== "" && (
<div className="flex flex-col gap-y-4">
<label className="text-sm font-medium text-white invisible">
Submit
</label>
<div className="flex justify-center">
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Save
</button>
</div>
</div>
)}
</div>
</div>
</>
)}
</form>
);
}

View File

@ -1,36 +1,37 @@
import React, { useState, useEffect } from "react";
import showToast from "@/utils/toast";
import { Plus, X } from "@phosphor-icons/react";
import { ICON_COMPONENTS, MAX_ICONS } from "@/components/Footer";
import { safeJsonParse } from "@/utils/request";
import NewIconForm from "./NewIconForm";
import Admin from "@/models/admin";
import System from "@/models/system";
export default function FooterCustomization() {
const [loading, setLoading] = useState(true);
const [footerIcons, setFooterIcons] = useState([]);
const [showForm, setShowForm] = useState(false);
const [footerIcons, setFooterIcons] = useState(Array(3).fill(null));
useEffect(() => {
async function fetchFooterIcons() {
const settings = (await Admin.systemPreferences())?.settings;
if (settings && settings.footer_data) {
setFooterIcons(safeJsonParse(settings.footer_data, []));
const parsedIcons = safeJsonParse(settings.footer_data, []);
setFooterIcons((prevIcons) => {
const updatedIcons = [...prevIcons];
parsedIcons.forEach((icon, index) => {
updatedIcons[index] = icon;
});
return updatedIcons;
});
}
setLoading(false);
}
fetchFooterIcons();
}, []);
const removeFooterIcon = async (index) => {
const updatedIcons = footerIcons.filter((_, i) => i !== index);
const updateFooterIcons = async (updatedIcons) => {
const { success, error } = await Admin.updateSystemPreferences({
footer_data: JSON.stringify(updatedIcons),
footer_data: JSON.stringify(updatedIcons.filter((icon) => icon !== null)),
});
if (!success) {
showToast(`Failed to remove footer icon - ${error}`, "error", {
showToast(`Failed to update footer icons - ${error}`, "error", {
clear: true,
});
return;
@ -38,103 +39,44 @@ export default function FooterCustomization() {
window.localStorage.removeItem(System.cacheKeys.footerIcons);
setFooterIcons(updatedIcons);
showToast("Successfully removed footer icon.", "success", { clear: true });
showToast("Successfully updated footer icons.", "success", { clear: true });
};
const onSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
const icon = form.get("icon");
const url = form.get("url");
const newIcon = { icon, url };
setFooterIcons([...footerIcons, newIcon]);
const { success, error } = await Admin.updateSystemPreferences({
footer_data: JSON.stringify([...footerIcons, newIcon]),
});
if (!success) {
showToast(`Failed to add footer icon - ${error}`, "error", {
clear: true,
});
return;
}
window.localStorage.removeItem(System.cacheKeys.footerIcons);
setShowForm(false);
showToast("Successfully added footer icon.", "success", { clear: true });
const handleRemoveIcon = (index) => {
const updatedIcons = [...footerIcons];
updatedIcons[index] = null;
updateFooterIcons(updatedIcons);
};
return (
<div className="mb-6">
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">
<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Custom Footer Icons
</h2>
<p className="text-sm font-base text-white/60">
<p className="text-xs leading-[18px] font-base text-white/60">
Customize the footer icons displayed on the bottom of the sidebar.
</p>
</div>
<CurrentIcons footerIcons={footerIcons} remove={removeFooterIcon} />
<NewIconForm
handleSubmit={onSubmit}
showing={footerIcons.length < MAX_ICONS && showForm}
/>
<div hidden={!(!showForm && footerIcons.length < MAX_ICONS) || loading}>
<div className="flex gap-2 mt-6">
<button
onClick={() => setShowForm(true)}
className="flex gap-x-2 items-center justify-center text-white text-sm hover:text-sky-400 transition-all duration-300"
>
Add new footer icon
<Plus className="" size={24} weight="fill" />
</button>
</div>
<div className="mt-3 flex gap-x-3 font-bold text-white text-sm">
<div>Icon</div>
<div>Link</div>
</div>
<div className="mt-2 flex flex-col gap-y-[10px]">
{footerIcons.map((icon, index) => (
<NewIconForm
key={index}
icon={icon?.icon}
url={icon?.url}
onSave={(newIcon, newUrl) => {
const updatedIcons = [...footerIcons];
updatedIcons[index] = { icon: newIcon, url: newUrl };
updateFooterIcons(updatedIcons);
}}
onRemove={() => handleRemoveIcon(index)}
/>
))}
</div>
</div>
);
}
function CurrentIcons({ footerIcons, remove }) {
if (footerIcons.length === 0) return null;
return (
<div className="flex flex-col w-fit gap-y-2 mt-4">
{footerIcons.map((icon, index) => (
<div
key={index}
className="flex items-center justify-between bg-zinc-900 p-2 rounded-md gap-x-4"
>
<div className="flex items-center gap-x-2">
<IconPreview symbol={icon.icon} disabled={true} />
<span className="text-white/60">{icon.url}</span>
</div>
<button
type="button"
className="transition-all duration-300 text-neutral-700 bg-transparent rounded-full hover:bg-zinc-600 hover:border-zinc-600 hover:text-white border-transparent border shadow-lg mr-2"
onClick={() => remove(index)}
>
<X className="m-[1px]" size={20} />
</button>
</div>
))}
</div>
);
}
const IconPreview = ({ symbol, disabled = false }) => {
const IconComponent = ICON_COMPONENTS.hasOwnProperty(symbol)
? ICON_COMPONENTS[symbol]
: ICON_COMPONENTS.Info;
return (
<button
type="button"
disabled={disabled}
className="disabled:pointer-events-none border-transparent transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border mx-1"
>
<IconComponent className="h-5 w-5 text-white" weight="fill" />
</button>
);
};

View File

@ -53,9 +53,11 @@ export default function SupportEmail() {
if (loading || !user?.role) return null;
return (
<form className="mb-6" onSubmit={updateSupportEmail}>
<div className="flex flex-col gap-y-2">
<h2 className="leading-tight font-medium text-white">Support Email</h2>
<p className="text-sm font-base text-white/60">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Support Email
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Set the support email address that shows up in the user menu while
logged into this instance.
</p>
@ -64,7 +66,7 @@ export default function SupportEmail() {
<input
name="supportEmail"
type="email"
className="bg-zinc-900 mt-4 text-white placeholder:text-white/20 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px]"
className="bg-zinc-900 mt-3 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px] placeholder:text-white/20"
placeholder="support@mycompany.com"
required={true}
autoComplete="off"

View File

@ -11,16 +11,16 @@ export default function Appearance() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Appearance Settings
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Appearance
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Customize the appearance settings of your platform.
</p>
</div>

View File

@ -7,7 +7,7 @@ import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import showToast from "@/utils/toast";
import System from "@/models/system";
import { CaretDown } from "@phosphor-icons/react";
import { CaretDown, Download } from "@phosphor-icons/react";
import { saveAs } from "file-saver";
const exportOptions = {
@ -47,11 +47,9 @@ const exportOptions = {
export default function WorkspaceChats() {
const [showMenu, setShowMenu] = useState(false);
const [exportType, setExportType] = useState("jsonl");
const menuRef = useRef();
const openMenuButton = useRef();
const handleDumpChats = async () => {
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType);
if (!!chats) {
const { name, mimeType, fileExtension, filenameFunc } =
@ -90,56 +88,48 @@ export default function WorkspaceChats() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Workspace Chats
</p>
<div className="flex gap-x-1 relative">
<button
onClick={handleDumpChats}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
>
Export as {exportOptions[exportType].name}
</button>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className={`transition-all duration-300 border border-slate-200 p-1 rounded-lg text-slate-200 text-sm items-center flex hover:bg-slate-200 hover:text-slate-800 ${
showMenu ? "bg-slate-200 text-slate-800" : ""
}`}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<CaretDown weight="bold" className="h-4 w-4" />
<Download size={18} weight="bold" />
Export
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-sidebar p-4 flex items-center justify-center mt-2`}
} z-20 w-fit rounded-lg absolute top-full right-0 bg-[#2C2F36] mt-2 shadow-md`}
>
<div className="flex flex-col gap-y-2">
{Object.entries(exportOptions)
.filter(([type, _]) => type !== exportType)
.map(([key, data]) => (
<button
key={key}
onClick={() => {
setExportType(key);
setShowMenu(false);
}}
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
>
{data.name}
</button>
))}
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147]"
>
{data.name}
</button>
))}
</div>
</div>
</div>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages that have been sent
by users ordered by their creation date.
</p>
@ -195,8 +185,8 @@ function ChatsContainer() {
return (
<>
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Id
@ -228,7 +218,7 @@ function ChatsContainer() {
))}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<div className="flex w-full justify-between items-center mt-6">
<button
onClick={handlePrevious}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"

View File

@ -67,19 +67,19 @@ export default function GithubConnectorSetup() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<img src={image} alt="Github" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Import GitHub Repository
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Import all files from a public or private Github repository
and have its files be available in your workspace.
</p>
@ -88,7 +88,7 @@ export default function GithubConnectorSetup() {
<form className="w-full" onSubmit={handleSubmit}>
{!accessToken && (
<div className="flex flex-col gap-y-1 py-4 ">
<div className="flex flex-col gap-y-1 py-4">
<div className="flex flex-col w-fit gap-y-2 bg-blue-600/20 rounded-lg px-4 py-2">
<div className="flex items-center gap-x-2">
<Info size={20} className="shrink-0 text-blue-400" />

View File

@ -48,19 +48,19 @@ export default function YouTubeTranscriptConnectorSetup() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
<img src={image} alt="YouTube" className="rounded-lg h-16 w-16" />
<div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Import YouTube transcription
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
From a youtube link, import the entire transcript of that
video for embedding.
</p>

View File

@ -9,26 +9,31 @@ export default function DataConnectors() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="items-center">
<p className="text-lg leading-6 font-bold text-white">
Data Connectors
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Verified data connectors allow you to add more content to your
AnythingLLM workspaces with no custom code or complexity.
<br />
Guaranteed to work with your AnythingLLM instance.
</p>
</div>
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
<DataConnectorOption slug="github" />
<DataConnectorOption slug="youtube-transcript" />
<div className="text-sm font-medium text-white mt-6 mb-4">
Available Data Connectors
</div>
<div className="w-full">
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
<DataConnectorOption slug="github" />
<DataConnectorOption slug="youtube-transcript" />
</div>
</div>
</div>
</div>

View File

@ -14,14 +14,16 @@ export default function EmbedChats() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Embed Chats</p>
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Embed Chats
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages from any embed that
you have published.
</p>

View File

@ -12,27 +12,28 @@ import Embed from "@/models/embed";
export default function EmbedConfigs() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<p className="text-lg leading-6 font-bold text-white">
Embeddable Chat Widgets
</p>
<button
onClick={openModal}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
<CodeBlock className="h-4 w-4" /> Create embed
</button>
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Embeddable chat widgets are public facing chat interfaces that are
tied to a single workspace. These allow you to build workspaces
that then you can publish to the world.
@ -51,6 +52,7 @@ export default function EmbedConfigs() {
function EmbedContainer() {
const [loading, setLoading] = useState(true);
const [embeds, setEmbeds] = useState([]);
useEffect(() => {
async function fetchUsers() {
const _embeds = await Embed.embeds();
@ -75,8 +77,8 @@ function EmbedContainer() {
}
return (
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<table className="w-full text-sm text-left rounded-lg mt-6">
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Workspace

View File

@ -128,18 +128,11 @@ export default function GeneralEmbeddingPreference() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
<Sidebar />
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient p-[18px] h-full overflow-y-scroll animate-pulse border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -148,30 +141,30 @@ export default function GeneralEmbeddingPreference() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
id="embedding-form"
onSubmit={handleSubmit}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Embedding Preference
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
When using an LLM that does not natively support an embedding
engine - you may need to additionally specify credentials to
for embedding text.
@ -181,63 +174,67 @@ export default function GeneralEmbeddingPreference() {
format which AnythingLLM can use to process.
</p>
</div>
<>
<div className="text-white text-sm font-medium py-4">
Embedding Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0 z-20">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search Embedding providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredEmbedders.map((embedder) => {
return (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
<div className="text-sm font-medium text-white mt-6 mb-4">
Embedding Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search Embedding providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredEmbedders.map((embedder) => {
return (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
</div>
</div>
</>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
</div>
</div>
</div>
</form>
</div>
)}
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
</div>
);
}

View File

@ -16,6 +16,7 @@ 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 GroqLogo from "@/media/llmprovider/groq.png";
import PreLoader from "@/components/Preloader";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
@ -28,11 +29,12 @@ import OllamaLLMOptions from "@/components/LLMSelection/OllamaLLMOptions";
import TogetherAiOptions from "@/components/LLMSelection/TogetherAiOptions";
import MistralOptions from "@/components/LLMSelection/MistralOptions";
import HuggingFaceOptions from "@/components/LLMSelection/HuggingFaceOptions";
import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
export default function GeneralLLMPreference() {
const [saving, setSaving] = useState(false);
@ -173,6 +175,14 @@ export default function GeneralLLMPreference() {
options: <OpenRouterOptions settings={settings} />,
description: "A unified interface for LLMs.",
},
{
name: "Groq",
value: "groq",
logo: GroqLogo,
options: <GroqAiOptions settings={settings} />,
description:
"The fastest LLM inferencing available for real-time AI applications.",
},
{
name: "Native",
value: "native",
@ -189,7 +199,7 @@ export default function GeneralLLMPreference() {
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-main-gradient p-[18px] h-full overflow-y-scroll animate-pulse border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -198,33 +208,33 @@ export default function GeneralLLMPreference() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form onSubmit={handleSubmit} className="flex w-full">
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
LLM Preference
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for your preferred LLM
chat & embedding provider. Its important these keys are
current and correct or else AnythingLLM will not function
properly.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
<div className="text-sm font-medium text-white mt-6 mb-4">
LLM Providers
</div>
<div className="w-full">

View File

@ -0,0 +1,206 @@
import { useEffect, useState } from "react";
import Sidebar from "@/components/SettingsSidebar";
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,
} from "@/pages/OnboardingFlow/Steps/DataHandling";
export default function PrivacyAndDataHandling() {
const [settings, setSettings] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchSettings() {
setLoading(true);
const settings = await System.keys();
setSettings(settings);
setLoading(false);
}
fetchSettings();
}, []);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
Privacy & Data-Handling
</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
This is your configuration for how connected third party providers
and AnythingLLM handle your data.
</p>
</div>
{loading ? (
<div className="h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] p-[18px] h-full overflow-y-scroll">
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
) : (
<>
<ThirdParty settings={settings} />
<TelemetryLogs settings={settings} />
</>
)}
</div>
</div>
</div>
);
}
function ThirdParty({ settings }) {
const llmChoice = settings?.LLMProvider || "openai";
const embeddingEngine = settings?.EmbeddingEngine || "openai";
const vectorDb = settings?.VectorDB || "pinecone";
return (
<div className="py-8 w-full flex items-start justify-center flex-col gap-y-6 border-b-2 border-white/10">
<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-white text-base font-bold">LLM Selection</div>
<div className="flex items-center gap-2.5">
<img
src={LLM_SELECTION_PRIVACY[llmChoice].logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-white text-sm font-bold">
{LLM_SELECTION_PRIVACY[llmChoice].name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{LLM_SELECTION_PRIVACY[llmChoice].description.map((desc) => (
<li className="text-white/90 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-white text-base font-bold">Embedding Engine</div>
<div className="flex items-center gap-2.5">
<img
src={EMBEDDING_ENGINE_PRIVACY[embeddingEngine].logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-white text-sm font-bold">
{EMBEDDING_ENGINE_PRIVACY[embeddingEngine].name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{EMBEDDING_ENGINE_PRIVACY[embeddingEngine].description.map(
(desc) => (
<li className="text-white/90 text-sm">{desc}</li>
)
)}
</ul>
</div>
<div className="flex flex-col gap-y-2 pb-4">
<div className="text-white text-base font-bold">Vector Database</div>
<div className="flex items-center gap-2.5">
<img
src={VECTOR_DB_PRIVACY[vectorDb].logo}
alt="LLM Logo"
className="w-8 h-8 rounded"
/>
<p className="text-white text-sm font-bold">
{VECTOR_DB_PRIVACY[vectorDb].name}
</p>
</div>
<ul className="flex flex-col list-disc ml-4">
{VECTOR_DB_PRIVACY[vectorDb].description.map((desc) => (
<li className="text-white/90 text-sm">{desc}</li>
))}
</ul>
</div>
</div>
</div>
);
}
function TelemetryLogs({ settings }) {
const [telemetry, setTelemetry] = useState(
settings?.DisableTelemetry !== "true"
);
async function toggleTelemetry() {
await System.updateSystem({
DisableTelemetry: !telemetry ? "false" : "true",
});
setTelemetry(!telemetry);
showToast(
`Anonymous Telemetry has been ${!telemetry ? "enabled" : "disabled"}.`,
"info",
{ clear: true }
);
}
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="">
<label className="mb-2.5 block font-medium text-white">
Anonymous Telemetry Enabled
</label>
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
onClick={toggleTelemetry}
checked={telemetry}
className="peer sr-only pointer-events-none"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
</label>
</div>
</div>
</div>
<div className="flex flex-col items-left space-y-2">
<p className="text-white/80 text-xs rounded-lg w-96">
All events do not record IP-address and contain{" "}
<b>no identifying</b> content, settings, chats, or other non-usage
based information. To see the list of event tags collected you can
look on{" "}
<a
href="https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code"
className="underline text-blue-400"
target="_blank"
>
Github here
</a>
.
</p>
<p className="text-white/80 text-xs rounded-lg w-96">
As an open-source project we respect your right to privacy. We are
dedicated to building the best solution for integrating AI and
documents privately and securely. If you do decide to turn off
telemetry all we ask is to consider sending us feedback and thoughts
so that we can continue to improve AnythingLLM for you.{" "}
<a
href="mailto:team@mintplexlabs.com"
className="underline text-blue-400"
target="_blank"
>
team@mintplexlabs.com
</a>
.
</p>
</div>
</div>
</div>
);
}

View File

@ -13,7 +13,7 @@ export default function GeneralSecurity() {
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
>
<MultiUserMode />
<PasswordProtection />

View File

@ -0,0 +1,180 @@
import React, { useEffect, useState } from "react";
import { isMobile } from "react-device-detect";
import Sidebar from "@/components/SettingsSidebar";
import System from "@/models/system";
import showToast from "@/utils/toast";
import PreLoader from "@/components/Preloader";
import OpenAiLogo from "@/media/llmprovider/openai.png";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import OpenAiWhisperOptions from "@/components/TranscriptionSelection/OpenAiOptions";
import NativeTranscriptionOptions from "@/components/TranscriptionSelection/NativeTranscriptionOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
export default function TranscriptionModelPreference() {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [settings, setSettings] = useState(null);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [filteredProviders, setFilteredProviders] = useState([]);
const [selectedProvider, setSelectedProvider] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const form = e.target;
const data = { WhisperProvider: selectedProvider };
const formData = new FormData(form);
for (var [key, value] of formData.entries()) data[key] = value;
const { error } = await System.updateSystem(data);
setSaving(true);
if (error) {
showToast(`Failed to save preferences: ${error}`, "error");
} else {
showToast("Transcription preferences saved successfully.", "success");
}
setSaving(false);
setHasChanges(!!error);
};
const updateProviderChoice = (selection) => {
setSelectedProvider(selection);
setHasChanges(true);
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
setSettings(_settings);
setSelectedProvider(_settings?.WhisperProvider || "local");
setLoading(false);
}
fetchKeys();
}, []);
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredProviders(filtered);
}, [searchQuery, selectedProvider]);
const PROVIDERS = [
{
name: "OpenAI",
value: "openai",
logo: OpenAiLogo,
options: <OpenAiWhisperOptions settings={settings} />,
description:
"Leverage the OpenAI Whisper-large model using your API key.",
},
{
name: "AnythingLLM Built-In",
value: "local",
logo: AnythingLLMIcon,
options: <NativeTranscriptionOptions settings={settings} />,
description: "Run a built-in whisper model on this instance privately.",
},
];
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
</div>
</div>
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form onSubmit={handleSubmit} className="flex w-full">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Transcription Model Preference
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for your preferred
transcription model provider. Its important these keys are
current and correct or else media files and audio will not
transcribe.
</p>
</div>
<div className="text-sm font-medium text-white mt-6 mb-4">
Transcription Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search audio transcription providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredProviders.map((provider) => {
return (
<LLMItem
key={provider.name}
name={provider.name}
value={provider.value}
image={provider.logo}
description={provider.description}
checked={selectedProvider === provider.value}
onClick={() => updateProviderChoice(provider.value)}
/>
);
})}
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedProvider &&
PROVIDERS.find(
(provider) => provider.value === selectedProvider
)?.options}
</div>
</div>
</div>
</form>
</div>
)}
</div>
);
}

View File

@ -154,18 +154,11 @@ export default function GeneralVectorDatabase() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
<Sidebar />
{loading ? (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent animate-pulse"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<div className="w-full h-full flex justify-center items-center">
<PreLoader />
@ -174,42 +167,42 @@ export default function GeneralVectorDatabase() {
) : (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll"
>
<form
id="vectordb-form"
onSubmit={handleSubmit}
className="flex w-full"
>
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">
<div className="flex items-center gap-x-4">
<p className="text-lg leading-6 font-bold text-white">
Vector Database
</p>
{hasChanges && (
<button
type="submit"
disabled={saving}
className="border border-slate-200 px-4 py-1 rounded-lg text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800"
className="flex items-center gap-x-2 px-4 py-2 rounded-lg bg-[#2C2F36] text-white text-sm hover:bg-[#3D4147] shadow-md border border-[#3D4147]"
>
{saving ? "Saving..." : "Save changes"}
</button>
)}
</div>
<p className="text-sm font-base text-white text-opacity-60">
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for how your
AnythingLLM instance will function. It's important these keys
are current and correct.
</p>
</div>
<div className="text-white text-sm font-medium py-4">
Select your preferred vector database provider
<div className="text-sm font-medium text-white mt-6 mb-4">
Vector Database Providers
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0 z-20">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
@ -257,6 +250,13 @@ export default function GeneralVectorDatabase() {
</form>
</div>
)}
<ModalWrapper isOpen={isOpen}>
<ChangeWarningModal
warningText="Switching the vector database will ignore previously embedded documents and future similarity search results. They will need to be re-added to each workspace."
onClose={closeModal}
onConfirm={handleSaveSettings}
/>
</ModalWrapper>
</div>
);
}

View File

@ -13,6 +13,7 @@ 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 GroqLogo from "@/media/llmprovider/groq.png";
import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import ChromaLogo from "@/media/vectordbs/chroma.png";
@ -28,7 +29,7 @@ import { useNavigate } from "react-router-dom";
const TITLE = "Data Handling & Privacy";
const DESCRIPTION =
"We are committed to transparency and control when it comes to your personal data.";
const LLM_SELECTION_PRIVACY = {
export const LLM_SELECTION_PRIVACY = {
openai: {
name: "OpenAI",
description: [
@ -127,9 +128,17 @@ const LLM_SELECTION_PRIVACY = {
],
logo: OpenRouterLogo,
},
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,
},
};
const VECTOR_DB_PRIVACY = {
export const VECTOR_DB_PRIVACY = {
chroma: {
name: "Chroma",
description: [
@ -190,7 +199,7 @@ const VECTOR_DB_PRIVACY = {
},
};
const EMBEDDING_ENGINE_PRIVACY = {
export const EMBEDDING_ENGINE_PRIVACY = {
native: {
name: "AnythingLLM Embedder",
description: [

View File

@ -13,6 +13,7 @@ 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 GroqLogo from "@/media/llmprovider/groq.png";
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
@ -25,12 +26,13 @@ import MistralOptions from "@/components/LLMSelection/MistralOptions";
import HuggingFaceOptions from "@/components/LLMSelection/HuggingFaceOptions";
import TogetherAiOptions from "@/components/LLMSelection/TogetherAiOptions";
import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import System from "@/models/system";
import paths from "@/utils/paths";
import showToast from "@/utils/toast";
import { useNavigate } from "react-router-dom";
import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
const TITLE = "LLM Preference";
const DESCRIPTION =
@ -147,6 +149,14 @@ export default function LLMPreference({
options: <OpenRouterOptions settings={settings} />,
description: "A unified interface for LLMs.",
},
{
name: "Groq",
value: "groq",
logo: GroqLogo,
options: <GroqAiOptions settings={settings} />,
description:
"The fastest LLM inferencing available for real-time AI applications.",
},
{
name: "Native",
value: "native",

View File

@ -19,7 +19,7 @@ export default function WorkspaceChat() {
}
function ShowWorkspaceChat() {
const { slug, threadSlug = null } = useParams();
const { slug } = useParams();
const [workspace, setWorkspace] = useState(null);
const [loading, setLoading] = useState(true);
@ -32,9 +32,11 @@ function ShowWorkspaceChat() {
return;
}
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
const pfpUrl = await Workspace.fetchPfp(slug);
setWorkspace({
..._workspace,
suggestedMessages,
pfpUrl,
});
setLoading(false);
}

View File

@ -101,7 +101,7 @@ export default function SuggestedChatMessages({ slug }) {
</div>
);
return (
<div className="w-screen">
<div className="w-screen mt-6">
<div className="flex flex-col">
<label className="block input-label">Suggested Chat Messages</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">

View File

@ -0,0 +1,96 @@
import Workspace from "@/models/workspace";
import showToast from "@/utils/toast";
import { Plus } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
export default function WorkspacePfp({ workspace, slug }) {
const [pfp, setPfp] = useState(null);
useEffect(() => {
async function fetchWorkspace() {
const pfpUrl = await Workspace.fetchPfp(slug);
setPfp(pfpUrl);
}
fetchWorkspace();
}, [slug]);
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return false;
const formData = new FormData();
formData.append("file", file);
const { success, error } = await Workspace.uploadPfp(
formData,
workspace.slug
);
if (!success) {
showToast(`Failed to upload profile picture: ${error}`, "error");
return;
}
const pfpUrl = await Workspace.fetchPfp(workspace.slug);
setPfp(pfpUrl);
showToast("Profile picture uploaded.", "success");
};
const handleRemovePfp = async () => {
const { success, error } = await Workspace.removePfp(workspace.slug);
if (!success) {
showToast(`Failed to remove profile picture: ${error}`, "error");
return;
}
setPfp(null);
};
return (
<div className="mt-6">
<div className="flex flex-col">
<label className="block input-label">Assistant Profile Image</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the profile image of the assistant for this workspace.
</p>
</div>
<div className="flex flex-col md:flex-row items-center gap-8">
<div className="flex flex-col items-center">
<label className="w-36 h-36 flex flex-col items-center justify-center bg-zinc-900/50 transition-all duration-300 rounded-full mt-8 border-2 border-dashed border-white border-opacity-60 cursor-pointer hover:opacity-60">
<input
id="workspace-pfp-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
{pfp ? (
<img
src={pfp}
alt="User profile picture"
className="w-36 h-36 rounded-full object-cover bg-white"
/>
) : (
<div className="flex flex-col items-center justify-center p-3">
<Plus className="w-8 h-8 text-white/80 m-2" />
<span className="text-white text-opacity-80 text-xs font-semibold">
Workspace Image
</span>
<span className="text-white text-opacity-60 text-xs">
800 x 800
</span>
</div>
)}
</label>
{pfp && (
<button
type="button"
onClick={handleRemovePfp}
className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline"
>
Remove Workspace Image
</button>
)}
</div>
</div>
</div>
);
}

View File

@ -6,6 +6,7 @@ import VectorCount from "./VectorCount";
import WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
import WorkspacePfp from "./WorkspacePfp";
export default function GeneralInfo({ slug }) {
const [workspace, setWorkspace] = useState(null);
@ -66,9 +67,8 @@ export default function GeneralInfo({ slug }) {
</button>
)}
</form>
<div className="mt-6">
<SuggestedChatMessages slug={workspace.slug} />
</div>
<SuggestedChatMessages slug={workspace.slug} />
<WorkspacePfp workspace={workspace} slug={slug} />
<DeleteWorkspace workspace={workspace} />
</>
);

View File

@ -67,14 +67,14 @@ function ShowWorkspaceChat() {
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-main-gradient w-full h-full overflow-y-scroll border-2 border-outline"
>
<div className="flex gap-x-10 pt-6 pb-4 ml-16 mr-8 border-b-2 border-white border-opacity-10">
<Link
to={paths.workspace.chat(slug)}
className="absolute top-2 left-2 md:top-4 md:left-4 transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border z-10"
>
<ArrowUUpLeft className="h-4 w-4" />
<ArrowUUpLeft className="h-5 w-5" weight="fill" />
</Link>
<TabItem
title="General Settings"

View File

@ -1,3 +1,5 @@
export const ABORT_STREAM_EVENT = "abort-chat-stream";
// For handling of chat responses in the frontend by their various types.
export default function handleChat(
chatResult,
@ -108,6 +110,22 @@ export default function handleChat(
_chatHistory[chatIdx] = updatedHistory;
}
setChatHistory([..._chatHistory]);
setLoadingResponse(false);
} else if (type === "stopGeneration") {
const chatIdx = _chatHistory.length - 1;
const existingHistory = { ..._chatHistory[chatIdx] };
const updatedHistory = {
...existingHistory,
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
};
_chatHistory[chatIdx] = updatedHistory;
setChatHistory([..._chatHistory]);
setLoadingResponse(false);
}
}

View File

@ -92,6 +92,9 @@ export default {
llmPreference: () => {
return "/settings/llm-preference";
},
transcriptionPreference: () => {
return "/settings/transcription-preference";
},
embeddingPreference: () => {
return "/settings/embedding-preference";
},
@ -110,6 +113,9 @@ export default {
logs: () => {
return "/settings/event-logs";
},
privacy: () => {
return "/settings/privacy";
},
embedSetup: () => {
return `/settings/embed-config`;
},

View File

@ -24,7 +24,8 @@ export default {
"sidebar-button": "#31353A",
sidebar: "#25272C",
"historical-msg-system": "rgba(255, 255, 255, 0.05);",
"historical-msg-user": "#2C2F35"
"historical-msg-user": "#2C2F35",
outline: "#4E5153"
},
backgroundImage: {
"preference-gradient":

View File

@ -58,6 +58,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
# HUGGING_FACE_LLM_API_KEY=hf_xxxxxx
# HUGGING_FACE_LLM_TOKEN_LIMIT=8000
# LLM_PROVIDER='groq'
# GROQ_API_KEY=gsk_abcxyz
# GROQ_MODEL_PREF=llama2-70b-4096
###########################################
######## Embedding API SElECTION ##########
###########################################
@ -124,6 +128,16 @@ VECTOR_DB="lancedb"
# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com"
# ZILLIZ_API_TOKEN=api-token-here
###########################################
######## Audio Model Selection ############
###########################################
# (default) use built-in whisper-small model.
WHISPER_PROVIDER="local"
# use openai hosted whisper model.
# WHISPER_PROVIDER="openai"
# OPEN_AI_KEY=sk-xxxxxxxx
# CLOUD DEPLOYMENT VARIRABLES ONLY
# AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
# STORAGE_DIR= # absolute filesystem path with no trailing slash

View File

@ -59,14 +59,17 @@ function adminEndpoints(app) {
}
const { user: newUser, error } = await User.create(newUserParams);
await EventLogs.logEvent(
"user_created",
{
userName: newUser.username,
createdBy: currUser.username,
},
currUser.id
);
if (!!newUser) {
await EventLogs.logEvent(
"user_created",
{
userName: newUser.username,
createdBy: currUser.username,
},
currUser.id
);
}
response.status(200).json({ user: newUser, error });
} catch (e) {
console.error(e);

View File

@ -4,11 +4,19 @@ const { setupMulter } = require("../../../utils/files/multer");
const {
viewLocalFiles,
findDocumentInDocuments,
normalizePath,
} = require("../../../utils/files");
const { reqBody } = require("../../../utils/http");
const { EventLogs } = require("../../../models/eventLogs");
const { CollectorApi } = require("../../../utils/collectorApi");
const { handleUploads } = setupMulter();
const fs = require("fs");
const path = require("path");
const { Document } = require("../../../models/documents");
const documentsPath =
process.env.NODE_ENV === "development"
? path.resolve(__dirname, "../../../storage/documents")
: path.resolve(process.env.STORAGE_DIR, `documents`);
function apiDocumentEndpoints(app) {
if (!app) return;
@ -552,6 +560,169 @@ function apiDocumentEndpoints(app) {
response.sendStatus(500).end();
}
});
app.post(
"/v1/document/create-folder",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Documents']
#swagger.description = 'Create a new folder inside the documents storage directory.'
#swagger.requestBody = {
description: 'Name of the folder to create.',
required: true,
type: 'object',
content: {
"application/json": {
schema: {
type: 'object',
example: {
"name": "new-folder"
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
message: null
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (fs.existsSync(storagePath)) {
response.status(500).json({
success: false,
message: "Folder by that name already exists",
});
return;
}
fs.mkdirSync(storagePath, { recursive: true });
response.status(200).json({ success: true, message: null });
} catch (e) {
console.error(e);
response.status(500).json({
success: false,
message: `Failed to create folder: ${e.message}`,
});
}
}
);
app.post(
"/v1/document/move-files",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Documents']
#swagger.description = 'Move files within the documents storage directory.'
#swagger.requestBody = {
description: 'Array of objects containing source and destination paths of files to move.',
required: true,
type: 'object',
content: {
"application/json": {
schema: {
type: 'object',
example: {
"files": [
{
"from": "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json",
"to": "folder/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
}
]
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
message: null
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
try {
const { files } = reqBody(request);
const docpaths = files.map(({ from }) => from);
const documents = await Document.where({ docpath: { in: docpaths } });
const embeddedFiles = documents.map((doc) => doc.docpath);
const moveableFiles = files.filter(
({ from }) => !embeddedFiles.includes(from)
);
const movePromises = moveableFiles.map(({ from, to }) => {
const sourcePath = path.join(documentsPath, normalizePath(from));
const destinationPath = path.join(documentsPath, normalizePath(to));
return new Promise((resolve, reject) => {
fs.rename(sourcePath, destinationPath, (err) => {
if (err) {
console.error(`Error moving file ${from} to ${to}:`, err);
reject(err);
} else {
resolve();
}
});
});
});
Promise.all(movePromises)
.then(() => {
const unmovableCount = files.length - moveableFiles.length;
if (unmovableCount > 0) {
response.status(200).json({
success: true,
message: `${unmovableCount}/${files.length} files not moved. Unembed them from all workspaces.`,
});
} else {
response.status(200).json({
success: true,
message: null,
});
}
})
.catch((err) => {
console.error("Error moving files:", err);
response
.status(500)
.json({ success: false, message: "Failed to move some files." });
});
} catch (e) {
console.error(e);
response
.status(500)
.json({ success: false, message: "Failed to move files." });
}
}
);
}
module.exports = { apiDocumentEndpoints };

View File

@ -0,0 +1,103 @@
const { Document } = require("../models/documents");
const { normalizePath, documentsPath } = require("../utils/files");
const { reqBody } = require("../utils/http");
const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const fs = require("fs");
const path = require("path");
function documentEndpoints(app) {
if (!app) return;
app.post(
"/document/create-folder",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { name } = reqBody(request);
const storagePath = path.join(documentsPath, normalizePath(name));
if (fs.existsSync(storagePath)) {
response.status(500).json({
success: false,
message: "Folder by that name already exists",
});
return;
}
fs.mkdirSync(storagePath, { recursive: true });
response.status(200).json({ success: true, message: null });
} catch (e) {
console.error(e);
response.status(500).json({
success: false,
message: `Failed to create folder: ${e.message} `,
});
}
}
);
app.post(
"/document/move-files",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { files } = reqBody(request);
const docpaths = files.map(({ from }) => from);
const documents = await Document.where({ docpath: { in: docpaths } });
const embeddedFiles = documents.map((doc) => doc.docpath);
const moveableFiles = files.filter(
({ from }) => !embeddedFiles.includes(from)
);
const movePromises = moveableFiles.map(({ from, to }) => {
const sourcePath = path.join(documentsPath, normalizePath(from));
const destinationPath = path.join(documentsPath, normalizePath(to));
return new Promise((resolve, reject) => {
fs.rename(sourcePath, destinationPath, (err) => {
if (err) {
console.error(`Error moving file ${from} to ${to}:`, err);
reject(err);
} else {
resolve();
}
});
});
});
Promise.all(movePromises)
.then(() => {
const unmovableCount = files.length - moveableFiles.length;
if (unmovableCount > 0) {
response.status(200).json({
success: true,
message: `${unmovableCount}/${files.length} files not moved. Unembed them from all workspaces.`,
});
} else {
response.status(200).json({
success: true,
message: null,
});
}
})
.catch((err) => {
console.error("Error moving files:", err);
response
.status(500)
.json({ success: false, message: "Failed to move some files." });
});
} catch (e) {
console.error(e);
response
.status(500)
.json({ success: false, message: "Failed to move files." });
}
}
);
}
module.exports = { documentEndpoints };

View File

@ -548,8 +548,6 @@ function systemEndpoints(app) {
const userRecord = await User.get({ id: user.id });
const oldPfpFilename = userRecord.pfpFilename;
console.log("oldPfpFilename", oldPfpFilename);
if (oldPfpFilename) {
const oldPfpPath = path.join(
__dirname,

View File

@ -19,10 +19,21 @@ const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
const { convertToChatHistory } = require("../utils/helpers/chat/responses");
const { CollectorApi } = require("../utils/collectorApi");
const { handleUploads } = setupMulter();
const { setupPfpUploads } = require("../utils/files/multer");
const { normalizePath } = require("../utils/files");
const { handlePfpUploads } = setupPfpUploads();
const path = require("path");
const fs = require("fs");
const {
determineWorkspacePfpFilepath,
fetchPfp,
} = require("../utils/files/pfp");
function workspaceEndpoints(app) {
if (!app) return;
const responseCache = new Map();
app.post(
"/workspace/new",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
@ -76,7 +87,7 @@ function workspaceEndpoints(app) {
response.sendStatus(400).end();
return;
}
await Workspace.trackChange(currWorkspace, data, user);
const { workspace, message } = await Workspace.update(
currWorkspace.id,
data
@ -422,6 +433,138 @@ function workspaceEndpoints(app) {
}
}
);
app.get(
"/workspace/:slug/pfp",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async function (request, response) {
try {
const { slug } = request.params;
const cachedResponse = responseCache.get(slug);
if (cachedResponse) {
response.writeHead(200, {
"Content-Type": cachedResponse.mime || "image/png",
});
response.end(cachedResponse.buffer);
return;
}
const pfpPath = await determineWorkspacePfpFilepath(slug);
if (!pfpPath) {
response.sendStatus(204).end();
return;
}
const { found, buffer, mime } = fetchPfp(pfpPath);
if (!found) {
response.sendStatus(204).end();
return;
}
responseCache.set(slug, { buffer, mime });
response.writeHead(200, {
"Content-Type": mime || "image/png",
});
response.end(buffer);
return;
} catch (error) {
console.error("Error processing the logo request:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.post(
"/workspace/:slug/upload-pfp",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
handlePfpUploads.single("file"),
async function (request, response) {
try {
const { slug } = request.params;
const uploadedFileName = request.randomFileName;
if (!uploadedFileName) {
return response.status(400).json({ message: "File upload failed." });
}
const workspaceRecord = await Workspace.get({
slug,
});
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(
workspaceRecord.pfpFilename
)}`
);
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
const { workspace, message } = await Workspace.update(
workspaceRecord.id,
{
pfpFilename: uploadedFileName,
}
);
return response.status(workspace ? 200 : 500).json({
message: workspace
? "Profile picture uploaded successfully."
: message,
});
} catch (error) {
console.error("Error processing the profile picture upload:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.delete(
"/workspace/:slug/remove-pfp",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async function (request, response) {
try {
const { slug } = request.params;
const workspaceRecord = await Workspace.get({
slug,
});
const oldPfpFilename = workspaceRecord.pfpFilename;
if (oldPfpFilename) {
const oldPfpPath = path.join(
__dirname,
`../storage/assets/pfp/${normalizePath(oldPfpFilename)}`
);
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
}
const { workspace, message } = await Workspace.update(
workspaceRecord.id,
{
pfpFilename: null,
}
);
// Clear the cache
responseCache.delete(slug);
return response.status(workspace ? 200 : 500).json({
message: workspace
? "Profile picture removed successfully."
: message,
});
} catch (error) {
console.error("Error processing the profile picture removal:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
}
module.exports = { workspaceEndpoints };

View File

@ -20,6 +20,7 @@ const { developerEndpoints } = require("./endpoints/api");
const { extensionEndpoints } = require("./endpoints/extensions");
const { bootHTTP, bootSSL } = require("./utils/boot");
const { workspaceThreadEndpoints } = require("./endpoints/workspaceThreads");
const { documentEndpoints } = require("./endpoints/document");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
@ -44,6 +45,7 @@ adminEndpoints(apiRouter);
inviteEndpoints(apiRouter);
embedManagementEndpoints(apiRouter);
utilEndpoints(apiRouter);
documentEndpoints(apiRouter);
developerEndpoints(app, apiRouter);
// Externally facing embedder endpoints

View File

@ -43,6 +43,7 @@ const SystemSettings = {
EmbeddingModelMaxChunkLength:
process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH,
LocalAiApiKey: !!process.env.LOCAL_AI_API_KEY,
DisableTelemetry: process.env.DISABLE_TELEMETRY || "false",
...(vectorDB === "pinecone"
? {
PineConeKey: !!process.env.PINECONE_API_KEY,
@ -219,12 +220,25 @@ const SystemSettings = {
AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
}
: {}),
...(llmProvider === "groq"
? {
GroqApiKey: !!process.env.GROQ_API_KEY,
GroqModelPref: process.env.GROQ_MODEL_PREF,
// For embedding credentials when groq is selected.
OpenAiKey: !!process.env.OPEN_AI_KEY,
AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,
AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
}
: {}),
...(llmProvider === "native"
? {
NativeLLMModelPref: process.env.NATIVE_LLM_MODEL_PREF,
NativeLLMTokenLimit: process.env.NATIVE_LLM_MODEL_TOKEN_LIMIT,
// For embedding credentials when ollama is selected.
// For embedding credentials when native is selected.
OpenAiKey: !!process.env.OPEN_AI_KEY,
AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,
@ -245,6 +259,7 @@ const SystemSettings = {
AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
}
: {}),
WhisperProvider: process.env.WHISPER_PROVIDER || "local",
};
},

View File

@ -6,6 +6,8 @@ const { ROLES } = require("../utils/middleware/multiUserProtected");
const { v4: uuidv4 } = require("uuid");
const Workspace = {
defaultPrompt:
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
writable: [
// Used for generic updates so we can validate keys in request body
"name",
@ -19,6 +21,7 @@ const Workspace = {
"chatModel",
"topN",
"chatMode",
"pfpFilename",
],
new: async function (name = null, creatorId = null) {
@ -212,6 +215,34 @@ const Workspace = {
return { success: false, error: error.message };
}
},
trackChange: async function (prevData, newData, user) {
try {
const { Telemetry } = require("./telemetry");
const { EventLogs } = require("./eventLogs");
if (
!newData?.openAiPrompt ||
newData?.openAiPrompt === this.defaultPrompt ||
newData?.openAiPrompt === prevData?.openAiPrompt
)
return;
await Telemetry.sendTelemetry("workspace_prompt_changed");
await EventLogs.logEvent(
"workspace_prompt_changed",
{
workspaceName: prevData?.name,
prevSystemPrompt: prevData?.openAiPrompt || this.defaultPrompt,
newSystemPrompt: newData?.openAiPrompt,
},
user?.id
);
return;
} catch (error) {
console.error("Error tracking workspace change:", error.message);
return;
}
},
};
module.exports = { Workspace };

View File

@ -20,13 +20,13 @@
"seed": "node prisma/seed.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.8.1",
"@anthropic-ai/sdk": "^0.16.1",
"@azure/openai": "1.0.0-beta.10",
"@datastax/astra-db-ts": "^0.1.3",
"@google/generative-ai": "^0.1.3",
"@googleapis/youtube": "^9.0.0",
"@pinecone-database/pinecone": "^2.0.1",
"@prisma/client": "5.3.0",
"@prisma/client": "5.3.1",
"@qdrant/js-client-rest": "^1.4.0",
"@xenova/transformers": "^2.14.0",
"@zilliz/milvus2-sdk-node": "^2.3.5",
@ -52,7 +52,7 @@
"openai": "^3.2.1",
"pinecone-client": "^1.1.0",
"posthog-node": "^3.1.1",
"prisma": "^5.3.1",
"prisma": "5.3.1",
"slugify": "^1.6.6",
"sqlite": "^4.2.1",
"sqlite3": "^5.1.6",
@ -78,4 +78,4 @@
"nodemon": "^2.0.22",
"prettier": "^3.0.3"
}
}
}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "pfpFilename" TEXT;

View File

@ -100,6 +100,7 @@ model workspaces {
chatModel String?
topN Int? @default(4)
chatMode String? @default("chat")
pfpFilename String?
workspace_users workspace_users[]
documents workspace_documents[]
workspace_suggested_messages workspace_suggested_messages[]

View File

@ -14,6 +14,9 @@ AnythingLLM allows you to upload various audio and video formats as source docum
Once transcribed you can embed these transcriptions into your workspace like you would any other file!
**Other external model/transcription providers are also live.**
- [OpenAI Whisper via API key.](https://openai.com/research/whisper)
## Text generation (LLM selection)
> [!IMPORTANT]
> Use of a locally running LLM model is **experimental** and may behave unexpectedly, crash, or not function at all.

View File

@ -1340,6 +1340,143 @@
}
}
},
"/v1/document/create-folder": {
"post": {
"tags": [
"Documents"
],
"description": "Create a new folder inside the documents storage directory.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"success": true,
"message": null
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"requestBody": {
"description": "Name of the folder to create.",
"required": true,
"type": "object",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"name": "new-folder"
}
}
}
}
}
}
},
"/v1/document/move-files": {
"post": {
"tags": [
"Documents"
],
"description": "Move files within the documents storage directory.",
"parameters": [
{
"name": "Authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"success": true,
"message": null
}
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/InvalidAPIKey"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"requestBody": {
"description": "Array of objects containing source and destination paths of files to move.",
"required": true,
"type": "object",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"files": [
{
"from": "custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json",
"to": "folder/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json"
}
]
}
}
}
}
}
}
},
"/v1/workspace/new": {
"post": {
"tags": [

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