📂 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:
Danny Avila 2024-03-22 19:07:08 -04:00 committed by GitHub
parent f427ad792a
commit 45a95acec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 715 additions and 2046 deletions

View file

@ -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

View 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,
};

View file

@ -0,0 +1,5 @@
const crud = require('./crud');
module.exports = {
...crud,
};

View file

@ -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, {

View file

@ -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');
} }

View file

@ -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

View file

@ -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>
);
}

View file

@ -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]" />

View file

@ -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>
); );
} }

View file

@ -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>

View file

@ -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';

View 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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
} }

View file

@ -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>
);
}

View 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>
);
}

View file

@ -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';

View file

@ -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';

View file

@ -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} />
)}
</>
);
}

View file

@ -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}
/>
))}
</>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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}
/>
))}
</>
);
}

View file

@ -1 +0,0 @@
export { default as EndpointMenu } from './EndpointMenu';

View file

@ -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>
);
}

View file

@ -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';

View file

@ -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>
);
}
}

View file

@ -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>
</>
);
}

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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"

View file

@ -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 />
</>
);
}

View file

@ -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} /> */}
</> </>
); );
} }

View file

@ -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 />,
},
], ],
}, },
], ],

View file

@ -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,

View file

@ -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

View file

@ -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 {

View file

@ -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: