🔥🚀 feat: CDN (Firebase) & feat: account section (#1438)

* localization + api-endpoint

* docs: added firebase documentation

* chore: icons

* chore: SettingsTabs

* feat: account pannel; fix: gear icons

* docs: position update

* feat: firebase

* feat: plugin support

* route

* fixed bugs with firebase and moved a lot of files

* chore(DALLE3): using UUID v4

* feat: support for social strategies; moved '/images' path

* fix: data ignored

* gitignore update

* docs: update firebase guide

* refactor: Firebase
- use singleton pattern for firebase initialization, initially on server start
- reorganize imports, move firebase specific files to own service under Files
- rename modules to remove 'avatar' redundancy
- fix imports based on changes

* ci(DALLE/DALLE3): fix tests to use logger and new expected outputs, add firebase tests

* refactor(loadToolWithAuth): pass userId to tool as field

* feat(images/parse): feat: Add URL Image Basename Extraction

Implement a new module to extract the basename of an image from a given URL. This addition includes the  function, which parses the URL and retrieves the basename using the Node.js 'url' and 'path' modules. The function is documented with JSDoc comments for better maintainability and understanding. This feature enhances the application's ability to handle and process image URLs efficiently.

* refactor(addImages): function to use a more specific regular expression for observedImagePath based on the generated image markdown standard across the app

* refactor(DALLE/DALLE3): utilize `getImageBasename` and `this.userId`; fix: pass correct image path to firebase url helper

* fix(addImages): make more general to match any image markdown descriptor

* fix(parse/getImageBasename): test result of this function for an actual image basename

* ci(DALLE3): mock getImageBasename

* refactor(AuthContext): use Recoil atom state for user

* feat: useUploadAvatarMutation, react-query hook for avatar upload

* fix(Toast): stack z-order of Toast over all components (1000)

* refactor(showToast): add optional status field to avoid importing NotificationSeverity on each use of the function

* refactor(routes/avatar): remove unnecessary get route, get userId from req.user.id, require auth on POST request

* chore(uploadAvatar): TODO: remove direct use of Model, `User`

* fix(client): fix Spinner imports

* refactor(Avatar): use react-query hook, Toast, remove unnecessary states, add optimistic UI to upload

* fix(avatar/localStrategy): correctly save local profile picture and cache bust for immediate rendering; fix: firebase init info message (only show once)

* fix: use `includes` instead of `endsWith` for checking manual query of avatar image path in case more queries are appended (as is done in avatar/localStrategy)

---------

Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
This commit is contained in:
Marco Beretta 2023-12-30 03:42:19 +01:00 committed by GitHub
parent bd4d23d314
commit f19f5dca8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1855 additions and 172 deletions

View file

@ -281,6 +281,17 @@ EMAIL_PASSWORD=
EMAIL_FROM_NAME= EMAIL_FROM_NAME=
EMAIL_FROM=noreply@librechat.ai EMAIL_FROM=noreply@librechat.ai
#========================#
# Firebase CDN #
#========================#
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=
#==================================================# #==================================================#
# Others # # Others #
#==================================================# #==================================================#

2
.gitignore vendored
View file

@ -83,3 +83,5 @@ auth.json
/packages/ux-shared/ /packages/ux-shared/
/images /images
!client/src/components/Nav/SettingsTabs/Data/

View file

@ -60,13 +60,11 @@ function addImages(intermediateSteps, responseMessage) {
if (!observation || !observation.includes('![')) { if (!observation || !observation.includes('![')) {
return; return;
} }
const observedImagePath = observation.match(/\(\/images\/.*\.\w*\)/g); const observedImagePath = observation.match(/!\[.*\]\([^)]*\)/g);
if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) { if (observedImagePath && !responseMessage.text.includes(observedImagePath[0])) {
responseMessage.text += '\n' + observation; responseMessage.text += '\n' + observation;
if (process.env.DEBUG_PLUGINS) {
logger.debug('[addImages] added image from intermediateSteps:', observation); logger.debug('[addImages] added image from intermediateSteps:', observation);
} }
}
}); });
} }

View file

@ -4,8 +4,15 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const OpenAI = require('openai'); const OpenAI = require('openai');
// const { genAzureEndpoint } = require('~/utils/genAzureEndpoints'); // const { genAzureEndpoint } = require('~/utils/genAzureEndpoints');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools'); const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
const {
saveImageToFirebaseStorage,
getFirebaseStorageImageUrl,
getFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const { getImageBasename } = require('~/server/services/Files/images');
const extractBaseURL = require('~/utils/extractBaseURL'); const extractBaseURL = require('~/utils/extractBaseURL');
const saveImageFromUrl = require('./saveImageFromUrl'); const saveImageFromUrl = require('./saveImageFromUrl');
const { logger } = require('~/config'); const { logger } = require('~/config');
@ -15,7 +22,9 @@ class OpenAICreateImage extends Tool {
constructor(fields = {}) { constructor(fields = {}) {
super(); super();
this.userId = fields.userId;
let apiKey = fields.DALLE_API_KEY || this.getApiKey(); let apiKey = fields.DALLE_API_KEY || this.getApiKey();
const config = { apiKey }; const config = { apiKey };
if (DALLE_REVERSE_PROXY) { if (DALLE_REVERSE_PROXY) {
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY); config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
@ -24,7 +33,6 @@ class OpenAICreateImage extends Tool {
if (PROXY) { if (PROXY) {
config.httpAgent = new HttpsProxyAgent(PROXY); config.httpAgent = new HttpsProxyAgent(PROXY);
} }
// let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY; // let azureKey = fields.AZURE_API_KEY || process.env.AZURE_API_KEY;
// if (azureKey) { // if (azureKey) {
@ -97,12 +105,11 @@ Guidelines:
throw new Error('No image URL returned from OpenAI API.'); throw new Error('No image URL returned from OpenAI API.');
} }
const regex = /img-[\w\d]+.png/; const imageBasename = getImageBasename(theImageUrl);
const match = theImageUrl.match(regex); let imageName = `image_${uuidv4()}.png`;
let imageName = '1.png';
if (match) { if (imageBasename) {
imageName = match[0]; imageName = imageBasename;
logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png logger.debug('[DALL-E]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png
} else { } else {
logger.debug('[DALL-E] No image name found in the string.', { logger.debug('[DALL-E] No image name found in the string.', {
@ -111,7 +118,18 @@ Guidelines:
}); });
} }
this.outputPath = path.resolve(__dirname, '..', '..', '..', '..', 'client', 'public', 'images'); this.outputPath = path.resolve(
__dirname,
'..',
'..',
'..',
'..',
'client',
'public',
'images',
this.userId,
);
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', 'client'); const appRoot = path.resolve(__dirname, '..', '..', '..', '..', 'client');
this.relativeImageUrl = path.relative(appRoot, this.outputPath); this.relativeImageUrl = path.relative(appRoot, this.outputPath);
@ -120,14 +138,25 @@ Guidelines:
fs.mkdirSync(this.outputPath, { recursive: true }); fs.mkdirSync(this.outputPath, { recursive: true });
} }
const storage = getFirebaseStorage();
if (storage) {
try {
await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName);
this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`);
logger.debug('[DALL-E] result: ' + this.result);
} catch (error) {
logger.error('Error while saving the image to Firebase Storage:', error);
this.result = `Failed to save the image to Firebase Storage. ${error.message}`;
}
} else {
try { try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName); await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName); this.result = this.getMarkdownImageUrl(imageName);
} catch (error) { } catch (error) {
logger.error('Error while saving the DALL-E image:', error); logger.error('Error while saving the image locally:', error);
this.result = theImageUrl; this.result = `Failed to save the image locally. ${error.message}`;
}
} }
return this.result; return this.result;
} }
} }

View file

@ -4,10 +4,17 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { z } = require('zod'); const { z } = require('zod');
const OpenAI = require('openai'); const OpenAI = require('openai');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('langchain/tools'); const { Tool } = require('langchain/tools');
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpsProxyAgent } = require('https-proxy-agent');
const saveImageFromUrl = require('../saveImageFromUrl'); const {
saveImageToFirebaseStorage,
getFirebaseStorageImageUrl,
getFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const { getImageBasename } = require('~/server/services/Files/images');
const extractBaseURL = require('~/utils/extractBaseURL'); const extractBaseURL = require('~/utils/extractBaseURL');
const saveImageFromUrl = require('../saveImageFromUrl');
const { logger } = require('~/config'); const { logger } = require('~/config');
const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env; const { DALLE3_SYSTEM_PROMPT, DALLE_REVERSE_PROXY, PROXY } = process.env;
@ -15,6 +22,7 @@ class DALLE3 extends Tool {
constructor(fields = {}) { constructor(fields = {}) {
super(); super();
this.userId = fields.userId;
let apiKey = fields.DALLE_API_KEY || this.getApiKey(); let apiKey = fields.DALLE_API_KEY || this.getApiKey();
const config = { apiKey }; const config = { apiKey };
if (DALLE_REVERSE_PROXY) { if (DALLE_REVERSE_PROXY) {
@ -108,12 +116,12 @@ class DALLE3 extends Tool {
n: 1, n: 1,
}); });
} catch (error) { } catch (error) {
return `Something went wrong when trying to generate the image. The DALL-E API may unavailable: return `Something went wrong when trying to generate the image. The DALL-E API may be unavailable:
Error Message: ${error.message}`; Error Message: ${error.message}`;
} }
if (!resp) { if (!resp) {
return 'Something went wrong when trying to generate the image. The DALL-E API may unavailable'; return 'Something went wrong when trying to generate the image. The DALL-E API may be unavailable';
} }
const theImageUrl = resp.data[0].url; const theImageUrl = resp.data[0].url;
@ -122,12 +130,11 @@ Error Message: ${error.message}`;
return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.'; return 'No image URL returned from OpenAI API. There may be a problem with the API or your configuration.';
} }
const regex = /img-[\w\d]+.png/; const imageBasename = getImageBasename(theImageUrl);
const match = theImageUrl.match(regex); let imageName = `image_${uuidv4()}.png`;
let imageName = '1.png';
if (match) { if (imageBasename) {
imageName = match[0]; imageName = imageBasename;
logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png logger.debug('[DALL-E-3]', { imageName }); // Output: img-lgCf7ppcbhqQrz6a5ear6FOb.png
} else { } else {
logger.debug('[DALL-E-3] No image name found in the string.', { logger.debug('[DALL-E-3] No image name found in the string.', {
@ -146,6 +153,7 @@ Error Message: ${error.message}`;
'client', 'client',
'public', 'public',
'images', 'images',
this.userId,
); );
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client'); const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client');
this.relativeImageUrl = path.relative(appRoot, this.outputPath); this.relativeImageUrl = path.relative(appRoot, this.outputPath);
@ -154,13 +162,24 @@ Error Message: ${error.message}`;
if (!fs.existsSync(this.outputPath)) { if (!fs.existsSync(this.outputPath)) {
fs.mkdirSync(this.outputPath, { recursive: true }); fs.mkdirSync(this.outputPath, { recursive: true });
} }
const storage = getFirebaseStorage();
if (storage) {
try {
await saveImageToFirebaseStorage(this.userId, theImageUrl, imageName);
this.result = await getFirebaseStorageImageUrl(`${this.userId}/${imageName}`);
logger.debug('[DALL-E-3] result: ' + this.result);
} catch (error) {
logger.error('Error while saving the image to Firebase Storage:', error);
this.result = `Failed to save the image to Firebase Storage. ${error.message}`;
}
} else {
try { try {
await saveImageFromUrl(theImageUrl, this.outputPath, imageName); await saveImageFromUrl(theImageUrl, this.outputPath, imageName);
this.result = this.getMarkdownImageUrl(imageName); this.result = this.getMarkdownImageUrl(imageName);
} catch (error) { } catch (error) {
logger.error('Error while saving the image:', error); logger.error('Error while saving the image locally:', error);
this.result = theImageUrl; this.result = `Failed to save the image locally. ${error.message}`;
}
} }
return this.result; return this.result;

View file

@ -2,11 +2,40 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const OpenAI = require('openai'); const OpenAI = require('openai');
const DALLE3 = require('../DALLE3'); const DALLE3 = require('../DALLE3');
const {
getFirebaseStorage,
saveImageToFirebaseStorage,
} = require('~/server/services/Files/Firebase');
const saveImageFromUrl = require('../../saveImageFromUrl'); const saveImageFromUrl = require('../../saveImageFromUrl');
const { logger } = require('~/config'); const { logger } = require('~/config');
jest.mock('openai'); jest.mock('openai');
jest.mock('~/server/services/Files/Firebase', () => ({
getFirebaseStorage: jest.fn(),
saveImageToFirebaseStorage: jest.fn(),
getFirebaseStorageImageUrl: jest.fn(),
}));
jest.mock('~/server/services/Files/images', () => ({
getImageBasename: jest.fn().mockImplementation((url) => {
// Split the URL by '/'
const parts = url.split('/');
// Get the last part of the URL
const lastPart = parts.pop();
// Check if the last part of the URL matches the image extension regex
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i;
if (imageExtensionRegex.test(lastPart)) {
return lastPart;
}
// If the regex test fails, return an empty string
return '';
}),
}));
const generate = jest.fn(); const generate = jest.fn();
OpenAI.mockImplementation(() => ({ OpenAI.mockImplementation(() => ({
images: { images: {
@ -187,7 +216,48 @@ describe('DALLE3', () => {
generate.mockResolvedValue(mockResponse); generate.mockResolvedValue(mockResponse);
saveImageFromUrl.mockRejectedValue(error); saveImageFromUrl.mockRejectedValue(error);
const result = await dalle._call(mockData); const result = await dalle._call(mockData);
expect(logger.error).toHaveBeenCalledWith('Error while saving the image:', error); expect(logger.error).toHaveBeenCalledWith('Error while saving the image locally:', error);
expect(result).toBe(mockResponse.data[0].url); expect(result).toBe('Failed to save the image locally. Error while saving the image');
});
it('should save image to Firebase Storage if Firebase is initialized', async () => {
const mockData = {
prompt: 'A test prompt',
};
const mockImageUrl = 'http://example.com/img-test.png';
const mockResponse = { data: [{ url: mockImageUrl }] };
generate.mockResolvedValue(mockResponse);
getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized
await dalle._call(mockData);
expect(getFirebaseStorage).toHaveBeenCalled();
expect(saveImageToFirebaseStorage).toHaveBeenCalledWith(
undefined,
mockImageUrl,
expect.any(String),
);
});
it('should handle error when saving image to Firebase Storage fails', async () => {
const mockData = {
prompt: 'A test prompt',
};
const mockImageUrl = 'http://example.com/img-test.png';
const mockResponse = { data: [{ url: mockImageUrl }] };
const error = new Error('Error while saving to Firebase');
generate.mockResolvedValue(mockResponse);
getFirebaseStorage.mockReturnValue({}); // Simulate Firebase being initialized
saveImageToFirebaseStorage.mockRejectedValue(error);
const result = await dalle._call(mockData);
expect(logger.error).toHaveBeenCalledWith(
'Error while saving the image to Firebase Storage:',
error,
);
expect(result).toBe(
'Failed to save the image to Firebase Storage. Error while saving to Firebase',
);
}); });
}); });

View file

@ -67,19 +67,19 @@ const validateTools = async (user, tools = []) => {
} }
}; };
const loadToolWithAuth = async (user, authFields, ToolConstructor, options = {}) => { const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => {
return async function () { return async function () {
let authValues = {}; let authValues = {};
for (const authField of authFields) { for (const authField of authFields) {
let authValue = process.env[authField]; let authValue = process.env[authField];
if (!authValue) { if (!authValue) {
authValue = await getUserPluginAuthValue(user, authField); authValue = await getUserPluginAuthValue(userId, authField);
} }
authValues[authField] = authValue; authValues[authField] = authValue;
} }
return new ToolConstructor({ ...options, ...authValues }); return new ToolConstructor({ ...options, ...authValues, userId });
}; };
}; };

View file

@ -44,6 +44,7 @@
"express-mongo-sanitize": "^2.2.0", "express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^6.9.0", "express-rate-limit": "^6.9.0",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"firebase": "^10.6.0",
"googleapis": "^126.0.1", "googleapis": "^126.0.1",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html": "^1.0.0", "html": "^1.0.0",

View file

@ -4,6 +4,7 @@ const cors = require('cors');
const express = require('express'); const express = require('express');
const passport = require('passport'); const passport = require('passport');
const mongoSanitize = require('express-mongo-sanitize'); const mongoSanitize = require('express-mongo-sanitize');
const { initializeFirebase } = require('~/server/services/Files/Firebase/initialize');
const errorController = require('./controllers/ErrorController'); const errorController = require('./controllers/ErrorController');
const configureSocialLogins = require('./socialLogins'); const configureSocialLogins = require('./socialLogins');
const { connectDb, indexSync } = require('~/lib/db'); const { connectDb, indexSync } = require('~/lib/db');
@ -23,6 +24,7 @@ const { jwtLogin, passportLogin } = require('~/strategies');
const startServer = async () => { const startServer = async () => {
await connectDb(); await connectDb();
logger.info('Connected to MongoDB'); logger.info('Connected to MongoDB');
initializeFirebase();
await indexSync(); await indexSync();
const app = express(); const app = express();

View file

@ -0,0 +1,34 @@
const express = require('express');
const multer = require('multer');
const uploadAvatar = require('~/server/services/Files/images/avatar/uploadAvatar');
const { requireJwtAuth } = require('~/server/middleware/');
const User = require('~/models/User');
const upload = multer();
const router = express.Router();
router.post('/', requireJwtAuth, upload.single('input'), async (req, res) => {
try {
const userId = req.user.id;
const { manual } = req.body;
const input = req.file.buffer;
if (!userId) {
throw new Error('User ID is undefined');
}
// TODO: do not use Model directly, instead use a service method that uses the model
const user = await User.findById(userId).lean();
if (!user) {
throw new Error('User not found');
}
const url = await uploadAvatar(userId, input, manual);
res.json({ url });
} catch (error) {
res.status(500).json({ message: 'An error occurred while uploading the profile picture' });
}
});
module.exports = router;

View file

@ -18,5 +18,6 @@ router.use(uaParser);
router.use('/', files); router.use('/', files);
router.use('/images', images); router.use('/images', images);
router.use('/images/avatar', require('./avatar'));
module.exports = router; module.exports = router;

View file

@ -0,0 +1,45 @@
const fetch = require('node-fetch');
const { ref, uploadBytes, getDownloadURL } = require('firebase/storage');
const { getFirebaseStorage } = require('./initialize');
async function saveImageToFirebaseStorage(userId, imageUrl, imageName) {
const storage = getFirebaseStorage();
if (!storage) {
console.error('Firebase is not initialized. Cannot save image to Firebase Storage.');
return null;
}
const storageRef = ref(storage, `images/${userId.toString()}/${imageName}`);
try {
// Upload image to Firebase Storage using the image URL
await uploadBytes(storageRef, await fetch(imageUrl).then((response) => response.buffer()));
return imageName;
} catch (error) {
console.error('Error uploading image to Firebase Storage:', error.message);
return null;
}
}
async function getFirebaseStorageImageUrl(imageName) {
const storage = getFirebaseStorage();
if (!storage) {
console.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.');
return null;
}
const storageRef = ref(storage, `images/${imageName}`);
try {
// Get the download URL for the image from Firebase Storage
return `![generated image](${await getDownloadURL(storageRef)})`;
} catch (error) {
console.error('Error fetching image URL from Firebase Storage:', error.message);
return null;
}
}
module.exports = {
saveImageToFirebaseStorage,
getFirebaseStorageImageUrl,
};

View file

@ -0,0 +1,7 @@
const images = require('./images');
const initialize = require('./initialize');
module.exports = {
...images,
...initialize,
};

View file

@ -0,0 +1,42 @@
const firebase = require('firebase/app');
const { getStorage } = require('firebase/storage');
const { logger } = require('~/config');
let i = 0;
let firebaseApp = null;
const initializeFirebase = () => {
// Return existing instance if already initialized
if (firebaseApp) {
return firebaseApp;
}
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
if (Object.values(firebaseConfig).some((value) => !value)) {
i === 0 &&
logger.info(
'[Optional] Firebase configuration missing or incomplete. Firebase will not be initialized.',
);
i++;
return null;
}
firebaseApp = firebase.initializeApp(firebaseConfig);
logger.info('Firebase initialized');
return firebaseApp;
};
const getFirebaseStorage = () => {
const app = initializeFirebase();
return app ? getStorage(app) : null;
};
module.exports = { initializeFirebase, getFirebaseStorage };

View file

@ -0,0 +1,29 @@
const { ref, uploadBytes, getDownloadURL } = require('firebase/storage');
const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize');
const { logger } = require('~/config');
async function firebaseStrategy(userId, webPBuffer, oldUser, manual) {
try {
const storage = getFirebaseStorage();
if (!storage) {
throw new Error('Firebase is not initialized.');
}
const avatarRef = ref(storage, `images/${userId.toString()}/avatar`);
await uploadBytes(avatarRef, webPBuffer);
const urlFirebase = await getDownloadURL(avatarRef);
const isManual = manual === 'true';
const url = `${urlFirebase}?manual=${isManual}`;
if (isManual) {
oldUser.avatar = url;
await oldUser.save();
}
return url;
} catch (error) {
logger.error('Error uploading profile picture:', error);
throw error;
}
}
module.exports = firebaseStrategy;

View file

@ -0,0 +1,32 @@
const fs = require('fs').promises;
const path = require('path');
async function localStrategy(userId, webPBuffer, oldUser, manual) {
const userDir = path.resolve(
__dirname,
'..',
'..',
'..',
'..',
'..',
'..',
'client',
'public',
'images',
userId,
);
let avatarPath = path.join(userDir, 'avatar.png');
const urlRoute = `/images/${userId}/avatar.png`;
await fs.mkdir(userDir, { recursive: true });
await fs.writeFile(avatarPath, webPBuffer);
const isManual = manual === 'true';
let url = `${urlRoute}?manual=${isManual}&timestamp=${new Date().getTime()}`;
if (isManual) {
oldUser.avatar = url;
await oldUser.save();
}
return url;
}
module.exports = localStrategy;

View file

@ -0,0 +1,63 @@
const sharp = require('sharp');
const fetch = require('node-fetch');
const fs = require('fs').promises;
const User = require('~/models/User');
const { getFirebaseStorage } = require('~/server/services/Files/Firebase/initialize');
const firebaseStrategy = require('./firebaseStrategy');
const localStrategy = require('./localStrategy');
const { logger } = require('~/config');
async function convertToWebP(inputBuffer) {
return sharp(inputBuffer).resize({ width: 150 }).toFormat('webp').toBuffer();
}
async function uploadAvatar(userId, input, manual) {
try {
if (userId === undefined) {
throw new Error('User ID is undefined');
}
const _id = userId;
// TODO: remove direct use of Model, `User`
const oldUser = await User.findOne({ _id });
let imageBuffer;
if (typeof input === 'string') {
const response = await fetch(input);
if (!response.ok) {
throw new Error(`Failed to fetch image from URL. Status: ${response.status}`);
}
imageBuffer = await response.buffer();
} else if (input instanceof Buffer) {
imageBuffer = input;
} else if (typeof input === 'object' && input instanceof File) {
const fileContent = await fs.readFile(input.path);
imageBuffer = Buffer.from(fileContent);
} else {
throw new Error('Invalid input type. Expected URL, Buffer, or File.');
}
const { width, height } = await sharp(imageBuffer).metadata();
const minSize = Math.min(width, height);
const squaredBuffer = await sharp(imageBuffer)
.extract({
left: Math.floor((width - minSize) / 2),
top: Math.floor((height - minSize) / 2),
width: minSize,
height: minSize,
})
.toBuffer();
const webPBuffer = await convertToWebP(squaredBuffer);
const storage = getFirebaseStorage();
if (storage) {
const url = await firebaseStrategy(userId, webPBuffer, oldUser, manual);
return url;
}
const url = await localStrategy(userId, webPBuffer, oldUser, manual);
return url;
} catch (error) {
logger.error('Error uploading the avatar:', error);
throw error;
}
}
module.exports = uploadAvatar;

View file

@ -1,11 +1,15 @@
const convert = require('./convert'); const convert = require('./convert');
const encode = require('./encode'); const encode = require('./encode');
const parse = require('./parse');
const resize = require('./resize'); const resize = require('./resize');
const validate = require('./validate'); const validate = require('./validate');
const uploadAvatar = require('./avatar/uploadAvatar');
module.exports = { module.exports = {
...convert, ...convert,
...encode, ...encode,
...parse,
...resize, ...resize,
...validate, ...validate,
uploadAvatar,
}; };

View file

@ -0,0 +1,27 @@
const URL = require('url').URL;
const path = require('path');
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i;
/**
* Extracts the image basename from a given URL.
*
* @param {string} urlString - The URL string from which the image basename is to be extracted.
* @returns {string} The basename of the image file from the URL.
* Returns an empty string if the URL does not contain a valid image basename.
*/
function getImageBasename(urlString) {
try {
const url = new URL(urlString);
const basename = path.basename(url.pathname);
return imageExtensionRegex.test(basename) ? basename : '';
} catch (error) {
// If URL parsing fails, return an empty string
return '';
}
}
module.exports = {
getImageBasename,
};

View file

@ -1,49 +1,70 @@
const { Strategy: DiscordStrategy } = require('passport-discord'); const { Strategy: DiscordStrategy } = require('passport-discord');
const { logger } = require('~/config'); const { logger } = require('~/config');
const User = require('~/models/User'); const User = require('~/models/User');
const { useFirebase, uploadAvatar } = require('~/server/services/Files/images');
const discordLogin = async (accessToken, refreshToken, profile, cb) => { const discordLogin = async (accessToken, refreshToken, profile, cb) => {
try { try {
const email = profile.email; const email = profile.email;
const discordId = profile.id; const discordId = profile.id;
const oldUser = await User.findOne({ const oldUser = await User.findOne({ email });
email,
});
const ALLOW_SOCIAL_REGISTRATION = const ALLOW_SOCIAL_REGISTRATION =
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
let avatarURL; let avatarUrl;
if (profile.avatar) { if (profile.avatar) {
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; const format = profile.avatar.startsWith('a_') ? 'gif' : 'png';
avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; avatarUrl = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`;
} else { } else {
const defaultAvatarNum = Number(profile.discriminator) % 5; const defaultAvatarNum = Number(profile.discriminator) % 5;
avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; avatarUrl = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`;
} }
if (oldUser) { if (oldUser) {
oldUser.avatar = avatarURL; await handleExistingUser(oldUser, avatarUrl, useFirebase);
await oldUser.save();
return cb(null, oldUser); return cb(null, oldUser);
} else if (ALLOW_SOCIAL_REGISTRATION) { }
if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await createNewUser(profile, discordId, email, avatarUrl, useFirebase);
return cb(null, newUser);
}
} catch (err) {
logger.error('[discordLogin]', err);
return cb(err);
}
};
const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => {
if (!useFirebase && !oldUser.avatar.includes('?manual=true')) {
oldUser.avatar = avatarUrl;
await oldUser.save();
} else if (useFirebase && !oldUser.avatar.includes('?manual=true')) {
const userId = oldUser._id;
const newavatarUrl = await uploadAvatar(userId, avatarUrl);
oldUser.avatar = newavatarUrl;
await oldUser.save();
}
};
const createNewUser = async (profile, discordId, email, avatarUrl, useFirebase) => {
const newUser = await new User({ const newUser = await new User({
provider: 'discord', provider: 'discord',
discordId, discordId,
username: profile.username, username: profile.username,
email, email,
name: profile.global_name, name: profile.global_name,
avatar: avatarURL, avatar: avatarUrl,
}).save(); }).save();
return cb(null, newUser); if (useFirebase) {
const userId = newUser._id;
const newavatarUrl = await uploadAvatar(userId, avatarUrl);
newUser.avatar = newavatarUrl;
await newUser.save();
} }
return cb(null, false, { return newUser;
message: 'User not found.',
});
} catch (err) {
logger.error('[discordLogin]', err);
return cb(err);
}
}; };
module.exports = () => module.exports = () =>

View file

@ -1,41 +1,62 @@
const FacebookStrategy = require('passport-facebook').Strategy; const FacebookStrategy = require('passport-facebook').Strategy;
const { logger } = require('~/config'); const { logger } = require('~/config');
const User = require('~/models/User'); const User = require('~/models/User');
const { useFirebase, uploadAvatar } = require('~/server/services/Files/images');
const facebookLogin = async (accessToken, refreshToken, profile, cb) => { const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
try { try {
const email = profile.emails[0]?.value; const email = profile.emails[0]?.value;
const facebookId = profile.id; const facebookId = profile.id;
const oldUser = await User.findOne({ const oldUser = await User.findOne({ email });
email,
});
const ALLOW_SOCIAL_REGISTRATION = const ALLOW_SOCIAL_REGISTRATION =
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
const avatarUrl = profile.photos[0]?.value;
if (oldUser) { if (oldUser) {
oldUser.avatar = profile.photo; await handleExistingUser(oldUser, avatarUrl, useFirebase);
await oldUser.save();
return cb(null, oldUser); return cb(null, oldUser);
} else if (ALLOW_SOCIAL_REGISTRATION) { }
if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await createNewUser(profile, facebookId, email, avatarUrl, useFirebase);
return cb(null, newUser);
}
} catch (err) {
logger.error('[facebookLogin]', err);
return cb(err);
}
};
const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => {
if (!useFirebase && !oldUser.avatar.includes('?manual=true')) {
oldUser.avatar = avatarUrl;
await oldUser.save();
} else if (useFirebase && !oldUser.avatar.includes('?manual=true')) {
const userId = oldUser._id;
const newavatarUrl = await uploadAvatar(userId, avatarUrl);
oldUser.avatar = newavatarUrl;
await oldUser.save();
}
};
const createNewUser = async (profile, facebookId, email, avatarUrl, useFirebase) => {
const newUser = await new User({ const newUser = await new User({
provider: 'facebook', provider: 'facebook',
facebookId, facebookId,
username: profile.displayName, username: profile.displayName,
email, email,
name: profile.name?.givenName + ' ' + profile.name?.familyName, name: profile.name?.givenName + ' ' + profile.name?.familyName,
avatar: profile.photos[0]?.value, avatar: avatarUrl,
}).save(); }).save();
return cb(null, newUser); if (useFirebase) {
const userId = newUser._id;
const newavatarUrl = await uploadAvatar(userId, avatarUrl);
newUser.avatar = newavatarUrl;
await newUser.save();
} }
return cb(null, false, { return newUser;
message: 'User not found.',
});
} catch (err) {
logger.error('[facebookLogin]', err);
return cb(err);
}
}; };
module.exports = () => module.exports = () =>

View file

@ -1,6 +1,7 @@
const { Strategy: GitHubStrategy } = require('passport-github2'); const { Strategy: GitHubStrategy } = require('passport-github2');
const { logger } = require('~/config'); const { logger } = require('~/config');
const User = require('~/models/User'); const User = require('~/models/User');
const { useFirebase, uploadAvatar } = require('~/server/services/Files/images');
const githubLogin = async (accessToken, refreshToken, profile, cb) => { const githubLogin = async (accessToken, refreshToken, profile, cb) => {
try { try {
@ -9,12 +10,36 @@ const githubLogin = async (accessToken, refreshToken, profile, cb) => {
const oldUser = await User.findOne({ email }); const oldUser = await User.findOne({ email });
const ALLOW_SOCIAL_REGISTRATION = const ALLOW_SOCIAL_REGISTRATION =
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
const avatarUrl = profile.photos[0].value;
if (oldUser) { if (oldUser) {
oldUser.avatar = profile.photos[0].value; await handleExistingUser(oldUser, avatarUrl, useFirebase);
await oldUser.save();
return cb(null, oldUser); return cb(null, oldUser);
} else if (ALLOW_SOCIAL_REGISTRATION) { }
if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await createNewUser(profile, githubId, email, avatarUrl, useFirebase);
return cb(null, newUser);
}
} catch (err) {
logger.error('[githubLogin]', err);
return cb(err);
}
};
const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => {
if (!useFirebase && !oldUser.avatar.includes('?manual=true')) {
oldUser.avatar = avatarUrl;
await oldUser.save();
} else if (useFirebase && !oldUser.avatar.includes('?manual=true')) {
const userId = oldUser._id;
const avatarURL = await uploadAvatar(userId, avatarUrl);
oldUser.avatar = avatarURL;
await oldUser.save();
}
};
const createNewUser = async (profile, githubId, email, avatarUrl, useFirebase) => {
const newUser = await new User({ const newUser = await new User({
provider: 'github', provider: 'github',
githubId, githubId,
@ -22,17 +47,17 @@ const githubLogin = async (accessToken, refreshToken, profile, cb) => {
email, email,
emailVerified: profile.emails[0].verified, emailVerified: profile.emails[0].verified,
name: profile.displayName, name: profile.displayName,
avatar: profile.photos[0].value, avatar: avatarUrl,
}).save(); }).save();
return cb(null, newUser); if (useFirebase) {
const userId = newUser._id;
const avatarURL = await uploadAvatar(userId, avatarUrl);
newUser.avatar = avatarURL;
await newUser.save();
} }
return cb(null, false, { message: 'User not found.' }); return newUser;
} catch (err) {
logger.error('[githubLogin]', err);
return cb(err);
}
}; };
module.exports = () => module.exports = () =>

View file

@ -1,6 +1,7 @@
const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const { logger } = require('~/config'); const { logger } = require('~/config');
const User = require('~/models/User'); const User = require('~/models/User');
const { useFirebase, uploadAvatar } = require('~/server/services/Files/images');
const googleLogin = async (accessToken, refreshToken, profile, cb) => { const googleLogin = async (accessToken, refreshToken, profile, cb) => {
try { try {
@ -9,12 +10,36 @@ const googleLogin = async (accessToken, refreshToken, profile, cb) => {
const oldUser = await User.findOne({ email }); const oldUser = await User.findOne({ email });
const ALLOW_SOCIAL_REGISTRATION = const ALLOW_SOCIAL_REGISTRATION =
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
const avatarUrl = profile.photos[0].value;
if (oldUser) { if (oldUser) {
oldUser.avatar = profile.photos[0].value; await handleExistingUser(oldUser, avatarUrl, useFirebase);
await oldUser.save();
return cb(null, oldUser); return cb(null, oldUser);
} else if (ALLOW_SOCIAL_REGISTRATION) { }
if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await createNewUser(profile, googleId, email, avatarUrl, useFirebase);
return cb(null, newUser);
}
} catch (err) {
logger.error('[googleLogin]', err);
return cb(err);
}
};
const handleExistingUser = async (oldUser, avatarUrl, useFirebase) => {
if ((!useFirebase && !oldUser.avatar.includes('?manual=true')) || oldUser.avatar === null) {
oldUser.avatar = avatarUrl;
await oldUser.save();
} else if (useFirebase && !oldUser.avatar.includes('?manual=true')) {
const userId = oldUser._id;
const avatarURL = await uploadAvatar(userId, avatarUrl);
oldUser.avatar = avatarURL;
await oldUser.save();
}
};
const createNewUser = async (profile, googleId, email, avatarUrl, useFirebase) => {
const newUser = await new User({ const newUser = await new User({
provider: 'google', provider: 'google',
googleId, googleId,
@ -22,17 +47,17 @@ const googleLogin = async (accessToken, refreshToken, profile, cb) => {
email, email,
emailVerified: profile.emails[0].verified, emailVerified: profile.emails[0].verified,
name: `${profile.name.givenName} ${profile.name.familyName}`, name: `${profile.name.givenName} ${profile.name.familyName}`,
avatar: profile.photos[0].value, avatar: avatarUrl,
}).save(); }).save();
return cb(null, newUser); if (useFirebase) {
const userId = newUser._id;
const avatarURL = await uploadAvatar(userId, avatarUrl);
newUser.avatar = avatarURL;
await newUser.save();
} }
return cb(null, false, { message: 'User not found.' }); return newUser;
} catch (err) {
logger.error('[googleLogin]', err);
return cb(err);
}
}; };
module.exports = () => module.exports = () =>

View file

@ -34,7 +34,7 @@ const App = () => {
<RouterProvider router={router} /> <RouterProvider router={router} />
<ReactQueryDevtools initialIsOpen={false} position="top-right" /> <ReactQueryDevtools initialIsOpen={false} position="top-right" />
<Toast /> <Toast />
<RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[60] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" /> <RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[1000] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" />
</DndProvider> </DndProvider>
</AssistantsProvider> </AssistantsProvider>
</ToastProvider> </ToastProvider>

View file

@ -27,6 +27,7 @@ export type TShowToast = {
severity?: NotificationSeverity; severity?: NotificationSeverity;
showIcon?: boolean; showIcon?: boolean;
duration?: number; duration?: number;
status?: 'error' | 'success' | 'warning' | 'info';
}; };
export type TBaseSettingsProps = { export type TBaseSettingsProps = {

View file

@ -6,10 +6,10 @@ import { useChatHelpers, useSSE } from '~/hooks';
// import GenerationButtons from './Input/GenerationButtons'; // import GenerationButtons from './Input/GenerationButtons';
import MessagesView from './Messages/MessagesView'; import MessagesView from './Messages/MessagesView';
// import OptionsBar from './Input/OptionsBar'; // import OptionsBar from './Input/OptionsBar';
import { Spinner } from '~/components/svg';
import { ChatContext } from '~/Providers'; import { ChatContext } from '~/Providers';
import Presentation from './Presentation'; import Presentation from './Presentation';
import ChatForm from './Input/ChatForm'; import ChatForm from './Input/ChatForm';
import { Spinner } from '~/components';
import { buildTree } from '~/utils'; import { buildTree } from '~/utils';
import Landing from './Landing'; import Landing from './Landing';
import Header from './Header'; import Header from './Header';

View file

@ -11,7 +11,7 @@ import {
} from '~/hooks'; } from '~/hooks';
import { TooltipProvider, Tooltip } from '~/components/ui'; import { TooltipProvider, Tooltip } from '~/components/ui';
import { Conversations, Pages } from '../Conversations'; import { Conversations, Pages } from '../Conversations';
import { Spinner } from '~/components'; import { Spinner } from '~/components/svg';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import NavToggle from './NavToggle'; import NavToggle from './NavToggle';
import NavLinks from './NavLinks'; import NavLinks from './NavLinks';

View file

@ -119,7 +119,7 @@ function NavLinks() {
<Menu.Item as="div"> <Menu.Item as="div">
<NavLink <NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700" className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <GearIcon />} svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')} text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)} clickHandler={() => setShowSettings(true)}
/> />

View file

@ -1,9 +1,9 @@
import * as Tabs from '@radix-ui/react-tabs'; import * as Tabs from '@radix-ui/react-tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { GearIcon, DataIcon } from '~/components/svg'; import { GearIcon, DataIcon, UserIcon } from '~/components/svg';
import { useMediaQuery, useLocalize } from '~/hooks'; import { useMediaQuery, useLocalize } from '~/hooks';
import type { TDialogProps } from '~/common'; import type { TDialogProps } from '~/common';
import { General, Data } from './SettingsTabs'; import { General, Data, Account } from './SettingsTabs';
import { cn } from '~/utils'; import { cn } from '~/utils';
export default function Settings({ open, onOpenChange }: TDialogProps) { export default function Settings({ open, onOpenChange }: TDialogProps) {
@ -39,7 +39,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
> >
<Tabs.Trigger <Tabs.Trigger
className={cn( className={cn(
'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-gray-500 radix-state-active:bg-gray-800 radix-state-active:text-white', 'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-800',
isSmallScreen isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white' ? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '', : '',
@ -51,7 +51,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger <Tabs.Trigger
className={cn( className={cn(
'group flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-gray-500 radix-state-active:bg-gray-800 radix-state-active:text-white', 'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-800',
isSmallScreen isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white' ? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '', : '',
@ -61,9 +61,22 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<DataIcon /> <DataIcon />
{localize('com_nav_setting_data')} {localize('com_nav_setting_data')}
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-800',
isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '',
)}
value="account"
>
<UserIcon />
{localize('com_nav_setting_account')}
</Tabs.Trigger>
</Tabs.List> </Tabs.List>
<General /> <General />
<Data /> <Data />
<Account />
</Tabs.Root> </Tabs.Root>
</div> </div>
</DialogContent> </DialogContent>

View file

@ -0,0 +1,18 @@
import * as Tabs from '@radix-ui/react-tabs';
import Avatar from './Avatar';
import React from 'react';
function Account() {
return (
<Tabs.Content value="account" role="tabpanel" className="w-full md:min-h-[300px]">
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<Avatar />
</div>
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"></div>
</Tabs.Content>
);
}
export default React.memo(Account);

View file

@ -0,0 +1,145 @@
import { FileImage } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import { useState, useEffect } from 'react';
import type { TUser } from 'librechat-data-provider';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { useUploadAvatarMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
import store from '~/store';
const sizeLimit = 2 * 1024 * 1024; // 2MB
function Avatar() {
const setUser = useSetRecoilState(store.user);
const [input, setinput] = useState<File | null>(null);
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const localize = useLocalize();
const { showToast } = useToastContext();
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
onSuccess: (data) => {
showToast({ message: localize('com_ui_upload_success') });
setDialogOpen(false);
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
},
onError: (error) => {
console.error('Error:', error);
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
},
});
useEffect(() => {
if (input) {
const reader = new FileReader();
reader.onloadend = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(input);
} else {
setPreviewUrl(null);
}
}, [input]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
if (file && file.size <= sizeLimit) {
setinput(file);
setDialogOpen(true);
} else {
showToast({
message: localize('com_ui_upload_invalid'),
status: 'error',
});
}
};
const handleUpload = () => {
if (!input) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('input', input, input.name);
formData.append('manual', 'true');
uploadAvatar(formData);
};
return (
<>
<div className="flex items-center justify-between">
<span>{localize('com_nav_profile_picture')}</span>
<label
htmlFor={'file-upload-avatar'}
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal transition-colors hover:bg-slate-200 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-800 dark:hover:text-green-500"
>
<FileImage className="mr-1 flex w-[22px] items-center stroke-1" />
<span>{localize('com_nav_change_picture')}</span>
<input
id={'file-upload-avatar'}
value=""
type="file"
className={cn('hidden')}
accept=".png, .jpg"
onChange={handleFileChange}
/>
</label>
</div>
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
<DialogContent
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:h-[350px] md:w-[450px] ')}
style={{ borderRadius: '12px' }}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_ui_preview')}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center justify-center">
{previewUrl && (
<img
src={previewUrl}
alt="Preview"
className="mb-2 rounded-full"
style={{
maxWidth: '100%',
maxHeight: '150px',
width: '150px',
height: '150px',
objectFit: 'cover',
}}
/>
)}
<button
className={cn(
'mt-4 rounded px-4 py-2 text-white hover:bg-green-600 hover:text-gray-200',
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
)}
onClick={handleUpload}
disabled={isUploading}
>
{isUploading ? (
<div className="flex h-6">
<Spinner className="icon-sm m-auto" />
</div>
) : (
localize('com_ui_upload')
)}
</button>
</div>
</DialogContent>
</Dialog>
</>
);
}
export default Avatar;

View file

@ -5,7 +5,7 @@ import {
} from 'librechat-data-provider/react-query'; } from 'librechat-data-provider/react-query';
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import { useOnClickOutside } from '~/hooks'; import { useOnClickOutside } from '~/hooks';
import DangerButton from './DangerButton'; import DangerButton from '../DangerButton';
export const RevokeKeysButton = ({ export const RevokeKeysButton = ({
showText = true, showText = true,

View file

@ -12,7 +12,7 @@ import {
} from '~/hooks'; } from '~/hooks';
import type { TDangerButtonProps } from '~/common'; import type { TDangerButtonProps } from '~/common';
import AutoScrollSwitch from './AutoScrollSwitch'; import AutoScrollSwitch from './AutoScrollSwitch';
import DangerButton from './DangerButton'; import DangerButton from '../DangerButton';
import store from '~/store'; import store from '~/store';
import { Dropdown } from '~/components/ui'; import { Dropdown } from '~/components/ui';

View file

@ -1,4 +1,5 @@
export { default as General } from './General'; export { default as General } from './General/General';
export { ClearChatsButton } from './General'; export { ClearChatsButton } from './General/General';
export { default as Data } from './Data'; export { default as Data } from './Data/Data';
export { RevokeKeysButton } from './Data'; export { RevokeKeysButton } from './Data/Data';
export { default as Account } from './Account/Account';

View file

@ -1,14 +1,18 @@
import React from 'react'; import React from 'react';
export default function GearIcon() { interface GearIconProps {
className?: string;
}
const GearIcon: React.FC<GearIconProps> = ({ className = '' }) => {
return ( return (
<svg <svg
width="18" className={className}
height="18" width="17"
height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="icon-md"
> >
<path <path
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z" d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
@ -19,4 +23,6 @@ export default function GearIcon() {
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2"></circle> <circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2"></circle>
</svg> </svg>
); );
} };
export default GearIcon;

View file

@ -1,20 +1,17 @@
import React from 'react';
export default function UserIcon() { export default function UserIcon() {
return ( return (
<svg <svg
stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
fill="none" width="18"
strokeWidth="2" height="18"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
> >
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" /> <circle cx="12" cy="7" r="4" />
</svg> </svg>
); );

View file

@ -40,3 +40,4 @@ export { default as GeminiIcon } from './GeminiIcon';
export { default as GoogleMinimalIcon } from './GoogleMinimalIcon'; export { default as GoogleMinimalIcon } from './GoogleMinimalIcon';
export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon'; export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon';
export { default as SendMessageIcon } from './SendMessageIcon'; export { default as SendMessageIcon } from './SendMessageIcon';
export { default as UserIcon } from './UserIcon';

View file

@ -12,6 +12,8 @@ import type {
PresetDeleteResponse, PresetDeleteResponse,
LogoutOptions, LogoutOptions,
TPreset, TPreset,
UploadAvatarOptions,
AvatarUploadResponse,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { dataService, MutationKeys } from 'librechat-data-provider'; import { dataService, MutationKeys } from 'librechat-data-provider';
@ -99,3 +101,18 @@ export const useLogoutUserMutation = (
}, },
}); });
}; };
/* Avatar upload */
export const useUploadAvatarMutation = (
options?: UploadAvatarOptions,
): UseMutationResult<
AvatarUploadResponse, // response data
unknown, // error
FormData, // request
unknown // context
> => {
return useMutation([MutationKeys.avatarUpload], {
mutationFn: (variables: FormData) => dataService.uploadAvatar(variables),
...(options || {}),
});
};

View file

@ -7,6 +7,7 @@ import {
createContext, createContext,
useContext, useContext,
} from 'react'; } from 'react';
import { useRecoilState } from 'recoil';
import { TUser, TLoginResponse, setTokenHeader, TLoginUser } from 'librechat-data-provider'; import { TUser, TLoginResponse, setTokenHeader, TLoginUser } from 'librechat-data-provider';
import { import {
useGetUserQuery, useGetUserQuery,
@ -17,6 +18,7 @@ import { useNavigate } from 'react-router-dom';
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
import { useLogoutUserMutation } from '~/data-provider'; import { useLogoutUserMutation } from '~/data-provider';
import useTimeout from './useTimeout'; import useTimeout from './useTimeout';
import store from '~/store';
const AuthContext = createContext<TAuthContext | undefined>(undefined); const AuthContext = createContext<TAuthContext | undefined>(undefined);
@ -27,11 +29,13 @@ const AuthContextProvider = ({
authConfig?: TAuthConfig; authConfig?: TAuthConfig;
children: ReactNode; children: ReactNode;
}) => { }) => {
const navigate = useNavigate(); const [user, setUser] = useRecoilState(store.user);
const [user, setUser] = useState<TUser | undefined>(undefined);
const [token, setToken] = useState<string | undefined>(undefined); const [token, setToken] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false); const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const navigate = useNavigate();
const setUserContext = useCallback( const setUserContext = useCallback(
(userContext: TUserContext) => { (userContext: TUserContext) => {
const { token, isAuthenticated, user, redirect } = userContext; const { token, isAuthenticated, user, redirect } = userContext;
@ -46,7 +50,7 @@ const AuthContextProvider = ({
navigate(redirect, { replace: true }); navigate(redirect, { replace: true });
} }
}, },
[navigate], [navigate, setUser],
); );
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });

View file

@ -25,6 +25,7 @@ export default function useToast(showDelay = 100) {
severity = NotificationSeverity.SUCCESS, severity = NotificationSeverity.SUCCESS,
showIcon = true, showIcon = true,
duration = 3000, // default duration for the toast to be visible duration = 3000, // default duration for the toast to be visible
status,
}: TShowToast) => { }: TShowToast) => {
// Clear existing timeouts // Clear existing timeouts
if (showTimerRef.current !== null) { if (showTimerRef.current !== null) {
@ -36,7 +37,12 @@ export default function useToast(showDelay = 100) {
// Timeout to show the toast // Timeout to show the toast
showTimerRef.current = window.setTimeout(() => { showTimerRef.current = window.setTimeout(() => {
setToast({ open: true, message, severity, showIcon }); setToast({
open: true,
message,
severity: (status as NotificationSeverity) ?? severity,
showIcon,
});
// Hides the toast after the specified duration // Hides the toast after the specified duration
hideTimerRef.current = window.setTimeout(() => { hideTimerRef.current = window.setTimeout(() => {
setToast((prevToast) => ({ ...prevToast, open: false })); setToast((prevToast) => ({ ...prevToast, open: false }));

View file

@ -33,7 +33,8 @@ export default {
com_ui_enter: 'Enter', com_ui_enter: 'Enter',
com_ui_submit: 'Submit', com_ui_submit: 'Submit',
com_ui_upload_success: 'Successfully uploaded file', com_ui_upload_success: 'Successfully uploaded file',
com_ui_upload_invalid: 'Invalid file for upload', com_ui_upload_error: 'There was an error uploading your file',
com_ui_upload_invalid: 'Invalid file for upload. Must be an image not exceeding 2 MB',
com_ui_cancel: 'Cancel', com_ui_cancel: 'Cancel',
com_ui_save: 'Save', com_ui_save: 'Save',
com_ui_copy_to_clipboard: 'Copy to clipboard', com_ui_copy_to_clipboard: 'Copy to clipboard',
@ -51,6 +52,9 @@ export default {
com_ui_delete: 'Delete', com_ui_delete: 'Delete',
com_ui_delete_conversation: 'Delete chat?', com_ui_delete_conversation: 'Delete chat?',
com_ui_delete_conversation_confirm: 'This will delete', com_ui_delete_conversation_confirm: 'This will delete',
com_ui_preview: 'Preview',
com_ui_upload: 'Upload',
com_ui_connect: 'Connect',
com_auth_error_login: com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.', 'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl: com_auth_error_login_rl:
@ -253,6 +257,8 @@ export default {
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_welcome_message: 'How can I help you today?', com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-scroll to Newest on Open', com_nav_auto_scroll: 'Auto-scroll to Newest on Open',
com_nav_profile_picture: 'Profile Picture',
com_nav_change_picture: 'Change picture',
com_nav_plugin_store: 'Plugin store', com_nav_plugin_store: 'Plugin store',
com_nav_plugin_search: 'Search plugins', com_nav_plugin_search: 'Search plugins',
com_nav_plugin_auth_error: com_nav_plugin_auth_error:
@ -286,6 +292,7 @@ export default {
com_nav_search_placeholder: 'Search messages', com_nav_search_placeholder: 'Search messages',
com_nav_setting_general: 'General', com_nav_setting_general: 'General',
com_nav_setting_data: 'Data controls', com_nav_setting_data: 'Data controls',
com_nav_setting_account: 'Account',
com_nav_language: 'Language', com_nav_language: 'Language',
com_nav_lang_auto: 'Auto detect', com_nav_lang_auto: 'Auto detect',
com_nav_lang_english: 'English', com_nav_lang_english: 'English',

View file

@ -53,6 +53,9 @@ export default {
com_ui_delete: 'Elimina', com_ui_delete: 'Elimina',
com_ui_delete_conversation: 'Eliminare la chat?', com_ui_delete_conversation: 'Eliminare la chat?',
com_ui_delete_conversation_confirm: 'Questo eliminerà', com_ui_delete_conversation_confirm: 'Questo eliminerà',
com_ui_preview: 'Anteprima',
com_ui_upload: 'Carica',
com_ui_connect: 'Connetti',
com_auth_error_login: com_auth_error_login:
'Impossibile accedere con le informazioni fornite. Per favore controlla le tue credenziali e riprova.', 'Impossibile accedere con le informazioni fornite. Per favore controlla le tue credenziali e riprova.',
com_auth_error_login_rl: com_auth_error_login_rl:
@ -263,7 +266,9 @@ export default {
'Assicurati di fare clic su "Crea e continua" per dare almeno il ruolo "Vertex AI User". Infine, crea una chiave JSON da importare qui.', 'Assicurati di fare clic su "Crea e continua" per dare almeno il ruolo "Vertex AI User". Infine, crea una chiave JSON da importare qui.',
com_nav_welcome_message: 'Come posso aiutarti oggi?', com_nav_welcome_message: 'Come posso aiutarti oggi?',
com_nav_auto_scroll: 'Scorri automaticamente al Più recente all\'apertura', com_nav_auto_scroll: 'Scorri automaticamente al Più recente all\'apertura',
com_nav_plugin_store: 'Negozio plugin', com_nav_profile_picture: 'Immagine del profilo',
com_nav_change_picture: 'Cambia immagine',
com_nav_plugin_store: 'Negozio dei plugin',
com_nav_plugin_search: 'Cerca plugin', com_nav_plugin_search: 'Cerca plugin',
com_nav_plugin_auth_error: com_nav_plugin_auth_error:
'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore riprova.', 'Si è verificato un errore durante il tentativo di autenticare questo plugin. Per favore riprova.',

View file

@ -1,9 +1,9 @@
import { atom } from 'recoil'; import { atom } from 'recoil';
import { TPlugin } from 'librechat-data-provider'; import type { TUser, TPlugin } from 'librechat-data-provider';
const user = atom({ const user = atom<TUser | undefined>({
key: 'user', key: 'user',
default: null, default: undefined,
}); });
const availableTools = atom<TPlugin[]>({ const availableTools = atom<TPlugin[]>({

View file

@ -1,7 +1,7 @@
--- ---
title: 😈 Bing Jailbreak title: 😈 Bing Jailbreak
description: Quick overview of the Bing jailbreak and Sydney's system message description: Quick overview of the Bing jailbreak and Sydney's system message
weight: -3 weight: -2
--- ---
# Bing Jailbreak # Bing Jailbreak

74
docs/features/firebase.md Normal file
View file

@ -0,0 +1,74 @@
---
title: 🔥 Firebase CDN Setup
description: This document provides instructions for setting up Firebase CDN for LibreChat
weight: -6
---
# Firebase CDN Setup
## Steps to Set Up Firebase
1. Open the [Firebase website](https://firebase.google.com/).
2. Click on "Get started."
3. Sign in with your Google account.
### Create a New Project
- Name your project (you can use the same project as Google OAuth).
![Project Name](https://github.com/danny-avila/LibreChat/assets/81851188/dccce3e0-b639-41ef-8142-19d24911c65c)
- Optionally, you can disable Google Analytics.
![Google Analytics](https://github.com/danny-avila/LibreChat/assets/81851188/5d4d58c5-451c-498b-97c0-f123fda79514)
- Wait for 20/30 seconds for the project to be ready, then click on "Continue."
![Continue](https://github.com/danny-avila/LibreChat/assets/81851188/6929802e-a30b-4b1e-b124-1d4b281d0403)
- Click on "All Products."
![All Products](https://github.com/danny-avila/LibreChat/assets/81851188/92866c82-2b03-4ebe-807e-73a0ccce695e)
- Select "Storage."
![Storage](https://github.com/danny-avila/LibreChat/assets/81851188/b22dcda1-256b-494b-a835-a05aeea02e89)
- Click on "Get Started."
![Get Started](https://github.com/danny-avila/LibreChat/assets/81851188/c3f0550f-8184-4c79-bb84-fa79655b7978)
- Click on "Next."
![Next](https://github.com/danny-avila/LibreChat/assets/81851188/2a65632d-fe22-4c71-b8f1-aac53ee74fb6)
- Select your "Cloud Storage location."
![Cloud Storage Location](https://github.com/danny-avila/LibreChat/assets/81851188/c094d4bc-8e5b-43c7-96d9-a05bcf4e2af6)
- Return to the Project Overview.
![Project Overview](https://github.com/danny-avila/LibreChat/assets/81851188/c425f4bb-a494-42f2-9fdc-ff2c8ce005e1)
- Click on "+ Add app" under your project name, then click on "Web."
![Web](https://github.com/danny-avila/LibreChat/assets/81851188/22dab877-93cb-4828-9436-10e14374e57e)
- Register the app.
![Register App](https://github.com/danny-avila/LibreChat/assets/81851188/0a1b0a75-7285-4f03-95cf-bf971bd7d874)
- Save all this information in a text file.
![Save Information](https://github.com/danny-avila/LibreChat/assets/81851188/056754ad-9d36-4662-888e-f189ddb38fd3)
- Fill all the `firebaseConfig` variables in the `.env` file.
```bash
FIREBASE_API_KEY=api_key #apiKey
FIREBASE_AUTH_DOMAIN=auth_domain #authDomain
FIREBASE_PROJECT_ID=project_id #projectId
FIREBASE_STORAGE_BUCKET=storage_bucket #storageBucket
FIREBASE_MESSAGING_SENDER_ID=messaging_sender_id #messagingSenderId
FIREBASE_APP_ID=1:your_app_id #appId
```

View file

@ -22,6 +22,7 @@ weight: 2
* 🔨 [Automated Moderation](./mod_system.md) * 🔨 [Automated Moderation](./mod_system.md)
* 🪙 [Token Usage](./token_usage.md) * 🪙 [Token Usage](./token_usage.md)
* 🔥 [Firebase CDN](./firebase.md)
* 🍃 [Manage Your Database](./manage_your_database.md) * 🍃 [Manage Your Database](./manage_your_database.md)
* 🪵 [Logging System](./logging_system.md) * 🪵 [Logging System](./logging_system.md)
* 📦 [PandoraNext](./pandoranext.md) * 📦 [PandoraNext](./pandoranext.md)

View file

@ -1,7 +1,7 @@
--- ---
title: 🪵 Logging System title: 🪵 Logging System
weight: -4
description: This doc explains how to use the logging feature of LibreChat, which saves error and debug logs in the `/api/logs` folder. You can use these logs to troubleshoot issues, monitor your server, and report bugs. You can also disable debug logs if you want to save space. description: This doc explains how to use the logging feature of LibreChat, which saves error and debug logs in the `/api/logs` folder. You can use these logs to troubleshoot issues, monitor your server, and report bugs. You can also disable debug logs if you want to save space.
weight: -5
--- ---
### General ### General

View file

@ -1,7 +1,7 @@
--- ---
title: 🍃 Manage Your Database title: 🍃 Manage Your Database
description: How to install and configure Mongo Express to securely access and manage your MongoDB database in Docker. description: How to install and configure Mongo Express to securely access and manage your MongoDB database in Docker.
weight: -6 weight: -5
--- ---
<img src="https://github.com/danny-avila/LibreChat/assets/32828263/4572dd35-8489-4cb1-a968-4fb5a871d6e5" height="50"> <img src="https://github.com/danny-avila/LibreChat/assets/32828263/4572dd35-8489-4cb1-a968-4fb5a871d6e5" height="50">

View file

@ -1,7 +1,7 @@
--- ---
title: 📦 PandoraNext title: 📦 PandoraNext
description: How to deploy PandoraNext to enable the `CHATGPT_REVERSE_PROXY` for use with LibreChat. description: How to deploy PandoraNext to enable the `CHATGPT_REVERSE_PROXY` for use with LibreChat.
weight: -4 weight: -3
--- ---
# PandoraNext Deployment Guide # PandoraNext Deployment Guide

902
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -69,3 +69,5 @@ export const assistants = (id?: string) => `/api/assistants${id ? `/${id}` : ''}
export const files = () => '/api/files'; export const files = () => '/api/files';
export const images = () => `${files()}/images`; export const images = () => `${files()}/images`;
export const avatar = () => `${images()}/avatar`;

View file

@ -197,6 +197,10 @@ export const uploadImage = (data: FormData): Promise<f.FileUploadResponse> => {
return request.postMultiPart(endpoints.images(), data); return request.postMultiPart(endpoints.images(), data);
}; };
export const uploadAvatar = (data: FormData): Promise<f.AvatarUploadResponse> => {
return request.postMultiPart(endpoints.avatar(), data);
};
export const deleteFiles = async (files: f.BatchFile[]): Promise<f.DeleteFilesResponse> => export const deleteFiles = async (files: f.BatchFile[]): Promise<f.DeleteFilesResponse> =>
request.deleteWithOptions(endpoints.files(), { request.deleteWithOptions(endpoints.files(), {
data: { files }, data: { files },

View file

@ -24,4 +24,5 @@ export enum MutationKeys {
updatePreset = 'updatePreset', updatePreset = 'updatePreset',
deletePreset = 'deletePreset', deletePreset = 'deletePreset',
logoutUser = 'logoutUser', logoutUser = 'logoutUser',
avatarUpload = 'avatarUpload',
} }

View file

@ -10,6 +10,10 @@ export type FileUploadResponse = {
width: number; width: number;
}; };
export type AvatarUploadResponse = {
url: string;
};
export type FileUploadBody = { export type FileUploadBody = {
formData: FormData; formData: FormData;
file_id: string; file_id: string;
@ -21,6 +25,12 @@ export type UploadMutationOptions = {
onError?: (error: unknown, variables: FileUploadBody, context?: unknown) => void; onError?: (error: unknown, variables: FileUploadBody, context?: unknown) => void;
}; };
export type UploadAvatarOptions = {
onSuccess?: (data: AvatarUploadResponse, variables: FormData, context?: unknown) => void;
onMutate?: (variables: FormData) => void | Promise<unknown>;
onError?: (error: unknown, variables: FormData, context?: unknown) => void;
};
export type DeleteFilesResponse = { export type DeleteFilesResponse = {
message: string; message: string;
result: Record<string, unknown>; result: Record<string, unknown>;