Merge branch 'Mintplex-Labs:master' into feature/devcontv2
This commit is contained in:
commit
eee187ed00
|
@ -0,0 +1,8 @@
|
|||
failure-threshold: warning
|
||||
ignored:
|
||||
- DL3008
|
||||
- DL3013
|
||||
format: tty
|
||||
trustedRegistries:
|
||||
- docker.io
|
||||
- gcr.io
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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 = {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 $?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 <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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 |
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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`,
|
||||
{
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 →
|
||||
</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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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`;
|
||||
},
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "workspaces" ADD COLUMN "pfpFilename" TEXT;
|
|
@ -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[]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue