mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
📂 feat: RAG Improvements (#2169)
* feat: new vector file processing strategy * chore: remove unused client files * chore: remove more unused client files * chore: remove more unused client files and move used to new dir * chore(DataIcon): add className * WIP: Model Endpoint Settings Update, draft additional context settings * feat: improve parsing for augmented prompt, add full context option * chore: remove volume mounting from rag.yml as no longer necessary
This commit is contained in:
parent
f427ad792a
commit
45a95acec2
40 changed files with 715 additions and 2046 deletions
|
@ -1,4 +1,15 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
|
const footer = `Use the context as your learned knowledge to better answer the user.
|
||||||
|
|
||||||
|
In your response, remember to follow these guidelines:
|
||||||
|
- If you don't know the answer, simply say that you don't know.
|
||||||
|
- If you are unsure how to answer, ask for clarification.
|
||||||
|
- Avoid mentioning that you obtained the information from the context.
|
||||||
|
|
||||||
|
Answer appropriately in the user's language.
|
||||||
|
`;
|
||||||
|
|
||||||
function createContextHandlers(req, userMessageContent) {
|
function createContextHandlers(req, userMessageContent) {
|
||||||
if (!process.env.RAG_API_URL) {
|
if (!process.env.RAG_API_URL) {
|
||||||
|
@ -9,25 +20,37 @@ function createContextHandlers(req, userMessageContent) {
|
||||||
const processedFiles = [];
|
const processedFiles = [];
|
||||||
const processedIds = new Set();
|
const processedIds = new Set();
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
const jwtToken = req.headers.authorization.split(' ')[1];
|
||||||
|
const useFullContext = isEnabled(process.env.RAG_USE_FULL_CONTEXT);
|
||||||
|
|
||||||
|
const query = async (file) => {
|
||||||
|
if (useFullContext) {
|
||||||
|
return axios.get(`${process.env.RAG_API_URL}/documents/${file.file_id}/context`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(
|
||||||
|
`${process.env.RAG_API_URL}/query`,
|
||||||
|
{
|
||||||
|
file_id: file.file_id,
|
||||||
|
query: userMessageContent,
|
||||||
|
k: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const processFile = async (file) => {
|
const processFile = async (file) => {
|
||||||
if (file.embedded && !processedIds.has(file.file_id)) {
|
if (file.embedded && !processedIds.has(file.file_id)) {
|
||||||
try {
|
try {
|
||||||
const promise = axios.post(
|
const promise = query(file);
|
||||||
`${process.env.RAG_API_URL}/query`,
|
|
||||||
{
|
|
||||||
file_id: file.file_id,
|
|
||||||
query: userMessageContent,
|
|
||||||
k: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
queryPromises.push(promise);
|
queryPromises.push(promise);
|
||||||
processedFiles.push(file);
|
processedFiles.push(file);
|
||||||
processedIds.add(file.file_id);
|
processedIds.add(file.file_id);
|
||||||
|
@ -43,67 +66,83 @@ function createContextHandlers(req, userMessageContent) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oneFile = processedFiles.length === 1;
|
||||||
|
const header = `The user has attached ${oneFile ? 'a' : processedFiles.length} file${
|
||||||
|
!oneFile ? 's' : ''
|
||||||
|
} to the conversation:`;
|
||||||
|
|
||||||
|
const files = `${
|
||||||
|
oneFile
|
||||||
|
? ''
|
||||||
|
: `
|
||||||
|
<files>`
|
||||||
|
}${processedFiles
|
||||||
|
.map(
|
||||||
|
(file) => `
|
||||||
|
<file>
|
||||||
|
<filename>${file.filename}</filename>
|
||||||
|
<type>${file.type}</type>
|
||||||
|
</file>`,
|
||||||
|
)
|
||||||
|
.join('')}${
|
||||||
|
oneFile
|
||||||
|
? ''
|
||||||
|
: `
|
||||||
|
</files>`
|
||||||
|
}`;
|
||||||
|
|
||||||
const resolvedQueries = await Promise.all(queryPromises);
|
const resolvedQueries = await Promise.all(queryPromises);
|
||||||
|
|
||||||
const context = resolvedQueries
|
const context = resolvedQueries
|
||||||
.map((queryResult, index) => {
|
.map((queryResult, index) => {
|
||||||
const file = processedFiles[index];
|
const file = processedFiles[index];
|
||||||
const contextItems = queryResult.data
|
let contextItems = queryResult.data;
|
||||||
|
|
||||||
|
const generateContext = (currentContext) =>
|
||||||
|
`
|
||||||
|
<file>
|
||||||
|
<filename>${file.filename}</filename>
|
||||||
|
<context>${currentContext}
|
||||||
|
</context>
|
||||||
|
</file>`;
|
||||||
|
|
||||||
|
if (useFullContext) {
|
||||||
|
return generateContext(`\n${contextItems}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
contextItems = queryResult.data
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const pageContent = item[0].page_content;
|
const pageContent = item[0].page_content;
|
||||||
return `
|
return `
|
||||||
<contextItem>
|
<contextItem>
|
||||||
<![CDATA[${pageContent}]]>
|
<![CDATA[${pageContent?.trim()}]]>
|
||||||
</contextItem>
|
</contextItem>`;
|
||||||
`;
|
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
return `
|
return generateContext(contextItems);
|
||||||
<file>
|
|
||||||
<filename>${file.filename}</filename>
|
|
||||||
<context>
|
|
||||||
${contextItems}
|
|
||||||
</context>
|
|
||||||
</file>
|
|
||||||
`;
|
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
const template = `The user has attached ${
|
if (useFullContext) {
|
||||||
processedFiles.length === 1 ? 'a' : processedFiles.length
|
const prompt = `${header}
|
||||||
} file${processedFiles.length !== 1 ? 's' : ''} to the conversation:
|
${context}
|
||||||
|
${footer}`;
|
||||||
|
|
||||||
<files>
|
return prompt;
|
||||||
${processedFiles
|
}
|
||||||
.map(
|
|
||||||
(file) => `
|
const prompt = `${header}
|
||||||
<file>
|
${files}
|
||||||
<filename>${file.filename}</filename>
|
|
||||||
<type>${file.type}</type>
|
|
||||||
</file>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</files>
|
|
||||||
|
|
||||||
A semantic search was executed with the user's message as the query, retrieving the following context inside <context></context> XML tags.
|
A semantic search was executed with the user's message as the query, retrieving the following context inside <context></context> XML tags.
|
||||||
|
|
||||||
<context>
|
<context>${context}
|
||||||
${context}
|
|
||||||
</context>
|
</context>
|
||||||
|
|
||||||
Use the context as your learned knowledge to better answer the user.
|
${footer}`;
|
||||||
|
|
||||||
In your response, remember to follow these guidelines:
|
return prompt;
|
||||||
- If you don't know the answer, simply say that you don't know.
|
|
||||||
- If you are unsure how to answer, ask for clarification.
|
|
||||||
- Avoid mentioning that you obtained the information from the context.
|
|
||||||
|
|
||||||
Answer appropriately in the user's language.
|
|
||||||
`;
|
|
||||||
|
|
||||||
return template;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating context:', error);
|
console.error('Error creating context:', error);
|
||||||
throw error; // Re-throw the error to propagate it to the caller
|
throw error; // Re-throw the error to propagate it to the caller
|
||||||
|
|
96
api/server/services/Files/VectorDB/crud.js
Normal file
96
api/server/services/Files/VectorDB/crud.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const { FileSources } = require('librechat-data-provider');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
||||||
|
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
||||||
|
*
|
||||||
|
* @param {Express.Request} req - The request object from Express. It should have an `app.locals.paths` object with
|
||||||
|
* a `publicPath` property.
|
||||||
|
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
||||||
|
* a string representing the path of the file relative to the publicPath.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* A promise that resolves when the file has been successfully deleted, or throws an error if the
|
||||||
|
* file path is invalid or if there is an error in deletion.
|
||||||
|
*/
|
||||||
|
const deleteVectors = async (req, file) => {
|
||||||
|
if (file.embedded && process.env.RAG_API_URL) {
|
||||||
|
const jwtToken = req.headers.authorization.split(' ')[1];
|
||||||
|
axios.delete(`${process.env.RAG_API_URL}/documents`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
accept: 'application/json',
|
||||||
|
},
|
||||||
|
data: [file.file_id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to the configured Vector database
|
||||||
|
*
|
||||||
|
* @param {Object} params - The params object.
|
||||||
|
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id`
|
||||||
|
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
||||||
|
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||||
|
* have a `path` property that points to the location of the uploaded file.
|
||||||
|
* @param {string} params.file_id - The file ID.
|
||||||
|
*
|
||||||
|
* @returns {Promise<{ filepath: string, bytes: number }>}
|
||||||
|
* A promise that resolves to an object containing:
|
||||||
|
* - filepath: The path where the file is saved.
|
||||||
|
* - bytes: The size of the file in bytes.
|
||||||
|
*/
|
||||||
|
async function uploadVectors({ req, file, file_id }) {
|
||||||
|
if (!process.env.RAG_API_URL) {
|
||||||
|
throw new Error('RAG_API_URL not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jwtToken = req.headers.authorization.split(' ')[1];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file_id', file_id);
|
||||||
|
formData.append('file', fs.createReadStream(file.path));
|
||||||
|
|
||||||
|
const formHeaders = formData.getHeaders(); // Automatically sets the correct Content-Type
|
||||||
|
|
||||||
|
const response = await axios.post(`${process.env.RAG_API_URL}/embed`, formData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
accept: 'application/json',
|
||||||
|
...formHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = response.data;
|
||||||
|
logger.debug('Response from embedding file', responseData);
|
||||||
|
|
||||||
|
if (responseData.known_type === false) {
|
||||||
|
throw new Error(`File embedding failed. The filetype ${file.mimetype} is not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseData.status) {
|
||||||
|
throw new Error('File embedding failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bytes: file.size,
|
||||||
|
filename: file.originalname,
|
||||||
|
filepath: FileSources.vectordb,
|
||||||
|
embedded: Boolean(responseData.known_type),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error embedding file', error);
|
||||||
|
throw new Error(error.message || 'An error occurred during file upload.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
deleteVectors,
|
||||||
|
uploadVectors,
|
||||||
|
};
|
5
api/server/services/Files/VectorDB/index.js
Normal file
5
api/server/services/Files/VectorDB/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const crud = require('./crud');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...crud,
|
||||||
|
};
|
|
@ -1,6 +1,5 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const axios = require('axios');
|
|
||||||
const mime = require('mime/lite');
|
const mime = require('mime/lite');
|
||||||
const {
|
const {
|
||||||
isUUID,
|
isUUID,
|
||||||
|
@ -265,50 +264,22 @@ const uploadImageBuffer = async ({ req, context }) => {
|
||||||
*/
|
*/
|
||||||
const processFileUpload = async ({ req, res, file, metadata }) => {
|
const processFileUpload = async ({ req, res, file, metadata }) => {
|
||||||
const isAssistantUpload = metadata.endpoint === EModelEndpoint.assistants;
|
const isAssistantUpload = metadata.endpoint === EModelEndpoint.assistants;
|
||||||
const source = isAssistantUpload ? FileSources.openai : req.app.locals.fileStrategy;
|
const source = isAssistantUpload ? FileSources.openai : FileSources.vectordb;
|
||||||
const { handleFileUpload } = getStrategyFunctions(source);
|
const { handleFileUpload } = getStrategyFunctions(source);
|
||||||
const { file_id, temp_file_id } = metadata;
|
const { file_id, temp_file_id } = metadata;
|
||||||
|
|
||||||
let embedded = false;
|
|
||||||
if (process.env.RAG_API_URL) {
|
|
||||||
try {
|
|
||||||
const jwtToken = req.headers.authorization.split(' ')[1];
|
|
||||||
const filepath = `./uploads/temp/${file.path.split('uploads/temp/')[1]}`;
|
|
||||||
const response = await axios.post(
|
|
||||||
`${process.env.RAG_API_URL}/embed`,
|
|
||||||
{
|
|
||||||
filename: file.originalname,
|
|
||||||
file_content_type: file.mimetype,
|
|
||||||
filepath,
|
|
||||||
file_id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${jwtToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
embedded = true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error embedding file', error);
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
} else if (!isAssistantUpload) {
|
|
||||||
logger.error('RAG_API_URL not set, cannot support process file upload');
|
|
||||||
throw new Error('RAG_API_URL not set, cannot support process file upload');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {OpenAI | undefined} */
|
/** @type {OpenAI | undefined} */
|
||||||
let openai;
|
let openai;
|
||||||
if (source === FileSources.openai) {
|
if (source === FileSources.openai) {
|
||||||
({ openai } = await initializeClient({ req }));
|
({ openai } = await initializeClient({ req }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, bytes, filename, filepath } = await handleFileUpload({ req, file, file_id, openai });
|
const { id, bytes, filename, filepath, embedded } = await handleFileUpload({
|
||||||
|
req,
|
||||||
|
file,
|
||||||
|
file_id,
|
||||||
|
openai,
|
||||||
|
});
|
||||||
|
|
||||||
if (isAssistantUpload && !metadata.message_file) {
|
if (isAssistantUpload && !metadata.message_file) {
|
||||||
await openai.beta.assistants.files.create(metadata.assistant_id, {
|
await openai.beta.assistants.files.create(metadata.assistant_id, {
|
||||||
|
|
|
@ -5,22 +5,20 @@ const {
|
||||||
saveURLToFirebase,
|
saveURLToFirebase,
|
||||||
deleteFirebaseFile,
|
deleteFirebaseFile,
|
||||||
saveBufferToFirebase,
|
saveBufferToFirebase,
|
||||||
uploadFileToFirebase,
|
|
||||||
uploadImageToFirebase,
|
uploadImageToFirebase,
|
||||||
processFirebaseAvatar,
|
processFirebaseAvatar,
|
||||||
} = require('./Firebase');
|
} = require('./Firebase');
|
||||||
const {
|
const {
|
||||||
// saveLocalFile,
|
|
||||||
getLocalFileURL,
|
getLocalFileURL,
|
||||||
saveFileFromURL,
|
saveFileFromURL,
|
||||||
saveLocalBuffer,
|
saveLocalBuffer,
|
||||||
deleteLocalFile,
|
deleteLocalFile,
|
||||||
uploadLocalFile,
|
|
||||||
uploadLocalImage,
|
uploadLocalImage,
|
||||||
prepareImagesLocal,
|
prepareImagesLocal,
|
||||||
processLocalAvatar,
|
processLocalAvatar,
|
||||||
} = require('./Local');
|
} = require('./Local');
|
||||||
const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI');
|
const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI');
|
||||||
|
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firebase Storage Strategy Functions
|
* Firebase Storage Strategy Functions
|
||||||
|
@ -28,13 +26,14 @@ const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI');
|
||||||
* */
|
* */
|
||||||
const firebaseStrategy = () => ({
|
const firebaseStrategy = () => ({
|
||||||
// saveFile:
|
// saveFile:
|
||||||
|
/** @type {typeof uploadVectors | null} */
|
||||||
|
handleFileUpload: null,
|
||||||
saveURL: saveURLToFirebase,
|
saveURL: saveURLToFirebase,
|
||||||
getFileURL: getFirebaseURL,
|
getFileURL: getFirebaseURL,
|
||||||
deleteFile: deleteFirebaseFile,
|
deleteFile: deleteFirebaseFile,
|
||||||
saveBuffer: saveBufferToFirebase,
|
saveBuffer: saveBufferToFirebase,
|
||||||
prepareImagePayload: prepareImageURL,
|
prepareImagePayload: prepareImageURL,
|
||||||
processAvatar: processFirebaseAvatar,
|
processAvatar: processFirebaseAvatar,
|
||||||
handleFileUpload: uploadFileToFirebase,
|
|
||||||
handleImageUpload: uploadImageToFirebase,
|
handleImageUpload: uploadImageToFirebase,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,17 +42,38 @@ const firebaseStrategy = () => ({
|
||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
const localStrategy = () => ({
|
const localStrategy = () => ({
|
||||||
// saveFile: saveLocalFile,
|
/** @type {typeof uploadVectors | null} */
|
||||||
|
handleFileUpload: null,
|
||||||
saveURL: saveFileFromURL,
|
saveURL: saveFileFromURL,
|
||||||
getFileURL: getLocalFileURL,
|
getFileURL: getLocalFileURL,
|
||||||
saveBuffer: saveLocalBuffer,
|
saveBuffer: saveLocalBuffer,
|
||||||
deleteFile: deleteLocalFile,
|
deleteFile: deleteLocalFile,
|
||||||
processAvatar: processLocalAvatar,
|
processAvatar: processLocalAvatar,
|
||||||
handleFileUpload: uploadLocalFile,
|
|
||||||
handleImageUpload: uploadLocalImage,
|
handleImageUpload: uploadLocalImage,
|
||||||
prepareImagePayload: prepareImagesLocal,
|
prepareImagePayload: prepareImagesLocal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VectorDB Storage Strategy Functions
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
const vectorStrategy = () => ({
|
||||||
|
/** @type {typeof saveFileFromURL | null} */
|
||||||
|
saveURL: null,
|
||||||
|
/** @type {typeof getLocalFileURL | null} */
|
||||||
|
getFileURL: null,
|
||||||
|
/** @type {typeof saveLocalBuffer | null} */
|
||||||
|
saveBuffer: null,
|
||||||
|
/** @type {typeof processLocalAvatar | null} */
|
||||||
|
processAvatar: null,
|
||||||
|
/** @type {typeof uploadLocalImage | null} */
|
||||||
|
handleImageUpload: null,
|
||||||
|
/** @type {typeof prepareImagesLocal | null} */
|
||||||
|
prepareImagePayload: null,
|
||||||
|
handleFileUpload: uploadVectors,
|
||||||
|
deleteFile: deleteVectors,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenAI Strategy Functions
|
* OpenAI Strategy Functions
|
||||||
*
|
*
|
||||||
|
@ -84,6 +104,8 @@ const getStrategyFunctions = (fileSource) => {
|
||||||
return localStrategy();
|
return localStrategy();
|
||||||
} else if (fileSource === FileSources.openai) {
|
} else if (fileSource === FileSources.openai) {
|
||||||
return openAIStrategy();
|
return openAIStrategy();
|
||||||
|
} else if (fileSource === FileSources.vectordb) {
|
||||||
|
return vectorStrategy();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid file source');
|
throw new Error('Invalid file source');
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Root, Anchor } from '@radix-ui/react-popover';
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
import { EndpointSettings, SaveAsPresetDialog } from '~/components/Endpoints';
|
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||||
import { ModelSelect } from '~/components/Input/ModelSelect';
|
import { ModelSelect } from '~/components/Input/ModelSelect';
|
||||||
import { PluginStoreDialog } from '~/components';
|
import { PluginStoreDialog } from '~/components';
|
||||||
import OptionsPopover from './OptionsPopover';
|
import OptionsPopover from './OptionsPopover';
|
||||||
|
@ -15,7 +15,7 @@ import { Button } from '~/components/ui';
|
||||||
import { cn, cardStyle } from '~/utils/';
|
import { cn, cardStyle } from '~/utils/';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function OptionsBar() {
|
export default function HeaderOptions() {
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
||||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
||||||
store.showPluginStoreDialog,
|
store.showPluginStoreDialog,
|
||||||
|
@ -102,6 +102,7 @@ export default function OptionsBar() {
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
isMultiChat={true}
|
isMultiChat={true}
|
||||||
/>
|
/>
|
||||||
|
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
||||||
</div>
|
</div>
|
||||||
</OptionsPopover>
|
</OptionsPopover>
|
||||||
<SaveAsPresetDialog
|
<SaveAsPresetDialog
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Settings2 } from 'lucide-react';
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
|
||||||
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
|
||||||
import { PluginStoreDialog } from '~/components';
|
|
||||||
import {
|
|
||||||
EndpointSettings,
|
|
||||||
SaveAsPresetDialog,
|
|
||||||
EndpointOptionsPopover,
|
|
||||||
} from '~/components/Endpoints';
|
|
||||||
import { ModelSelect } from '~/components/Input/ModelSelect';
|
|
||||||
import GenerationButtons from './GenerationButtons';
|
|
||||||
import PopoverButtons from './PopoverButtons';
|
|
||||||
import { useSetIndexOptions } from '~/hooks';
|
|
||||||
import { useChatContext } from '~/Providers';
|
|
||||||
import { Button } from '~/components/ui';
|
|
||||||
import { cn, cardStyle } from '~/utils/';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function OptionsBar({ messagesTree }) {
|
|
||||||
const [opacityClass, setOpacityClass] = useState('full-opacity');
|
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
|
||||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
|
||||||
store.showPluginStoreDialog,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { showPopover, conversation, latestMessage, setShowPopover, setShowBingToneSetting } =
|
|
||||||
useChatContext();
|
|
||||||
const { setOption } = useSetIndexOptions();
|
|
||||||
|
|
||||||
const { endpoint, conversationId, jailbreak } = conversation ?? {};
|
|
||||||
|
|
||||||
const altConditions: { [key: string]: boolean } = {
|
|
||||||
bingAI: !!(latestMessage && conversation?.jailbreak && endpoint === 'bingAI'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const altSettings: { [key: string]: () => void } = {
|
|
||||||
bingAI: () => setShowBingToneSetting((prev) => !prev),
|
|
||||||
};
|
|
||||||
|
|
||||||
const noSettings = useMemo<{ [key: string]: boolean }>(
|
|
||||||
() => ({
|
|
||||||
[EModelEndpoint.chatGPTBrowser]: true,
|
|
||||||
[EModelEndpoint.bingAI]: jailbreak ? false : conversationId !== 'new',
|
|
||||||
}),
|
|
||||||
[jailbreak, conversationId],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
} else if (messagesTree && messagesTree.length >= 1) {
|
|
||||||
setOpacityClass('show');
|
|
||||||
} else {
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}
|
|
||||||
}, [messagesTree, showPopover]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (endpoint && noSettings[endpoint]) {
|
|
||||||
setShowPopover(false);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [endpoint, noSettings]);
|
|
||||||
|
|
||||||
const saveAsPreset = () => {
|
|
||||||
setSaveAsDialogShow(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!endpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerAdvancedMode = altConditions[endpoint]
|
|
||||||
? altSettings[endpoint]
|
|
||||||
: () => setShowPopover((prev) => !prev);
|
|
||||||
return (
|
|
||||||
<div className="absolute left-0 right-0 mx-auto mb-2 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
|
|
||||||
<GenerationButtons
|
|
||||||
endpoint={endpoint}
|
|
||||||
showPopover={showPopover}
|
|
||||||
opacityClass={opacityClass}
|
|
||||||
/>
|
|
||||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'options-bar z-[61] flex w-full flex-wrap items-center justify-center gap-2',
|
|
||||||
showPopover ? '' : opacityClass,
|
|
||||||
)}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!messagesTree || messagesTree.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('show');
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!messagesTree || messagesTree.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('show');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModelSelect conversation={conversation} setOption={setOption} isMultiChat={true} />
|
|
||||||
{!noSettings[endpoint] && (
|
|
||||||
<Button
|
|
||||||
id="advanced-mode-button"
|
|
||||||
customId="advanced-mode-button"
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
cardStyle,
|
|
||||||
'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
|
|
||||||
)}
|
|
||||||
onClick={triggerAdvancedMode}
|
|
||||||
>
|
|
||||||
<Settings2 id="advanced-settings" className="w-4 text-gray-600 dark:text-white" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<EndpointOptionsPopover
|
|
||||||
visible={showPopover}
|
|
||||||
saveAsPreset={saveAsPreset}
|
|
||||||
closePopover={() => setShowPopover(false)}
|
|
||||||
PopoverButtons={<PopoverButtons />}
|
|
||||||
>
|
|
||||||
<div className="px-4 py-4">
|
|
||||||
<EndpointSettings
|
|
||||||
conversation={conversation}
|
|
||||||
setOption={setOption}
|
|
||||||
isMultiChat={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</EndpointOptionsPopover>
|
|
||||||
<SaveAsPresetDialog
|
|
||||||
open={saveAsDialogShow}
|
|
||||||
onOpenChange={setSaveAsDialogShow}
|
|
||||||
preset={
|
|
||||||
tPresetUpdateSchema.parse({
|
|
||||||
...conversation,
|
|
||||||
}) as TPreset
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PluginStoreDialog isOpen={showPluginStoreDialog} setIsOpen={setShowPluginStoreDialog} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -63,7 +63,7 @@ export default function OptionsPopover({
|
||||||
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-auto justify-start rounded-md bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600"
|
className="h-auto w-[150px] justify-start rounded-md border-2 border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500"
|
||||||
onClick={saveAsPreset}
|
onClick={saveAsPreset}
|
||||||
>
|
>
|
||||||
<Save className="mr-1 w-[14px]" />
|
<Save className="mr-1 w-[14px]" />
|
||||||
|
|
|
@ -1,24 +1,33 @@
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { EModelEndpoint, SettingsViews } from 'librechat-data-provider';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { MessagesSquared, GPTIcon } from '~/components/svg';
|
import { MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '~/components/svg';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import { Button } from '~/components/ui';
|
import { Button } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
type TPopoverButton = {
|
type TPopoverButton = {
|
||||||
label: string;
|
label: string;
|
||||||
buttonClass: string;
|
buttonClass: string;
|
||||||
handler: () => void;
|
handler: () => void;
|
||||||
|
type?: 'alternative';
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PopoverButtons({
|
export default function PopoverButtons({
|
||||||
buttonClass,
|
buttonClass,
|
||||||
iconClass = '',
|
iconClass = '',
|
||||||
|
endpoint: _overrideEndpoint,
|
||||||
|
endpointType: overrideEndpointType,
|
||||||
|
model: overrideModel,
|
||||||
}: {
|
}: {
|
||||||
buttonClass?: string;
|
buttonClass?: string;
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
|
endpoint?: EModelEndpoint | string;
|
||||||
|
endpointType?: EModelEndpoint | string;
|
||||||
|
model?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -28,9 +37,13 @@ export default function PopoverButtons({
|
||||||
setShowAgentSettings,
|
setShowAgentSettings,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const [settingsView, setSettingsView] = useRecoilState(store.currentSettingsView);
|
||||||
|
|
||||||
|
const { model: _model, endpoint: _endpoint, endpointType } = conversation ?? {};
|
||||||
|
const overrideEndpoint = overrideEndpointType ?? _overrideEndpoint;
|
||||||
|
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint;
|
||||||
|
const model = overrideModel ?? _model;
|
||||||
|
|
||||||
const { model, endpoint: _endpoint, endpointType } = conversation ?? {};
|
|
||||||
const endpoint = endpointType ?? _endpoint;
|
|
||||||
const isGenerativeModel = model?.toLowerCase()?.includes('gemini');
|
const isGenerativeModel = model?.toLowerCase()?.includes('gemini');
|
||||||
const isChatModel = !isGenerativeModel && model?.toLowerCase()?.includes('chat');
|
const isChatModel = !isGenerativeModel && model?.toLowerCase()?.includes('chat');
|
||||||
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
|
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
|
||||||
|
@ -38,10 +51,12 @@ export default function PopoverButtons({
|
||||||
const { showExamples } = optionSettings;
|
const { showExamples } = optionSettings;
|
||||||
const showExamplesButton = !isGenerativeModel && !isTextModel && isChatModel;
|
const showExamplesButton = !isGenerativeModel && !isTextModel && isChatModel;
|
||||||
|
|
||||||
const triggerExamples = () =>
|
const triggerExamples = () => {
|
||||||
|
setSettingsView(SettingsViews.default);
|
||||||
setOptionSettings((prev) => ({ ...prev, showExamples: !prev.showExamples }));
|
setOptionSettings((prev) => ({ ...prev, showExamples: !prev.showExamples }));
|
||||||
|
};
|
||||||
|
|
||||||
const buttons: { [key: string]: TPopoverButton[] } = {
|
const endpointSpecificbuttons: { [key: string]: TPopoverButton[] } = {
|
||||||
[EModelEndpoint.google]: [
|
[EModelEndpoint.google]: [
|
||||||
{
|
{
|
||||||
label: localize(showExamples ? 'com_hide_examples' : 'com_show_examples'),
|
label: localize(showExamples ? 'com_hide_examples' : 'com_show_examples'),
|
||||||
|
@ -56,14 +71,16 @@ export default function PopoverButtons({
|
||||||
showAgentSettings ? 'com_show_completion_settings' : 'com_show_agent_settings',
|
showAgentSettings ? 'com_show_completion_settings' : 'com_show_agent_settings',
|
||||||
),
|
),
|
||||||
buttonClass: '',
|
buttonClass: '',
|
||||||
handler: () => setShowAgentSettings((prev) => !prev),
|
handler: () => {
|
||||||
|
setSettingsView(SettingsViews.default);
|
||||||
|
setShowAgentSettings((prev) => !prev);
|
||||||
|
},
|
||||||
icon: <GPTIcon className={cn('mr-1 w-[14px]', iconClass)} size={24} />,
|
icon: <GPTIcon className={cn('mr-1 w-[14px]', iconClass)} size={24} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpointButtons = buttons[endpoint ?? ''];
|
if (!endpoint) {
|
||||||
if (!endpointButtons) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,23 +88,71 @@ export default function PopoverButtons({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const additionalButtons: { [key: string]: TPopoverButton[] } = {
|
||||||
|
[SettingsViews.default]: [
|
||||||
|
{
|
||||||
|
label: 'Context Settings',
|
||||||
|
buttonClass: '',
|
||||||
|
type: 'alternative',
|
||||||
|
handler: () => setSettingsView(SettingsViews.advanced),
|
||||||
|
icon: <DataIcon className={cn('mr-1 h-6 w-[14px]', iconClass)} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[SettingsViews.advanced]: [
|
||||||
|
{
|
||||||
|
label: 'Model Settings',
|
||||||
|
buttonClass: '',
|
||||||
|
type: 'alternative',
|
||||||
|
handler: () => setSettingsView(SettingsViews.default),
|
||||||
|
icon: <AssistantIcon className={cn('mr-1 h-6 w-[14px]', iconClass)} />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpointButtons = endpointSpecificbuttons[endpoint] ?? [];
|
||||||
|
|
||||||
|
const disabled = true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex w-full justify-between">
|
||||||
{endpointButtons.map((button, index) => (
|
<div className="flex items-center justify-start">
|
||||||
<Button
|
{endpointButtons.map((button, index) => (
|
||||||
key={`button-${index}`}
|
<Button
|
||||||
type="button"
|
key={`button-${index}`}
|
||||||
className={cn(
|
type="button"
|
||||||
button.buttonClass,
|
className={cn(
|
||||||
'ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0',
|
button.buttonClass,
|
||||||
buttonClass ?? '',
|
'border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
|
||||||
)}
|
'ml-1 h-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
|
||||||
onClick={button.handler}
|
buttonClass ?? '',
|
||||||
>
|
)}
|
||||||
{button.icon}
|
onClick={button.handler}
|
||||||
{button.label}
|
>
|
||||||
</Button>
|
{button.icon}
|
||||||
))}
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{disabled ? null : (
|
||||||
|
<div className="flex w-[150px] items-center justify-end">
|
||||||
|
{additionalButtons[settingsView].map((button, index) => (
|
||||||
|
<Button
|
||||||
|
key={`button-${index}`}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
button.buttonClass,
|
||||||
|
'flex justify-center border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
|
||||||
|
'h-full w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
|
||||||
|
buttonClass ?? '',
|
||||||
|
)}
|
||||||
|
onClick={button.handler}
|
||||||
|
>
|
||||||
|
{button.icon}
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ const EditPresetDialog = ({
|
||||||
select: mapEndpoints,
|
select: mapEndpoints,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { endpoint } = preset || {};
|
const { endpoint, endpointType, model } = preset || {};
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ const EditPresetDialog = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-start justify-start gap-4 sm:col-span-1">
|
<div className="col-span-2 flex items-start justify-between gap-4 sm:col-span-4">
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Label
|
<Label
|
||||||
htmlFor="endpoint"
|
htmlFor="endpoint"
|
||||||
|
@ -92,6 +92,9 @@ const EditPresetDialog = ({
|
||||||
<PopoverButtons
|
<PopoverButtons
|
||||||
buttonClass="ml-0 w-full border border-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 p-2 h-[40px] justify-center mt-0"
|
buttonClass="ml-0 w-full border border-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 p-2 h-[40px] justify-center mt-0"
|
||||||
iconClass="hidden lg:block w-4 "
|
iconClass="hidden lg:block w-4 "
|
||||||
|
endpoint={endpoint}
|
||||||
|
endpointType={endpointType}
|
||||||
|
model={model}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Flipper, Flipped } from 'react-flip-toolkit';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
import FileUpload from '~/components/Input/EndpointMenu/FileUpload';
|
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
||||||
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||||
import { getPresetTitle, getEndpointField } from '~/utils';
|
import { getPresetTitle, getEndpointField } from '~/utils';
|
||||||
|
|
24
client/src/components/Endpoints/AlternativeSettings.tsx
Normal file
24
client/src/components/Endpoints/AlternativeSettings.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { SettingsViews } from 'librechat-data-provider';
|
||||||
|
import type { TSettingsProps } from '~/common';
|
||||||
|
import { Advanced } from './Settings';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function AlternativeSettings({
|
||||||
|
conversation,
|
||||||
|
setOption,
|
||||||
|
isPreset = false,
|
||||||
|
className = '',
|
||||||
|
}: TSettingsProps & { isMultiChat?: boolean }) {
|
||||||
|
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||||
|
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('hide-scrollbar h-[500px] overflow-y-auto md:mb-2 md:h-[350px]', className)}>
|
||||||
|
<Advanced conversation={conversation} setOption={setOption} isPreset={isPreset} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,145 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import filenamify from 'filenamify';
|
|
||||||
import exportFromJSON from 'export-from-json';
|
|
||||||
import { useSetRecoilState, useRecoilState } from 'recoil';
|
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
|
||||||
import type { TEditPresetProps } from '~/common';
|
|
||||||
import { useSetOptions, useLocalize } from '~/hooks';
|
|
||||||
import { Input, Label, Dropdown, Dialog, DialogClose, DialogButton } from '~/components/';
|
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
|
||||||
import PopoverButtons from './PopoverButtons';
|
|
||||||
import EndpointSettings from './EndpointSettings';
|
|
||||||
import { cn, defaultTextProps, removeFocusOutlines, cleanupPreset, mapEndpoints } from '~/utils/';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }: TEditPresetProps) => {
|
|
||||||
const [preset, setPreset] = useRecoilState(store.preset);
|
|
||||||
const setPresets = useSetRecoilState(store.presets);
|
|
||||||
const { data: availableEndpoints = [] } = useGetEndpointsQuery({
|
|
||||||
select: mapEndpoints,
|
|
||||||
});
|
|
||||||
const { setOption } = useSetOptions(_preset);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const submitPreset = () => {
|
|
||||||
if (!preset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
axios({
|
|
||||||
method: 'post',
|
|
||||||
url: '/api/presets',
|
|
||||||
data: cleanupPreset({ preset }),
|
|
||||||
withCredentials: true,
|
|
||||||
}).then((res) => {
|
|
||||||
setPresets(res?.data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportPreset = () => {
|
|
||||||
if (!preset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fileName = filenamify(preset?.title || 'preset');
|
|
||||||
exportFromJSON({
|
|
||||||
data: cleanupPreset({ preset }),
|
|
||||||
fileName,
|
|
||||||
exportType: exportFromJSON.types.json,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPreset(_preset);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const { endpoint } = preset || {};
|
|
||||||
if (!endpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogTemplate
|
|
||||||
title={`${title || localize('com_ui_edit') + ' ' + localize('com_endpoint_preset')} - ${
|
|
||||||
preset?.title
|
|
||||||
}`}
|
|
||||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
|
|
||||||
main={
|
|
||||||
<div className="flex w-full flex-col items-center gap-2 md:h-[530px]">
|
|
||||||
<div className="grid w-full grid-cols-5 gap-6">
|
|
||||||
<div className="col-span-4 flex items-start justify-start gap-4">
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<Label htmlFor="preset-name" className="mb-1 text-left text-sm font-medium">
|
|
||||||
{localize('com_endpoint_preset_name')}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="preset-name"
|
|
||||||
value={preset?.title || ''}
|
|
||||||
onChange={(e) => setOption('title')(e.target.value || '')}
|
|
||||||
placeholder={localize('com_endpoint_set_custom_name')}
|
|
||||||
className={cn(
|
|
||||||
defaultTextProps,
|
|
||||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
|
||||||
removeFocusOutlines,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<Label htmlFor="endpoint" className="mb-1 text-left text-sm font-medium">
|
|
||||||
{localize('com_endpoint')}
|
|
||||||
</Label>
|
|
||||||
<Dropdown
|
|
||||||
value={endpoint || ''}
|
|
||||||
onChange={(value) => setOption('endpoint')(value)}
|
|
||||||
options={availableEndpoints}
|
|
||||||
className={cn()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 flex items-start justify-start gap-4 sm:col-span-1">
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<Label
|
|
||||||
htmlFor="endpoint"
|
|
||||||
className="mb-1 hidden text-left text-sm font-medium sm:block"
|
|
||||||
>
|
|
||||||
{'ㅤ'}
|
|
||||||
</Label>
|
|
||||||
<PopoverButtons
|
|
||||||
endpoint={endpoint}
|
|
||||||
buttonClass="ml-0 w-full dark:bg-gray-700 dark:hover:bg-gray-800 p-2 h-[40px] justify-center mt-0"
|
|
||||||
iconClass="hidden lg:block w-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
|
||||||
<div className="w-full p-0">
|
|
||||||
<EndpointSettings
|
|
||||||
conversation={preset}
|
|
||||||
setOption={setOption}
|
|
||||||
isPreset={true}
|
|
||||||
className="h-full md:mb-4 md:h-[440px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
buttons={
|
|
||||||
<div className="mb-6 md:mb-2">
|
|
||||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
|
||||||
{localize('com_endpoint_export')}
|
|
||||||
</DialogButton>
|
|
||||||
<DialogClose
|
|
||||||
onClick={submitPreset}
|
|
||||||
className="dark:hover:gray-400 ml-2 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
|
||||||
>
|
|
||||||
{localize('com_ui_save')}
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditPresetDialog;
|
|
|
@ -1,113 +0,0 @@
|
||||||
import exportFromJSON from 'export-from-json';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { tPresetSchema } from 'librechat-data-provider';
|
|
||||||
import type { TSetOption, TEditPresetProps } from '~/common';
|
|
||||||
import { Dialog, DialogButton } from '~/components/ui';
|
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
|
||||||
import SaveAsPresetDialog from './SaveAsPresetDialog';
|
|
||||||
import EndpointSettings from './EndpointSettings';
|
|
||||||
import PopoverButtons from './PopoverButtons';
|
|
||||||
import { cleanupPreset } from '~/utils';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
// A preset dialog to show readonly preset values.
|
|
||||||
const EndpointOptionsDialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
preset: _preset,
|
|
||||||
title,
|
|
||||||
}: TEditPresetProps) => {
|
|
||||||
const [preset, setPreset] = useRecoilState(store.preset);
|
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const setOption: TSetOption = (param) => (newValue) => {
|
|
||||||
const update = {};
|
|
||||||
update[param] = newValue;
|
|
||||||
setPreset((prevState) =>
|
|
||||||
tPresetSchema.parse({
|
|
||||||
...prevState,
|
|
||||||
...update,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveAsPreset = () => {
|
|
||||||
setSaveAsDialogShow(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportPreset = () => {
|
|
||||||
if (!preset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
exportFromJSON({
|
|
||||||
data: cleanupPreset({ preset }),
|
|
||||||
fileName: `${preset?.title}.json`,
|
|
||||||
exportType: exportFromJSON.types.json,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPreset(_preset);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const { endpoint } = preset ?? {};
|
|
||||||
if (!endpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preset) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogTemplate
|
|
||||||
title={`${title || localize('com_endpoint_save_convo_as_preset')}`}
|
|
||||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[680px] md:w-[750px] md:overflow-y-hidden lg:w-[950px]"
|
|
||||||
// headerClassName="sm:p-2 h-16"
|
|
||||||
main={
|
|
||||||
<div className="flex w-full flex-col items-center gap-2 md:h-[530px]">
|
|
||||||
<div className="w-full p-0">
|
|
||||||
<PopoverButtons
|
|
||||||
endpoint={endpoint}
|
|
||||||
buttonClass="ml-0 mb-4 col-span-2 dark:bg-gray-700 dark:hover:bg-gray-800 p-2"
|
|
||||||
/>
|
|
||||||
<EndpointSettings
|
|
||||||
conversation={preset}
|
|
||||||
setOption={setOption}
|
|
||||||
isPreset={true}
|
|
||||||
className="h-full md:mb-0 md:h-[490px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
buttons={
|
|
||||||
<div className="mb-6 md:mb-2">
|
|
||||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
|
||||||
{localize('com_endpoint_export')}
|
|
||||||
</DialogButton>
|
|
||||||
<DialogButton
|
|
||||||
onClick={saveAsPreset}
|
|
||||||
className="dark:hover:gray-400 ml-2 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
|
||||||
>
|
|
||||||
{localize('com_endpoint_save_as_preset')}
|
|
||||||
</DialogButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
<SaveAsPresetDialog
|
|
||||||
open={saveAsDialogShow}
|
|
||||||
onOpenChange={setSaveAsDialogShow}
|
|
||||||
preset={preset}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EndpointOptionsDialog;
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { SettingsViews } from 'librechat-data-provider';
|
||||||
import type { TSettingsProps } from '~/common';
|
import type { TSettingsProps } from '~/common';
|
||||||
import { getSettings } from './Settings';
|
import { getSettings } from './Settings';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
@ -12,7 +13,8 @@ export default function Settings({
|
||||||
isMultiChat = false,
|
isMultiChat = false,
|
||||||
}: TSettingsProps & { isMultiChat?: boolean }) {
|
}: TSettingsProps & { isMultiChat?: boolean }) {
|
||||||
const modelsConfig = useRecoilValue(store.modelsConfig);
|
const modelsConfig = useRecoilValue(store.modelsConfig);
|
||||||
if (!conversation?.endpoint) {
|
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||||
|
if (!conversation?.endpoint || currentSettingsView !== SettingsViews.default) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import { MessagesSquared, GPTIcon } from '~/components/svg';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Button } from '~/components';
|
|
||||||
import { cn } from '~/utils/';
|
|
||||||
import store from '~/store';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
type TPopoverButton = {
|
|
||||||
label: string;
|
|
||||||
buttonClass: string;
|
|
||||||
handler: () => void;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PopoverButtons({
|
|
||||||
endpoint,
|
|
||||||
buttonClass,
|
|
||||||
iconClass = '',
|
|
||||||
}: {
|
|
||||||
endpoint: EModelEndpoint | string;
|
|
||||||
buttonClass?: string;
|
|
||||||
iconClass?: string;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettings);
|
|
||||||
const [showAgentSettings, setShowAgentSettings] = useRecoilState(store.showAgentSettings);
|
|
||||||
const { showExamples, isCodeChat } = optionSettings;
|
|
||||||
const triggerExamples = () =>
|
|
||||||
setOptionSettings((prev) => ({ ...prev, showExamples: !prev.showExamples }));
|
|
||||||
|
|
||||||
const buttons: { [key: string]: TPopoverButton[] } = {
|
|
||||||
google: [
|
|
||||||
{
|
|
||||||
label:
|
|
||||||
(showExamples ? localize('com_endpoint_hide') : localize('com_endpoint_show')) +
|
|
||||||
localize('com_endpoint_examples'),
|
|
||||||
buttonClass: isCodeChat ? 'disabled' : '',
|
|
||||||
handler: triggerExamples,
|
|
||||||
icon: <MessagesSquared className={cn('mr-1 w-[14px]', iconClass)} />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
gptPlugins: [
|
|
||||||
{
|
|
||||||
label: localize(
|
|
||||||
'com_endpoint_show_what_settings',
|
|
||||||
showAgentSettings ? localize('com_endpoint_completion') : localize('com_endpoint_agent'),
|
|
||||||
),
|
|
||||||
buttonClass: '',
|
|
||||||
handler: () => setShowAgentSettings((prev) => !prev),
|
|
||||||
icon: <GPTIcon className={cn('mr-1 w-[14px]', iconClass)} size={24} />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpointButtons = buttons[endpoint];
|
|
||||||
if (!endpointButtons) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{endpointButtons.map((button, index) => (
|
|
||||||
<Button
|
|
||||||
key={`${endpoint}-button-${index}`}
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
button.buttonClass,
|
|
||||||
'ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0',
|
|
||||||
buttonClass ?? '',
|
|
||||||
)}
|
|
||||||
onClick={button.handler}
|
|
||||||
>
|
|
||||||
{button.icon}
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
333
client/src/components/Endpoints/Settings/Advanced.tsx
Normal file
333
client/src/components/Endpoints/Settings/Advanced.tsx
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
import { ImageDetail, imageDetailNumeric, imageDetailValue } from 'librechat-data-provider';
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Switch,
|
||||||
|
Slider,
|
||||||
|
HoverCard,
|
||||||
|
InputNumber,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '~/components/ui';
|
||||||
|
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
||||||
|
import { useLocalize, useDebouncedInput } from '~/hooks';
|
||||||
|
import type { TModelSelectProps } from '~/common';
|
||||||
|
import OptionHover from './OptionHover';
|
||||||
|
import { ESide } from '~/common';
|
||||||
|
|
||||||
|
export default function Settings({
|
||||||
|
conversation,
|
||||||
|
setOption,
|
||||||
|
readonly,
|
||||||
|
}: Omit<TModelSelectProps, 'models'>) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const {
|
||||||
|
endpoint,
|
||||||
|
endpointType,
|
||||||
|
chatGptLabel,
|
||||||
|
promptPrefix,
|
||||||
|
temperature,
|
||||||
|
top_p: topP,
|
||||||
|
frequency_penalty: freqP,
|
||||||
|
presence_penalty: presP,
|
||||||
|
resendFiles,
|
||||||
|
imageDetail,
|
||||||
|
} = conversation ?? {};
|
||||||
|
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'chatGptLabel',
|
||||||
|
initialValue: chatGptLabel,
|
||||||
|
});
|
||||||
|
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'promptPrefix',
|
||||||
|
initialValue: promptPrefix,
|
||||||
|
});
|
||||||
|
const [setTemperature, temperatureValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'temperature',
|
||||||
|
initialValue: temperature,
|
||||||
|
});
|
||||||
|
const [setTopP, topPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'top_p',
|
||||||
|
initialValue: topP,
|
||||||
|
});
|
||||||
|
const [setFreqP, freqPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'frequency_penalty',
|
||||||
|
initialValue: freqP,
|
||||||
|
});
|
||||||
|
const [setPresP, presPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'presence_penalty',
|
||||||
|
initialValue: presP,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setResendFiles = setOption('resendFiles');
|
||||||
|
const setImageDetail = setOption('imageDetail');
|
||||||
|
|
||||||
|
const optionEndpoint = endpointType ?? endpoint;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-5 gap-6">
|
||||||
|
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_custom_name')}{' '}
|
||||||
|
<small className="opacity-40">({localize('com_endpoint_default_blank')})</small>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="chatGptLabel"
|
||||||
|
disabled={readonly}
|
||||||
|
value={(chatGptLabelValue as string) || ''}
|
||||||
|
onChange={setChatGptLabel}
|
||||||
|
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||||
|
removeFocusOutlines,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_prompt_prefix')}{' '}
|
||||||
|
<small className="opacity-40">({localize('com_endpoint_default_blank')})</small>
|
||||||
|
</Label>
|
||||||
|
<TextareaAutosize
|
||||||
|
id="promptPrefix"
|
||||||
|
disabled={readonly}
|
||||||
|
value={(promptPrefixValue as string) || ''}
|
||||||
|
onChange={setPromptPrefix}
|
||||||
|
placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
||||||
|
<HoverCard openDelay={300}>
|
||||||
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_temperature')}{' '}
|
||||||
|
<small className="opacity-40">
|
||||||
|
({localize('com_endpoint_default_with_num', '1')})
|
||||||
|
</small>
|
||||||
|
</Label>
|
||||||
|
<InputNumber
|
||||||
|
id="temp-int"
|
||||||
|
disabled={readonly}
|
||||||
|
value={temperatureValue as number}
|
||||||
|
onChange={setTemperature}
|
||||||
|
max={2}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
controls={false}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
cn(
|
||||||
|
optionText,
|
||||||
|
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
disabled={readonly}
|
||||||
|
value={[(temperatureValue as number) ?? 1]}
|
||||||
|
onValueChange={(value) => setTemperature(value[0])}
|
||||||
|
doubleClickHandler={() => setTemperature(1)}
|
||||||
|
max={2}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
className="flex h-4 w-full"
|
||||||
|
/>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="temp" side={ESide.Left} />
|
||||||
|
</HoverCard>
|
||||||
|
<HoverCard openDelay={300}>
|
||||||
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_top_p')}{' '}
|
||||||
|
<small className="opacity-40">({localize('com_endpoint_default')}: 1)</small>
|
||||||
|
</Label>
|
||||||
|
<InputNumber
|
||||||
|
id="top-p-int"
|
||||||
|
disabled={readonly}
|
||||||
|
value={topPValue as number}
|
||||||
|
onChange={(value) => setTopP(Number(value))}
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
controls={false}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
cn(
|
||||||
|
optionText,
|
||||||
|
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
disabled={readonly}
|
||||||
|
value={[(topPValue as number) ?? 1]}
|
||||||
|
onValueChange={(value) => setTopP(value[0])}
|
||||||
|
doubleClickHandler={() => setTopP(1)}
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
className="flex h-4 w-full"
|
||||||
|
/>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="topp" side={ESide.Left} />
|
||||||
|
</HoverCard>
|
||||||
|
|
||||||
|
<HoverCard openDelay={300}>
|
||||||
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_frequency_penalty')}{' '}
|
||||||
|
<small className="opacity-40">({localize('com_endpoint_default')}: 0)</small>
|
||||||
|
</Label>
|
||||||
|
<InputNumber
|
||||||
|
id="freq-penalty-int"
|
||||||
|
disabled={readonly}
|
||||||
|
value={freqPValue as number}
|
||||||
|
onChange={(value) => setFreqP(Number(value))}
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.01}
|
||||||
|
controls={false}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
cn(
|
||||||
|
optionText,
|
||||||
|
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
disabled={readonly}
|
||||||
|
value={[(freqPValue as number) ?? 0]}
|
||||||
|
onValueChange={(value) => setFreqP(value[0])}
|
||||||
|
doubleClickHandler={() => setFreqP(0)}
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.01}
|
||||||
|
className="flex h-4 w-full"
|
||||||
|
/>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="freq" side={ESide.Left} />
|
||||||
|
</HoverCard>
|
||||||
|
|
||||||
|
<HoverCard openDelay={300}>
|
||||||
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||||
|
{localize('com_endpoint_presence_penalty')}{' '}
|
||||||
|
<small className="opacity-40">({localize('com_endpoint_default')}: 0)</small>
|
||||||
|
</Label>
|
||||||
|
<InputNumber
|
||||||
|
id="pres-penalty-int"
|
||||||
|
disabled={readonly}
|
||||||
|
value={presPValue as number}
|
||||||
|
onChange={(value) => setPresP(Number(value))}
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.01}
|
||||||
|
controls={false}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
cn(
|
||||||
|
optionText,
|
||||||
|
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
disabled={readonly}
|
||||||
|
value={[(presPValue as number) ?? 0]}
|
||||||
|
onValueChange={(value) => setPresP(value[0])}
|
||||||
|
doubleClickHandler={() => setPresP(0)}
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.01}
|
||||||
|
className="flex h-4 w-full"
|
||||||
|
/>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="pres" side={ESide.Left} />
|
||||||
|
</HoverCard>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-2 flex w-full justify-between gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="resend-files"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
<small>{localize('com_endpoint_plug_resend_files')}</small>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
htmlFor="image-detail-value"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
<small>{localize('com_endpoint_plug_image_detail')}</small>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="image-detail-value"
|
||||||
|
disabled={true}
|
||||||
|
value={imageDetail ?? ImageDetail.auto}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
optionText,
|
||||||
|
'flex rounded-md bg-transparent py-2 text-xs focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:border-gray-700',
|
||||||
|
'pointer-events-none max-h-5 w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between gap-2">
|
||||||
|
<HoverCard openDelay={500}>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Switch
|
||||||
|
id="resend-files"
|
||||||
|
checked={resendFiles ?? true}
|
||||||
|
onCheckedChange={(checked: boolean) => setResendFiles(checked)}
|
||||||
|
disabled={readonly}
|
||||||
|
className="flex"
|
||||||
|
/>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="resend" side={ESide.Bottom} />
|
||||||
|
</HoverCardTrigger>
|
||||||
|
</HoverCard>
|
||||||
|
<HoverCard openDelay={500}>
|
||||||
|
<HoverCardTrigger className="flex w-[52%] md:w-[125px]">
|
||||||
|
<Slider
|
||||||
|
id="image-detail-slider"
|
||||||
|
disabled={readonly}
|
||||||
|
value={[
|
||||||
|
imageDetailNumeric[imageDetail ?? ''] ?? imageDetailNumeric[ImageDetail.auto],
|
||||||
|
]}
|
||||||
|
onValueChange={(value) => setImageDetail(imageDetailValue[value[0]])}
|
||||||
|
doubleClickHandler={() => setImageDetail(ImageDetail.auto)}
|
||||||
|
max={2}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<OptionHover endpoint={optionEndpoint ?? ''} type="detail" side={ESide.Bottom} />
|
||||||
|
</HoverCardTrigger>
|
||||||
|
</HoverCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { default as Advanced } from './Advanced';
|
||||||
export { default as AssistantsSettings } from './Assistants';
|
export { default as AssistantsSettings } from './Assistants';
|
||||||
export { default as OpenAISettings } from './OpenAI';
|
export { default as OpenAISettings } from './OpenAI';
|
||||||
export { default as BingAISettings } from './BingAI';
|
export { default as BingAISettings } from './BingAI';
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export { default as Icon } from './Icon';
|
export { default as Icon } from './Icon';
|
||||||
export { default as MinimalIcon } from './MinimalIcon';
|
export { default as MinimalIcon } from './MinimalIcon';
|
||||||
export { default as PopoverButtons } from './PopoverButtons';
|
|
||||||
export { default as EndpointSettings } from './EndpointSettings';
|
export { default as EndpointSettings } from './EndpointSettings';
|
||||||
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
||||||
export { default as EndpointOptionsDialog } from './EndpointOptionsDialog';
|
export { default as AlternativeSettings } from './AlternativeSettings';
|
||||||
export { default as EndpointOptionsPopover } from './EndpointOptionsPopover';
|
export { default as EndpointOptionsPopover } from './EndpointOptionsPopover';
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { Settings } from 'lucide-react';
|
|
||||||
import { alternateName } from 'librechat-data-provider';
|
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
|
||||||
import { DropdownMenuRadioItem } from '~/components';
|
|
||||||
import { SetKeyDialog } from '../SetKeyDialog';
|
|
||||||
import { cn, getEndpointField } from '~/utils';
|
|
||||||
import { Icon } from '~/components/Endpoints';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
export default function ModelItem({
|
|
||||||
endpoint,
|
|
||||||
value,
|
|
||||||
isSelected,
|
|
||||||
}: {
|
|
||||||
endpoint: string;
|
|
||||||
value: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
}) {
|
|
||||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
|
||||||
|
|
||||||
const icon = Icon({
|
|
||||||
size: 20,
|
|
||||||
endpoint,
|
|
||||||
error: false,
|
|
||||||
className: 'mr-2',
|
|
||||||
message: false,
|
|
||||||
isCreatedByUser: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const userProvidesKey: boolean | null | undefined = getEndpointField(
|
|
||||||
endpointsConfig,
|
|
||||||
endpoint,
|
|
||||||
'userProvide',
|
|
||||||
);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
// regular model
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropdownMenuRadioItem
|
|
||||||
value={value}
|
|
||||||
className={cn(
|
|
||||||
'group dark:font-semibold dark:text-gray-200 dark:hover:bg-gray-800',
|
|
||||||
isSelected ? 'active bg-gray-50 dark:bg-gray-800' : '',
|
|
||||||
)}
|
|
||||||
id={endpoint}
|
|
||||||
data-testid={`endpoint-item-${endpoint}`}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{alternateName[endpoint] || endpoint}
|
|
||||||
{endpoint === 'gptPlugins' && (
|
|
||||||
<span className="py-0.25 ml-1 rounded bg-blue-200 px-1 text-[10px] font-semibold text-[#4559A4]">
|
|
||||||
Beta
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="flex w-4 flex-1" />
|
|
||||||
{userProvidesKey ? (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200',
|
|
||||||
isSelected ? 'visible text-gray-700 dark:text-gray-200' : '',
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings className="mr-1 inline-block w-[16px] items-center stroke-1" />
|
|
||||||
{localize('com_endpoint_config_key')}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
{userProvidesKey && (
|
|
||||||
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import EndpointItem from './EndpointItem';
|
|
||||||
|
|
||||||
interface EndpointItemsProps {
|
|
||||||
endpoints: string[];
|
|
||||||
onSelect: (endpoint: string) => void;
|
|
||||||
selectedEndpoint: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EndpointItems({ endpoints, selectedEndpoint }: EndpointItemsProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{endpoints.map((endpoint) => (
|
|
||||||
<EndpointItem
|
|
||||||
isSelected={selectedEndpoint === endpoint}
|
|
||||||
key={endpoint}
|
|
||||||
value={endpoint}
|
|
||||||
endpoint={endpoint}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,275 +0,0 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
useDeletePresetMutation,
|
|
||||||
useCreatePresetMutation,
|
|
||||||
useGetEndpointsQuery,
|
|
||||||
} from 'librechat-data-provider/react-query';
|
|
||||||
import { Icon } from '~/components/Endpoints';
|
|
||||||
import EndpointItems from './EndpointItems';
|
|
||||||
import PresetItems from './PresetItems';
|
|
||||||
import FileUpload from './FileUpload';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
TooltipProvider,
|
|
||||||
Tooltip,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
} from '~/components/ui/';
|
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
|
||||||
import { cn, cleanupPreset, mapEndpoints } from '~/utils';
|
|
||||||
import { useLocalize, useLocalStorage, useConversation, useDefaultConvo } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function NewConversationMenu() {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const getDefaultConversation = useDefaultConvo();
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const [showPresets, setShowPresets] = useState(true);
|
|
||||||
const [showEndpoints, setShowEndpoints] = useState(true);
|
|
||||||
const [conversation, setConversation] = useRecoilState(store.conversation) ?? {};
|
|
||||||
const [messages, setMessages] = useRecoilState(store.messages);
|
|
||||||
|
|
||||||
const { data: availableEndpoints = [] } = useGetEndpointsQuery({
|
|
||||||
select: mapEndpoints,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [presets, setPresets] = useRecoilState(store.presets);
|
|
||||||
const modularEndpoints = new Set(['gptPlugins', 'anthropic', 'google', 'openAI']);
|
|
||||||
|
|
||||||
const { endpoint } = conversation;
|
|
||||||
const { newConversation } = useConversation();
|
|
||||||
|
|
||||||
const deletePresetsMutation = useDeletePresetMutation();
|
|
||||||
const createPresetMutation = useCreatePresetMutation();
|
|
||||||
|
|
||||||
const importPreset = (jsonData) => {
|
|
||||||
createPresetMutation.mutate(
|
|
||||||
{ ...jsonData },
|
|
||||||
{
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setPresets(data);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Error uploading the preset:', error);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFileSelected = (jsonData) => {
|
|
||||||
const jsonPreset = { ...cleanupPreset({ preset: jsonData }), presetId: null };
|
|
||||||
importPreset(jsonPreset);
|
|
||||||
};
|
|
||||||
|
|
||||||
// save states to localStorage
|
|
||||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
|
||||||
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {});
|
|
||||||
const setLastConvo = useLocalStorage('lastConversationSetup', {})[1];
|
|
||||||
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
|
|
||||||
useEffect(() => {
|
|
||||||
if (endpoint && endpoint !== 'bingAI') {
|
|
||||||
const lastModelUpdate = { ...lastModel, [endpoint]: conversation?.model };
|
|
||||||
if (endpoint === 'gptPlugins') {
|
|
||||||
lastModelUpdate.secondaryModel = conversation.agentOptions.model;
|
|
||||||
}
|
|
||||||
setLastModel(lastModelUpdate);
|
|
||||||
} else if (endpoint === 'bingAI') {
|
|
||||||
const { jailbreak, toneStyle } = conversation;
|
|
||||||
setLastBingSettings({ ...lastBingSettings, jailbreak, toneStyle });
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastConvo(conversation);
|
|
||||||
}, [conversation]);
|
|
||||||
|
|
||||||
// set the current model
|
|
||||||
const onSelectEndpoint = (newEndpoint) => {
|
|
||||||
setMenuOpen(false);
|
|
||||||
if (!newEndpoint) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
newConversation(null, { endpoint: newEndpoint });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// set the current model
|
|
||||||
const isModular = modularEndpoints.has(endpoint);
|
|
||||||
const onSelectPreset = (newPreset) => {
|
|
||||||
setMenuOpen(false);
|
|
||||||
if (!newPreset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isModular &&
|
|
||||||
modularEndpoints.has(newPreset?.endpoint) &&
|
|
||||||
endpoint === newPreset?.endpoint
|
|
||||||
) {
|
|
||||||
const currentConvo = getDefaultConversation({
|
|
||||||
conversation,
|
|
||||||
preset: newPreset,
|
|
||||||
});
|
|
||||||
|
|
||||||
setConversation(currentConvo);
|
|
||||||
setMessages(messages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newConversation({}, newPreset);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAllPresets = () => {
|
|
||||||
deletePresetsMutation.mutate({ arg: {} });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeletePreset = (preset) => {
|
|
||||||
deletePresetsMutation.mutate({ arg: preset });
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = Icon({
|
|
||||||
size: 32,
|
|
||||||
...conversation,
|
|
||||||
error: false,
|
|
||||||
button: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onOpenChange = (open) => {
|
|
||||||
setMenuOpen(open);
|
|
||||||
if (newUser) {
|
|
||||||
setNewUser(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={250}>
|
|
||||||
<Tooltip>
|
|
||||||
<Dialog className="z-[100]">
|
|
||||||
<DropdownMenu open={menuOpen} onOpenChange={onOpenChange}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
id="new-conversation-menu"
|
|
||||||
data-testid="new-conversation-menu"
|
|
||||||
variant="outline"
|
|
||||||
className={
|
|
||||||
'group relative mb-[-12px] ml-1 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-0 md:ml-[-12px] md:pl-1'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent forceMount={newUser} sideOffset={5}>
|
|
||||||
{localize('com_endpoint_open_menu')}
|
|
||||||
</TooltipContent>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="z-[100] w-[375px] dark:bg-gray-800 md:w-96"
|
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
||||||
side="top"
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel
|
|
||||||
className="cursor-pointer dark:text-gray-300"
|
|
||||||
onClick={() => setShowEndpoints((prev) => !prev)}
|
|
||||||
>
|
|
||||||
{showEndpoints ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
|
|
||||||
{localize('com_endpoint')}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
value={endpoint}
|
|
||||||
onValueChange={onSelectEndpoint}
|
|
||||||
className="flex flex-col gap-1 overflow-y-auto"
|
|
||||||
>
|
|
||||||
{showEndpoints &&
|
|
||||||
(availableEndpoints.length ? (
|
|
||||||
<EndpointItems
|
|
||||||
selectedEndpoint={endpoint}
|
|
||||||
endpoints={availableEndpoints}
|
|
||||||
onSelect={onSelectEndpoint}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DropdownMenuLabel className="dark:text-gray-300">
|
|
||||||
{localize('com_endpoint_not_available')}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
))}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
|
|
||||||
<div className="mt-2 w-full" />
|
|
||||||
|
|
||||||
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
|
|
||||||
<span
|
|
||||||
className="mr-auto cursor-pointer "
|
|
||||||
onClick={() => setShowPresets((prev) => !prev)}
|
|
||||||
>
|
|
||||||
{showPresets ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
|
|
||||||
{localize('com_endpoint_presets')}
|
|
||||||
</span>
|
|
||||||
<FileUpload onFileSelected={onFileSelected} />
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<label
|
|
||||||
htmlFor="file-upload"
|
|
||||||
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="mr-1 flex w-[22px] items-center"
|
|
||||||
>
|
|
||||||
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M6.854 7.146 8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 1 1 .708-.708"></path>
|
|
||||||
</svg>
|
|
||||||
{localize('com_ui_clear')} {localize('com_ui_all')}
|
|
||||||
</label>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogTemplate
|
|
||||||
title={`${localize('com_ui_clear')} ${localize('com_endpoint_presets')}`}
|
|
||||||
description={localize('com_endpoint_presets_clear_warning')}
|
|
||||||
selection={{
|
|
||||||
selectHandler: clearAllPresets,
|
|
||||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
|
||||||
selectText: localize('com_ui_clear'),
|
|
||||||
}}
|
|
||||||
className="max-w-[500px]"
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
onValueChange={onSelectPreset}
|
|
||||||
className={cn(
|
|
||||||
'overflow-y-auto overflow-x-hidden',
|
|
||||||
showEndpoints ? 'max-h-[210px]' : 'max-h-[315px]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{showPresets &&
|
|
||||||
(presets.length ? (
|
|
||||||
<PresetItems
|
|
||||||
presets={presets}
|
|
||||||
onSelect={onSelectPreset}
|
|
||||||
onDeletePreset={onDeletePreset}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DropdownMenuLabel className="dark:text-gray-300">
|
|
||||||
{localize('com_endpoint_no_presets')}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
))}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dialog>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
import type { TPresetItemProps } from '~/common';
|
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import { DropdownMenuRadioItem, EditIcon, TrashIcon } from '~/components';
|
|
||||||
import { Icon } from '~/components/Endpoints';
|
|
||||||
|
|
||||||
export default function PresetItem({
|
|
||||||
preset = {} as TPreset,
|
|
||||||
value,
|
|
||||||
onChangePreset,
|
|
||||||
onDeletePreset,
|
|
||||||
}: TPresetItemProps) {
|
|
||||||
const { endpoint } = preset;
|
|
||||||
|
|
||||||
const icon = Icon({
|
|
||||||
size: 20,
|
|
||||||
endpoint: preset?.endpoint,
|
|
||||||
model: preset?.model,
|
|
||||||
error: false,
|
|
||||||
className: 'mr-2',
|
|
||||||
isCreatedByUser: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getPresetTitle = () => {
|
|
||||||
let _title = `${endpoint}`;
|
|
||||||
const { chatGptLabel, modelLabel, model, jailbreak, toneStyle } = preset;
|
|
||||||
|
|
||||||
if (endpoint === EModelEndpoint.azureOpenAI || endpoint === EModelEndpoint.openAI) {
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
if (chatGptLabel) {
|
|
||||||
_title += ` as ${chatGptLabel}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.google) {
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
if (modelLabel) {
|
|
||||||
_title += ` as ${modelLabel}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.bingAI) {
|
|
||||||
if (toneStyle) {
|
|
||||||
_title += `: ${toneStyle}`;
|
|
||||||
}
|
|
||||||
if (jailbreak) {
|
|
||||||
_title += ' as Sydney';
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.chatGPTBrowser) {
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.gptPlugins) {
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === null) {
|
|
||||||
null;
|
|
||||||
} else {
|
|
||||||
null;
|
|
||||||
}
|
|
||||||
return _title;
|
|
||||||
};
|
|
||||||
|
|
||||||
// regular model
|
|
||||||
return (
|
|
||||||
<DropdownMenuRadioItem
|
|
||||||
/* @ts-ignore, value can be an object as well */
|
|
||||||
value={value}
|
|
||||||
className="group flex h-10 max-h-[44px] flex-row justify-between dark:font-semibold dark:text-gray-200 dark:hover:bg-gray-800 sm:h-auto"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-start">
|
|
||||||
{icon}
|
|
||||||
<small className="text-[11px]">{preset?.title}</small>
|
|
||||||
<small className="invisible ml-1 flex w-0 flex-shrink text-[10px] sm:visible sm:w-auto">
|
|
||||||
({getPresetTitle()})
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full items-center justify-end">
|
|
||||||
<button
|
|
||||||
className="m-0 mr-1 h-full rounded-md px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:p-2 sm:group-hover:visible"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onChangePreset(preset);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="m-0 h-full rounded-md px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:p-2 sm:group-hover:visible"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onDeletePreset(preset);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PresetItem from './PresetItem';
|
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
|
||||||
|
|
||||||
export default function PresetItems({ presets, onSelect, onChangePreset, onDeletePreset }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{presets.map((preset: TPreset) => (
|
|
||||||
<PresetItem
|
|
||||||
key={preset?.presetId ?? Math.random()}
|
|
||||||
value={preset}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onChangePreset={onChangePreset}
|
|
||||||
onDeletePreset={onDeletePreset}
|
|
||||||
preset={preset}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as EndpointMenu } from './EndpointMenu';
|
|
|
@ -1,155 +0,0 @@
|
||||||
import { Settings2 } from 'lucide-react';
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
|
||||||
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
|
|
||||||
import { tPresetSchema, EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import { PluginStoreDialog } from '~/components';
|
|
||||||
import {
|
|
||||||
PopoverButtons,
|
|
||||||
EndpointSettings,
|
|
||||||
SaveAsPresetDialog,
|
|
||||||
EndpointOptionsPopover,
|
|
||||||
} from '~/components/Endpoints';
|
|
||||||
import { Button } from '~/components/ui';
|
|
||||||
import { cn, cardStyle } from '~/utils/';
|
|
||||||
import { useSetOptions } from '~/hooks';
|
|
||||||
import { ModelSelect } from './ModelSelect';
|
|
||||||
import { GenerationButtons } from './Generations';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function OptionsBar() {
|
|
||||||
const conversation = useRecoilValue(store.conversation);
|
|
||||||
const messagesTree = useRecoilValue(store.messagesTree);
|
|
||||||
const latestMessage = useRecoilValue(store.latestMessage);
|
|
||||||
const setShowBingToneSetting = useSetRecoilState(store.showBingToneSetting);
|
|
||||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
|
||||||
store.showPluginStoreDialog,
|
|
||||||
);
|
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
|
||||||
const [showPopover, setShowPopover] = useRecoilState(store.showPopover);
|
|
||||||
const [opacityClass, setOpacityClass] = useState('full-opacity');
|
|
||||||
const { setOption } = useSetOptions();
|
|
||||||
|
|
||||||
const { endpoint, conversationId, jailbreak } = conversation ?? {};
|
|
||||||
|
|
||||||
const altConditions: { [key: string]: boolean } = {
|
|
||||||
bingAI: !!(latestMessage && conversation?.jailbreak && endpoint === 'bingAI'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const altSettings: { [key: string]: () => void } = {
|
|
||||||
bingAI: () => setShowBingToneSetting((prev) => !prev),
|
|
||||||
};
|
|
||||||
|
|
||||||
const noSettings = useMemo<{ [key: string]: boolean }>(
|
|
||||||
() => ({
|
|
||||||
[EModelEndpoint.chatGPTBrowser]: true,
|
|
||||||
[EModelEndpoint.bingAI]: jailbreak ? false : conversationId !== 'new',
|
|
||||||
}),
|
|
||||||
[jailbreak, conversationId],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
} else if (messagesTree && messagesTree.length >= 1) {
|
|
||||||
setOpacityClass('show');
|
|
||||||
} else {
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}
|
|
||||||
}, [messagesTree, showPopover]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (endpoint && noSettings[endpoint]) {
|
|
||||||
setShowPopover(false);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [endpoint, noSettings]);
|
|
||||||
|
|
||||||
const saveAsPreset = () => {
|
|
||||||
setSaveAsDialogShow(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!endpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerAdvancedMode = altConditions[endpoint]
|
|
||||||
? altSettings[endpoint]
|
|
||||||
: () => setShowPopover((prev) => !prev);
|
|
||||||
return (
|
|
||||||
<div className="relative py-2 last:mb-2 md:mx-4 md:mb-[-16px] md:py-4 md:pt-2 md:last:mb-6 lg:mx-auto lg:mb-[-32px] lg:max-w-2xl lg:pt-6 xl:max-w-3xl">
|
|
||||||
<GenerationButtons
|
|
||||||
endpoint={endpoint}
|
|
||||||
showPopover={showPopover}
|
|
||||||
opacityClass={opacityClass}
|
|
||||||
/>
|
|
||||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'options-bar z-[61] flex w-full flex-wrap items-center justify-center gap-2',
|
|
||||||
showPopover ? '' : opacityClass,
|
|
||||||
)}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!messagesTree || messagesTree.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('show');
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('full-opacity');
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if (showPopover) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!messagesTree || messagesTree.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpacityClass('show');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModelSelect conversation={conversation} setOption={setOption} />
|
|
||||||
{!noSettings[endpoint] && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
cardStyle,
|
|
||||||
'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
|
|
||||||
)}
|
|
||||||
onClick={triggerAdvancedMode}
|
|
||||||
>
|
|
||||||
<Settings2 className="w-4 text-gray-600 dark:text-white" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<EndpointOptionsPopover
|
|
||||||
visible={showPopover}
|
|
||||||
saveAsPreset={saveAsPreset}
|
|
||||||
closePopover={() => setShowPopover(false)}
|
|
||||||
PopoverButtons={<PopoverButtons endpoint={endpoint} />}
|
|
||||||
>
|
|
||||||
<div className="px-4 py-4">
|
|
||||||
<EndpointSettings conversation={conversation} setOption={setOption} />
|
|
||||||
</div>
|
|
||||||
</EndpointOptionsPopover>
|
|
||||||
<SaveAsPresetDialog
|
|
||||||
open={saveAsDialogShow}
|
|
||||||
onOpenChange={setSaveAsDialogShow}
|
|
||||||
preset={tPresetSchema.parse({ ...conversation })}
|
|
||||||
/>
|
|
||||||
<PluginStoreDialog isOpen={showPluginStoreDialog} setIsOpen={setShowPluginStoreDialog} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { object, string } from 'zod';
|
import { object, string } from 'zod';
|
||||||
import { AuthKeys } from 'librechat-data-provider';
|
import { AuthKeys } from 'librechat-data-provider';
|
||||||
import type { TConfigProps } from '~/common';
|
import type { TConfigProps } from '~/common';
|
||||||
import FileUpload from '~/components/Input/EndpointMenu/FileUpload';
|
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
||||||
import { useLocalize, useMultipleKeys } from '~/hooks';
|
import { useLocalize, useMultipleKeys } from '~/hooks';
|
||||||
import InputWithLabel from './InputWithLabel';
|
import InputWithLabel from './InputWithLabel';
|
||||||
import { Label } from '~/components/ui';
|
import { Label } from '~/components/ui';
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { StopGeneratingIcon } from '~/components';
|
|
||||||
import { Settings } from 'lucide-react';
|
|
||||||
import { SetKeyDialog } from './SetKeyDialog';
|
|
||||||
import { useUserKey, useLocalize, useMediaQuery } from '~/hooks';
|
|
||||||
import { SendMessageIcon } from '~/components/svg';
|
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui/';
|
|
||||||
|
|
||||||
export default function SubmitButton({
|
|
||||||
conversation,
|
|
||||||
submitMessage,
|
|
||||||
handleStopGenerating,
|
|
||||||
disabled,
|
|
||||||
isSubmitting,
|
|
||||||
userProvidesKey,
|
|
||||||
hasText,
|
|
||||||
}) {
|
|
||||||
const { endpoint } = conversation;
|
|
||||||
const [isDialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const { checkExpiry } = useUserKey(endpoint);
|
|
||||||
const [isKeyProvided, setKeyProvided] = useState(userProvidesKey ? checkExpiry() : true);
|
|
||||||
const isKeyActive = checkExpiry();
|
|
||||||
const localize = useLocalize();
|
|
||||||
const dots = ['·', '··', '···'];
|
|
||||||
const [dotIndex, setDotIndex] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setDotIndex((prevDotIndex) => (prevDotIndex + 1) % dots.length);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [dots.length]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userProvidesKey) {
|
|
||||||
setKeyProvided(isKeyActive);
|
|
||||||
} else {
|
|
||||||
setKeyProvided(true);
|
|
||||||
}
|
|
||||||
}, [checkExpiry, endpoint, userProvidesKey, isKeyActive]);
|
|
||||||
|
|
||||||
const clickHandler = useCallback(
|
|
||||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
submitMessage();
|
|
||||||
},
|
|
||||||
[submitMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isSquareGreen, setIsSquareGreen] = useState(false);
|
|
||||||
|
|
||||||
const setKey = useCallback(() => {
|
|
||||||
setDialogOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
|
||||||
|
|
||||||
const iconContainerClass = `m-1 mr-0 rounded-md pb-[5px] pl-[6px] pr-[4px] pt-[5px] ${
|
|
||||||
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
|
|
||||||
} group-hover:bg-19C37D group-disabled:hover:bg-transparent dark:${
|
|
||||||
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
|
|
||||||
} dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent`;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsSquareGreen(hasText);
|
|
||||||
}, [hasText]);
|
|
||||||
|
|
||||||
if (isSubmitting && isSmallScreen) {
|
|
||||||
return (
|
|
||||||
<button onClick={handleStopGenerating} type="button">
|
|
||||||
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-200 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-800 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
|
||||||
<StopGeneratingIcon />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
} else if (isSubmitting) {
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-full">
|
|
||||||
<div
|
|
||||||
className="absolute text-2xl"
|
|
||||||
style={{ top: '50%', transform: 'translateY(-20%) translateX(-33px)' }}
|
|
||||||
>
|
|
||||||
{dots[dotIndex]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (!isKeyProvided) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={setKey}
|
|
||||||
type="button"
|
|
||||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-auto items-center justify-center bg-transparent pr-1 text-gray-500"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center rounded-md text-xs group-hover:bg-gray-200 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-800 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
|
||||||
<div className="m-0 mr-0 flex items-center justify-center rounded-md p-2 sm:p-2">
|
|
||||||
<Settings className="mr-1 inline-block h-auto w-[18px]" />
|
|
||||||
{localize('com_endpoint_config_key_name_placeholder')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{userProvidesKey && (
|
|
||||||
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={250}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={clickHandler}
|
|
||||||
disabled={disabled}
|
|
||||||
data-testid="submit-button"
|
|
||||||
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
|
||||||
>
|
|
||||||
<div className={iconContainerClass}>
|
|
||||||
{hasText ? (
|
|
||||||
<div className="bg-19C37D flex h-[24px] w-[24px] items-center justify-center rounded-full text-white">
|
|
||||||
<SendMessageIcon />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<SendMessageIcon />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" sideOffset={-5}>
|
|
||||||
{localize('com_nav_send_message')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
|
||||||
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
|
|
||||||
import React, { useEffect, useContext, useRef, useState, useCallback } from 'react';
|
|
||||||
|
|
||||||
import { EndpointMenu } from './EndpointMenu';
|
|
||||||
import SubmitButton from './SubmitButton';
|
|
||||||
import OptionsBar from './OptionsBar';
|
|
||||||
import Footer from './Footer';
|
|
||||||
|
|
||||||
import { useMessageHandler, ThemeContext } from '~/hooks';
|
|
||||||
import { cn, getEndpointField } from '~/utils';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
interface TextChatProps {
|
|
||||||
isSearchView?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TextChat({ isSearchView = false }: TextChatProps) {
|
|
||||||
const { ask, isSubmitting, handleStopGenerating, latestMessage, endpointsConfig } =
|
|
||||||
useMessageHandler();
|
|
||||||
const conversation = useRecoilValue(store.conversation);
|
|
||||||
const setShowBingToneSetting = useSetRecoilState(store.showBingToneSetting);
|
|
||||||
const [text, setText] = useRecoilState(store.text);
|
|
||||||
const { theme } = useContext(ThemeContext);
|
|
||||||
const isComposing = useRef(false);
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const [hasText, setHasText] = useState(false);
|
|
||||||
|
|
||||||
// TODO: do we need this?
|
|
||||||
const disabled = false;
|
|
||||||
|
|
||||||
const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error;
|
|
||||||
const { conversationId, jailbreak } = conversation || {};
|
|
||||||
|
|
||||||
// auto focus to input, when entering a conversation.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversationId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevents Settings from not showing on a new conversation, also prevents showing toneStyle change without jailbreak
|
|
||||||
if (conversationId === 'new' || !jailbreak) {
|
|
||||||
setShowBingToneSetting(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversationId !== 'search') {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
// setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [conversationId, jailbreak]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [isSubmitting]);
|
|
||||||
|
|
||||||
const submitMessage = () => {
|
|
||||||
ask({ text });
|
|
||||||
setText('');
|
|
||||||
setHasText(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' && isSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !isComposing?.current) {
|
|
||||||
submitMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.keyCode === 8 && e.currentTarget.value.trim() === '') {
|
|
||||||
setText(e.currentTarget.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter' && e.shiftKey) {
|
|
||||||
return console.log('Enter + Shift');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompositionStart = () => {
|
|
||||||
isComposing.current = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompositionEnd = () => {
|
|
||||||
isComposing.current = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeHandler = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const { value } = e.target;
|
|
||||||
|
|
||||||
setText(value);
|
|
||||||
updateHasText(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateHasText = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
setHasText(!!text.trim() || !!latestMessage?.error);
|
|
||||||
},
|
|
||||||
[setHasText, latestMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateHasText(text);
|
|
||||||
}, [text, latestMessage, updateHasText]);
|
|
||||||
|
|
||||||
const getPlaceholderText = () => {
|
|
||||||
if (isSearchView) {
|
|
||||||
return 'Click a message title to open its conversation.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
return 'Choose another model or customize GPT again';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNotAppendable) {
|
|
||||||
return 'Edit your message or Regenerate.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSearchView) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isDark = theme === 'dark';
|
|
||||||
|
|
||||||
if (theme === 'system') {
|
|
||||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="no-gradient-sm fixed bottom-0 left-0 w-full pt-6 sm:bg-gradient-to-b md:absolute md:w-[calc(100%-.5rem)]"
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(to bottom,
|
|
||||||
${isDark ? 'rgba(23, 23, 23, 0)' : 'rgba(255, 255, 255, 0)'},
|
|
||||||
${isDark ? 'rgba(23, 23, 23, 0.08)' : 'rgba(255, 255, 255, 0.08)'},
|
|
||||||
${isDark ? 'rgba(23, 23, 23, 0.38)' : 'rgba(255, 255, 255, 0.38)'},
|
|
||||||
${isDark ? 'rgba(23, 23, 23, 1)' : 'rgba(255, 255, 255, 1)'},
|
|
||||||
${isDark ? '#171717' : '#ffffff'})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<OptionsBar />
|
|
||||||
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient relative w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
|
|
||||||
<form className="stretch z-[60] mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
|
|
||||||
<div className="relative flex h-full flex-1 md:flex-col">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative flex flex-grow flex-row rounded-xl border border-black/10 py-[10px] md:py-4 md:pl-4',
|
|
||||||
'shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]',
|
|
||||||
'dark:border-gray-800/50 dark:text-white',
|
|
||||||
disabled ? 'bg-gray-200 dark:bg-gray-800' : 'bg-white dark:bg-gray-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<EndpointMenu />
|
|
||||||
<TextareaAutosize
|
|
||||||
// set test id for e2e testing
|
|
||||||
data-testid="text-input"
|
|
||||||
tabIndex={0}
|
|
||||||
autoFocus
|
|
||||||
ref={inputRef}
|
|
||||||
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
|
|
||||||
rows={1}
|
|
||||||
value={disabled || isNotAppendable ? '' : text}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onChange={changeHandler}
|
|
||||||
onCompositionStart={handleCompositionStart}
|
|
||||||
onCompositionEnd={handleCompositionEnd}
|
|
||||||
placeholder={getPlaceholderText()}
|
|
||||||
disabled={disabled || isNotAppendable}
|
|
||||||
className="m-0 flex h-auto max-h-52 flex-1 resize-none overflow-auto border-0 bg-transparent p-0 pl-2 pr-12 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-2"
|
|
||||||
/>
|
|
||||||
<SubmitButton
|
|
||||||
conversation={conversation}
|
|
||||||
submitMessage={submitMessage}
|
|
||||||
handleStopGenerating={handleStopGenerating}
|
|
||||||
disabled={disabled || isNotAppendable}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
userProvidesKey={
|
|
||||||
conversation?.endpoint
|
|
||||||
? getEndpointField(endpointsConfig, conversation.endpoint, 'userProvide')
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
hasText={hasText}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
|
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
|
||||||
import EndpointOptionsDialog from '../Endpoints/EndpointOptionsDialog';
|
|
||||||
import { Plugin } from '~/components/svg';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { cn } from '~/utils';
|
|
||||||
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
const MessageHeader = ({ isSearchView = false }) => {
|
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
|
||||||
const conversation = useRecoilValue(store.conversation);
|
|
||||||
const searchQuery = useRecoilValue(store.searchQuery);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { endpoint, model } = conversation;
|
|
||||||
|
|
||||||
if (!endpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNotClickable = endpoint === EModelEndpoint.chatGPTBrowser;
|
|
||||||
|
|
||||||
const plugins = (
|
|
||||||
<>
|
|
||||||
<Plugin /> <span className="px-1">•</span>
|
|
||||||
{/* <span className="py-0.25 ml-1 rounded bg-blue-200 px-1 text-[10px] font-semibold uppercase text-[#4559A4]">
|
|
||||||
beta
|
|
||||||
</span>
|
|
||||||
<span className="px-1">•</span> */}
|
|
||||||
{localize('com_ui_model')}: {model}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getConversationTitle = () => {
|
|
||||||
if (isSearchView) {
|
|
||||||
return `Search: ${searchQuery}`;
|
|
||||||
} else {
|
|
||||||
let _title = `${alternateName[endpoint] ?? endpoint}`;
|
|
||||||
|
|
||||||
if (endpoint === EModelEndpoint.azureOpenAI || endpoint === EModelEndpoint.openAI) {
|
|
||||||
const { chatGptLabel } = conversation;
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
if (chatGptLabel) {
|
|
||||||
_title += ` as ${chatGptLabel}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.google) {
|
|
||||||
_title = 'PaLM';
|
|
||||||
const { modelLabel, model } = conversation;
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
if (modelLabel) {
|
|
||||||
_title += ` as ${modelLabel}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.bingAI) {
|
|
||||||
const { jailbreak, toneStyle } = conversation;
|
|
||||||
if (toneStyle) {
|
|
||||||
_title += `: ${toneStyle}`;
|
|
||||||
}
|
|
||||||
if (jailbreak) {
|
|
||||||
_title += ' as Sydney';
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.chatGPTBrowser) {
|
|
||||||
if (model) {
|
|
||||||
_title += `: ${model}`;
|
|
||||||
}
|
|
||||||
} else if (endpoint === EModelEndpoint.gptPlugins) {
|
|
||||||
return plugins;
|
|
||||||
} else if (endpoint === EModelEndpoint.anthropic) {
|
|
||||||
_title = 'Claude';
|
|
||||||
} else if (endpoint === null) {
|
|
||||||
null;
|
|
||||||
} else {
|
|
||||||
null;
|
|
||||||
}
|
|
||||||
return _title;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex min-h-[60px] w-full flex-wrap items-center justify-between gap-3 border-b border-black/10 bg-white text-sm text-gray-500 transition-all hover:bg-gray-50 dark:border-gray-800/50 dark:bg-gray-800 dark:hover:bg-gray-700',
|
|
||||||
isNotClickable ? '' : 'cursor-pointer',
|
|
||||||
'sticky top-0 z-10',
|
|
||||||
)}
|
|
||||||
onClick={() => (isNotClickable ? null : setSaveAsDialogShow(true))}
|
|
||||||
>
|
|
||||||
<div className="flex flex-1 flex-grow items-center justify-center gap-1 p-1 text-gray-600 dark:text-gray-200 sm:p-0">
|
|
||||||
{getConversationTitle()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EndpointOptionsDialog
|
|
||||||
open={saveAsDialogShow}
|
|
||||||
onOpenChange={setSaveAsDialogShow}
|
|
||||||
preset={conversation as TPreset}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageHeader;
|
|
|
@ -1,124 +0,0 @@
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { CSSTransition } from 'react-transition-group';
|
|
||||||
|
|
||||||
import ScrollToBottom from './ScrollToBottom';
|
|
||||||
import MessageHeader from './MessageHeader';
|
|
||||||
import MultiMessage from './MultiMessage';
|
|
||||||
import { Spinner } from '~/components';
|
|
||||||
import { useScreenshot, useScrollToRef } from '~/hooks';
|
|
||||||
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function Messages({ isSearchView = false }) {
|
|
||||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
||||||
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const messagesTree = useRecoilValue(store.messagesTree);
|
|
||||||
const showPopover = useRecoilValue(store.showPopover);
|
|
||||||
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
|
||||||
const searchResultMessagesTree = useRecoilValue(store.searchResultMessagesTree);
|
|
||||||
|
|
||||||
const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree;
|
|
||||||
|
|
||||||
const conversation = useRecoilValue(store.conversation);
|
|
||||||
const { conversationId } = conversation ?? {};
|
|
||||||
|
|
||||||
const { screenshotTargetRef } = useScreenshot();
|
|
||||||
|
|
||||||
const checkIfAtBottom = useCallback(() => {
|
|
||||||
if (!scrollableRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
|
|
||||||
const diff = Math.abs(scrollHeight - scrollTop);
|
|
||||||
const percent = Math.abs(clientHeight - diff) / clientHeight;
|
|
||||||
const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15;
|
|
||||||
setShowScrollButton(hasScrollbar);
|
|
||||||
}, [scrollableRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
checkIfAtBottom();
|
|
||||||
}, 650);
|
|
||||||
|
|
||||||
// Add a listener on the window object
|
|
||||||
window.addEventListener('scroll', checkIfAtBottom);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
window.removeEventListener('scroll', checkIfAtBottom);
|
|
||||||
};
|
|
||||||
}, [_messagesTree, checkIfAtBottom]);
|
|
||||||
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
const debouncedHandleScroll = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = setTimeout(checkIfAtBottom, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollCallback = () => setShowScrollButton(false);
|
|
||||||
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
|
|
||||||
targetRef: messagesEndRef,
|
|
||||||
callback: scrollCallback,
|
|
||||||
smoothCallback: () => {
|
|
||||||
scrollCallback();
|
|
||||||
setAbortScroll(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-1 overflow-y-auto pt-0"
|
|
||||||
ref={scrollableRef}
|
|
||||||
onScroll={debouncedHandleScroll}
|
|
||||||
>
|
|
||||||
<div className="dark:gpt-dark-gray mb-32 h-auto md:mb-48" ref={screenshotTargetRef}>
|
|
||||||
<div className="dark:gpt-dark-gray flex h-auto flex-col items-center text-sm">
|
|
||||||
<MessageHeader isSearchView={isSearchView} />
|
|
||||||
{_messagesTree === null ? (
|
|
||||||
<div className="flex h-screen items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
) : _messagesTree?.length == 0 && isSearchView ? (
|
|
||||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
|
||||||
Nothing found
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MultiMessage
|
|
||||||
key={conversationId} // avoid internal state mixture
|
|
||||||
messageId={conversationId ?? null}
|
|
||||||
conversation={conversation}
|
|
||||||
messagesTree={_messagesTree}
|
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
currentEditId={currentEditId ?? null}
|
|
||||||
setCurrentEditId={setCurrentEditId}
|
|
||||||
isSearchView={isSearchView}
|
|
||||||
/>
|
|
||||||
<CSSTransition
|
|
||||||
in={showScrollButton}
|
|
||||||
timeout={400}
|
|
||||||
classNames="scroll-down"
|
|
||||||
unmountOnExit={false}
|
|
||||||
// appear
|
|
||||||
>
|
|
||||||
{() =>
|
|
||||||
showScrollButton &&
|
|
||||||
!showPopover && <ScrollToBottom scrollHandler={handleSmoothToRef} />
|
|
||||||
}
|
|
||||||
</CSSTransition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="dark:gpt-dark-gray group h-0 w-full flex-shrink-0 dark:border-gray-800/50"
|
|
||||||
ref={messagesEndRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function DataIcon() {
|
export default function DataIcon({ className = 'icon-sm' }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
|
@ -6,7 +6,7 @@ export default function DataIcon() {
|
||||||
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-sm"
|
className={className}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import {
|
|
||||||
useGetStartupConfig,
|
|
||||||
useGetMessagesByConvoId,
|
|
||||||
useGetConversationByIdMutation,
|
|
||||||
} from 'librechat-data-provider/react-query';
|
|
||||||
|
|
||||||
import Landing from '~/components/ui/Landing';
|
|
||||||
import Messages from '~/components/Messages/Messages';
|
|
||||||
import TextChat from '~/components/Input/TextChat';
|
|
||||||
|
|
||||||
import { useAuthContext, useConversation } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function Chat() {
|
|
||||||
const { isAuthenticated } = useAuthContext();
|
|
||||||
const [shouldNavigate, setShouldNavigate] = useState(true);
|
|
||||||
const searchQuery = useRecoilValue(store.searchQuery);
|
|
||||||
const [conversation, setConversation] = useRecoilState(store.conversation);
|
|
||||||
const setMessages = useSetRecoilState(store.messages);
|
|
||||||
const messagesTree = useRecoilValue(store.messagesTree);
|
|
||||||
const isSubmitting = useRecoilValue(store.isSubmitting);
|
|
||||||
const { newConversation } = useConversation();
|
|
||||||
const { conversationId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
//disabled by default, we only enable it when messagesTree is null
|
|
||||||
const messagesQuery = useGetMessagesByConvoId(conversationId ?? '', { enabled: !messagesTree });
|
|
||||||
const getConversationMutation = useGetConversationByIdMutation(conversationId ?? '');
|
|
||||||
const { data: config } = useGetStartupConfig();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [isAuthenticated, navigate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSubmitting && !shouldNavigate) {
|
|
||||||
setShouldNavigate(true);
|
|
||||||
}
|
|
||||||
}, [shouldNavigate, isSubmitting]);
|
|
||||||
|
|
||||||
// when conversation changed or conversationId (in url) changed
|
|
||||||
useEffect(() => {
|
|
||||||
// No current conversation and conversationId is 'new'
|
|
||||||
if (conversation === null && conversationId === 'new') {
|
|
||||||
newConversation();
|
|
||||||
setShouldNavigate(true);
|
|
||||||
}
|
|
||||||
// No current conversation and conversationId exists
|
|
||||||
else if (conversation === null && conversationId) {
|
|
||||||
getConversationMutation.mutate(conversationId, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
console.log('Conversation fetched successfully');
|
|
||||||
setConversation(data);
|
|
||||||
setShouldNavigate(true);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to fetch the conversation');
|
|
||||||
console.error(error);
|
|
||||||
navigate('/c/new');
|
|
||||||
newConversation();
|
|
||||||
setShouldNavigate(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setMessages(null);
|
|
||||||
}
|
|
||||||
// No current conversation and no conversationId
|
|
||||||
else if (conversation === null) {
|
|
||||||
navigate('/c/new');
|
|
||||||
setShouldNavigate(true);
|
|
||||||
}
|
|
||||||
// Current conversationId is 'search'
|
|
||||||
else if (conversation?.conversationId === 'search') {
|
|
||||||
navigate(`/search/${searchQuery}`);
|
|
||||||
setShouldNavigate(true);
|
|
||||||
}
|
|
||||||
// Conversation change and isSubmitting
|
|
||||||
else if (conversation?.conversationId !== conversationId && isSubmitting) {
|
|
||||||
setShouldNavigate(false);
|
|
||||||
}
|
|
||||||
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
|
|
||||||
// messagesTree is null when user navigates, but not on page refresh, so we need to navigate in this case
|
|
||||||
else if (conversation?.conversationId !== conversationId && !messagesTree) {
|
|
||||||
if (shouldNavigate) {
|
|
||||||
navigate(`/chat/${conversation?.conversationId}`);
|
|
||||||
} else {
|
|
||||||
setShouldNavigate(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.title = conversation?.title || config?.appTitle || 'Chat';
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [conversation, conversationId, config]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (messagesTree === null && conversation?.conversationId) {
|
|
||||||
messagesQuery.refetch({ queryKey: [conversation?.conversationId] });
|
|
||||||
}
|
|
||||||
}, [conversation?.conversationId, messagesQuery, messagesTree]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (messagesQuery.data) {
|
|
||||||
setMessages(messagesQuery.data);
|
|
||||||
} else if (messagesQuery.isError) {
|
|
||||||
console.error('failed to fetch the messages');
|
|
||||||
console.error(messagesQuery.error);
|
|
||||||
setMessages(null);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [messagesQuery.data, messagesQuery.isError, setMessages]);
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not a conversation
|
|
||||||
if (conversation?.conversationId === 'search') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// if conversationId not match
|
|
||||||
if (conversation?.conversationId !== conversationId && !conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// if conversationId is null
|
|
||||||
if (!conversationId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversationId && !messagesTree) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Messages />
|
|
||||||
<TextChat />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{conversationId === 'new' && !messagesTree?.length ? <Landing /> : <Messages />}
|
|
||||||
<TextChat />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
// import TextChat from '~/components/Input/TextChat';
|
||||||
import Messages from '~/components/Messages/Messages';
|
|
||||||
import TextChat from '~/components/Input/TextChat';
|
|
||||||
|
|
||||||
import { useConversation } from '~/hooks';
|
import { useConversation } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
@ -53,8 +51,8 @@ export default function Search() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Messages isSearchView={true} />
|
{/* <Messages isSearchView={true} /> */}
|
||||||
<TextChat isSearchView={true} />
|
{/* <TextChat isSearchView={true} /> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom';
|
import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom';
|
||||||
import Root from './Root';
|
import Root from './Root';
|
||||||
import Chat from './Chat';
|
|
||||||
import ChatRoute from './ChatRoute';
|
import ChatRoute from './ChatRoute';
|
||||||
import Search from './Search';
|
// import Search from './Search';
|
||||||
import {
|
import {
|
||||||
Login,
|
Login,
|
||||||
Registration,
|
Registration,
|
||||||
|
@ -51,14 +50,10 @@ export const router = createBrowserRouter([
|
||||||
path: 'c/:conversationId?',
|
path: 'c/:conversationId?',
|
||||||
element: <ChatRoute />,
|
element: <ChatRoute />,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
path: 'chat/:conversationId?',
|
// path: 'search/:query?',
|
||||||
element: <Chat />,
|
// element: <Search />,
|
||||||
},
|
// },
|
||||||
{
|
|
||||||
path: 'search/:query?',
|
|
||||||
element: <Search />,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { atom } from 'recoil';
|
import { atom } from 'recoil';
|
||||||
|
import { SettingsViews } from 'librechat-data-provider';
|
||||||
import type { TOptionSettings } from '~/common';
|
import type { TOptionSettings } from '~/common';
|
||||||
|
|
||||||
const abortScroll = atom<boolean>({
|
const abortScroll = atom<boolean>({
|
||||||
|
@ -26,6 +27,11 @@ const showAgentSettings = atom<boolean>({
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentSettingsView = atom<SettingsViews>({
|
||||||
|
key: 'currentSettingsView',
|
||||||
|
default: SettingsViews.default,
|
||||||
|
});
|
||||||
|
|
||||||
const showBingToneSetting = atom<boolean>({
|
const showBingToneSetting = atom<boolean>({
|
||||||
key: 'showBingToneSetting',
|
key: 'showBingToneSetting',
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -137,6 +143,7 @@ export default {
|
||||||
optionSettings,
|
optionSettings,
|
||||||
showPluginStoreDialog,
|
showPluginStoreDialog,
|
||||||
showAgentSettings,
|
showAgentSettings,
|
||||||
|
currentSettingsView,
|
||||||
showBingToneSetting,
|
showBingToneSetting,
|
||||||
showPopover,
|
showPopover,
|
||||||
autoScroll,
|
autoScroll,
|
||||||
|
|
|
@ -18,6 +18,11 @@ export const defaultRetrievalModels = [
|
||||||
'gpt-4-1106',
|
'gpt-4-1106',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export enum SettingsViews {
|
||||||
|
default = 'default',
|
||||||
|
advanced = 'advanced',
|
||||||
|
}
|
||||||
|
|
||||||
export const fileSourceSchema = z.nativeEnum(FileSources);
|
export const fileSourceSchema = z.nativeEnum(FileSources);
|
||||||
|
|
||||||
export const modelConfigSchema = z
|
export const modelConfigSchema = z
|
||||||
|
|
|
@ -3,6 +3,7 @@ export enum FileSources {
|
||||||
firebase = 'firebase',
|
firebase = 'firebase',
|
||||||
openai = 'openai',
|
openai = 'openai',
|
||||||
s3 = 's3',
|
s3 = 's3',
|
||||||
|
vectordb = 'vectordb',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FileContext {
|
export enum FileContext {
|
||||||
|
|
2
rag.yml
2
rag.yml
|
@ -22,8 +22,6 @@ services:
|
||||||
- POSTGRES_PASSWORD=mypassword
|
- POSTGRES_PASSWORD=mypassword
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
|
||||||
- ./uploads/temp:/app/uploads/temp
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- vectordb
|
- vectordb
|
||||||
env_file:
|
env_file:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue