diff --git a/api/app/clients/prompts/createContextHandlers.js b/api/app/clients/prompts/createContextHandlers.js index 22eefb01252..3d8da24e564 100644 --- a/api/app/clients/prompts/createContextHandlers.js +++ b/api/app/clients/prompts/createContextHandlers.js @@ -1,4 +1,15 @@ 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) { if (!process.env.RAG_API_URL) { @@ -9,25 +20,37 @@ function createContextHandlers(req, userMessageContent) { const processedFiles = []; const processedIds = new Set(); 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) => { if (file.embedded && !processedIds.has(file.file_id)) { try { - const promise = 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 promise = query(file); queryPromises.push(promise); processedFiles.push(file); processedIds.add(file.file_id); @@ -43,67 +66,83 @@ function createContextHandlers(req, userMessageContent) { 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 + ? '' + : ` + ` + }${processedFiles + .map( + (file) => ` + + ${file.filename} + ${file.type} + `, + ) + .join('')}${ + oneFile + ? '' + : ` + ` + }`; + const resolvedQueries = await Promise.all(queryPromises); const context = resolvedQueries .map((queryResult, index) => { const file = processedFiles[index]; - const contextItems = queryResult.data + let contextItems = queryResult.data; + + const generateContext = (currentContext) => + ` + + ${file.filename} + ${currentContext} + + `; + + if (useFullContext) { + return generateContext(`\n${contextItems}`); + } + + contextItems = queryResult.data .map((item) => { const pageContent = item[0].page_content; return ` - - - `; + + `; }) .join(''); - return ` - - ${file.filename} - - ${contextItems} - - - `; + return generateContext(contextItems); }) .join(''); - const template = `The user has attached ${ - processedFiles.length === 1 ? 'a' : processedFiles.length - } file${processedFiles.length !== 1 ? 's' : ''} to the conversation: - - - ${processedFiles - .map( - (file) => ` - - ${file.filename} - ${file.type} - - `, - ) - .join('')} - + if (useFullContext) { + const prompt = `${header} + ${context} + ${footer}`; + + return prompt; + } + + const prompt = `${header} + ${files} A semantic search was executed with the user's message as the query, retrieving the following context inside XML tags. - - ${context} + ${context} - 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. - `; + ${footer}`; - return template; + return prompt; } catch (error) { console.error('Error creating context:', error); throw error; // Re-throw the error to propagate it to the caller diff --git a/api/server/services/Files/VectorDB/crud.js b/api/server/services/Files/VectorDB/crud.js new file mode 100644 index 00000000000..5a7d807a143 --- /dev/null +++ b/api/server/services/Files/VectorDB/crud.js @@ -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} + * 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, +}; diff --git a/api/server/services/Files/VectorDB/index.js b/api/server/services/Files/VectorDB/index.js new file mode 100644 index 00000000000..a6223d1ee5d --- /dev/null +++ b/api/server/services/Files/VectorDB/index.js @@ -0,0 +1,5 @@ +const crud = require('./crud'); + +module.exports = { + ...crud, +}; diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 69f8011dfc7..86efc570797 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -1,6 +1,5 @@ const path = require('path'); const { v4 } = require('uuid'); -const axios = require('axios'); const mime = require('mime/lite'); const { isUUID, @@ -265,50 +264,22 @@ const uploadImageBuffer = async ({ req, context }) => { */ const processFileUpload = async ({ req, res, file, metadata }) => { 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 { 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} */ let openai; if (source === FileSources.openai) { ({ 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) { await openai.beta.assistants.files.create(metadata.assistant_id, { diff --git a/api/server/services/Files/strategies.js b/api/server/services/Files/strategies.js index ecbe09c1e3d..bf0c8d051a4 100644 --- a/api/server/services/Files/strategies.js +++ b/api/server/services/Files/strategies.js @@ -5,22 +5,20 @@ const { saveURLToFirebase, deleteFirebaseFile, saveBufferToFirebase, - uploadFileToFirebase, uploadImageToFirebase, processFirebaseAvatar, } = require('./Firebase'); const { - // saveLocalFile, getLocalFileURL, saveFileFromURL, saveLocalBuffer, deleteLocalFile, - uploadLocalFile, uploadLocalImage, prepareImagesLocal, processLocalAvatar, } = require('./Local'); const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI'); +const { uploadVectors, deleteVectors } = require('./VectorDB'); /** * Firebase Storage Strategy Functions @@ -28,13 +26,14 @@ const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI'); * */ const firebaseStrategy = () => ({ // saveFile: + /** @type {typeof uploadVectors | null} */ + handleFileUpload: null, saveURL: saveURLToFirebase, getFileURL: getFirebaseURL, deleteFile: deleteFirebaseFile, saveBuffer: saveBufferToFirebase, prepareImagePayload: prepareImageURL, processAvatar: processFirebaseAvatar, - handleFileUpload: uploadFileToFirebase, handleImageUpload: uploadImageToFirebase, }); @@ -43,17 +42,38 @@ const firebaseStrategy = () => ({ * * */ const localStrategy = () => ({ - // saveFile: saveLocalFile, + /** @type {typeof uploadVectors | null} */ + handleFileUpload: null, saveURL: saveFileFromURL, getFileURL: getLocalFileURL, saveBuffer: saveLocalBuffer, deleteFile: deleteLocalFile, processAvatar: processLocalAvatar, - handleFileUpload: uploadLocalFile, handleImageUpload: uploadLocalImage, 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 * @@ -84,6 +104,8 @@ const getStrategyFunctions = (fileSource) => { return localStrategy(); } else if (fileSource === FileSources.openai) { return openAIStrategy(); + } else if (fileSource === FileSources.vectordb) { + return vectorStrategy(); } else { throw new Error('Invalid file source'); } diff --git a/client/src/components/Input/EndpointMenu/FileUpload.tsx b/client/src/components/Chat/Input/Files/FileUpload.tsx similarity index 100% rename from client/src/components/Input/EndpointMenu/FileUpload.tsx rename to client/src/components/Chat/Input/Files/FileUpload.tsx diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx index c0fcf61c710..6470b84104a 100644 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ b/client/src/components/Chat/Input/HeaderOptions.tsx @@ -4,7 +4,7 @@ import { Root, Anchor } from '@radix-ui/react-popover'; import { useState, useEffect, useMemo } from 'react'; import { tPresetUpdateSchema, EModelEndpoint } 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 { PluginStoreDialog } from '~/components'; import OptionsPopover from './OptionsPopover'; @@ -15,7 +15,7 @@ import { Button } from '~/components/ui'; import { cn, cardStyle } from '~/utils/'; import store from '~/store'; -export default function OptionsBar() { +export default function HeaderOptions() { const [saveAsDialogShow, setSaveAsDialogShow] = useState(false); const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState( store.showPluginStoreDialog, @@ -102,6 +102,7 @@ export default function OptionsBar() { setOption={setOption} isMultiChat={true} /> + (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 ( -
- - -
{ - 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'); - }} - > - - {!noSettings[endpoint] && ( - - )} -
- setShowPopover(false)} - PopoverButtons={} - > -
- -
-
- - -
-
- ); -} diff --git a/client/src/components/Chat/Input/OptionsPopover.tsx b/client/src/components/Chat/Input/OptionsPopover.tsx index e936156f8aa..f59d94d26dc 100644 --- a/client/src/components/Chat/Input/OptionsPopover.tsx +++ b/client/src/components/Chat/Input/OptionsPopover.tsx @@ -63,7 +63,7 @@ export default function OptionsPopover({
- ))} +
+
+ {endpointButtons.map((button, index) => ( + + ))} +
+ {disabled ? null : ( +
+ {additionalButtons[settingsView].map((button, index) => ( + + ))} +
+ )}
); } diff --git a/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx b/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx index 488262cd9e9..da9bd8c113d 100644 --- a/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx +++ b/client/src/components/Chat/Menus/Presets/EditPresetDialog.tsx @@ -30,7 +30,7 @@ const EditPresetDialog = ({ select: mapEndpoints, }); - const { endpoint } = preset || {}; + const { endpoint, endpointType, model } = preset || {}; if (!endpoint) { return null; } @@ -81,7 +81,7 @@ const EditPresetDialog = ({ />
-
+
diff --git a/client/src/components/Chat/Menus/Presets/PresetItems.tsx b/client/src/components/Chat/Menus/Presets/PresetItems.tsx index 965300c53e8..957a7b06bbf 100644 --- a/client/src/components/Chat/Menus/Presets/PresetItems.tsx +++ b/client/src/components/Chat/Menus/Presets/PresetItems.tsx @@ -5,7 +5,7 @@ import { Flipper, Flipped } from 'react-flip-toolkit'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { FC } from 'react'; 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 DialogTemplate from '~/components/ui/DialogTemplate'; import { getPresetTitle, getEndpointField } from '~/utils'; diff --git a/client/src/components/Endpoints/AlternativeSettings.tsx b/client/src/components/Endpoints/AlternativeSettings.tsx new file mode 100644 index 00000000000..6420e369b75 --- /dev/null +++ b/client/src/components/Endpoints/AlternativeSettings.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/client/src/components/Endpoints/EditPresetDialog.tsx b/client/src/components/Endpoints/EditPresetDialog.tsx deleted file mode 100644 index 9dcffbbf901..00000000000 --- a/client/src/components/Endpoints/EditPresetDialog.tsx +++ /dev/null @@ -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 ( - - -
-
-
- - 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, - )} - /> -
-
- - setOption('endpoint')(value)} - options={availableEndpoints} - className={cn()} - /> -
-
-
-
- - -
-
-
-
-
- -
-
- } - buttons={ -
- - {localize('com_endpoint_export')} - - - {localize('com_ui_save')} - -
- } - /> -
- ); -}; - -export default EditPresetDialog; diff --git a/client/src/components/Endpoints/EndpointOptionsDialog.tsx b/client/src/components/Endpoints/EndpointOptionsDialog.tsx deleted file mode 100644 index d25036ed1b6..00000000000 --- a/client/src/components/Endpoints/EndpointOptionsDialog.tsx +++ /dev/null @@ -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 ( - <> - - -
- - -
-
- } - buttons={ -
- - {localize('com_endpoint_export')} - - - {localize('com_endpoint_save_as_preset')} - -
- } - /> - - - - ); -}; - -export default EndpointOptionsDialog; diff --git a/client/src/components/Endpoints/EndpointSettings.tsx b/client/src/components/Endpoints/EndpointSettings.tsx index 3a3c04f069b..f276d63e37e 100644 --- a/client/src/components/Endpoints/EndpointSettings.tsx +++ b/client/src/components/Endpoints/EndpointSettings.tsx @@ -1,4 +1,5 @@ import { useRecoilValue } from 'recoil'; +import { SettingsViews } from 'librechat-data-provider'; import type { TSettingsProps } from '~/common'; import { getSettings } from './Settings'; import { cn } from '~/utils'; @@ -12,7 +13,8 @@ export default function Settings({ isMultiChat = false, }: TSettingsProps & { isMultiChat?: boolean }) { const modelsConfig = useRecoilValue(store.modelsConfig); - if (!conversation?.endpoint) { + const currentSettingsView = useRecoilValue(store.currentSettingsView); + if (!conversation?.endpoint || currentSettingsView !== SettingsViews.default) { return null; } diff --git a/client/src/components/Endpoints/PopoverButtons.tsx b/client/src/components/Endpoints/PopoverButtons.tsx deleted file mode 100644 index 8d7f66da83e..00000000000 --- a/client/src/components/Endpoints/PopoverButtons.tsx +++ /dev/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: , - }, - ], - gptPlugins: [ - { - label: localize( - 'com_endpoint_show_what_settings', - showAgentSettings ? localize('com_endpoint_completion') : localize('com_endpoint_agent'), - ), - buttonClass: '', - handler: () => setShowAgentSettings((prev) => !prev), - icon: , - }, - ], - }; - - const endpointButtons = buttons[endpoint]; - if (!endpointButtons) { - return null; - } - - return ( -
- {endpointButtons.map((button, index) => ( - - ))} -
- ); -} diff --git a/client/src/components/Endpoints/Settings/Advanced.tsx b/client/src/components/Endpoints/Settings/Advanced.tsx new file mode 100644 index 00000000000..258849e85a2 --- /dev/null +++ b/client/src/components/Endpoints/Settings/Advanced.tsx @@ -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) { + 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 ( +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ setTemperature(value[0])} + doubleClickHandler={() => setTemperature(1)} + max={2} + min={0} + step={0.01} + className="flex h-4 w-full" + /> +
+ +
+ + +
+ + 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', + ), + )} + /> +
+ setTopP(value[0])} + doubleClickHandler={() => setTopP(1)} + max={1} + min={0} + step={0.01} + className="flex h-4 w-full" + /> +
+ +
+ + + +
+ + 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', + ), + )} + /> +
+ setFreqP(value[0])} + doubleClickHandler={() => setFreqP(0)} + max={2} + min={-2} + step={0.01} + className="flex h-4 w-full" + /> +
+ +
+ + + +
+ + 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', + ), + )} + /> +
+ setPresP(value[0])} + doubleClickHandler={() => setPresP(0)} + max={2} + min={-2} + step={0.01} + className="flex h-4 w-full" + /> +
+ +
+
+
+ + + +
+
+ + + setResendFiles(checked)} + disabled={readonly} + className="flex" + /> + + + + + + setImageDetail(imageDetailValue[value[0]])} + doubleClickHandler={() => setImageDetail(ImageDetail.auto)} + max={2} + min={0} + step={1} + /> + + + +
+
+
+
+ ); +} diff --git a/client/src/components/Endpoints/Settings/index.ts b/client/src/components/Endpoints/Settings/index.ts index 436946b2178..7d525d8a576 100644 --- a/client/src/components/Endpoints/Settings/index.ts +++ b/client/src/components/Endpoints/Settings/index.ts @@ -1,3 +1,4 @@ +export { default as Advanced } from './Advanced'; export { default as AssistantsSettings } from './Assistants'; export { default as OpenAISettings } from './OpenAI'; export { default as BingAISettings } from './BingAI'; diff --git a/client/src/components/Endpoints/index.ts b/client/src/components/Endpoints/index.ts index 71dd9855182..02f21d167b6 100644 --- a/client/src/components/Endpoints/index.ts +++ b/client/src/components/Endpoints/index.ts @@ -1,7 +1,6 @@ export { default as Icon } from './Icon'; export { default as MinimalIcon } from './MinimalIcon'; -export { default as PopoverButtons } from './PopoverButtons'; export { default as EndpointSettings } from './EndpointSettings'; export { default as SaveAsPresetDialog } from './SaveAsPresetDialog'; -export { default as EndpointOptionsDialog } from './EndpointOptionsDialog'; +export { default as AlternativeSettings } from './AlternativeSettings'; export { default as EndpointOptionsPopover } from './EndpointOptionsPopover'; diff --git a/client/src/components/Input/EndpointMenu/EndpointItem.tsx b/client/src/components/Input/EndpointMenu/EndpointItem.tsx deleted file mode 100644 index f1febf116ec..00000000000 --- a/client/src/components/Input/EndpointMenu/EndpointItem.tsx +++ /dev/null @@ -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 ( - <> - - {icon} - {alternateName[endpoint] || endpoint} - {endpoint === 'gptPlugins' && ( - - Beta - - )} -
- {userProvidesKey ? ( - - ) : null} - - {userProvidesKey && ( - - )} - - ); -} diff --git a/client/src/components/Input/EndpointMenu/EndpointItems.tsx b/client/src/components/Input/EndpointMenu/EndpointItems.tsx deleted file mode 100644 index 73ea4b8a825..00000000000 --- a/client/src/components/Input/EndpointMenu/EndpointItems.tsx +++ /dev/null @@ -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) => ( - - ))} - - ); -} diff --git a/client/src/components/Input/EndpointMenu/EndpointMenu.jsx b/client/src/components/Input/EndpointMenu/EndpointMenu.jsx deleted file mode 100644 index aa0e7724bdd..00000000000 --- a/client/src/components/Input/EndpointMenu/EndpointMenu.jsx +++ /dev/null @@ -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 ( - - - - - - - - - - - {localize('com_endpoint_open_menu')} - - event.preventDefault()} - side="top" - > - setShowEndpoints((prev) => !prev)} - > - {showEndpoints ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '} - {localize('com_endpoint')} - - - - {showEndpoints && - (availableEndpoints.length ? ( - - ) : ( - - {localize('com_endpoint_not_available')} - - ))} - - -
- - - setShowPresets((prev) => !prev)} - > - {showPresets ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '} - {localize('com_endpoint_presets')} - - - - - - - - - - - - {showPresets && - (presets.length ? ( - - ) : ( - - {localize('com_endpoint_no_presets')} - - ))} - - - -
-
-
- ); -} diff --git a/client/src/components/Input/EndpointMenu/PresetItem.tsx b/client/src/components/Input/EndpointMenu/PresetItem.tsx deleted file mode 100644 index acd0cec0327..00000000000 --- a/client/src/components/Input/EndpointMenu/PresetItem.tsx +++ /dev/null @@ -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 ( - -
- {icon} - {preset?.title} - - ({getPresetTitle()}) - -
-
- - -
-
- ); -} diff --git a/client/src/components/Input/EndpointMenu/PresetItems.tsx b/client/src/components/Input/EndpointMenu/PresetItems.tsx deleted file mode 100644 index 5e6e47b5097..00000000000 --- a/client/src/components/Input/EndpointMenu/PresetItems.tsx +++ /dev/null @@ -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) => ( - - ))} - - ); -} diff --git a/client/src/components/Input/EndpointMenu/index.ts b/client/src/components/Input/EndpointMenu/index.ts deleted file mode 100644 index ac2f9d3dbf4..00000000000 --- a/client/src/components/Input/EndpointMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as EndpointMenu } from './EndpointMenu'; diff --git a/client/src/components/Input/OptionsBar.tsx b/client/src/components/Input/OptionsBar.tsx deleted file mode 100644 index b786f21b704..00000000000 --- a/client/src/components/Input/OptionsBar.tsx +++ /dev/null @@ -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(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 ( -
- - -
{ - 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'); - }} - > - - {!noSettings[endpoint] && ( - - )} -
- setShowPopover(false)} - PopoverButtons={} - > -
- -
-
- - -
-
- ); -} diff --git a/client/src/components/Input/SetKeyDialog/GoogleConfig.tsx b/client/src/components/Input/SetKeyDialog/GoogleConfig.tsx index 8a1827cc28a..51abdd00557 100644 --- a/client/src/components/Input/SetKeyDialog/GoogleConfig.tsx +++ b/client/src/components/Input/SetKeyDialog/GoogleConfig.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { object, string } from 'zod'; import { AuthKeys } from 'librechat-data-provider'; 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 InputWithLabel from './InputWithLabel'; import { Label } from '~/components/ui'; diff --git a/client/src/components/Input/SubmitButton.tsx b/client/src/components/Input/SubmitButton.tsx deleted file mode 100644 index dd77d426df3..00000000000 --- a/client/src/components/Input/SubmitButton.tsx +++ /dev/null @@ -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) => { - 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 ( - - ); - } else if (isSubmitting) { - return ( -
-
- {dots[dotIndex]} -
-
- ); - } else if (!isKeyProvided) { - return ( - <> - - {userProvidesKey && ( - - )} - - ); - } else { - return ( - - - - - - - {localize('com_nav_send_message')} - - - - ); - } -} diff --git a/client/src/components/Input/TextChat.tsx b/client/src/components/Input/TextChat.tsx deleted file mode 100644 index cf4a5e07d30..00000000000 --- a/client/src/components/Input/TextChat.tsx +++ /dev/null @@ -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(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) => { - 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) => { - 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 ( - <> -
- -
-
-
-
- - - -
-
-
-
-
-
- - ); -} diff --git a/client/src/components/Messages/MessageHeader.tsx b/client/src/components/Messages/MessageHeader.tsx deleted file mode 100644 index d8e7703180d..00000000000 --- a/client/src/components/Messages/MessageHeader.tsx +++ /dev/null @@ -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 = ( - <> - - {/* - beta - - */} - {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 ( - <> -
(isNotClickable ? null : setSaveAsDialogShow(true))} - > -
- {getConversationTitle()} -
-
- - - - ); -}; - -export default MessageHeader; diff --git a/client/src/components/Messages/Messages.tsx b/client/src/components/Messages/Messages.tsx deleted file mode 100644 index 0e49b20866c..00000000000 --- a/client/src/components/Messages/Messages.tsx +++ /dev/null @@ -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(-1); - const [showScrollButton, setShowScrollButton] = useState(false); - const scrollableRef = useRef(null); - const messagesEndRef = useRef(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 | 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 ( -
-
-
- - {_messagesTree === null ? ( -
- -
- ) : _messagesTree?.length == 0 && isSearchView ? ( -
- Nothing found -
- ) : ( - <> - - - {() => - showScrollButton && - !showPopover && - } - - - )} -
-
-
-
- ); -} diff --git a/client/src/components/svg/DataIcon.tsx b/client/src/components/svg/DataIcon.tsx index 6f2be34e244..86e6f8c5693 100644 --- a/client/src/components/svg/DataIcon.tsx +++ b/client/src/components/svg/DataIcon.tsx @@ -1,4 +1,4 @@ -export default function DataIcon() { +export default function DataIcon({ className = 'icon-sm' }: { className?: string }) { return ( { - 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 ( - <> - - - - ); - } - - return ( - <> - {conversationId === 'new' && !messagesTree?.length ? : } - - - ); -} diff --git a/client/src/routes/Search.tsx b/client/src/routes/Search.tsx index 692305434f1..89c9ae4e2de 100644 --- a/client/src/routes/Search.tsx +++ b/client/src/routes/Search.tsx @@ -1,9 +1,7 @@ import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; - -import Messages from '~/components/Messages/Messages'; -import TextChat from '~/components/Input/TextChat'; +// import TextChat from '~/components/Input/TextChat'; import { useConversation } from '~/hooks'; import store from '~/store'; @@ -53,8 +51,8 @@ export default function Search() { return ( <> - - + {/* */} + {/* */} ); } diff --git a/client/src/routes/index.tsx b/client/src/routes/index.tsx index 0f2e19965e0..b35a62b5378 100644 --- a/client/src/routes/index.tsx +++ b/client/src/routes/index.tsx @@ -1,8 +1,7 @@ import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom'; import Root from './Root'; -import Chat from './Chat'; import ChatRoute from './ChatRoute'; -import Search from './Search'; +// import Search from './Search'; import { Login, Registration, @@ -51,14 +50,10 @@ export const router = createBrowserRouter([ path: 'c/:conversationId?', element: , }, - { - path: 'chat/:conversationId?', - element: , - }, - { - path: 'search/:query?', - element: , - }, + // { + // path: 'search/:query?', + // element: , + // }, ], }, ], diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 0e14a24c026..195ed85f57b 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -1,4 +1,5 @@ import { atom } from 'recoil'; +import { SettingsViews } from 'librechat-data-provider'; import type { TOptionSettings } from '~/common'; const abortScroll = atom({ @@ -26,6 +27,11 @@ const showAgentSettings = atom({ default: false, }); +const currentSettingsView = atom({ + key: 'currentSettingsView', + default: SettingsViews.default, +}); + const showBingToneSetting = atom({ key: 'showBingToneSetting', default: false, @@ -137,6 +143,7 @@ export default { optionSettings, showPluginStoreDialog, showAgentSettings, + currentSettingsView, showBingToneSetting, showPopover, autoScroll, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 879942ab835..d9b2f42c875 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -18,6 +18,11 @@ export const defaultRetrievalModels = [ 'gpt-4-1106', ]; +export enum SettingsViews { + default = 'default', + advanced = 'advanced', +} + export const fileSourceSchema = z.nativeEnum(FileSources); export const modelConfigSchema = z diff --git a/packages/data-provider/src/types/files.ts b/packages/data-provider/src/types/files.ts index a39a4349218..484c471a4e1 100644 --- a/packages/data-provider/src/types/files.ts +++ b/packages/data-provider/src/types/files.ts @@ -3,6 +3,7 @@ export enum FileSources { firebase = 'firebase', openai = 'openai', s3 = 's3', + vectordb = 'vectordb', } export enum FileContext { diff --git a/rag.yml b/rag.yml index 3f57584791c..690744b4ac0 100644 --- a/rag.yml +++ b/rag.yml @@ -22,8 +22,6 @@ services: - POSTGRES_PASSWORD=mypassword ports: - "8000:8000" - volumes: - - ./uploads/temp:/app/uploads/temp depends_on: - vectordb env_file: