mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🌿 feat: Multi-response Streaming (#3191)
* chore: comment back handlePlusCommand * chore: ignore .git dir * refactor: pass newConversation to `useSelectMention` refactor: pass newConversation to Mention component refactor: useChatFunctions for modular use of `ask` and `regenerate` refactor: set latest message only for the first index in useChatFunctions refactor: pass setLatestMessage to useChatFunctions refactor: Pass setSubmission to useChatFunctions for submission handling refactor: consolidate event handlers to separate hook from useSSE WIP: additional response handlers feat: responsive added convo, clears on new chat/navigating to chat, assistants excluded feat: Add conversationByKeySelector to select any conversation by index WIP: handle second submission with messages paired to root * style: surface-primary-contrast * refactor: remove unnecessary console.log statement in useChatFunctions * refactor: Consolidate imports in ChatForm and Input hooks * refactor: compositional usage of useSSE for multiple streams * WIP: set latest 'multi' message * WIP: first pass, added response streaming * pass: performant multi-message stream * fix: styling and message render * second pass: modular, performant multi-stream * fix: align parentMessageId of multiMessage * refactor: move resetting latestMultiMessage * chore: update footer text in Chat component * fix: stop button styling * fix: handle abortMessage request for multi-response * clear messages but bug with latest message reset present * fix: add delay for additional message generation * fix: access LAST_CONVO_SETUP by index * style: add div to prevent layout shift before hover buttons render * chore: Update Message component styling for card messages * chore: move hook use order * fix: abort middleware using unsent field from req.body * feat: support multi-response stream from initial message * refactor: buildTree function to improve readability and remove unused code * feat: add logger for frontend dev * refactor: use depth to track if message is really last in its branch * fix(buildTree): default export * fix: share parent message Id and avoid duplication error for multi-response streams * fix: prevent addedConvo reset to response convo * feat: allow setting multi message as latest message to control which to respond to * chore: wrap setSiblingIdxRev with useCallback * chore: styling and allow editing messages * style: styling fixes * feat: Add "AddMultiConvo" component to Chat Header * feat: prevent clearing added convos on endpoint, preset, mention, or modelSpec switch * fix: message styling fixes, mainly related to code blocks * fix: stop button visibility logic * fix: Handle edge case in abortMiddleware for non-existant `abortControllers` * refactor: optimize/memoize icons * chore(GoogleClient): change info to debug logs * style: active message styling * style: prevent layout shift due to placeholder row * chore: remove unused code * fix: Update BaseClient to handle optional request body properties * fix(ci): `onStart` now accepts 2 args, the 2nd being responseMessageId * chore: bump data-provider
This commit is contained in:
parent
eef894e608
commit
156c52e293
72 changed files with 2697 additions and 1326 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -11,6 +11,7 @@ logs
|
|||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
.git
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
@ -45,6 +46,7 @@ api/node_modules/
|
|||
client/node_modules/
|
||||
bower_components/
|
||||
*.d.ts
|
||||
!vite-env.d.ts
|
||||
|
||||
# Floobits
|
||||
.floo
|
||||
|
|
|
@ -19,6 +19,10 @@ class BaseClient {
|
|||
day: 'numeric',
|
||||
});
|
||||
this.fetch = this.fetch.bind(this);
|
||||
/** @type {boolean} */
|
||||
this.skipSaveConvo = false;
|
||||
/** @type {boolean} */
|
||||
this.skipSaveUserMessage = false;
|
||||
}
|
||||
|
||||
setOptions() {
|
||||
|
@ -84,19 +88,45 @@ class BaseClient {
|
|||
await stream.processTextStream(onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {[string|undefined, string|undefined]}
|
||||
*/
|
||||
processOverideIds() {
|
||||
/** @type {Record<string, string | undefined>} */
|
||||
let { overrideConvoId, overrideUserMessageId } = this.options?.req?.body ?? {};
|
||||
if (overrideConvoId) {
|
||||
const [conversationId, index] = overrideConvoId.split(Constants.COMMON_DIVIDER);
|
||||
overrideConvoId = conversationId;
|
||||
if (index !== '0') {
|
||||
this.skipSaveConvo = true;
|
||||
}
|
||||
}
|
||||
if (overrideUserMessageId) {
|
||||
const [userMessageId, index] = overrideUserMessageId.split(Constants.COMMON_DIVIDER);
|
||||
overrideUserMessageId = userMessageId;
|
||||
if (index !== '0') {
|
||||
this.skipSaveUserMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [overrideConvoId, overrideUserMessageId];
|
||||
}
|
||||
|
||||
async setMessageOptions(opts = {}) {
|
||||
if (opts && opts.replaceOptions) {
|
||||
this.setOptions(opts);
|
||||
}
|
||||
|
||||
const [overrideConvoId, overrideUserMessageId] = this.processOverideIds();
|
||||
const { isEdited, isContinued } = opts;
|
||||
const user = opts.user ?? null;
|
||||
this.user = user;
|
||||
const saveOptions = this.getSaveOptions();
|
||||
this.abortController = opts.abortController ?? new AbortController();
|
||||
const conversationId = opts.conversationId ?? crypto.randomUUID();
|
||||
const conversationId = overrideConvoId ?? opts.conversationId ?? crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId ?? Constants.NO_PARENT;
|
||||
const userMessageId = opts.overrideParentMessageId ?? crypto.randomUUID();
|
||||
const userMessageId =
|
||||
overrideUserMessageId ?? opts.overrideParentMessageId ?? crypto.randomUUID();
|
||||
let responseMessageId = opts.responseMessageId ?? crypto.randomUUID();
|
||||
let head = isEdited ? responseMessageId : parentMessageId;
|
||||
this.currentMessages = (await this.loadHistory(conversationId, head)) ?? [];
|
||||
|
@ -160,7 +190,7 @@ class BaseClient {
|
|||
}
|
||||
|
||||
if (typeof opts?.onStart === 'function') {
|
||||
opts.onStart(userMessage);
|
||||
opts.onStart(userMessage, responseMessageId);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -450,7 +480,7 @@ class BaseClient {
|
|||
this.handleTokenCountMap(tokenCountMap);
|
||||
}
|
||||
|
||||
if (!isEdited) {
|
||||
if (!isEdited && !this.skipSaveUserMessage) {
|
||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
}
|
||||
|
||||
|
@ -569,6 +599,10 @@ class BaseClient {
|
|||
unfinished: false,
|
||||
user,
|
||||
});
|
||||
|
||||
if (this.skipSaveConvo) {
|
||||
return;
|
||||
}
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: this.options.endpoint,
|
||||
|
|
|
@ -737,7 +737,7 @@ class GoogleClient extends BaseClient {
|
|||
|
||||
let clientOptions = { ...parameters, maxRetries: 2 };
|
||||
|
||||
logger.info('Initialized title client options');
|
||||
logger.debug('Initialized title client options');
|
||||
|
||||
if (this.project_id) {
|
||||
clientOptions['authOptions'] = {
|
||||
|
@ -764,7 +764,7 @@ class GoogleClient extends BaseClient {
|
|||
|
||||
const modelName = clientOptions.modelName ?? clientOptions.model ?? '';
|
||||
if (modelName?.includes('1.5') && !this.project_id) {
|
||||
logger.info('Identified titling model as 1.5 version');
|
||||
logger.debug('Identified titling model as 1.5 version');
|
||||
/** @type {GenerativeModel} */
|
||||
const client = model;
|
||||
const requestOptions = {
|
||||
|
@ -790,7 +790,7 @@ class GoogleClient extends BaseClient {
|
|||
|
||||
return reply;
|
||||
} else {
|
||||
logger.info('Beginning titling');
|
||||
logger.debug('Beginning titling');
|
||||
const safetySettings = _payload.safetySettings;
|
||||
|
||||
const titleResponse = await model.invoke(messages, {
|
||||
|
@ -840,7 +840,7 @@ class GoogleClient extends BaseClient {
|
|||
} catch (e) {
|
||||
logger.error('[GoogleClient] There was an issue generating the title', e);
|
||||
}
|
||||
logger.info(`Title response: ${title}`);
|
||||
logger.debug(`Title response: ${title}`);
|
||||
return title;
|
||||
}
|
||||
|
||||
|
|
|
@ -301,7 +301,10 @@ class PluginsClient extends OpenAIClient {
|
|||
if (payload) {
|
||||
this.currentMessages = payload;
|
||||
}
|
||||
|
||||
if (!this.skipSaveUserMessage) {
|
||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||
}
|
||||
|
||||
if (isEnabled(process.env.CHECK_BALANCE)) {
|
||||
await checkBalance({
|
||||
|
|
|
@ -576,7 +576,11 @@ describe('BaseClient', () => {
|
|||
const onStart = jest.fn();
|
||||
const opts = { onStart };
|
||||
await TestClient.sendMessage('Hello, world!', opts);
|
||||
expect(onStart).toHaveBeenCalledWith(expect.objectContaining({ text: 'Hello, world!' }));
|
||||
|
||||
expect(onStart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ text: 'Hello, world!' }),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
test('saveMessageToDatabase is called with the correct arguments', async () => {
|
||||
|
|
|
@ -81,7 +81,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
|||
promptTokens,
|
||||
});
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
res.on('close', () => {
|
||||
logger.debug('[AskController] Request closed');
|
||||
|
@ -144,7 +144,9 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
|||
await saveMessage({ ...response, user });
|
||||
}
|
||||
|
||||
if (!client.skipSaveUserMessage) {
|
||||
await saveMessage(userMessage);
|
||||
}
|
||||
|
||||
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
|
||||
addTitle(req, {
|
||||
|
|
|
@ -81,7 +81,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
|||
promptTokens,
|
||||
});
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
res.on('close', () => {
|
||||
logger.debug('[EditController] Request closed');
|
||||
|
|
|
@ -9,23 +9,28 @@ const { abortRun } = require('./abortRun');
|
|||
const { logger } = require('~/config');
|
||||
|
||||
async function abortMessage(req, res) {
|
||||
let { abortKey, conversationId, endpoint } = req.body;
|
||||
|
||||
if (!abortKey && conversationId) {
|
||||
abortKey = conversationId;
|
||||
}
|
||||
let { abortKey, endpoint } = req.body;
|
||||
|
||||
if (isAssistantsEndpoint(endpoint)) {
|
||||
return await abortRun(req, res);
|
||||
}
|
||||
|
||||
const conversationId = abortKey?.split(':')?.[0] ?? req.user.id;
|
||||
|
||||
if (!abortControllers.has(abortKey) && abortControllers.has(conversationId)) {
|
||||
abortKey = conversationId;
|
||||
}
|
||||
|
||||
if (!abortControllers.has(abortKey) && !res.headersSent) {
|
||||
return res.status(204).send({ message: 'Request not found' });
|
||||
}
|
||||
|
||||
const { abortController } = abortControllers.get(abortKey);
|
||||
const { abortController } = abortControllers.get(abortKey) ?? {};
|
||||
if (!abortController) {
|
||||
return res.status(204).send({ message: 'Request not found' });
|
||||
}
|
||||
const finalEvent = await abortController.abortCompletion();
|
||||
logger.debug('[abortMessage] Aborted request', { abortKey });
|
||||
logger.info('[abortMessage] Aborted request', { abortKey });
|
||||
abortControllers.delete(abortKey);
|
||||
|
||||
if (res.headersSent && finalEvent) {
|
||||
|
@ -50,12 +55,32 @@ const handleAbort = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const createAbortController = (req, res, getAbortData) => {
|
||||
const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
const abortController = new AbortController();
|
||||
const { endpointOption } = req.body;
|
||||
const onStart = (userMessage) => {
|
||||
|
||||
abortController.getAbortData = function () {
|
||||
return getAbortData();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {TMessage} userMessage
|
||||
* @param {string} responseMessageId
|
||||
*/
|
||||
const onStart = (userMessage, responseMessageId) => {
|
||||
sendMessage(res, { message: userMessage, created: true });
|
||||
const abortKey = userMessage?.conversationId ?? req.user.id;
|
||||
const prevRequest = abortControllers.get(abortKey);
|
||||
if (prevRequest && prevRequest?.abortController) {
|
||||
const data = prevRequest.abortController.getAbortData();
|
||||
getReqData({ userMessage: data?.userMessage });
|
||||
const addedAbortKey = `${abortKey}:${responseMessageId}`;
|
||||
abortControllers.set(addedAbortKey, { abortController, ...endpointOption });
|
||||
res.on('finish', function () {
|
||||
abortControllers.delete(addedAbortKey);
|
||||
});
|
||||
return;
|
||||
}
|
||||
abortControllers.set(abortKey, { abortController, ...endpointOption });
|
||||
|
||||
res.on('finish', function () {
|
||||
|
|
|
@ -148,15 +148,6 @@ router.post(
|
|||
}
|
||||
};
|
||||
|
||||
const onChainEnd = () => {
|
||||
saveMessage({ ...userMessage, user });
|
||||
sendIntermediateMessage(res, {
|
||||
plugins,
|
||||
parentMessageId: userMessage.messageId,
|
||||
messageId: responseMessageId,
|
||||
});
|
||||
};
|
||||
|
||||
const getAbortData = () => ({
|
||||
sender,
|
||||
conversationId,
|
||||
|
@ -167,12 +158,23 @@ router.post(
|
|||
userMessage,
|
||||
promptTokens,
|
||||
});
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
try {
|
||||
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
||||
const { client } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
const onChainEnd = () => {
|
||||
if (!client.skipSaveUserMessage) {
|
||||
saveMessage({ ...userMessage, user });
|
||||
}
|
||||
sendIntermediateMessage(res, {
|
||||
plugins,
|
||||
parentMessageId: userMessage.messageId,
|
||||
messageId: responseMessageId,
|
||||
});
|
||||
};
|
||||
|
||||
let response = await client.sendMessage(text, {
|
||||
user,
|
||||
conversationId,
|
||||
|
|
|
@ -103,21 +103,6 @@ router.post(
|
|||
},
|
||||
});
|
||||
|
||||
const onAgentAction = (action, start = false) => {
|
||||
const formattedAction = formatAction(action);
|
||||
plugin.inputs.push(formattedAction);
|
||||
plugin.latest = formattedAction.plugin;
|
||||
if (!start) {
|
||||
saveMessage({ ...userMessage, user });
|
||||
}
|
||||
sendIntermediateMessage(res, {
|
||||
plugin,
|
||||
parentMessageId: userMessage.messageId,
|
||||
messageId: responseMessageId,
|
||||
});
|
||||
// logger.debug('PLUGIN ACTION', formattedAction);
|
||||
};
|
||||
|
||||
const onChainEnd = (data) => {
|
||||
let { intermediateSteps: steps } = data;
|
||||
plugin.outputs = steps && steps[0].action ? formatSteps(steps) : 'An error occurred.';
|
||||
|
@ -141,12 +126,27 @@ router.post(
|
|||
userMessage,
|
||||
promptTokens,
|
||||
});
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
try {
|
||||
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
||||
const { client } = await initializeClient({ req, res, endpointOption });
|
||||
|
||||
const onAgentAction = (action, start = false) => {
|
||||
const formattedAction = formatAction(action);
|
||||
plugin.inputs.push(formattedAction);
|
||||
plugin.latest = formattedAction.plugin;
|
||||
if (!start && !client.skipSaveUserMessage) {
|
||||
saveMessage({ ...userMessage, user });
|
||||
}
|
||||
sendIntermediateMessage(res, {
|
||||
plugin,
|
||||
parentMessageId: userMessage.messageId,
|
||||
messageId: responseMessageId,
|
||||
});
|
||||
// logger.debug('PLUGIN ACTION', formattedAction);
|
||||
};
|
||||
|
||||
let response = await client.sendMessage(text, {
|
||||
user,
|
||||
generation,
|
||||
|
|
6
client/src/Providers/AddedChatContext.tsx
Normal file
6
client/src/Providers/AddedChatContext.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import useAddedResponse from '~/hooks/Chat/useAddedResponse';
|
||||
type TAddedChatContext = ReturnType<typeof useAddedResponse>;
|
||||
|
||||
export const AddedChatContext = createContext<TAddedChatContext>({} as TAddedChatContext);
|
||||
export const useAddedChatContext = () => useContext(AddedChatContext);
|
|
@ -1,5 +1,5 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import useChatHelpers from '~/hooks/useChatHelpers';
|
||||
import useChatHelpers from '~/hooks/Chat/useChatHelpers';
|
||||
type TChatContext = ReturnType<typeof useChatHelpers>;
|
||||
|
||||
export const ChatContext = createContext<TChatContext>({} as TChatContext);
|
||||
|
|
|
@ -5,6 +5,7 @@ export * from './ShareContext';
|
|||
export * from './ToastContext';
|
||||
export * from './SearchContext';
|
||||
export * from './FileMapContext';
|
||||
export * from './AddedChatContext';
|
||||
export * from './ChatFormContext';
|
||||
export * from './DashboardContext';
|
||||
export * from './AssistantsContext';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { FileSources, SystemRoles } from 'librechat-data-provider';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import type * as InputNumberPrimitive from 'rc-input-number';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
|
@ -11,8 +11,10 @@ import type {
|
|||
TPlugin,
|
||||
TMessage,
|
||||
Assistant,
|
||||
TResPlugin,
|
||||
TLoginUser,
|
||||
AuthTypeEnum,
|
||||
TModelsConfig,
|
||||
TConversation,
|
||||
TStartupConfig,
|
||||
EModelEndpoint,
|
||||
|
@ -229,6 +231,8 @@ export type TGenButtonProps = {
|
|||
|
||||
export type TAskProps = {
|
||||
text: string;
|
||||
overrideConvoId?: string;
|
||||
overrideUserMessageId?: string;
|
||||
parentMessageId?: string | null;
|
||||
conversationId?: string | null;
|
||||
messageId?: string | null;
|
||||
|
@ -241,6 +245,7 @@ export type TOptions = {
|
|||
isRegenerate?: boolean;
|
||||
isContinued?: boolean;
|
||||
isEdited?: boolean;
|
||||
overrideMessages?: TMessage[];
|
||||
};
|
||||
|
||||
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
|
||||
|
@ -409,6 +414,29 @@ export type TLoginLayoutContext = {
|
|||
headerText: string;
|
||||
setHeaderText: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export type NewConversationParams = {
|
||||
template?: Partial<TConversation>;
|
||||
preset?: Partial<TPreset>;
|
||||
modelsData?: TModelsConfig;
|
||||
buildDefault?: boolean;
|
||||
keepLatestMessage?: boolean;
|
||||
keepAddedConvos?: boolean;
|
||||
};
|
||||
|
||||
export type ConvoGenerator = (params: NewConversationParams) => void | TConversation;
|
||||
|
||||
export type TResData = {
|
||||
plugin?: TResPlugin;
|
||||
final?: boolean;
|
||||
initial?: boolean;
|
||||
previousMessages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversation: TConversation;
|
||||
conversationId?: string;
|
||||
runMessages?: TMessage[];
|
||||
};
|
||||
export type TVectorStore = {
|
||||
_id: string;
|
||||
object: 'vector_store';
|
||||
|
|
47
client/src/components/Chat/AddMultiConvo.tsx
Normal file
47
client/src/components/Chat/AddMultiConvo.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { PlusCircle } from 'lucide-react';
|
||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function AddMultiConvo({ className = '' }: { className?: string }) {
|
||||
const { conversation } = useChatContext();
|
||||
const { setConversation: setAddedConvo } = useAddedChatContext();
|
||||
|
||||
const clickHandler = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title: _t, ...convo } = conversation ?? ({} as TConversation);
|
||||
setAddedConvo({
|
||||
...convo,
|
||||
title: '',
|
||||
});
|
||||
|
||||
const textarea = document.getElementById(mainTextareaId);
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
};
|
||||
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAssistantsEndpoint(conversation.endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
className={cn(
|
||||
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-0',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddMultiConvo;
|
|
@ -4,9 +4,9 @@ import { useForm } from 'react-hook-form';
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import type { ChatFormValues } from '~/common';
|
||||
import { ChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
||||
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
||||
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
|
||||
import MessagesView from './Messages/MessagesView';
|
||||
import { useChatHelpers, useSSE } from '~/hooks';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import Presentation from './Presentation';
|
||||
import ChatForm from './Input/ChatForm';
|
||||
|
@ -18,8 +18,8 @@ import store from '~/store';
|
|||
|
||||
function ChatView({ index = 0 }: { index?: number }) {
|
||||
const { conversationId } = useParams();
|
||||
const submissionAtIndex = useRecoilValue(store.submissionByIndex(0));
|
||||
useSSE(submissionAtIndex);
|
||||
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
|
||||
const addedSubmission = useRecoilValue(store.submissionByIndex(index + 1));
|
||||
|
||||
const fileMap = useFileMapContext();
|
||||
|
||||
|
@ -32,20 +32,19 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
});
|
||||
|
||||
const chatHelpers = useChatHelpers(index, conversationId);
|
||||
const addedChatHelpers = useAddedResponse({ rootIndex: index });
|
||||
|
||||
useSSE(rootSubmission, chatHelpers, false);
|
||||
useSSE(addedSubmission, addedChatHelpers, true);
|
||||
|
||||
const methods = useForm<ChatFormValues>({
|
||||
defaultValues: { text: '' },
|
||||
});
|
||||
|
||||
return (
|
||||
<ChatFormProvider
|
||||
reset={methods.reset}
|
||||
control={methods.control}
|
||||
setValue={methods.setValue}
|
||||
register={methods.register}
|
||||
getValues={methods.getValues}
|
||||
handleSubmit={methods.handleSubmit}
|
||||
>
|
||||
<ChatFormProvider {...methods}>
|
||||
<ChatContext.Provider value={chatHelpers}>
|
||||
<AddedChatContext.Provider value={addedChatHelpers}>
|
||||
<Presentation useSidePanel={true}>
|
||||
{isLoading && conversationId !== 'new' ? (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
|
@ -61,6 +60,7 @@ function ChatView({ index = 0 }: { index?: number }) {
|
|||
<Footer />
|
||||
</div>
|
||||
</Presentation>
|
||||
</AddedChatContext.Provider>
|
||||
</ChatContext.Provider>
|
||||
</ChatFormProvider>
|
||||
);
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function Footer({ className }: { className?: string }) {
|
|||
: '[LibreChat ' +
|
||||
Constants.VERSION +
|
||||
'](https://librechat.ai) - ' +
|
||||
localize('com_ui_pay_per_call')
|
||||
localize('com_ui_latest_footer')
|
||||
).split('|');
|
||||
|
||||
const mainContentRender = mainContentParts.map((text, index) => (
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { ContextType } from '~/common';
|
|||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||
import HeaderOptions from './Input/HeaderOptions';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
import { useMediaQuery } from '~/hooks';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
@ -36,6 +37,7 @@ export default function Header() {
|
|||
className="pl-0"
|
||||
/>
|
||||
)}
|
||||
<AddMultiConvo />
|
||||
</div>
|
||||
{!isSmallScreen && (
|
||||
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />
|
||||
|
|
66
client/src/components/Chat/Input/AddedConvo.tsx
Normal file
66
client/src/components/Chat/Input/AddedConvo.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TConversation, TEndpointOption, TPreset } from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import { EndpointIcon } from '~/components/Endpoints';
|
||||
import { getPresetTitle } from '~/utils';
|
||||
|
||||
export default function AddedConvo({
|
||||
addedConvo,
|
||||
setAddedConvo,
|
||||
}: {
|
||||
addedConvo: TConversation | null;
|
||||
setAddedConvo: SetterOrUpdater<TConversation | null>;
|
||||
}) {
|
||||
const getSender = useGetSender();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const title = useMemo(() => {
|
||||
const sender = getSender(addedConvo as TEndpointOption);
|
||||
const title = getPresetTitle(addedConvo as TPreset);
|
||||
return `+ ${sender}: ${title}`;
|
||||
}, [addedConvo, getSender]);
|
||||
|
||||
if (!addedConvo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
||||
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
||||
<div className="icon-md">
|
||||
<EndpointIcon
|
||||
conversation={addedConvo}
|
||||
endpointsConfig={endpointsConfig}
|
||||
containerClassName="shadow-stroke overflow-hidden rounded-full"
|
||||
context="menu-item"
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
||||
{title}
|
||||
</span>
|
||||
<button
|
||||
className="text-token-text-secondary flex-shrink-0"
|
||||
type="button"
|
||||
onClick={() => setAddedConvo(null)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="icon-lg"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M7.293 7.293a1 1 0 0 1 1.414 0L12 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414L13.414 12l3.293 3.293a1 1 0 0 1-1.414 1.414L12 13.414l-3.293 3.293a1 1 0 0 1-1.414-1.414L10.586 12 7.293 8.707a1 1 0 0 1 0-1.414"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -6,12 +6,23 @@ import {
|
|||
isAssistantsEndpoint,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { useChatContext, useAssistantsMapContext, useChatFormContext } from '~/Providers';
|
||||
import { useRequiresKey, useTextarea, useSubmitMessage, useHandleKeyUp } from '~/hooks';
|
||||
import { useAutoSave } from '~/hooks/Input/useAutoSave';
|
||||
import {
|
||||
useChatContext,
|
||||
useAddedChatContext,
|
||||
useAssistantsMapContext,
|
||||
useChatFormContext,
|
||||
} from '~/Providers';
|
||||
import {
|
||||
useTextarea,
|
||||
useAutoSave,
|
||||
useRequiresKey,
|
||||
useHandleKeyUp,
|
||||
useSubmitMessage,
|
||||
} from '~/hooks';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { cn, removeFocusRings } from '~/utils';
|
||||
import TextareaHeader from './TextareaHeader';
|
||||
import AttachFile from './Files/AttachFile';
|
||||
import AudioRecorder from './AudioRecorder';
|
||||
import { mainTextareaId } from '~/common';
|
||||
|
@ -25,16 +36,19 @@ import store from '~/store';
|
|||
const ChatForm = ({ index = 0 }) => {
|
||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const SpeechToText = useRecoilValue(store.SpeechToText);
|
||||
const TextToSpeech = useRecoilValue(store.TextToSpeech);
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
|
||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
|
||||
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
||||
store.showMentionPopoverFamily(index),
|
||||
);
|
||||
const { requiresKey } = useRequiresKey();
|
||||
|
||||
const handleKeyUp = useHandleKeyUp({ index, textAreaRef });
|
||||
const { requiresKey } = useRequiresKey();
|
||||
const handleKeyUp = useHandleKeyUp({ textAreaRef, setShowPlusPopover, setShowMentionPopover });
|
||||
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
|
@ -48,9 +62,18 @@ const ChatForm = ({ index = 0 }) => {
|
|||
isSubmitting,
|
||||
filesLoading,
|
||||
setFilesLoading,
|
||||
newConversation,
|
||||
handleStopGenerating,
|
||||
} = useChatContext();
|
||||
const methods = useChatFormContext();
|
||||
const {
|
||||
addedIndex,
|
||||
generateConversation,
|
||||
conversation: addedConvo,
|
||||
setConversation: setAddedConvo,
|
||||
isSubmitting: isSubmittingAdded,
|
||||
} = useAddedChatContext();
|
||||
const showStopAdded = useRecoilValue(store.showStopButtonByIndex(addedIndex));
|
||||
|
||||
const { clearDraft } = useAutoSave({
|
||||
conversationId: useMemo(() => conversation?.conversationId, [conversation]),
|
||||
|
@ -96,10 +119,25 @@ const ChatForm = ({ index = 0 }) => {
|
|||
>
|
||||
<div className="relative flex h-full flex-1 items-stretch md:flex-col">
|
||||
<div className="flex w-full items-center">
|
||||
{showPlusPopover && !isAssistantsEndpoint(endpoint) && (
|
||||
<Mention
|
||||
setShowMentionPopover={setShowPlusPopover}
|
||||
newConversation={generateConversation}
|
||||
textAreaRef={textAreaRef}
|
||||
commandChar="+"
|
||||
placeholder="com_ui_add"
|
||||
includeAssistants={false}
|
||||
/>
|
||||
)}
|
||||
{showMentionPopover && (
|
||||
<Mention setShowMentionPopover={setShowMentionPopover} textAreaRef={textAreaRef} />
|
||||
<Mention
|
||||
setShowMentionPopover={setShowMentionPopover}
|
||||
newConversation={newConversation}
|
||||
textAreaRef={textAreaRef}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
|
||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
|
@ -145,7 +183,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
endpointType={endpointType}
|
||||
disabled={disableInputs}
|
||||
/>
|
||||
{isSubmitting && showStopButton ? (
|
||||
{(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
|
||||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||
) : (
|
||||
endpoint && (
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { MentionOption } from '~/common';
|
||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||
import { useAssistantsMapContext } from '~/Providers';
|
||||
import useMentions from '~/hooks/Input/useMentions';
|
||||
import { useLocalize, useCombobox } from '~/hooks';
|
||||
import { removeAtSymbolIfLast } from '~/utils';
|
||||
import { removeCharIfLast } from '~/utils';
|
||||
import MentionItem from './MentionItem';
|
||||
|
||||
export default function Mention({
|
||||
setShowMentionPopover,
|
||||
newConversation,
|
||||
textAreaRef,
|
||||
commandChar = '@',
|
||||
placeholder = 'com_ui_mention',
|
||||
includeAssistants = true,
|
||||
}: {
|
||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||
newConversation: ConvoGenerator;
|
||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
commandChar?: string;
|
||||
placeholder?: string;
|
||||
includeAssistants?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
|
||||
useMentions({ assistantMap });
|
||||
useMentions({ assistantMap, includeAssistants });
|
||||
const { onSelectMention } = useSelectMention({
|
||||
presets,
|
||||
modelSpecs,
|
||||
assistantMap,
|
||||
endpointsConfig,
|
||||
newConversation,
|
||||
});
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
|
@ -49,7 +58,7 @@ export default function Mention({
|
|||
onSelectMention(mention);
|
||||
|
||||
if (textAreaRef.current) {
|
||||
removeAtSymbolIfLast(textAreaRef.current);
|
||||
removeCharIfLast(textAreaRef.current, commandChar);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -105,7 +114,7 @@ export default function Mention({
|
|||
<input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
placeholder={localize('com_ui_mention')}
|
||||
placeholder={localize(placeholder)}
|
||||
className="mb-1 w-full border-0 bg-white p-2 text-sm focus:outline-none dark:bg-gray-700 dark:text-gray-200"
|
||||
autoComplete="off"
|
||||
value={searchValue}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
export default function StopButton({ stop, setShowStopButton }) {
|
||||
return (
|
||||
<div className="absolute bottom-0 right-2 top-0 p-1 md:right-3 md:p-2">
|
||||
<div className="flex h-full">
|
||||
<div className="flex h-full flex-row items-center justify-center gap-3">
|
||||
<div className="absolute bottom-3 right-2 md:bottom-4 md:right-4">
|
||||
<button
|
||||
type="button"
|
||||
className="border-gizmo-gray-900 rounded-full border-2 p-1 dark:border-gray-200"
|
||||
|
@ -27,7 +25,5 @@ export default function StopButton({ stop, setShowStopButton }) {
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
20
client/src/components/Chat/Input/TextareaHeader.tsx
Normal file
20
client/src/components/Chat/Input/TextareaHeader.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import AddedConvo from './AddedConvo';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
|
||||
export default function TextareaHeader({
|
||||
addedConvo,
|
||||
setAddedConvo,
|
||||
}: {
|
||||
addedConvo: TConversation | null;
|
||||
setAddedConvo: SetterOrUpdater<TConversation | null>;
|
||||
}) {
|
||||
if (!addedConvo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="divide-token-border-light m-1.5 flex flex-col divide-y overflow-hidden rounded-b-lg rounded-t-2xl bg-surface-primary-contrast">
|
||||
<AddedConvo addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -50,12 +50,12 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
}
|
||||
|
||||
const {
|
||||
template,
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
newEndpointType,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
newEndpointType,
|
||||
template,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint,
|
||||
modularChat,
|
||||
|
@ -63,7 +63,8 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||
if (isExistingConversation && isModular) {
|
||||
template.endpointType = newEndpointType;
|
||||
|
||||
const currentConvo = getDefaultConversation({
|
||||
|
@ -73,10 +74,18 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
});
|
||||
|
||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||
newConversation({ template: currentConvo, preset: currentConvo, keepLatestMessage: true });
|
||||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: currentConvo,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
||||
newConversation({
|
||||
template: { ...(template as Partial<TConversation>) },
|
||||
keepAddedConvos: isModular,
|
||||
});
|
||||
};
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
|
|
|
@ -29,12 +29,12 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[
|
|||
}
|
||||
|
||||
const {
|
||||
template,
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
newEndpointType,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
newEndpointType,
|
||||
template,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint,
|
||||
modularChat,
|
||||
|
@ -42,7 +42,8 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[
|
|||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||
if (isExistingConversation && isModular) {
|
||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||
|
||||
const currentConvo = getDefaultConversation({
|
||||
|
@ -52,11 +53,20 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[
|
|||
});
|
||||
|
||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||
newConversation({ template: currentConvo, preset, keepLatestMessage: true });
|
||||
newConversation({
|
||||
template: currentConvo,
|
||||
preset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newConversation({ template: { ...(template as Partial<TConversation>) }, preset });
|
||||
newConversation({
|
||||
template: { ...(template as Partial<TConversation>) },
|
||||
preset,
|
||||
keepAddedConvos: isModular,
|
||||
});
|
||||
};
|
||||
|
||||
const selected = useMemo(() => {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TEditProps } from '~/common';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { cn, removeFocusRings } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Container from './Container';
|
||||
import store from '~/store';
|
||||
|
||||
const EditMessage = ({
|
||||
text,
|
||||
|
@ -17,7 +19,11 @@ const EditMessage = ({
|
|||
siblingIdx,
|
||||
setSiblingIdx,
|
||||
}: TEditProps) => {
|
||||
const { addedIndex } = useAddedChatContext();
|
||||
const { getMessages, setMessages, conversation } = useChatContext();
|
||||
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||
store.latestMessageFamily(addedIndex),
|
||||
);
|
||||
|
||||
const [editedText, setEditedText] = useState<string>(text ?? '');
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
@ -85,6 +91,15 @@ const EditMessage = ({
|
|||
text: editedText,
|
||||
messageId,
|
||||
});
|
||||
|
||||
if (message.messageId === latestMultiMessage?.messageId) {
|
||||
setLatestMultiMessage({ ...latestMultiMessage, text: editedText });
|
||||
}
|
||||
|
||||
const isInMessages = messages?.some((message) => message?.messageId === messageId);
|
||||
if (!isInMessages) {
|
||||
message.text = editedText;
|
||||
} else {
|
||||
setMessages(
|
||||
messages.map((msg) =>
|
||||
msg.messageId === messageId
|
||||
|
@ -96,6 +111,8 @@ const EditMessage = ({
|
|||
: msg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
enterEdit(true);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useAuthContext, useMessageHelpers, useLocalize } from '~/hooks';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useMessageProcess, useMessageActions } from '~/hooks';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
|
@ -10,95 +11,140 @@ import MultiMessage from './MultiMessage';
|
|||
import HoverButtons from './HoverButtons';
|
||||
import SubRow from './SubRow';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function Message(props: TMessageProps) {
|
||||
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
|
||||
const { user } = useAuthContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const {
|
||||
ask,
|
||||
edit,
|
||||
index,
|
||||
isLast,
|
||||
assistant,
|
||||
enterEdit,
|
||||
handleScroll,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
} = useMessageHelpers(props);
|
||||
|
||||
const { message, siblingIdx, siblingCount, setSiblingIdx, currentEditId, setCurrentEditId } =
|
||||
props;
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { text, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {};
|
||||
|
||||
let messageLabel = '';
|
||||
if (isCreatedByUser) {
|
||||
messageLabel = UsernameDisplay ? user?.name || user?.username : localize('com_user_message');
|
||||
} else if (assistant) {
|
||||
messageLabel = assistant.name ?? 'Assistant';
|
||||
} else {
|
||||
messageLabel = message.sender;
|
||||
}
|
||||
|
||||
const MessageContainer = React.memo(
|
||||
({ handleScroll, children }: { handleScroll: () => void; children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||
onWheel={handleScroll}
|
||||
onTouchMove={handleScroll}
|
||||
>
|
||||
<div className="m-auto justify-center p-4 py-2 text-base md:gap-6 ">
|
||||
<div className="final-completion group mx-auto flex flex-1 gap-3 text-base md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const PlaceholderRow = React.memo(({ isLast, isCard }: { isLast: boolean; isCard?: boolean }) => {
|
||||
if (!isLast && !isCard) {
|
||||
return null;
|
||||
}
|
||||
return <div className="mt-1 h-[27px] bg-transparent" />;
|
||||
});
|
||||
|
||||
type MessageRenderProps = {
|
||||
message?: TMessage;
|
||||
isCard?: boolean;
|
||||
isMultiMessage?: boolean;
|
||||
isSubmittingFamily?: boolean;
|
||||
} & Pick<
|
||||
TMessageProps,
|
||||
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
|
||||
>;
|
||||
|
||||
const MessageRender = React.memo(
|
||||
({
|
||||
isCard,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
message: msg,
|
||||
setSiblingIdx,
|
||||
currentEditId,
|
||||
isMultiMessage,
|
||||
setCurrentEditId,
|
||||
isSubmittingFamily,
|
||||
}: MessageRenderProps) => {
|
||||
const {
|
||||
ask,
|
||||
edit,
|
||||
index,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
messageLabel,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
setLatestMessage,
|
||||
regenerateMessage,
|
||||
} = useMessageActions({
|
||||
message: msg,
|
||||
currentEditId,
|
||||
isMultiMessage,
|
||||
setCurrentEditId,
|
||||
});
|
||||
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const { isCreatedByUser, error, unfinished } = msg ?? {};
|
||||
const isLast = useMemo(
|
||||
() => !msg?.children?.length && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
|
||||
[msg?.children, msg?.depth, latestMessage?.depth],
|
||||
);
|
||||
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLatest = isCard && !isSubmittingFamily && msg.messageId === latestMessage?.messageId;
|
||||
const clickHandler =
|
||||
isLast && isCard && !isSubmittingFamily && msg.messageId !== latestMessage?.messageId
|
||||
? () => setLatestMessage(msg)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'final-completion group mx-auto flex flex-1 gap-3 text-base',
|
||||
isCard
|
||||
? 'relative w-full gap-1 rounded-lg border border-border-medium bg-surface-primary-alt p-2 md:w-1/2 md:gap-3 md:p-4'
|
||||
: '',
|
||||
isLatest ? 'bg-surface-secondary' : '',
|
||||
isLast && !isSubmittingFamily && isCard
|
||||
? 'cursor-pointer transition-colors duration-300'
|
||||
: '',
|
||||
)}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
{isLatest && (
|
||||
<div className="absolute right-0 top-0 m-2 h-3 w-3 rounded-full bg-text-primary"></div>
|
||||
)}
|
||||
<div className="relative flex flex-shrink-0 flex-col items-end">
|
||||
<div>
|
||||
<div className="pt-0.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<Icon message={message} conversation={conversation} assistant={assistant} />
|
||||
<Icon message={msg} conversation={conversation} assistant={assistant} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn('relative flex w-11/12 flex-col', isCreatedByUser ? '' : 'agent-turn')}
|
||||
className={cn('relative flex w-11/12 flex-col', msg?.isCreatedByUser ? '' : 'agent-turn')}
|
||||
>
|
||||
<div className="select-none font-semibold">{messageLabel}</div>
|
||||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
{/* Legacy Plugins */}
|
||||
{message?.plugin && <Plugin plugin={message?.plugin} />}
|
||||
{msg?.plugin && <Plugin plugin={msg?.plugin} />}
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
text={text ?? ''}
|
||||
message={message}
|
||||
text={msg.text ?? ''}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
error={!!error}
|
||||
isSubmitting={isSubmitting}
|
||||
unfinished={unfinished ?? false}
|
||||
isCreatedByUser={isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={
|
||||
setSiblingIdx ??
|
||||
(() => {
|
||||
return;
|
||||
})
|
||||
}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLast && isSubmitting ? null : (
|
||||
{!msg?.children?.length && (isSubmittingFamily || isSubmitting) ? (
|
||||
<PlaceholderRow isLast={isLast} isCard={isCard} />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
|
@ -108,11 +154,11 @@ export default function Message(props: TMessageProps) {
|
|||
<HoverButtons
|
||||
index={index}
|
||||
isEditing={edit}
|
||||
message={message}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
isSubmitting={isSubmitting}
|
||||
conversation={conversation ?? null}
|
||||
regenerate={() => regenerateMessage()}
|
||||
regenerate={handleRegenerateMessage}
|
||||
copyToClipboard={copyToClipboard}
|
||||
handleContinue={handleContinue}
|
||||
latestMessage={latestMessage}
|
||||
|
@ -122,8 +168,56 @@ export default function Message(props: TMessageProps) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default function Message(props: TMessageProps) {
|
||||
const {
|
||||
showSibling,
|
||||
conversation,
|
||||
handleScroll,
|
||||
siblingMessage,
|
||||
latestMultiMessage,
|
||||
isSubmittingFamily,
|
||||
} = useMessageProcess({ message: props.message });
|
||||
const { message, currentEditId, setCurrentEditId } = props;
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { children, messageId = null } = message ?? {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MessageContainer handleScroll={handleScroll}>
|
||||
{showSibling ? (
|
||||
<div className="m-auto my-2 flex justify-center p-4 py-2 text-base md:gap-6">
|
||||
<div className="flex w-full flex-row flex-wrap justify-between gap-1 md:max-w-5xl md:flex-nowrap md:gap-2 lg:max-w-5xl xl:max-w-6xl">
|
||||
<MessageRender
|
||||
{...props}
|
||||
message={message}
|
||||
isSubmittingFamily={isSubmittingFamily}
|
||||
isCard
|
||||
/>
|
||||
<MessageRender
|
||||
{...props}
|
||||
isMultiMessage
|
||||
isCard
|
||||
message={siblingMessage ?? latestMultiMessage ?? undefined}
|
||||
isSubmittingFamily={isSubmittingFamily}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-auto justify-center p-4 py-2 text-base md:gap-6 ">
|
||||
<div className="final-completion group mx-auto flex flex-1 gap-3 text-base md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5">
|
||||
<MessageRender {...props} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</MessageContainer>
|
||||
<MultiMessage
|
||||
key={messageId}
|
||||
messageId={messageId}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMemo, memo } from 'react';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TMessage, TPreset, Assistant } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
|
@ -6,7 +6,7 @@ import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
|||
import { getEndpointField, getIconEndpoint } from '~/utils';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
|
||||
export default function MessageIcon(
|
||||
function MessageIcon(
|
||||
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
||||
assistant?: false | Assistant;
|
||||
},
|
||||
|
@ -56,3 +56,5 @@ export default function MessageIcon(
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MessageIcon);
|
||||
|
|
|
@ -83,7 +83,9 @@ export default function Message(props: TMessageProps) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLast && isSubmitting ? null : (
|
||||
{isLast && isSubmitting ? (
|
||||
<div className="mt-1 h-[27px] bg-transparent" />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import type { TMessageProps } from '~/common';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import Message from './Message';
|
||||
|
@ -16,9 +16,12 @@ export default function MultiMessage({
|
|||
}: TMessageProps) {
|
||||
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
||||
|
||||
const setSiblingIdxRev = (value: number) => {
|
||||
const setSiblingIdxRev = useCallback(
|
||||
(value: number) => {
|
||||
setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
|
||||
};
|
||||
},
|
||||
[messagesTree?.length, setSiblingIdx],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { UserIcon } from '~/components/svg';
|
||||
import { memo } from 'react';
|
||||
import type { IconProps } from '~/common';
|
||||
import MessageEndpointIcon from './MessageEndpointIcon';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { IconProps } from '~/common';
|
||||
import { UserIcon } from '~/components/svg';
|
||||
import { cn } from '~/utils';
|
||||
import MessageEndpointIcon from './MessageEndpointIcon';
|
||||
|
||||
const Icon: React.FC<IconProps> = (props) => {
|
||||
const { user } = useAuthContext();
|
||||
|
@ -46,4 +47,4 @@ const Icon: React.FC<IconProps> = (props) => {
|
|||
return <MessageEndpointIcon {...props} />;
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
export default memo(Icon);
|
||||
|
|
|
@ -23,13 +23,15 @@ import { useConversationsInfiniteQuery, useSharedLinksInfiniteQuery } from './qu
|
|||
import { normalizeData } from '~/utils/collection';
|
||||
import store from '~/store';
|
||||
|
||||
/** Conversations */
|
||||
export const useGenTitleMutation = (): UseMutationResult<
|
||||
export type TGenTitleMutation = UseMutationResult<
|
||||
t.TGenTitleResponse,
|
||||
unknown,
|
||||
t.TGenTitleRequest,
|
||||
unknown
|
||||
> => {
|
||||
>;
|
||||
|
||||
/** Conversations */
|
||||
export const useGenTitleMutation = (): TGenTitleMutation => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation((payload: t.TGenTitleRequest) => dataService.genTitle(payload), {
|
||||
onSuccess: (response, vars) => {
|
||||
|
@ -525,6 +527,8 @@ export const useLogoutUserMutation = (
|
|||
setDefaultPreset(null);
|
||||
queryClient.removeQueries();
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_1`);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_MODEL);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||
|
@ -565,6 +569,8 @@ export const useDeleteUserMutation = (
|
|||
setDefaultPreset(null);
|
||||
queryClient.removeQueries();
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_1`);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_MODEL);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||
|
|
4
client/src/hooks/Chat/index.ts
Normal file
4
client/src/hooks/Chat/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { default as useChatHelpers } from './useChatHelpers';
|
||||
export { default as useAddedHelpers } from './useAddedHelpers';
|
||||
export { default as useAddedResponse } from './useAddedResponse';
|
||||
export { default as useChatFunctions } from './useChatFunctions';
|
128
client/src/hooks/Chat/useAddedHelpers.ts
Normal file
128
client/src/hooks/Chat/useAddedHelpers.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
||||
import store from '~/store';
|
||||
|
||||
// this to be set somewhere else
|
||||
export default function useAddedHelpers({
|
||||
rootIndex = 0,
|
||||
currentIndex,
|
||||
paramId,
|
||||
}: {
|
||||
rootIndex?: number;
|
||||
currentIndex: number;
|
||||
paramId?: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const clearAllSubmissions = store.useClearSubmissionState();
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(rootIndex));
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(rootIndex));
|
||||
const setLatestMultiMessage = useSetRecoilState(store.latestMessageFamily(currentIndex));
|
||||
|
||||
const { useCreateConversationAtom } = store;
|
||||
const { conversation, setConversation } = useCreateConversationAtom(currentIndex);
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(currentIndex));
|
||||
|
||||
const setSiblingIdx = useSetRecoilState(
|
||||
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null),
|
||||
);
|
||||
|
||||
const queryParam = paramId === 'new' ? paramId : conversation?.conversationId ?? paramId ?? '';
|
||||
|
||||
const setMessages = useCallback(
|
||||
(messages: TMessage[]) => {
|
||||
queryClient.setQueryData<TMessage[]>(
|
||||
[QueryKeys.messages, queryParam, currentIndex],
|
||||
messages,
|
||||
);
|
||||
const latestMultiMessage = messages[messages.length - 1];
|
||||
if (latestMultiMessage) {
|
||||
setLatestMultiMessage({ ...latestMultiMessage, depth: -1 });
|
||||
}
|
||||
},
|
||||
[queryParam, queryClient, currentIndex, setLatestMultiMessage],
|
||||
);
|
||||
|
||||
const getMessages = useCallback(() => {
|
||||
return queryClient.getQueryData<TMessage[]>([QueryKeys.messages, queryParam, currentIndex]);
|
||||
}, [queryParam, queryClient, currentIndex]);
|
||||
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(currentIndex));
|
||||
|
||||
const { ask, regenerate } = useChatFunctions({
|
||||
index: currentIndex,
|
||||
files,
|
||||
setFiles,
|
||||
getMessages,
|
||||
setMessages,
|
||||
isSubmitting,
|
||||
conversation,
|
||||
setSubmission,
|
||||
latestMessage,
|
||||
});
|
||||
|
||||
const continueGeneration = () => {
|
||||
if (!latestMessage) {
|
||||
console.error('Failed to regenerate the message: latestMessage not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = getMessages();
|
||||
|
||||
const parentMessage = messages?.find(
|
||||
(element) => element.messageId == latestMessage.parentMessageId,
|
||||
);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isContinued: true, isRegenerate: true, isEdited: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found, or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopGenerating = () => clearAllSubmissions();
|
||||
|
||||
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
stopGenerating();
|
||||
};
|
||||
|
||||
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const parentMessageId = latestMessage?.parentMessageId;
|
||||
if (!parentMessageId) {
|
||||
console.error('Failed to regenerate the message: parentMessageId not found.');
|
||||
return;
|
||||
}
|
||||
regenerate({ parentMessageId });
|
||||
};
|
||||
|
||||
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
continueGeneration();
|
||||
setSiblingIdx(0);
|
||||
};
|
||||
|
||||
return {
|
||||
ask,
|
||||
regenerate,
|
||||
getMessages,
|
||||
setMessages,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
setSiblingIdx,
|
||||
latestMessage,
|
||||
stopGenerating,
|
||||
handleContinue,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
handleRegenerate,
|
||||
handleStopGenerating,
|
||||
};
|
||||
}
|
39
client/src/hooks/Chat/useAddedResponse.ts
Normal file
39
client/src/hooks/Chat/useAddedResponse.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { useMemo } from 'react';
|
||||
import useGenerateConvo from '~/hooks/Conversations/useGenerateConvo';
|
||||
import useAddedHelpers from '~/hooks/Chat/useAddedHelpers';
|
||||
|
||||
export default function useAddedResponse({ rootIndex }: { rootIndex: number }) {
|
||||
const currentIndex = useMemo(() => rootIndex + 1, [rootIndex]);
|
||||
const {
|
||||
ask,
|
||||
regenerate,
|
||||
setMessages,
|
||||
getMessages,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
} = useAddedHelpers({
|
||||
rootIndex,
|
||||
currentIndex,
|
||||
});
|
||||
|
||||
const { generateConversation } = useGenerateConvo({
|
||||
index: currentIndex,
|
||||
rootIndex,
|
||||
setConversation,
|
||||
});
|
||||
|
||||
return {
|
||||
ask,
|
||||
regenerate,
|
||||
getMessages,
|
||||
setMessages,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
generateConversation,
|
||||
addedIndex: currentIndex,
|
||||
};
|
||||
}
|
264
client/src/hooks/Chat/useChatFunctions.ts
Normal file
264
client/src/hooks/Chat/useChatFunctions.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Constants,
|
||||
QueryKeys,
|
||||
ContentTypes,
|
||||
parseCompactConvo,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import type {
|
||||
TMessage,
|
||||
TSubmission,
|
||||
TConversation,
|
||||
TEndpointOption,
|
||||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { TAskFunction, ExtendedFile } from '~/common';
|
||||
import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import { getEndpointField, logger } from '~/utils';
|
||||
import useUserKey from '~/hooks/Input/useUserKey';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useChatFunctions({
|
||||
index = 0,
|
||||
files,
|
||||
setFiles,
|
||||
getMessages,
|
||||
setMessages,
|
||||
isSubmitting,
|
||||
conversation,
|
||||
latestMessage,
|
||||
setSubmission,
|
||||
setLatestMessage,
|
||||
}: {
|
||||
index?: number;
|
||||
isSubmitting: boolean;
|
||||
paramId?: string | undefined;
|
||||
conversation: TConversation | null;
|
||||
latestMessage: TMessage | null;
|
||||
getMessages: () => TMessage[] | undefined;
|
||||
setMessages: (messages: TMessage[]) => void;
|
||||
files?: Map<string, ExtendedFile>;
|
||||
setFiles?: SetterOrUpdater<Map<string, ExtendedFile>>;
|
||||
setSubmission: SetterOrUpdater<TSubmission | null>;
|
||||
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
||||
}) {
|
||||
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
const getSender = useGetSender();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
||||
|
||||
const ask: TAskFunction = (
|
||||
{
|
||||
text,
|
||||
overrideConvoId,
|
||||
overrideUserMessageId,
|
||||
parentMessageId = null,
|
||||
conversationId = null,
|
||||
messageId = null,
|
||||
},
|
||||
{
|
||||
editedText = null,
|
||||
editedMessageId = null,
|
||||
resubmitFiles = false,
|
||||
isRegenerate = false,
|
||||
isContinued = false,
|
||||
isEdited = false,
|
||||
overrideMessages,
|
||||
} = {},
|
||||
) => {
|
||||
setShowStopButton(false);
|
||||
resetLatestMultiMessage();
|
||||
if (!!isSubmitting || text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = conversation?.endpoint;
|
||||
if (endpoint === null) {
|
||||
console.error('No endpoint available');
|
||||
return;
|
||||
}
|
||||
|
||||
conversationId = conversationId ?? conversation?.conversationId ?? null;
|
||||
if (conversationId == 'search') {
|
||||
console.error('cannot send any message under search view!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContinued && !latestMessage) {
|
||||
console.error('cannot continue AI message without latestMessage!');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEditOrContinue = isEdited || isContinued;
|
||||
|
||||
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
||||
|
||||
// construct the query message
|
||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||
text = text.trim();
|
||||
const intermediateId = overrideUserMessageId ?? v4();
|
||||
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
|
||||
|
||||
if (conversationId == 'new') {
|
||||
parentMessageId = Constants.NO_PARENT;
|
||||
currentMessages = [];
|
||||
conversationId = null;
|
||||
}
|
||||
|
||||
const parentMessage = currentMessages?.find(
|
||||
(msg) => msg.messageId === latestMessage?.parentMessageId,
|
||||
);
|
||||
|
||||
let thread_id = parentMessage?.thread_id ?? latestMessage?.thread_id;
|
||||
if (!thread_id) {
|
||||
thread_id = currentMessages.find((message) => message.thread_id)?.thread_id;
|
||||
}
|
||||
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
|
||||
// set the endpoint option
|
||||
const convo = parseCompactConvo({
|
||||
endpoint,
|
||||
endpointType,
|
||||
conversation: conversation ?? {},
|
||||
});
|
||||
|
||||
const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {};
|
||||
const endpointOption = {
|
||||
...convo,
|
||||
endpoint,
|
||||
thread_id,
|
||||
endpointType,
|
||||
overrideConvoId,
|
||||
key: getExpiry(),
|
||||
modelDisplayLabel,
|
||||
overrideUserMessageId,
|
||||
} as TEndpointOption;
|
||||
const responseSender = getSender({ model: conversation?.model, ...endpointOption });
|
||||
|
||||
const currentMsg: TMessage = {
|
||||
text,
|
||||
sender: 'User',
|
||||
isCreatedByUser: true,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
messageId: isContinued && messageId ? messageId : intermediateId,
|
||||
thread_id,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const reuseFiles = (isRegenerate || resubmitFiles) && parentMessage?.files;
|
||||
if (setFiles && reuseFiles && parentMessage.files?.length) {
|
||||
currentMsg.files = parentMessage.files;
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
} else if (setFiles && files && files.size > 0) {
|
||||
currentMsg.files = Array.from(files.values()).map((file) => ({
|
||||
file_id: file.file_id,
|
||||
filepath: file.filepath,
|
||||
type: file.type || '', // Ensure type is not undefined
|
||||
height: file.height,
|
||||
width: file.width,
|
||||
}));
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
}
|
||||
|
||||
// construct the placeholder response message
|
||||
const generation = editedText ?? latestMessage?.text ?? '';
|
||||
const responseText = isEditOrContinue ? generation : '';
|
||||
|
||||
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
|
||||
const initialResponse: TMessage = {
|
||||
sender: responseSender,
|
||||
text: responseText,
|
||||
endpoint: endpoint ?? '',
|
||||
parentMessageId: isRegenerate ? messageId : intermediateId,
|
||||
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
||||
thread_id,
|
||||
conversationId,
|
||||
unfinished: false,
|
||||
isCreatedByUser: false,
|
||||
isEdited: isEditOrContinue,
|
||||
iconURL: convo.iconURL,
|
||||
error: false,
|
||||
};
|
||||
|
||||
if (isAssistantsEndpoint(endpoint)) {
|
||||
initialResponse.model = conversation?.assistant_id ?? '';
|
||||
initialResponse.text = '';
|
||||
initialResponse.content = [
|
||||
{
|
||||
type: ContentTypes.TEXT,
|
||||
[ContentTypes.TEXT]: {
|
||||
value: responseText,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
setShowStopButton(true);
|
||||
}
|
||||
|
||||
if (isContinued) {
|
||||
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
|
||||
}
|
||||
|
||||
const submission: TSubmission = {
|
||||
conversation: {
|
||||
...conversation,
|
||||
conversationId,
|
||||
},
|
||||
endpointOption,
|
||||
userMessage: {
|
||||
...currentMsg,
|
||||
generation,
|
||||
responseMessageId,
|
||||
overrideParentMessageId: isRegenerate ? messageId : null,
|
||||
},
|
||||
messages: currentMessages,
|
||||
isEdited: isEditOrContinue,
|
||||
isContinued,
|
||||
isRegenerate,
|
||||
initialResponse,
|
||||
};
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([...submission.messages, initialResponse]);
|
||||
} else {
|
||||
setMessages([...submission.messages, currentMsg, initialResponse]);
|
||||
}
|
||||
if (index === 0 && setLatestMessage) {
|
||||
setLatestMessage(initialResponse);
|
||||
}
|
||||
setSubmission(submission);
|
||||
logger.log('Submission:');
|
||||
logger.dir(submission, { depth: null });
|
||||
};
|
||||
|
||||
const regenerate = ({ parentMessageId }) => {
|
||||
const messages = getMessages();
|
||||
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isRegenerate: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
ask,
|
||||
regenerate,
|
||||
};
|
||||
}
|
182
client/src/hooks/Chat/useChatHelpers.ts
Normal file
182
client/src/hooks/Chat/useChatHelpers.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useNewConvo from '~/hooks/useNewConvo';
|
||||
import store from '~/store';
|
||||
|
||||
// this to be set somewhere else
|
||||
export default function useChatHelpers(index = 0, paramId?: string) {
|
||||
const clearAllSubmissions = store.useClearSubmissionState();
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||
const [filesLoading, setFilesLoading] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
|
||||
const { newConversation } = useNewConvo(index);
|
||||
const { useCreateConversationAtom } = store;
|
||||
const { conversation, setConversation } = useCreateConversationAtom(index);
|
||||
const { conversationId } = conversation ?? {};
|
||||
|
||||
const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? '';
|
||||
|
||||
/* Messages: here simply to fetch, don't export and use `getMessages()` instead */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { data: _messages } = useGetMessagesByConvoId(conversationId ?? '', {
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index));
|
||||
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index));
|
||||
const setSiblingIdx = useSetRecoilState(
|
||||
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null),
|
||||
);
|
||||
|
||||
const setMessages = useCallback(
|
||||
(messages: TMessage[]) => {
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, queryParam], messages);
|
||||
if (queryParam === 'new') {
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, conversationId], messages);
|
||||
}
|
||||
},
|
||||
[queryParam, queryClient, conversationId],
|
||||
);
|
||||
|
||||
const getMessages = useCallback(() => {
|
||||
return queryClient.getQueryData<TMessage[]>([QueryKeys.messages, queryParam]);
|
||||
}, [queryParam, queryClient]);
|
||||
|
||||
/* Conversation */
|
||||
// const setActiveConvos = useSetRecoilState(store.activeConversations);
|
||||
|
||||
// const setConversation = useCallback(
|
||||
// (convoUpdate: TConversation) => {
|
||||
// _setConversation(prev => {
|
||||
// const { conversationId: convoId } = prev ?? { conversationId: null };
|
||||
// const { conversationId: currentId } = convoUpdate;
|
||||
// if (currentId && convoId && convoId !== 'new' && convoId !== currentId) {
|
||||
// // for now, we delete the prev convoId from activeConversations
|
||||
// const newActiveConvos = { [currentId]: true };
|
||||
// setActiveConvos(newActiveConvos);
|
||||
// }
|
||||
// return convoUpdate;
|
||||
// });
|
||||
// },
|
||||
// [_setConversation, setActiveConvos],
|
||||
// );
|
||||
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
|
||||
const { ask, regenerate } = useChatFunctions({
|
||||
index,
|
||||
files,
|
||||
setFiles,
|
||||
getMessages,
|
||||
setMessages,
|
||||
isSubmitting,
|
||||
conversation,
|
||||
latestMessage,
|
||||
setSubmission,
|
||||
setLatestMessage,
|
||||
});
|
||||
|
||||
const continueGeneration = () => {
|
||||
if (!latestMessage) {
|
||||
console.error('Failed to regenerate the message: latestMessage not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = getMessages();
|
||||
|
||||
const parentMessage = messages?.find(
|
||||
(element) => element.messageId == latestMessage.parentMessageId,
|
||||
);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isContinued: true, isRegenerate: true, isEdited: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found, or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopGenerating = () => clearAllSubmissions();
|
||||
|
||||
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
stopGenerating();
|
||||
};
|
||||
|
||||
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const parentMessageId = latestMessage?.parentMessageId;
|
||||
if (!parentMessageId) {
|
||||
console.error('Failed to regenerate the message: parentMessageId not found.');
|
||||
return;
|
||||
}
|
||||
regenerate({ parentMessageId });
|
||||
};
|
||||
|
||||
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
continueGeneration();
|
||||
setSiblingIdx(0);
|
||||
};
|
||||
|
||||
const [showBingToneSetting, setShowBingToneSetting] = useRecoilState(
|
||||
store.showBingToneSettingFamily(index),
|
||||
);
|
||||
const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index));
|
||||
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index));
|
||||
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
|
||||
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index));
|
||||
const [showAgentSettings, setShowAgentSettings] = useRecoilState(
|
||||
store.showAgentSettingsFamily(index),
|
||||
);
|
||||
|
||||
return {
|
||||
newConversation,
|
||||
conversation,
|
||||
setConversation,
|
||||
// getConvos,
|
||||
// setConvos,
|
||||
isSubmitting,
|
||||
setIsSubmitting,
|
||||
getMessages,
|
||||
setMessages,
|
||||
setSiblingIdx,
|
||||
latestMessage,
|
||||
setLatestMessage,
|
||||
resetLatestMessage,
|
||||
ask,
|
||||
index,
|
||||
regenerate,
|
||||
stopGenerating,
|
||||
handleStopGenerating,
|
||||
handleRegenerate,
|
||||
handleContinue,
|
||||
showPopover,
|
||||
setShowPopover,
|
||||
abortScroll,
|
||||
setAbortScroll,
|
||||
showBingToneSetting,
|
||||
setShowBingToneSetting,
|
||||
preset,
|
||||
setPreset,
|
||||
optionSettings,
|
||||
setOptionSettings,
|
||||
showAgentSettings,
|
||||
setShowAgentSettings,
|
||||
files,
|
||||
setFiles,
|
||||
filesLoading,
|
||||
setFilesLoading,
|
||||
};
|
||||
}
|
|
@ -3,6 +3,7 @@ export { default as usePresets } from './usePresets';
|
|||
export { default as useGetSender } from './useGetSender';
|
||||
export { default as useDefaultConvo } from './useDefaultConvo';
|
||||
export { default as useConversation } from './useConversation';
|
||||
export { default as useGenerateConvo } from './useGenerateConvo';
|
||||
export { default as useConversations } from './useConversations';
|
||||
export { default as useDebouncedInput } from './useDebouncedInput';
|
||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
||||
|
|
147
client/src/hooks/Conversations/useGenerateConvo.ts
Normal file
147
client/src/hooks/Conversations/useGenerateConvo.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
TPreset,
|
||||
TModelsConfig,
|
||||
TConversation,
|
||||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type { AssistantListItem } from '~/common';
|
||||
import { getEndpointField, buildDefaultConvo, getDefaultEndpoint } from '~/utils';
|
||||
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
const useGenerateConvo = ({
|
||||
index = 0,
|
||||
rootIndex,
|
||||
setConversation,
|
||||
}: {
|
||||
index?: number;
|
||||
rootIndex: number;
|
||||
setConversation?: SetterOrUpdater<TConversation | null>;
|
||||
}) => {
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const assistantsListMap = useAssistantListMap();
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||
const rootConvo = useRecoilValue(store.conversationByKeySelector(rootIndex));
|
||||
|
||||
useEffect(() => {
|
||||
if (rootConvo?.conversationId && setConversation) {
|
||||
setConversation((prevState) => {
|
||||
if (!prevState) {
|
||||
return prevState;
|
||||
}
|
||||
const update = {
|
||||
...prevState,
|
||||
conversationId: rootConvo.conversationId,
|
||||
} as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
}
|
||||
}, [rootConvo?.conversationId, setConversation]);
|
||||
|
||||
const generateConversation = useCallback(
|
||||
({
|
||||
template = {},
|
||||
preset,
|
||||
modelsData,
|
||||
}: {
|
||||
template?: Partial<TConversation>;
|
||||
preset?: Partial<TPreset>;
|
||||
modelsData?: TModelsConfig;
|
||||
} = {}) => {
|
||||
let conversation = {
|
||||
conversationId: 'new',
|
||||
title: 'New Chat',
|
||||
endpoint: null,
|
||||
...template,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
};
|
||||
|
||||
if (rootConvo?.conversationId) {
|
||||
conversation.conversationId = rootConvo.conversationId;
|
||||
}
|
||||
|
||||
const modelsConfig = modelsData ?? modelsQuery.data;
|
||||
|
||||
const defaultEndpoint = getDefaultEndpoint({
|
||||
convoSetup: preset ?? conversation,
|
||||
endpointsConfig,
|
||||
});
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, defaultEndpoint, 'type');
|
||||
if (!conversation.endpointType && endpointType) {
|
||||
conversation.endpointType = endpointType;
|
||||
} else if (conversation.endpointType && !endpointType) {
|
||||
conversation.endpointType = undefined;
|
||||
}
|
||||
|
||||
const isAssistantEndpoint = isAssistantsEndpoint(defaultEndpoint);
|
||||
const assistants: AssistantListItem[] = assistantsListMap[defaultEndpoint] ?? [];
|
||||
|
||||
if (
|
||||
conversation.assistant_id &&
|
||||
!assistantsListMap[defaultEndpoint]?.[conversation.assistant_id]
|
||||
) {
|
||||
conversation.assistant_id = undefined;
|
||||
}
|
||||
|
||||
if (!conversation.assistant_id && isAssistantEndpoint) {
|
||||
conversation.assistant_id =
|
||||
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${defaultEndpoint}`) ??
|
||||
assistants[0]?.id;
|
||||
}
|
||||
|
||||
if (
|
||||
conversation.assistant_id &&
|
||||
isAssistantEndpoint &&
|
||||
conversation.conversationId === 'new'
|
||||
) {
|
||||
const assistant = assistants.find((asst) => asst.id === conversation.assistant_id);
|
||||
conversation.model = assistant?.model;
|
||||
}
|
||||
|
||||
if (conversation.assistant_id && !isAssistantEndpoint) {
|
||||
conversation.assistant_id = undefined;
|
||||
}
|
||||
|
||||
const models = modelsConfig?.[defaultEndpoint] ?? [];
|
||||
conversation = buildDefaultConvo({
|
||||
conversation,
|
||||
lastConversationSetup: preset as TConversation,
|
||||
endpoint: defaultEndpoint,
|
||||
models,
|
||||
});
|
||||
|
||||
if (preset?.title) {
|
||||
conversation.title = preset.title;
|
||||
}
|
||||
|
||||
if (setConversation) {
|
||||
setConversation(conversation);
|
||||
}
|
||||
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
timeoutIdRef.current = setTimeout(() => {
|
||||
const textarea = document.getElementById(mainTextareaId);
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}, 150);
|
||||
return conversation;
|
||||
},
|
||||
[assistantsListMap, endpointsConfig, index, modelsQuery.data, rootConvo, setConversation],
|
||||
);
|
||||
|
||||
return { generateConversation };
|
||||
};
|
||||
|
||||
export default useGenerateConvo;
|
|
@ -1,6 +1,6 @@
|
|||
import { useSetRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||
|
@ -9,9 +9,10 @@ import store from '~/store';
|
|||
const useNavigateToConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const clearAllLatestMessages = store.useClearLatestMessages();
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
|
||||
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
||||
if (!conversation) {
|
||||
|
@ -20,7 +21,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
}
|
||||
setSubmission(null);
|
||||
if (_resetLatestMessage) {
|
||||
resetLatestMessage();
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
|
||||
let convo = { ...conversation };
|
||||
|
@ -47,6 +48,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
models,
|
||||
});
|
||||
}
|
||||
clearAllConversations(true);
|
||||
setConversation(convo);
|
||||
navigate(`/c/${convo.conversationId ?? 'new'}`);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import filenamify from 'filenamify';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { QueryKeys, modularEndpoints, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import {
|
||||
|
@ -11,7 +11,7 @@ import {
|
|||
useDeletePresetMutation,
|
||||
useGetPresetsQuery,
|
||||
} from '~/data-provider';
|
||||
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
|
||||
import { cleanupPreset, removeUnavailableTools, getConvoSwitchLogic } from '~/utils';
|
||||
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
|
@ -124,8 +124,6 @@ export default function usePresets() {
|
|||
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
|
||||
const { endpoint } = conversation ?? {};
|
||||
|
||||
const importPreset = (jsonPreset: TPreset) => {
|
||||
createPresetMutation.mutate(
|
||||
{ ...jsonPreset },
|
||||
|
@ -171,34 +169,38 @@ export default function usePresets() {
|
|||
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
const currentEndpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const endpointType = getEndpointField(endpointsConfig, newPreset.endpoint, 'type');
|
||||
const isAssistantSwitch =
|
||||
isAssistantsEndpoint(newPreset.endpoint) &&
|
||||
isAssistantsEndpoint(conversation?.endpoint) &&
|
||||
conversation?.endpoint === newPreset.endpoint;
|
||||
const {
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
newEndpointType,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint: newPreset.endpoint ?? '',
|
||||
modularChat,
|
||||
conversation,
|
||||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (
|
||||
(modularEndpoints.has(endpoint ?? '') ||
|
||||
modularEndpoints.has(currentEndpointType ?? '') ||
|
||||
isAssistantSwitch) &&
|
||||
(modularEndpoints.has(newPreset?.endpoint ?? '') ||
|
||||
modularEndpoints.has(endpointType ?? '') ||
|
||||
isAssistantSwitch) &&
|
||||
(endpoint === newPreset?.endpoint || modularChat || isAssistantSwitch)
|
||||
) {
|
||||
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||
if (isExistingConversation && isModular) {
|
||||
const currentConvo = getDefaultConversation({
|
||||
/* target endpointType is necessary to avoid endpoint mixing */
|
||||
conversation: { ...(conversation ?? {}), endpointType },
|
||||
preset: { ...newPreset, endpointType },
|
||||
conversation: { ...(conversation ?? {}), endpointType: newEndpointType },
|
||||
preset: { ...newPreset, endpointType: newEndpointType },
|
||||
});
|
||||
|
||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||
newConversation({ template: currentConvo, preset: currentConvo, keepLatestMessage: true });
|
||||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: currentConvo,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newConversation({ preset: newPreset });
|
||||
newConversation({ preset: newPreset, keepAddedConvos: isModular });
|
||||
};
|
||||
|
||||
const onChangePreset = (preset: TPreset) => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './useAutoSave';
|
||||
export { default as useUserKey } from './useUserKey';
|
||||
export { default as useDebounce } from './useDebounce';
|
||||
export { default as useTextarea } from './useTextarea';
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import store from '~/store';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
|
||||
/** Event Keys that shouldn't trigger a command */
|
||||
const invalidKeys = {
|
||||
Escape: true,
|
||||
Backspace: true,
|
||||
Enter: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to determine if a command should trigger.
|
||||
|
@ -23,10 +29,6 @@ const shouldTriggerCommand = (
|
|||
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
|
||||
|
||||
const shouldTrigger = isAtStart || isPrecededBySpace;
|
||||
if (shouldTrigger) {
|
||||
// Blurring helps prevent the command from firing twice.
|
||||
textAreaRef.current.blur();
|
||||
}
|
||||
return shouldTrigger;
|
||||
};
|
||||
|
||||
|
@ -34,33 +36,32 @@ const shouldTriggerCommand = (
|
|||
* Custom hook for handling key up events with command triggers.
|
||||
*/
|
||||
const useHandleKeyUp = ({
|
||||
index,
|
||||
textAreaRef,
|
||||
setShowPlusPopover,
|
||||
setShowMentionPopover,
|
||||
}: {
|
||||
index: number;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
setShowPlusPopover: SetterOrUpdater<boolean>;
|
||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||
}) => {
|
||||
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
|
||||
|
||||
const handleAtCommand = useCallback(() => {
|
||||
if (shouldTriggerCommand(textAreaRef, '@')) {
|
||||
setShowMentionPopover(true);
|
||||
}
|
||||
}, [textAreaRef, setShowMentionPopover]);
|
||||
|
||||
// const handlePlusCommand = useCallback(() => {
|
||||
// if (shouldTriggerCommand(textAreaRef, '+')) {
|
||||
// console.log('+ command triggered');
|
||||
// }
|
||||
// }, [textAreaRef]);
|
||||
const handlePlusCommand = useCallback(() => {
|
||||
if (shouldTriggerCommand(textAreaRef, '+')) {
|
||||
setShowPlusPopover(true);
|
||||
}
|
||||
}, [textAreaRef, setShowPlusPopover]);
|
||||
|
||||
const commandHandlers = useMemo(
|
||||
() => ({
|
||||
'@': handleAtCommand,
|
||||
// '+': handlePlusCommand,
|
||||
'+': handlePlusCommand,
|
||||
}),
|
||||
[handleAtCommand],
|
||||
// [handleAtCommand, handlePlusCommand],
|
||||
[handleAtCommand, handlePlusCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -73,7 +74,7 @@ const useHandleKeyUp = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
if (invalidKeys[event.key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,12 @@ import {
|
|||
useGetStartupConfig,
|
||||
useGetEndpointsQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { getConfigDefaults, EModelEndpoint, alternateName } from 'librechat-data-provider';
|
||||
import {
|
||||
getConfigDefaults,
|
||||
EModelEndpoint,
|
||||
alternateName,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { MentionOption } from '~/common';
|
||||
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
|
||||
|
@ -39,7 +44,13 @@ const assistantMapFn =
|
|||
}),
|
||||
});
|
||||
|
||||
export default function useMentions({ assistantMap }: { assistantMap: TAssistantsMap }) {
|
||||
export default function useMentions({
|
||||
assistantMap,
|
||||
includeAssistants,
|
||||
}: {
|
||||
assistantMap: TAssistantsMap;
|
||||
includeAssistants: boolean;
|
||||
}) {
|
||||
const { data: presets } = useGetPresetsQuery();
|
||||
const { data: modelsConfig } = useGetModelsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
@ -85,6 +96,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
|||
);
|
||||
|
||||
const options: MentionOption[] = useMemo(() => {
|
||||
let validEndpoints = endpoints;
|
||||
if (!includeAssistants) {
|
||||
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
|
||||
}
|
||||
const mentions = [
|
||||
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
||||
value: modelSpec.name,
|
||||
|
@ -101,7 +116,7 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
|||
}),
|
||||
type: 'modelSpec' as const,
|
||||
})),
|
||||
...(interfaceConfig.endpointsMenu ? endpoints : []).map((endpoint) => ({
|
||||
...(interfaceConfig.endpointsMenu ? validEndpoints : []).map((endpoint) => ({
|
||||
value: endpoint,
|
||||
label: alternateName[endpoint] ?? endpoint ?? '',
|
||||
type: 'endpoint' as const,
|
||||
|
@ -112,10 +127,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
|||
size: 20,
|
||||
}),
|
||||
})),
|
||||
...(endpointsConfig?.[EModelEndpoint.assistants]
|
||||
...(endpointsConfig?.[EModelEndpoint.assistants] && includeAssistants
|
||||
? assistantListMap[EModelEndpoint.assistants] || []
|
||||
: []),
|
||||
...(endpointsConfig?.[EModelEndpoint.azureAssistants]
|
||||
...(endpointsConfig?.[EModelEndpoint.azureAssistants] && includeAssistants
|
||||
? assistantListMap[EModelEndpoint.azureAssistants] || []
|
||||
: []),
|
||||
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
|
||||
|
@ -142,6 +157,7 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
|||
assistantMap,
|
||||
endpointsConfig,
|
||||
assistantListMap,
|
||||
includeAssistants,
|
||||
interfaceConfig.presets,
|
||||
interfaceConfig.endpointsMenu,
|
||||
]);
|
||||
|
|
|
@ -8,25 +8,26 @@ import type {
|
|||
TAssistantsMap,
|
||||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { MentionOption } from '~/common';
|
||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools } from '~/utils';
|
||||
import { useDefaultConvo, useNewConvo } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useDefaultConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useSelectMention({
|
||||
presets,
|
||||
modelSpecs,
|
||||
endpointsConfig,
|
||||
assistantMap,
|
||||
endpointsConfig,
|
||||
newConversation,
|
||||
}: {
|
||||
presets?: TPreset[];
|
||||
modelSpecs: TModelSpec[];
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
assistantMap: TAssistantsMap;
|
||||
newConversation: ConvoGenerator;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
}) {
|
||||
const { conversation } = useChatContext();
|
||||
const { newConversation } = useNewConvo();
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
const modularChat = useRecoilValue(store.modularChat);
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
|
@ -45,12 +46,12 @@ export default function useSelectMention({
|
|||
}
|
||||
|
||||
const {
|
||||
template,
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
newEndpointType,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
newEndpointType,
|
||||
template,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint,
|
||||
modularChat,
|
||||
|
@ -58,7 +59,8 @@ export default function useSelectMention({
|
|||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||
if (isExistingConversation && isModular) {
|
||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||
|
||||
const currentConvo = getDefaultConversation({
|
||||
|
@ -68,11 +70,20 @@ export default function useSelectMention({
|
|||
});
|
||||
|
||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||
newConversation({ template: currentConvo, preset, keepLatestMessage: true });
|
||||
newConversation({
|
||||
template: currentConvo,
|
||||
preset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newConversation({ template: { ...(template as Partial<TConversation>) }, preset });
|
||||
newConversation({
|
||||
template: { ...(template as Partial<TConversation>) },
|
||||
preset,
|
||||
keepAddedConvos: isModular,
|
||||
});
|
||||
},
|
||||
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
|
||||
);
|
||||
|
@ -142,12 +153,12 @@ export default function useSelectMention({
|
|||
const newEndpoint = newPreset.endpoint ?? '';
|
||||
|
||||
const {
|
||||
template,
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
newEndpointType,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
newEndpointType,
|
||||
template,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint,
|
||||
modularChat,
|
||||
|
@ -155,7 +166,8 @@ export default function useSelectMention({
|
|||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||
if (isExistingConversation && isModular) {
|
||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||
|
||||
const currentConvo = getDefaultConversation({
|
||||
|
@ -165,19 +177,24 @@ export default function useSelectMention({
|
|||
});
|
||||
|
||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||
newConversation({ template: currentConvo, preset: newPreset, keepLatestMessage: true });
|
||||
newConversation({
|
||||
template: currentConvo,
|
||||
preset: newPreset,
|
||||
keepLatestMessage: true,
|
||||
keepAddedConvos: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newConversation({ preset: newPreset });
|
||||
newConversation({ preset: newPreset, keepAddedConvos: true });
|
||||
},
|
||||
[
|
||||
availableTools,
|
||||
conversation,
|
||||
getDefaultConversation,
|
||||
modularChat,
|
||||
conversation,
|
||||
availableTools,
|
||||
newConversation,
|
||||
endpointsConfig,
|
||||
getDefaultConversation,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export { default as useAvatar } from './useAvatar';
|
||||
export { default as useProgress } from './useProgress';
|
||||
export { default as useSubmitMessage } from './useSubmitMessage';
|
||||
export { default as useMessageActions } from './useMessageActions';
|
||||
export { default as useMessageProcess } from './useMessageProcess';
|
||||
export { default as useMessageHelpers } from './useMessageHelpers';
|
||||
export { default as useCopyToClipboard } from './useCopyToClipboard';
|
||||
export { default as useMessageScrolling } from './useMessageScrolling';
|
||||
|
|
|
@ -1,50 +1,45 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { initials } from '@dicebear/collection';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
|
||||
const avatarCache: Record<string, string> = {};
|
||||
|
||||
const useAvatar = (user: TUser | undefined) => {
|
||||
const [avatarSrc, setAvatarSrc] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarSrc.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user?.avatar) {
|
||||
return;
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
if (!user?.username) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
const generateAvatar = async () => {
|
||||
if (!user) {
|
||||
return;
|
||||
if (user.avatar) {
|
||||
return user.avatar;
|
||||
}
|
||||
|
||||
const { username } = user;
|
||||
|
||||
if (avatarCache[username]) {
|
||||
return avatarCache[username];
|
||||
}
|
||||
|
||||
const avatar = createAvatar(initials, {
|
||||
seed: username,
|
||||
fontFamily: ['Verdana'],
|
||||
fontSize: 36,
|
||||
});
|
||||
|
||||
try {
|
||||
const avatarDataUri = await avatar.toDataUri();
|
||||
setAvatarSrc(avatarDataUri);
|
||||
} catch (error) {
|
||||
let avatarDataUri = '';
|
||||
avatar
|
||||
.toDataUri()
|
||||
.then((dataUri) => {
|
||||
avatarDataUri = dataUri;
|
||||
avatarCache[username] = dataUri; // Store in cache
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to generate avatar:', error);
|
||||
setAvatarSrc('');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
generateAvatar();
|
||||
}, [user, avatarSrc.length]);
|
||||
|
||||
return avatarSrc;
|
||||
return avatarDataUri;
|
||||
}, [user]);
|
||||
};
|
||||
|
||||
export default useAvatar;
|
||||
|
|
95
client/src/hooks/Messages/useMessageActions.tsx
Normal file
95
client/src/hooks/Messages/useMessageActions.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import { useChatContext, useAddedChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import useCopyToClipboard from './useCopyToClipboard';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import store from '~/store';
|
||||
|
||||
export type TMessageActions = Pick<
|
||||
TMessageProps,
|
||||
'message' | 'currentEditId' | 'setCurrentEditId'
|
||||
> & {
|
||||
isMultiMessage?: boolean;
|
||||
};
|
||||
export default function useMessageActions(props: TMessageActions) {
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
|
||||
const { message, currentEditId, setCurrentEditId, isMultiMessage } = props;
|
||||
|
||||
const {
|
||||
ask,
|
||||
index,
|
||||
regenerate,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
setLatestMessage,
|
||||
conversation: rootConvo,
|
||||
isSubmitting: isSubmittingRoot,
|
||||
} = useChatContext();
|
||||
const { conversation: addedConvo, isSubmitting: isSubmittingAdditional } = useAddedChatContext();
|
||||
const conversation = useMemo(
|
||||
() => (isMultiMessage ? addedConvo : rootConvo),
|
||||
[isMultiMessage, addedConvo, rootConvo],
|
||||
);
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
|
||||
const { text, content, messageId = null, isCreatedByUser } = message ?? {};
|
||||
const edit = useMemo(() => messageId === currentEditId, [messageId, currentEditId]);
|
||||
|
||||
const enterEdit = useCallback(
|
||||
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId),
|
||||
[messageId, setCurrentEditId],
|
||||
);
|
||||
|
||||
const assistant = useMemo(
|
||||
() =>
|
||||
isAssistantsEndpoint(conversation?.endpoint) &&
|
||||
assistantMap?.[conversation?.endpoint ?? '']?.[message?.model ?? ''],
|
||||
[assistantMap, conversation?.endpoint, message?.model],
|
||||
);
|
||||
|
||||
const isSubmitting = useMemo(
|
||||
() => (isMultiMessage ? isSubmittingAdditional : isSubmittingRoot),
|
||||
[isMultiMessage, isSubmittingAdditional, isSubmittingRoot],
|
||||
);
|
||||
|
||||
const regenerateMessage = useCallback(() => {
|
||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
regenerate(message);
|
||||
}, [isSubmitting, isCreatedByUser, message, regenerate]);
|
||||
|
||||
const copyToClipboard = useCopyToClipboard({ text, content });
|
||||
|
||||
const messageLabel = useMemo(() => {
|
||||
if (message?.isCreatedByUser) {
|
||||
return UsernameDisplay ? user?.name || user?.username : localize('com_user_message');
|
||||
} else if (assistant) {
|
||||
return assistant.name ?? 'Assistant';
|
||||
} else {
|
||||
return message?.sender;
|
||||
}
|
||||
}, [message, assistant, UsernameDisplay, user, localize]);
|
||||
|
||||
return {
|
||||
ask,
|
||||
edit,
|
||||
index,
|
||||
assistant,
|
||||
enterEdit,
|
||||
conversation,
|
||||
messageLabel,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
setLatestMessage,
|
||||
regenerateMessage,
|
||||
};
|
||||
}
|
88
client/src/hooks/Messages/useMessageProcess.tsx
Normal file
88
client/src/hooks/Messages/useMessageProcess.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { getLatestText, getLengthAndFirstFiveChars } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useMessageProcess({ message }: { message?: TMessage | null }) {
|
||||
const latestText = useRef<string | number>('');
|
||||
const hasNoChildren = useMemo(() => !message?.children?.length, [message]);
|
||||
const [siblingMessage, setSiblingMessage] = useState<TMessage | null>(null);
|
||||
|
||||
const {
|
||||
index,
|
||||
conversation,
|
||||
latestMessage,
|
||||
setAbortScroll,
|
||||
setLatestMessage,
|
||||
isSubmitting: isSubmittingRoot,
|
||||
} = useChatContext();
|
||||
const { isSubmitting: isSubmittingAdditional } = useAddedChatContext();
|
||||
const latestMultiMessage = useRecoilValue(store.latestMessageFamily(index + 1));
|
||||
const isSubmittingFamily = useMemo(
|
||||
() => isSubmittingRoot || isSubmittingAdditional,
|
||||
[isSubmittingRoot, isSubmittingAdditional],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation?.conversationId === 'new') {
|
||||
return;
|
||||
}
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (!hasNoChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = getLatestText(message);
|
||||
const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`;
|
||||
|
||||
if (textKey === latestText.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestText.current = textKey;
|
||||
setLatestMessage({ ...message });
|
||||
}, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (isSubmittingFamily) {
|
||||
setAbortScroll(true);
|
||||
} else {
|
||||
setAbortScroll(false);
|
||||
}
|
||||
}, [isSubmittingFamily, setAbortScroll]);
|
||||
|
||||
const showSibling = useMemo(
|
||||
() =>
|
||||
(hasNoChildren && latestMultiMessage && !latestMultiMessage?.children?.length) ||
|
||||
siblingMessage,
|
||||
[hasNoChildren, latestMultiMessage, siblingMessage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
hasNoChildren &&
|
||||
latestMultiMessage &&
|
||||
latestMultiMessage.conversationId === message?.conversationId
|
||||
) {
|
||||
const newSibling = Object.assign({}, latestMultiMessage, {
|
||||
parentMessageId: message?.parentMessageId,
|
||||
depth: message?.depth,
|
||||
});
|
||||
setSiblingMessage(newSibling);
|
||||
}
|
||||
}, [hasNoChildren, latestMultiMessage, message, setSiblingMessage, latestMessage]);
|
||||
|
||||
return {
|
||||
showSibling,
|
||||
handleScroll,
|
||||
conversation,
|
||||
siblingMessage,
|
||||
setSiblingMessage,
|
||||
isSubmittingFamily,
|
||||
latestMultiMessage,
|
||||
};
|
||||
}
|
|
@ -1,15 +1,27 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useCallback } from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useChatContext, useChatFormContext } from '~/Providers';
|
||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const appendIndex = (index: number, value?: string) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return `${value}${Constants.COMMON_DIVIDER}${index}`;
|
||||
};
|
||||
|
||||
export default function useSubmitMessage(helpers?: { clearDraft?: () => void }) {
|
||||
const { user } = useAuthContext();
|
||||
const methods = useChatFormContext();
|
||||
const { ask, index } = useChatContext();
|
||||
const { ask, index, getMessages, setMessages, latestMessage } = useChatContext();
|
||||
const { addedIndex, ask: askAdditional, conversation: addedConvo } = useAddedChatContext();
|
||||
|
||||
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
||||
|
||||
const submitMessage = useCallback(
|
||||
|
@ -17,11 +29,52 @@ export default function useSubmitMessage(helpers?: { clearDraft?: () => void })
|
|||
if (!data) {
|
||||
return console.warn('No data provided to submitMessage');
|
||||
}
|
||||
ask({ text: data.text });
|
||||
const rootMessages = getMessages();
|
||||
const isLatestInRootMessages = rootMessages?.some(
|
||||
(message) => message?.messageId === latestMessage?.messageId,
|
||||
);
|
||||
if (!isLatestInRootMessages && latestMessage) {
|
||||
setMessages([...(rootMessages || []), latestMessage]);
|
||||
}
|
||||
|
||||
const hasAdded = addedIndex && activeConvos[addedIndex] && addedConvo;
|
||||
const isNewMultiConvo =
|
||||
hasAdded &&
|
||||
activeConvos.every((convoId) => convoId === Constants.NEW_CONVO) &&
|
||||
!rootMessages?.length;
|
||||
const overrideConvoId = isNewMultiConvo ? v4() : undefined;
|
||||
const overrideUserMessageId = hasAdded ? v4() : undefined;
|
||||
const rootIndex = addedIndex - 1;
|
||||
ask({
|
||||
text: data.text,
|
||||
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
||||
});
|
||||
if (hasAdded) {
|
||||
askAdditional(
|
||||
{
|
||||
text: data.text,
|
||||
overrideConvoId: appendIndex(addedIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(addedIndex, overrideUserMessageId),
|
||||
},
|
||||
{ overrideMessages: rootMessages },
|
||||
);
|
||||
}
|
||||
methods.reset();
|
||||
helpers?.clearDraft && helpers.clearDraft();
|
||||
},
|
||||
[ask, methods, helpers],
|
||||
[
|
||||
ask,
|
||||
methods,
|
||||
helpers,
|
||||
addedIndex,
|
||||
addedConvo,
|
||||
setMessages,
|
||||
getMessages,
|
||||
activeConvos,
|
||||
askAdditional,
|
||||
latestMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const submitPrompt = useCallback(
|
||||
|
|
544
client/src/hooks/SSE/useEventHandlers.ts
Normal file
544
client/src/hooks/SSE/useEventHandlers.ts
Normal file
|
@ -0,0 +1,544 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
QueryKeys,
|
||||
Constants,
|
||||
EndpointURLs,
|
||||
tPresetSchema,
|
||||
tMessageSchema,
|
||||
tConvoUpdateSchema,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
TMessage,
|
||||
TConversation,
|
||||
TSubmission,
|
||||
ConversationData,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||
import type { TResData, ConvoGenerator } from '~/common';
|
||||
import {
|
||||
addConversation,
|
||||
deleteConversation,
|
||||
updateConversation,
|
||||
getConversationById,
|
||||
} from '~/utils';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
import type { TGenTitleMutation } from '~/data-provider';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
|
||||
type TSyncData = {
|
||||
sync: boolean;
|
||||
thread_id: string;
|
||||
messages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export type EventHandlerParams = {
|
||||
isAddedRequest?: boolean;
|
||||
genTitle?: TGenTitleMutation;
|
||||
setCompleted: React.Dispatch<React.SetStateAction<Set<unknown>>>;
|
||||
setMessages: (messages: TMessage[]) => void;
|
||||
getMessages: () => TMessage[] | undefined;
|
||||
setIsSubmitting: SetterOrUpdater<boolean>;
|
||||
setConversation?: SetterOrUpdater<TConversation | null>;
|
||||
newConversation?: ConvoGenerator;
|
||||
setShowStopButton: SetterOrUpdater<boolean>;
|
||||
resetLatestMessage?: Resetter;
|
||||
};
|
||||
|
||||
export default function useEventHandlers({
|
||||
genTitle,
|
||||
setMessages,
|
||||
getMessages,
|
||||
setCompleted,
|
||||
isAddedRequest,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
newConversation,
|
||||
setShowStopButton,
|
||||
resetLatestMessage,
|
||||
}: EventHandlerParams) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { conversationId: paramId } = useParams();
|
||||
const { token } = useAuthContext();
|
||||
|
||||
const contentHandler = useContentHandler({ setMessages, getMessages });
|
||||
|
||||
const messageHandler = useCallback(
|
||||
(data: string, submission: TSubmission) => {
|
||||
const {
|
||||
messages,
|
||||
userMessage,
|
||||
plugin,
|
||||
plugins,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setMessages([
|
||||
...messages,
|
||||
userMessage,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
[setMessages],
|
||||
);
|
||||
|
||||
const cancelHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation } = data;
|
||||
const { messages, isRegenerate = false } = submission;
|
||||
|
||||
const convoUpdate = conversation ?? submission.conversation;
|
||||
|
||||
// update the messages
|
||||
if (isRegenerate) {
|
||||
const messagesUpdate = [...messages, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
} else {
|
||||
const messagesUpdate = [...messages, requestMessage, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submission.conversation.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submission.conversation.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (genTitle && isNewConvo && requestMessage?.parentMessageId === Constants.NO_PARENT) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: convoUpdate.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...convoUpdate,
|
||||
};
|
||||
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[setMessages, setConversation, genTitle, isAddedRequest, queryClient, setIsSubmitting],
|
||||
);
|
||||
|
||||
const syncHandler = useCallback(
|
||||
(data: TSyncData, submission: TSubmission) => {
|
||||
const { conversationId, thread_id, responseMessage, requestMessage } = data;
|
||||
const { initialResponse, messages: _messages, userMessage } = submission;
|
||||
|
||||
const messages = _messages.filter((msg) => msg.messageId !== userMessage.messageId);
|
||||
|
||||
setMessages([
|
||||
...messages,
|
||||
requestMessage,
|
||||
{
|
||||
...initialResponse,
|
||||
...responseMessage,
|
||||
},
|
||||
]);
|
||||
|
||||
let update = {} as TConversation;
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = requestMessage.parentMessageId;
|
||||
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
thread_id,
|
||||
title,
|
||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
} else if (setConversation) {
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
thread_id,
|
||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
||||
setShowStopButton(true);
|
||||
if (resetLatestMessage) {
|
||||
resetLatestMessage();
|
||||
}
|
||||
},
|
||||
[
|
||||
setMessages,
|
||||
setConversation,
|
||||
queryClient,
|
||||
isAddedRequest,
|
||||
setShowStopButton,
|
||||
resetLatestMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const createdHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { messages, userMessage, isRegenerate = false } = submission;
|
||||
const initialResponse = {
|
||||
...submission.initialResponse,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
messageId: userMessage?.messageId + '_',
|
||||
};
|
||||
if (isRegenerate) {
|
||||
setMessages([...messages, initialResponse]);
|
||||
} else {
|
||||
setMessages([...messages, userMessage, initialResponse]);
|
||||
}
|
||||
|
||||
const { conversationId, parentMessageId } = userMessage;
|
||||
|
||||
let update = {} as TConversation;
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = isRegenerate ? userMessage?.overrideParentMessageId : parentMessageId;
|
||||
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
title,
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
} else if (setConversation) {
|
||||
setConversation((prevState) => {
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
}) as TConversation;
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
||||
if (resetLatestMessage) {
|
||||
resetLatestMessage();
|
||||
}
|
||||
},
|
||||
[setMessages, setConversation, queryClient, isAddedRequest, resetLatestMessage],
|
||||
);
|
||||
|
||||
const finalHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation, runMessages } = data;
|
||||
const { messages, conversation: submissionConvo, isRegenerate = false } = submission;
|
||||
|
||||
setShowStopButton(false);
|
||||
setCompleted((prev) => new Set(prev.add(submission?.initialResponse?.messageId)));
|
||||
|
||||
const currentMessages = getMessages();
|
||||
// Early return if messages are empty; i.e., the user navigated away
|
||||
if (!currentMessages?.length) {
|
||||
return setIsSubmitting(false);
|
||||
}
|
||||
|
||||
// update the messages; if assistants endpoint, client doesn't receive responseMessage
|
||||
if (runMessages) {
|
||||
setMessages([...runMessages]);
|
||||
} else if (isRegenerate && responseMessage) {
|
||||
setMessages([...messages, responseMessage]);
|
||||
} else if (responseMessage) {
|
||||
setMessages([...messages, requestMessage, responseMessage]);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submissionConvo.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (
|
||||
genTitle &&
|
||||
isNewConvo &&
|
||||
requestMessage &&
|
||||
requestMessage.parentMessageId === Constants.NO_PARENT
|
||||
) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: conversation.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
if (setConversation && !isAddedRequest) {
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...conversation,
|
||||
};
|
||||
|
||||
if (prevState?.model && prevState.model !== submissionConvo.model) {
|
||||
update.model = prevState.model;
|
||||
}
|
||||
|
||||
return update;
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[
|
||||
genTitle,
|
||||
queryClient,
|
||||
getMessages,
|
||||
setMessages,
|
||||
setCompleted,
|
||||
isAddedRequest,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
setShowStopButton,
|
||||
],
|
||||
);
|
||||
|
||||
const errorHandler = useCallback(
|
||||
({ data, submission }: { data?: TResData; submission: TSubmission }) => {
|
||||
const { messages, userMessage, initialResponse } = submission;
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(initialResponse.messageId)));
|
||||
|
||||
const conversationId = userMessage?.conversationId ?? submission?.conversationId;
|
||||
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||
const metadata = data['responseMessage'] ?? data;
|
||||
const errorMessage = {
|
||||
...initialResponse,
|
||||
...metadata,
|
||||
error: true,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
};
|
||||
|
||||
if (!errorMessage.messageId) {
|
||||
errorMessage.messageId = v4();
|
||||
}
|
||||
|
||||
return tMessageSchema.parse(errorMessage);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
const convoId = conversationId ?? v4();
|
||||
const errorResponse = parseErrorResponse({
|
||||
text: 'Error connecting to server, try refreshing the page.',
|
||||
...submission,
|
||||
conversationId: convoId,
|
||||
});
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conversationId && !data.conversationId) {
|
||||
const convoId = v4();
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
} else if (!data.conversationId) {
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Error:', data);
|
||||
const errorResponse = tMessageSchema.parse({
|
||||
...data,
|
||||
error: true,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
});
|
||||
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
if (data.conversationId && paramId === 'new' && newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: data.conversationId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
},
|
||||
[setMessages, paramId, setIsSubmitting, setCompleted, newConversation],
|
||||
);
|
||||
|
||||
const abortConversation = useCallback(
|
||||
async (conversationId = '', submission: TSubmission, messages?: TMessage[]) => {
|
||||
const runAbortKey = `${conversationId}:${messages?.[messages.length - 1]?.messageId ?? ''}`;
|
||||
console.log({ conversationId, submission, messages, runAbortKey });
|
||||
const { endpoint: _endpoint, endpointType } = submission?.conversation || {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
try {
|
||||
const response = await fetch(`${EndpointURLs[endpoint ?? '']}/abort`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
abortKey: runAbortKey,
|
||||
endpoint,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check if the response is JSON
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
console.log(`[aborted] RESPONSE STATUS: ${response.status}`, data);
|
||||
if (response.status === 404) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (data.final) {
|
||||
finalHandler(data, submission);
|
||||
} else {
|
||||
cancelHandler(data, submission);
|
||||
}
|
||||
} else if (response.status === 204) {
|
||||
const responseMessage = {
|
||||
...submission.initialResponse,
|
||||
};
|
||||
|
||||
const data = {
|
||||
requestMessage: submission.userMessage,
|
||||
responseMessage: responseMessage,
|
||||
conversation: submission.conversation,
|
||||
};
|
||||
console.log(`[aborted] RESPONSE STATUS: ${response.status}`, data);
|
||||
setIsSubmitting(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unexpected response from server; Status: ' +
|
||||
response.status +
|
||||
' ' +
|
||||
response.statusText,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling request');
|
||||
console.error(error);
|
||||
const convoId = conversationId ?? v4();
|
||||
const text =
|
||||
submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : '';
|
||||
const errorMessage = {
|
||||
...submission,
|
||||
...submission.initialResponse,
|
||||
text: text ?? (error as Error).message ?? 'Error cancelling request',
|
||||
unfinished: !!text.length,
|
||||
error: true,
|
||||
};
|
||||
const errorResponse = tMessageSchema.parse(errorMessage);
|
||||
setMessages([...submission.messages, submission.userMessage, errorResponse]);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[token, setIsSubmitting, finalHandler, cancelHandler, setMessages, newConversation],
|
||||
);
|
||||
|
||||
return {
|
||||
syncHandler,
|
||||
finalHandler,
|
||||
errorHandler,
|
||||
messageHandler,
|
||||
contentHandler,
|
||||
createdHandler,
|
||||
abortConversation,
|
||||
};
|
||||
}
|
|
@ -1,72 +1,44 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
/* @ts-ignore */
|
||||
SSE,
|
||||
QueryKeys,
|
||||
Constants,
|
||||
EndpointURLs,
|
||||
createPayload,
|
||||
tPresetSchema,
|
||||
tMessageSchema,
|
||||
LocalStorageKeys,
|
||||
tConvoUpdateSchema,
|
||||
removeNullishValues,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
TResPlugin,
|
||||
TMessage,
|
||||
TConversation,
|
||||
TSubmission,
|
||||
ConversationData,
|
||||
} from 'librechat-data-provider';
|
||||
import {
|
||||
addConversation,
|
||||
deleteConversation,
|
||||
updateConversation,
|
||||
getConversationById,
|
||||
} from '~/utils';
|
||||
import type { TSubmission } from 'librechat-data-provider';
|
||||
import type { EventHandlerParams } from './useEventHandlers';
|
||||
import type { TResData } from '~/common';
|
||||
import { useGenTitleMutation } from '~/data-provider';
|
||||
import useContentHandler from './useContentHandler';
|
||||
import { useAuthContext } from '../AuthContext';
|
||||
import useChatHelpers from '../useChatHelpers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useEventHandlers from './useEventHandlers';
|
||||
import store from '~/store';
|
||||
|
||||
type TResData = {
|
||||
plugin?: TResPlugin;
|
||||
final?: boolean;
|
||||
initial?: boolean;
|
||||
previousMessages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversation: TConversation;
|
||||
conversationId?: string;
|
||||
runMessages?: TMessage[];
|
||||
};
|
||||
type ChatHelpers = Pick<
|
||||
EventHandlerParams,
|
||||
| 'setMessages'
|
||||
| 'getMessages'
|
||||
| 'setConversation'
|
||||
| 'setIsSubmitting'
|
||||
| 'newConversation'
|
||||
| 'resetLatestMessage'
|
||||
>;
|
||||
|
||||
type TSyncData = {
|
||||
sync: boolean;
|
||||
thread_id: string;
|
||||
messages?: TMessage[];
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||
const queryClient = useQueryClient();
|
||||
export default function useSSE(
|
||||
submission: TSubmission | null,
|
||||
chatHelpers: ChatHelpers,
|
||||
isAddedRequest = false,
|
||||
runIndex = 0,
|
||||
) {
|
||||
const genTitle = useGenTitleMutation();
|
||||
const setActiveRunId = useSetRecoilState(store.activeRunFamily(index));
|
||||
const setActiveRunId = useSetRecoilState(store.activeRunFamily(runIndex));
|
||||
|
||||
const { conversationId: paramId } = useParams();
|
||||
const { token, isAuthenticated } = useAuthContext();
|
||||
const [completed, setCompleted] = useState(new Set());
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(runIndex));
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
|
@ -75,435 +47,34 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
setIsSubmitting,
|
||||
newConversation,
|
||||
resetLatestMessage,
|
||||
} = useChatHelpers(index, paramId);
|
||||
const contentHandler = useContentHandler({ setMessages, getMessages });
|
||||
} = chatHelpers;
|
||||
|
||||
const {
|
||||
syncHandler,
|
||||
finalHandler,
|
||||
errorHandler,
|
||||
messageHandler,
|
||||
contentHandler,
|
||||
createdHandler,
|
||||
abortConversation,
|
||||
} = useEventHandlers({
|
||||
genTitle,
|
||||
setMessages,
|
||||
getMessages,
|
||||
setCompleted,
|
||||
isAddedRequest,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
newConversation,
|
||||
setShowStopButton,
|
||||
resetLatestMessage,
|
||||
});
|
||||
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const balanceQuery = useGetUserBalance({
|
||||
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
||||
});
|
||||
|
||||
const messageHandler = useCallback(
|
||||
(data: string, submission: TSubmission) => {
|
||||
const {
|
||||
messages,
|
||||
userMessage,
|
||||
plugin,
|
||||
plugins,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setMessages([
|
||||
...messages,
|
||||
userMessage,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
plugin: plugin ?? null,
|
||||
plugins: plugins ?? [],
|
||||
// unfinished: true
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
[setMessages],
|
||||
);
|
||||
|
||||
const cancelHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation } = data;
|
||||
const { messages, isRegenerate = false } = submission;
|
||||
|
||||
const convoUpdate = conversation ?? submission.conversation;
|
||||
|
||||
// update the messages
|
||||
if (isRegenerate) {
|
||||
const messagesUpdate = [...messages, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
} else {
|
||||
const messagesUpdate = [...messages, requestMessage, responseMessage].filter((msg) => msg);
|
||||
setMessages(messagesUpdate);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submission.conversation.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submission.conversation.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (isNewConvo && requestMessage?.parentMessageId === Constants.NO_PARENT) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: convoUpdate.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...convoUpdate,
|
||||
};
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[setMessages, setConversation, genTitle, queryClient, setIsSubmitting],
|
||||
);
|
||||
|
||||
const syncHandler = useCallback(
|
||||
(data: TSyncData, submission: TSubmission) => {
|
||||
const { conversationId, thread_id, responseMessage, requestMessage } = data;
|
||||
const { initialResponse, messages: _messages, userMessage } = submission;
|
||||
|
||||
const messages = _messages.filter((msg) => msg.messageId !== userMessage.messageId);
|
||||
|
||||
setMessages([
|
||||
...messages,
|
||||
requestMessage,
|
||||
{
|
||||
...initialResponse,
|
||||
...responseMessage,
|
||||
},
|
||||
]);
|
||||
|
||||
let update = {} as TConversation;
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = requestMessage.parentMessageId;
|
||||
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
thread_id,
|
||||
title,
|
||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
|
||||
setShowStopButton(true);
|
||||
|
||||
resetLatestMessage();
|
||||
},
|
||||
[setMessages, setConversation, queryClient, setShowStopButton, resetLatestMessage],
|
||||
);
|
||||
|
||||
const createdHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { messages, userMessage, isRegenerate = false } = submission;
|
||||
const initialResponse = {
|
||||
...submission.initialResponse,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
messageId: userMessage?.messageId + '_',
|
||||
};
|
||||
if (isRegenerate) {
|
||||
setMessages([...messages, initialResponse]);
|
||||
} else {
|
||||
setMessages([...messages, userMessage, initialResponse]);
|
||||
}
|
||||
|
||||
const { conversationId, parentMessageId } = userMessage;
|
||||
|
||||
let update = {} as TConversation;
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
const parentId = isRegenerate ? userMessage?.overrideParentMessageId : parentMessageId;
|
||||
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
}
|
||||
|
||||
update = tConvoUpdateSchema.parse({
|
||||
...prevState,
|
||||
conversationId,
|
||||
title,
|
||||
}) as TConversation;
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
if (parentMessageId === Constants.NO_PARENT) {
|
||||
return addConversation(convoData, update);
|
||||
} else {
|
||||
return updateConversation(convoData, update);
|
||||
}
|
||||
});
|
||||
resetLatestMessage();
|
||||
},
|
||||
[setMessages, setConversation, queryClient, resetLatestMessage],
|
||||
);
|
||||
|
||||
const finalHandler = useCallback(
|
||||
(data: TResData, submission: TSubmission) => {
|
||||
const { requestMessage, responseMessage, conversation, runMessages } = data;
|
||||
const { messages, conversation: submissionConvo, isRegenerate = false } = submission;
|
||||
|
||||
setShowStopButton(false);
|
||||
setCompleted((prev) => new Set(prev.add(submission?.initialResponse?.messageId)));
|
||||
|
||||
const currentMessages = getMessages();
|
||||
// Early return if messages are empty; i.e., the user navigated away
|
||||
if (!currentMessages?.length) {
|
||||
return setIsSubmitting(false);
|
||||
}
|
||||
|
||||
// update the messages; if assistants endpoint, client doesn't receive responseMessage
|
||||
if (runMessages) {
|
||||
setMessages([...runMessages]);
|
||||
} else if (isRegenerate && responseMessage) {
|
||||
setMessages([...messages, responseMessage]);
|
||||
} else if (responseMessage) {
|
||||
setMessages([...messages, requestMessage, responseMessage]);
|
||||
}
|
||||
|
||||
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
|
||||
if (isNewConvo) {
|
||||
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
|
||||
if (!convoData) {
|
||||
return convoData;
|
||||
}
|
||||
return deleteConversation(convoData, submissionConvo.conversationId as string);
|
||||
});
|
||||
}
|
||||
|
||||
// refresh title
|
||||
if (isNewConvo && requestMessage && requestMessage.parentMessageId === Constants.NO_PARENT) {
|
||||
setTimeout(() => {
|
||||
genTitle.mutate({ conversationId: conversation.conversationId as string });
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
...conversation,
|
||||
};
|
||||
|
||||
if (prevState?.model && prevState.model !== submissionConvo.model) {
|
||||
update.model = prevState.model;
|
||||
}
|
||||
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[
|
||||
genTitle,
|
||||
queryClient,
|
||||
getMessages,
|
||||
setMessages,
|
||||
setConversation,
|
||||
setIsSubmitting,
|
||||
setShowStopButton,
|
||||
],
|
||||
);
|
||||
|
||||
const errorHandler = useCallback(
|
||||
({ data, submission }: { data?: TResData; submission: TSubmission }) => {
|
||||
const { messages, userMessage, initialResponse } = submission;
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(initialResponse.messageId)));
|
||||
|
||||
const conversationId = userMessage?.conversationId ?? submission?.conversationId;
|
||||
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||
const metadata = data['responseMessage'] ?? data;
|
||||
const errorMessage = {
|
||||
...initialResponse,
|
||||
...metadata,
|
||||
error: true,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
};
|
||||
|
||||
if (!errorMessage.messageId) {
|
||||
errorMessage.messageId = v4();
|
||||
}
|
||||
|
||||
return tMessageSchema.parse(errorMessage);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
const convoId = conversationId ?? v4();
|
||||
const errorResponse = parseErrorResponse({
|
||||
text: 'Error connecting to server, try refreshing the page.',
|
||||
...submission,
|
||||
conversationId: convoId,
|
||||
});
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!conversationId && !data.conversationId) {
|
||||
const convoId = v4();
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
} else if (!data.conversationId) {
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Error:', data);
|
||||
const errorResponse = tMessageSchema.parse({
|
||||
...data,
|
||||
error: true,
|
||||
parentMessageId: userMessage?.messageId,
|
||||
});
|
||||
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
if (data.conversationId && paramId === 'new') {
|
||||
newConversation({
|
||||
template: { conversationId: data.conversationId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
},
|
||||
[setMessages, paramId, setIsSubmitting, newConversation],
|
||||
);
|
||||
|
||||
const abortConversation = useCallback(
|
||||
async (conversationId = '', submission: TSubmission) => {
|
||||
let runAbortKey = '';
|
||||
try {
|
||||
const conversation = (JSON.parse(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) ?? '',
|
||||
) ?? {}) as TConversation;
|
||||
const { conversationId, messages } = conversation;
|
||||
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
|
||||
} catch (error) {
|
||||
console.error('Error getting last conversation setup');
|
||||
console.error(error);
|
||||
}
|
||||
const { endpoint: _endpoint, endpointType } = submission?.conversation || {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
try {
|
||||
const response = await fetch(`${EndpointURLs[endpoint ?? '']}/abort`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
abortKey: isAssistantsEndpoint(_endpoint) ? runAbortKey : conversationId,
|
||||
endpoint,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check if the response is JSON
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
console.log('aborted', data);
|
||||
if (response.status === 404) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (data.final) {
|
||||
finalHandler(data, submission);
|
||||
} else {
|
||||
cancelHandler(data, submission);
|
||||
}
|
||||
} else if (response.status === 204) {
|
||||
const responseMessage = {
|
||||
...submission.initialResponse,
|
||||
};
|
||||
|
||||
const data = {
|
||||
requestMessage: submission.userMessage,
|
||||
responseMessage: responseMessage,
|
||||
conversation: submission.conversation,
|
||||
};
|
||||
console.log('aborted', data);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unexpected response from server; Status: ' +
|
||||
response.status +
|
||||
' ' +
|
||||
response.statusText,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling request');
|
||||
console.error(error);
|
||||
const convoId = conversationId ?? v4();
|
||||
const text =
|
||||
submission.initialResponse?.text?.length > 45 ? submission.initialResponse?.text : '';
|
||||
const errorMessage = {
|
||||
...submission,
|
||||
...submission.initialResponse,
|
||||
text: text ?? (error as Error).message ?? 'Error cancelling request',
|
||||
unfinished: !!text.length,
|
||||
error: true,
|
||||
};
|
||||
const errorResponse = tMessageSchema.parse(errorMessage);
|
||||
setMessages([...submission.messages, submission.userMessage, errorResponse]);
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission?.conversation),
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[token, setIsSubmitting, finalHandler, cancelHandler, setMessages, newConversation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (submission === null || Object.keys(submission).length === 0) {
|
||||
return;
|
||||
|
@ -571,16 +142,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
}
|
||||
};
|
||||
|
||||
// events.onaudio = (e: MessageEvent) => {
|
||||
// const data = JSON.parse(e.data);
|
||||
// console.log('audio', data);
|
||||
// if (data.audio) {
|
||||
// audioSource.addBase64Data(data.audio);
|
||||
// }
|
||||
// };
|
||||
|
||||
// events.onend = () => audioSource.close();
|
||||
|
||||
events.onopen = () => console.log('connection is opened');
|
||||
|
||||
events.oncancel = async () => {
|
||||
|
@ -595,9 +156,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
}
|
||||
|
||||
setCompleted((prev) => new Set(prev.add(streamKey)));
|
||||
const latestMessages = getMessages();
|
||||
const conversationId = latestMessages?.[latestMessages?.length - 1]?.conversationId;
|
||||
return await abortConversation(
|
||||
userMessage?.conversationId ?? submission?.conversationId,
|
||||
conversationId ?? userMessage?.conversationId ?? submission?.conversationId,
|
||||
submission,
|
||||
latestMessages,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './Assistants';
|
||||
export * from './Chat';
|
||||
export * from './Config';
|
||||
export * from './Conversations';
|
||||
export * from './Nav';
|
||||
|
@ -19,11 +20,10 @@ export { default as useTimeout } from './useTimeout';
|
|||
export { default as useNewConvo } from './useNewConvo';
|
||||
export { default as useLocalize } from './useLocalize';
|
||||
export { default as useMediaQuery } from './useMediaQuery';
|
||||
export { default as useChatHelpers } from './useChatHelpers';
|
||||
export { default as useScrollToRef } from './useScrollToRef';
|
||||
export { default as useLocalStorage } from './useLocalStorage';
|
||||
export { default as useDelayedRender } from './useDelayedRender';
|
||||
export { default as useOnClickOutside } from './useOnClickOutside';
|
||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||
export { default as useSpeechToText } from './Input/useSpeechToText';
|
||||
export { default as useTextToSpeech } from './Input/useTextToSpeech';
|
||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||
|
|
|
@ -1,373 +0,0 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Constants,
|
||||
QueryKeys,
|
||||
ContentTypes,
|
||||
parseCompactConvo,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
TMessage,
|
||||
TSubmission,
|
||||
TEndpointOption,
|
||||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TAskFunction } from '~/common';
|
||||
import useSetFilesToDelete from './Files/useSetFilesToDelete';
|
||||
import useGetSender from './Conversations/useGetSender';
|
||||
import { useAuthContext } from './AuthContext';
|
||||
import useUserKey from './Input/useUserKey';
|
||||
import { getEndpointField } from '~/utils';
|
||||
import useNewConvo from './useNewConvo';
|
||||
import store from '~/store';
|
||||
|
||||
// this to be set somewhere else
|
||||
export default function useChatHelpers(index = 0, paramId: string | undefined) {
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||
const [filesLoading, setFilesLoading] = useState(false);
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
const getSender = useGetSender();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
|
||||
const { newConversation } = useNewConvo(index);
|
||||
const { useCreateConversationAtom } = store;
|
||||
const { conversation, setConversation } = useCreateConversationAtom(index);
|
||||
const { conversationId, endpoint } = conversation ?? {};
|
||||
|
||||
const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? '';
|
||||
|
||||
/* Messages: here simply to fetch, don't export and use `getMessages()` instead */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { data: _messages } = useGetMessagesByConvoId(conversationId ?? '', {
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index));
|
||||
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index));
|
||||
const setSiblingIdx = useSetRecoilState(
|
||||
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null),
|
||||
);
|
||||
|
||||
const setMessages = useCallback(
|
||||
(messages: TMessage[]) => {
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, queryParam], messages);
|
||||
},
|
||||
// [conversationId, queryClient],
|
||||
[queryParam, queryClient],
|
||||
);
|
||||
|
||||
const getMessages = useCallback(() => {
|
||||
return queryClient.getQueryData<TMessage[]>([QueryKeys.messages, queryParam]);
|
||||
}, [queryParam, queryClient]);
|
||||
|
||||
/* Conversation */
|
||||
// const setActiveConvos = useSetRecoilState(store.activeConversations);
|
||||
|
||||
// const setConversation = useCallback(
|
||||
// (convoUpdate: TConversation) => {
|
||||
// _setConversation(prev => {
|
||||
// const { conversationId: convoId } = prev ?? { conversationId: null };
|
||||
// const { conversationId: currentId } = convoUpdate;
|
||||
// if (currentId && convoId && convoId !== 'new' && convoId !== currentId) {
|
||||
// // for now, we delete the prev convoId from activeConversations
|
||||
// const newActiveConvos = { [currentId]: true };
|
||||
// setActiveConvos(newActiveConvos);
|
||||
// }
|
||||
// return convoUpdate;
|
||||
// });
|
||||
// },
|
||||
// [_setConversation, setActiveConvos],
|
||||
// );
|
||||
const { getExpiry } = useUserKey(endpoint ?? '');
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
|
||||
const ask: TAskFunction = (
|
||||
{ text, parentMessageId = null, conversationId = null, messageId = null },
|
||||
{
|
||||
editedText = null,
|
||||
editedMessageId = null,
|
||||
resubmitFiles = false,
|
||||
isRegenerate = false,
|
||||
isContinued = false,
|
||||
isEdited = false,
|
||||
} = {},
|
||||
) => {
|
||||
setShowStopButton(false);
|
||||
if (!!isSubmitting || text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (endpoint === null) {
|
||||
console.error('No endpoint available');
|
||||
return;
|
||||
}
|
||||
|
||||
conversationId = conversationId ?? conversation?.conversationId ?? null;
|
||||
if (conversationId == 'search') {
|
||||
console.error('cannot send any message under search view!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContinued && !latestMessage) {
|
||||
console.error('cannot continue AI message without latestMessage!');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEditOrContinue = isEdited || isContinued;
|
||||
|
||||
let currentMessages: TMessage[] | null = getMessages() ?? [];
|
||||
|
||||
// construct the query message
|
||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||
text = text.trim();
|
||||
const fakeMessageId = v4();
|
||||
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
|
||||
|
||||
if (conversationId == 'new') {
|
||||
parentMessageId = Constants.NO_PARENT;
|
||||
currentMessages = [];
|
||||
conversationId = null;
|
||||
}
|
||||
|
||||
const parentMessage = currentMessages?.find(
|
||||
(msg) => msg.messageId === latestMessage?.parentMessageId,
|
||||
);
|
||||
|
||||
let thread_id = parentMessage?.thread_id ?? latestMessage?.thread_id;
|
||||
if (!thread_id) {
|
||||
thread_id = currentMessages.find((message) => message.thread_id)?.thread_id;
|
||||
}
|
||||
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
|
||||
// set the endpoint option
|
||||
const convo = parseCompactConvo({
|
||||
endpoint,
|
||||
endpointType,
|
||||
conversation: conversation ?? {},
|
||||
});
|
||||
|
||||
const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {};
|
||||
const endpointOption = {
|
||||
...convo,
|
||||
endpoint,
|
||||
endpointType,
|
||||
modelDisplayLabel,
|
||||
key: getExpiry(),
|
||||
thread_id,
|
||||
} as TEndpointOption;
|
||||
const responseSender = getSender({ model: conversation?.model, ...endpointOption });
|
||||
|
||||
const currentMsg: TMessage = {
|
||||
text,
|
||||
sender: 'User',
|
||||
isCreatedByUser: true,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
messageId: isContinued && messageId ? messageId : fakeMessageId,
|
||||
thread_id,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const reuseFiles = (isRegenerate || resubmitFiles) && parentMessage?.files;
|
||||
if (reuseFiles && parentMessage.files?.length) {
|
||||
currentMsg.files = parentMessage.files;
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
} else if (files.size > 0) {
|
||||
currentMsg.files = Array.from(files.values()).map((file) => ({
|
||||
file_id: file.file_id,
|
||||
filepath: file.filepath,
|
||||
type: file.type || '', // Ensure type is not undefined
|
||||
height: file.height,
|
||||
width: file.width,
|
||||
}));
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
}
|
||||
|
||||
// construct the placeholder response message
|
||||
const generation = editedText ?? latestMessage?.text ?? '';
|
||||
const responseText = isEditOrContinue ? generation : '';
|
||||
|
||||
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
|
||||
const initialResponse: TMessage = {
|
||||
sender: responseSender,
|
||||
text: responseText,
|
||||
endpoint: endpoint ?? '',
|
||||
parentMessageId: isRegenerate ? messageId : fakeMessageId,
|
||||
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
|
||||
thread_id,
|
||||
conversationId,
|
||||
unfinished: false,
|
||||
isCreatedByUser: false,
|
||||
isEdited: isEditOrContinue,
|
||||
iconURL: convo.iconURL,
|
||||
error: false,
|
||||
};
|
||||
|
||||
if (isAssistantsEndpoint(endpoint)) {
|
||||
initialResponse.model = conversation?.assistant_id ?? '';
|
||||
initialResponse.text = '';
|
||||
initialResponse.content = [
|
||||
{
|
||||
type: ContentTypes.TEXT,
|
||||
[ContentTypes.TEXT]: {
|
||||
value: responseText,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
setShowStopButton(true);
|
||||
}
|
||||
|
||||
if (isContinued) {
|
||||
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
|
||||
}
|
||||
|
||||
const submission: TSubmission = {
|
||||
conversation: {
|
||||
...conversation,
|
||||
conversationId,
|
||||
},
|
||||
endpointOption,
|
||||
userMessage: {
|
||||
...currentMsg,
|
||||
generation,
|
||||
responseMessageId,
|
||||
overrideParentMessageId: isRegenerate ? messageId : null,
|
||||
},
|
||||
messages: currentMessages,
|
||||
isEdited: isEditOrContinue,
|
||||
isContinued,
|
||||
isRegenerate,
|
||||
initialResponse,
|
||||
};
|
||||
|
||||
if (isRegenerate) {
|
||||
setMessages([...submission.messages, initialResponse]);
|
||||
} else {
|
||||
setMessages([...submission.messages, currentMsg, initialResponse]);
|
||||
}
|
||||
setLatestMessage(initialResponse);
|
||||
setSubmission(submission);
|
||||
};
|
||||
|
||||
const regenerate = ({ parentMessageId }) => {
|
||||
const messages = getMessages();
|
||||
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isRegenerate: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const continueGeneration = () => {
|
||||
if (!latestMessage) {
|
||||
console.error('Failed to regenerate the message: latestMessage not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = getMessages();
|
||||
|
||||
const parentMessage = messages?.find(
|
||||
(element) => element.messageId == latestMessage.parentMessageId,
|
||||
);
|
||||
|
||||
if (parentMessage && parentMessage.isCreatedByUser) {
|
||||
ask({ ...parentMessage }, { isContinued: true, isRegenerate: true, isEdited: true });
|
||||
} else {
|
||||
console.error(
|
||||
'Failed to regenerate the message: parentMessage not found, or not created by user.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const stopGenerating = () => {
|
||||
setSubmission(null);
|
||||
};
|
||||
|
||||
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
stopGenerating();
|
||||
};
|
||||
|
||||
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const parentMessageId = latestMessage?.parentMessageId;
|
||||
if (!parentMessageId) {
|
||||
console.error('Failed to regenerate the message: parentMessageId not found.');
|
||||
return;
|
||||
}
|
||||
regenerate({ parentMessageId });
|
||||
};
|
||||
|
||||
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
continueGeneration();
|
||||
setSiblingIdx(0);
|
||||
};
|
||||
|
||||
const [showBingToneSetting, setShowBingToneSetting] = useRecoilState(
|
||||
store.showBingToneSettingFamily(index),
|
||||
);
|
||||
const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index));
|
||||
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index));
|
||||
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
|
||||
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index));
|
||||
const [showAgentSettings, setShowAgentSettings] = useRecoilState(
|
||||
store.showAgentSettingsFamily(index),
|
||||
);
|
||||
|
||||
return {
|
||||
newConversation,
|
||||
conversation,
|
||||
setConversation,
|
||||
// getConvos,
|
||||
// setConvos,
|
||||
isSubmitting,
|
||||
setIsSubmitting,
|
||||
getMessages,
|
||||
setMessages,
|
||||
setSiblingIdx,
|
||||
latestMessage,
|
||||
setLatestMessage,
|
||||
resetLatestMessage,
|
||||
ask,
|
||||
index,
|
||||
regenerate,
|
||||
stopGenerating,
|
||||
handleStopGenerating,
|
||||
handleRegenerate,
|
||||
handleContinue,
|
||||
showPopover,
|
||||
setShowPopover,
|
||||
abortScroll,
|
||||
setAbortScroll,
|
||||
showBingToneSetting,
|
||||
setShowBingToneSetting,
|
||||
preset,
|
||||
setPreset,
|
||||
optionSettings,
|
||||
setOptionSettings,
|
||||
showAgentSettings,
|
||||
setShowAgentSettings,
|
||||
files,
|
||||
setFiles,
|
||||
filesLoading,
|
||||
setFilesLoading,
|
||||
};
|
||||
}
|
|
@ -6,13 +6,7 @@ import {
|
|||
} from 'librechat-data-provider/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import {
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
useRecoilCallback,
|
||||
useResetRecoilState,
|
||||
} from 'recoil';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState, useRecoilCallback } from 'recoil';
|
||||
import type {
|
||||
TPreset,
|
||||
TSubmission,
|
||||
|
@ -38,12 +32,14 @@ import store from '~/store';
|
|||
const useNewConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const clearAllLatestMessages = store.useClearLatestMessages();
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||
const assistantsListMap = useAssistantListMap();
|
||||
|
@ -67,6 +63,7 @@ const useNewConvo = (index = 0) => {
|
|||
modelsData?: TModelsConfig,
|
||||
buildDefault?: boolean,
|
||||
keepLatestMessage?: boolean,
|
||||
keepAddedConvos?: boolean,
|
||||
) => {
|
||||
const modelsConfig = modelsData ?? modelsQuery.data;
|
||||
const { endpoint = null } = conversation;
|
||||
|
@ -139,10 +136,13 @@ const useNewConvo = (index = 0) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (!keepAddedConvos) {
|
||||
clearAllConversations(true);
|
||||
}
|
||||
setConversation(conversation);
|
||||
setSubmission({} as TSubmission);
|
||||
if (!keepLatestMessage) {
|
||||
resetLatestMessage();
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
|
||||
if (conversation.conversationId === 'new' && !modelsData) {
|
||||
|
@ -171,12 +171,14 @@ const useNewConvo = (index = 0) => {
|
|||
modelsData,
|
||||
buildDefault = true,
|
||||
keepLatestMessage = false,
|
||||
keepAddedConvos = false,
|
||||
}: {
|
||||
template?: Partial<TConversation>;
|
||||
preset?: Partial<TPreset>;
|
||||
modelsData?: TModelsConfig;
|
||||
buildDefault?: boolean;
|
||||
keepLatestMessage?: boolean;
|
||||
keepAddedConvos?: boolean;
|
||||
} = {}) => {
|
||||
pauseGlobalAudio();
|
||||
|
||||
|
@ -217,7 +219,14 @@ const useNewConvo = (index = 0) => {
|
|||
}
|
||||
}
|
||||
|
||||
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
|
||||
switchToConversation(
|
||||
conversation,
|
||||
preset,
|
||||
modelsData,
|
||||
buildDefault,
|
||||
keepLatestMessage,
|
||||
keepAddedConvos,
|
||||
);
|
||||
},
|
||||
[
|
||||
pauseGlobalAudio,
|
||||
|
|
|
@ -150,6 +150,7 @@ export default {
|
|||
com_ui_entries: 'Entries',
|
||||
com_ui_pay_per_call: 'All AI conversations in one place. Pay per call and not per month',
|
||||
com_ui_new_footer: 'All AI conversations in one place.',
|
||||
com_ui_latest_footer: 'Every AI for Everyone.',
|
||||
com_ui_enter: 'Enter',
|
||||
com_ui_submit: 'Submit',
|
||||
com_ui_none_selected: 'None selected',
|
||||
|
@ -198,6 +199,7 @@ export default {
|
|||
com_ui_fork_visible: 'Visible messages only',
|
||||
com_ui_fork_from_message: 'Select a fork option',
|
||||
com_ui_mention: 'Mention an endpoint, assistant, or preset to quickly switch to it',
|
||||
com_ui_add: 'Add a model or preset for an additional response',
|
||||
com_ui_regenerate: 'Regenerate',
|
||||
com_ui_continue: 'Continue',
|
||||
com_ui_edit: 'Edit',
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
atom,
|
||||
atomFamily,
|
||||
selector,
|
||||
atomFamily,
|
||||
selectorFamily,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
|
@ -10,9 +11,53 @@ import {
|
|||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
|
||||
import type { TOptionSettings, ExtendedFile } from '~/common';
|
||||
import { storeEndpointSettings } from '~/utils';
|
||||
import { storeEndpointSettings, logger } from '~/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const latestMessageKeysAtom = atom<(string | number)[]>({
|
||||
key: 'latestMessageKeys',
|
||||
default: [],
|
||||
});
|
||||
|
||||
const submissionKeysAtom = atom<(string | number)[]>({
|
||||
key: 'submissionKeys',
|
||||
default: [],
|
||||
});
|
||||
|
||||
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
|
||||
key: 'latestMessageByIndex',
|
||||
default: null,
|
||||
});
|
||||
|
||||
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
|
||||
key: 'submissionByIndex',
|
||||
default: null,
|
||||
});
|
||||
|
||||
const latestMessageKeysSelector = selector<(string | number)[]>({
|
||||
key: 'latestMessageKeysSelector',
|
||||
get: ({ get }) => {
|
||||
const keys = get(conversationKeysAtom);
|
||||
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
|
||||
},
|
||||
set: ({ set }, newKeys) => {
|
||||
logger.log('setting latestMessageKeys', newKeys);
|
||||
set(latestMessageKeysAtom, newKeys);
|
||||
},
|
||||
});
|
||||
|
||||
const submissionKeysSelector = selector<(string | number)[]>({
|
||||
key: 'submissionKeysSelector',
|
||||
get: ({ get }) => {
|
||||
const keys = get(conversationKeysAtom);
|
||||
return keys.filter((key) => get(submissionByIndex(key)) !== null);
|
||||
},
|
||||
set: ({ set }, newKeys) => {
|
||||
logger.log('setting submissionKeysAtom', newKeys);
|
||||
set(submissionKeysAtom, newKeys);
|
||||
},
|
||||
});
|
||||
|
||||
const conversationByIndex = atomFamily<TConversation | null, string | number>({
|
||||
key: 'conversationByIndex',
|
||||
default: null,
|
||||
|
@ -41,7 +86,10 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
|
|||
}
|
||||
|
||||
storeEndpointSettings(newValue);
|
||||
localStorage.setItem(LocalStorageKeys.LAST_CONVO_SETUP, JSON.stringify(newValue));
|
||||
localStorage.setItem(
|
||||
`${LocalStorageKeys.LAST_CONVO_SETUP}_${index}`,
|
||||
JSON.stringify(newValue),
|
||||
);
|
||||
});
|
||||
},
|
||||
] as const,
|
||||
|
@ -70,11 +118,6 @@ const presetByIndex = atomFamily<TPreset | null, string | number>({
|
|||
default: null,
|
||||
});
|
||||
|
||||
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
|
||||
key: 'submissionByIndex',
|
||||
default: null,
|
||||
});
|
||||
|
||||
const textByIndex = atomFamily<string, string | number>({
|
||||
key: 'textByIndex',
|
||||
default: '',
|
||||
|
@ -125,6 +168,11 @@ const showMentionPopoverFamily = atomFamily<boolean, string | number | null>({
|
|||
default: false,
|
||||
});
|
||||
|
||||
const showPlusPopoverFamily = atomFamily<boolean, string | number | null>({
|
||||
key: 'showPlusPopoverByIndex',
|
||||
default: false,
|
||||
});
|
||||
|
||||
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
|
||||
key: 'globalAudioURLByIndex',
|
||||
default: null,
|
||||
|
@ -150,11 +198,6 @@ const audioRunFamily = atomFamily<string | null, string | number | null>({
|
|||
default: null,
|
||||
});
|
||||
|
||||
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
|
||||
key: 'latestMessageByIndex',
|
||||
default: null,
|
||||
});
|
||||
|
||||
function useCreateConversationAtom(key: string | number) {
|
||||
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
|
||||
const setConversation = useSetRecoilState(conversationByIndex(key));
|
||||
|
@ -170,12 +213,17 @@ function useCreateConversationAtom(key: string | number) {
|
|||
}
|
||||
|
||||
function useClearConvoState() {
|
||||
/** Clears all active conversations. Pass `true` to skip the first or root conversation */
|
||||
const clearAllConversations = useRecoilCallback(
|
||||
({ reset, snapshot }) =>
|
||||
async () => {
|
||||
async (skipFirst?: boolean) => {
|
||||
const conversationKeys = await snapshot.getPromise(conversationKeysAtom);
|
||||
|
||||
for (const conversationKey of conversationKeys) {
|
||||
if (skipFirst && conversationKey == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reset(conversationByIndex(conversationKey));
|
||||
|
||||
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
|
||||
|
@ -192,6 +240,64 @@ function useClearConvoState() {
|
|||
return clearAllConversations;
|
||||
}
|
||||
|
||||
const conversationByKeySelector = selectorFamily({
|
||||
key: 'conversationByKeySelector',
|
||||
get:
|
||||
(index: string | number) =>
|
||||
({ get }) => {
|
||||
const conversation = get(conversationByIndex(index));
|
||||
return conversation;
|
||||
},
|
||||
});
|
||||
|
||||
function useClearSubmissionState() {
|
||||
const clearAllSubmissions = useRecoilCallback(
|
||||
({ reset, set, snapshot }) =>
|
||||
async (skipFirst?: boolean) => {
|
||||
const submissionKeys = await snapshot.getPromise(submissionKeysSelector);
|
||||
logger.log('submissionKeys', submissionKeys);
|
||||
|
||||
for (const key of submissionKeys) {
|
||||
if (skipFirst && key == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.log('resetting submission', key);
|
||||
reset(submissionByIndex(key));
|
||||
}
|
||||
|
||||
set(submissionKeysSelector, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return clearAllSubmissions;
|
||||
}
|
||||
|
||||
function useClearLatestMessages() {
|
||||
const clearAllLatestMessages = useRecoilCallback(
|
||||
({ reset, set, snapshot }) =>
|
||||
async (skipFirst?: boolean) => {
|
||||
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
|
||||
logger.log('latestMessageKeys', latestMessageKeys);
|
||||
|
||||
for (const key of latestMessageKeys) {
|
||||
if (skipFirst && key == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.log('resetting latest message', key);
|
||||
reset(latestMessageFamily(key));
|
||||
}
|
||||
|
||||
set(latestMessageKeysSelector, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return clearAllLatestMessages;
|
||||
}
|
||||
|
||||
export default {
|
||||
conversationByIndex,
|
||||
filesByIndex,
|
||||
|
@ -207,6 +313,7 @@ export default {
|
|||
showPopoverFamily,
|
||||
latestMessageFamily,
|
||||
allConversationsSelector,
|
||||
conversationByKeySelector,
|
||||
useClearConvoState,
|
||||
useCreateConversationAtom,
|
||||
showMentionPopoverFamily,
|
||||
|
@ -215,5 +322,8 @@ export default {
|
|||
audioRunFamily,
|
||||
globalAudioPlayingFamily,
|
||||
globalAudioFetchingFamily,
|
||||
showPlusPopoverFamily,
|
||||
activePromptByIndex,
|
||||
useClearSubmissionState,
|
||||
useClearLatestMessages,
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ html {
|
|||
--text-tertiary:var(--gray-500);
|
||||
--surface-primary:var(--white);
|
||||
--surface-primary-alt:var(--white);
|
||||
--surface-primary-contrast:var(--gray-100);
|
||||
--surface-secondary:var(--gray-50);
|
||||
--surface-tertiary:var(--gray-100);
|
||||
--border-light:var(--gray-100);
|
||||
|
@ -41,8 +42,9 @@ html {
|
|||
--text-secondary:var(--gray-300);
|
||||
--text-tertiary:var(--gray-500);
|
||||
--surface-primary:var(--gray-900);
|
||||
--surface-secondary:var(--gray-800);
|
||||
--surface-primary-alt:var(--gray-850);
|
||||
--surface-primary-contrast:var(--gray-850);
|
||||
--surface-secondary:var(--gray-800);
|
||||
--surface-tertiary:var(--gray-700);
|
||||
--border-light:var(--gray-700);
|
||||
--border-medium-alt:var(--gray-600);
|
||||
|
|
|
@ -1,78 +1,64 @@
|
|||
import { TFile, TMessage } from 'librechat-data-provider';
|
||||
|
||||
const even =
|
||||
'w-full border-b border-black/10 dark:border-gray-800/50 text-gray-800 bg-white dark:text-gray-200 group dark:bg-gray-800 hover:bg-gray-200/25 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200';
|
||||
const odd =
|
||||
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-800/50 text-gray-800 dark:text-gray-200 group bg-gray-200 dark:bg-gray-700 hover:bg-gray-200/40 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200';
|
||||
|
||||
type ParentMessage = TMessage & { children: TMessage[]; depth: number };
|
||||
export default function buildTree({
|
||||
messages,
|
||||
fileMap,
|
||||
groupAll = false,
|
||||
}: {
|
||||
messages: TMessage[] | null;
|
||||
fileMap?: Record<string, TFile>;
|
||||
groupAll?: boolean;
|
||||
}) {
|
||||
if (messages === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageMap: Record<string, TMessage & { children: TMessage[] }> = {};
|
||||
const messageMap: Record<string, ParentMessage> = {};
|
||||
const rootMessages: TMessage[] = [];
|
||||
const childrenCount: Record<string, number> = {};
|
||||
|
||||
if (groupAll) {
|
||||
return messages.map((m, idx) => ({ ...m, bg: idx % 2 === 0 ? even : odd }));
|
||||
}
|
||||
if (!groupAll) {
|
||||
// Traverse the messages array and store each element in messageMap.
|
||||
messages.forEach((message) => {
|
||||
messageMap[message.messageId] = { ...message, children: [] };
|
||||
const parentId = message.parentMessageId ?? '';
|
||||
childrenCount[parentId] = (childrenCount[parentId] || 0) + 1;
|
||||
|
||||
const extendedMessage: ParentMessage = {
|
||||
...message,
|
||||
children: [],
|
||||
depth: 0,
|
||||
siblingIndex: childrenCount[parentId] - 1,
|
||||
};
|
||||
|
||||
if (message.files && fileMap) {
|
||||
messageMap[message.messageId].files = message.files.map(
|
||||
(file) => fileMap[file.file_id ?? ''] ?? file,
|
||||
);
|
||||
extendedMessage.files = message.files.map((file) => fileMap[file.file_id ?? ''] ?? file);
|
||||
}
|
||||
|
||||
const parentMessage = messageMap[message.parentMessageId ?? ''];
|
||||
messageMap[message.messageId] = extendedMessage;
|
||||
|
||||
const parentMessage = messageMap[parentId];
|
||||
if (parentMessage) {
|
||||
parentMessage.children.push(messageMap[message.messageId]);
|
||||
parentMessage.children.push(extendedMessage);
|
||||
extendedMessage.depth = parentMessage.depth + 1;
|
||||
} else {
|
||||
rootMessages.push(messageMap[message.messageId]);
|
||||
rootMessages.push(extendedMessage);
|
||||
}
|
||||
});
|
||||
|
||||
return rootMessages;
|
||||
}
|
||||
|
||||
// // Group all messages into one tree
|
||||
// let parentId = null;
|
||||
// messages.forEach((message, i) => {
|
||||
// messageMap[message.messageId] = { ...message, bg: i % 2 === 0 ? even : odd, children: [] };
|
||||
// const currentMessage = messageMap[message.messageId];
|
||||
// const parentMessage = messageMap[parentId];
|
||||
// if (parentMessage) parentMessage.children.push(currentMessage);
|
||||
// else rootMessages.push(currentMessage);
|
||||
// parentId = message.messageId;
|
||||
// });
|
||||
const even =
|
||||
'w-full border-b border-black/10 dark:border-gray-800/50 text-gray-800 bg-white dark:text-gray-200 group dark:bg-gray-800 hover:bg-gray-200/25 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200';
|
||||
const odd =
|
||||
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-800/50 text-gray-800 dark:text-gray-200 group bg-gray-200 dark:bg-gray-700 hover:bg-gray-200/40 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200';
|
||||
|
||||
// return rootMessages;
|
||||
|
||||
// Group all messages by conversation, doesn't look great
|
||||
// Traverse the messages array and store each element in messageMap.
|
||||
// rootMessages = {};
|
||||
// let parents = 0;
|
||||
// messages.forEach(message => {
|
||||
// if (message.conversationId in messageMap) {
|
||||
// messageMap[message.conversationId].children.push(message);
|
||||
// } else {
|
||||
// messageMap[message.conversationId] = { ...message, bg: parents % 2 === 0 ? even : odd, children: [] };
|
||||
// rootMessages.push(messageMap[message.conversationId]);
|
||||
// parents++;
|
||||
// }
|
||||
// });
|
||||
|
||||
// // return Object.values(rootMessages);
|
||||
// return rootMessages;
|
||||
export function groupIntoList({
|
||||
messages,
|
||||
}: // fileMap,
|
||||
{
|
||||
messages: TMessage[] | null;
|
||||
// fileMap?: Record<string, TFile>;
|
||||
}) {
|
||||
if (messages === null) {
|
||||
return null;
|
||||
}
|
||||
return messages.map((m, idx) => ({ ...m, bg: idx % 2 === 0 ? even : odd }));
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@ export function mapEndpoints(endpointsConfig: TEndpointsConfig) {
|
|||
);
|
||||
}
|
||||
|
||||
const firstLocalConvoKey = LocalStorageKeys.LAST_CONVO_SETUP + '_0';
|
||||
|
||||
/**
|
||||
* Ensures the last selected model stays up to date, as conversation may
|
||||
* update without updating last convo setup when same endpoint */
|
||||
|
@ -98,13 +100,11 @@ export function updateLastSelectedModel({
|
|||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const lastConversationSetup = JSON.parse(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) || '{}',
|
||||
);
|
||||
const lastConversationSetup = JSON.parse(localStorage.getItem(firstLocalConvoKey) || '{}');
|
||||
|
||||
if (lastConversationSetup.endpoint === endpoint) {
|
||||
lastConversationSetup.model = model;
|
||||
localStorage.setItem(LocalStorageKeys.LAST_CONVO_SETUP, JSON.stringify(lastConversationSetup));
|
||||
localStorage.setItem(firstLocalConvoKey, JSON.stringify(lastConversationSetup));
|
||||
}
|
||||
|
||||
const lastSelectedModels = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) || '{}');
|
||||
|
|
|
@ -5,7 +5,7 @@ export default function getLocalStorageItems() {
|
|||
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL),
|
||||
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS),
|
||||
lastBingSettings: localStorage.getItem(LocalStorageKeys.LAST_BING),
|
||||
lastConversationSetup: localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP),
|
||||
lastConversationSetup: localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'),
|
||||
};
|
||||
|
||||
const lastSelectedModel = items.lastSelectedModel ? JSON.parse(items.lastSelectedModel) : {};
|
||||
|
|
|
@ -12,6 +12,7 @@ export * from './endpoints';
|
|||
export * from './sharedLink';
|
||||
export * from './promptGroups';
|
||||
export { default as cn } from './cn';
|
||||
export { default as logger } from './logger';
|
||||
export { default as buildTree } from './buildTree';
|
||||
export { default as getLoginError } from './getLoginError';
|
||||
export { default as cleanupPreset } from './cleanupPreset';
|
||||
|
|
37
client/src/utils/logger.ts
Normal file
37
client/src/utils/logger.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
const isDevelopment = import.meta.env.MODE === 'development';
|
||||
const isLoggerEnabled = import.meta.env.VITE_ENABLE_LOGGER === 'true';
|
||||
|
||||
const logger = {
|
||||
log: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.log(...args);
|
||||
}
|
||||
},
|
||||
warn: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.warn(...args);
|
||||
}
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.error(...args);
|
||||
}
|
||||
},
|
||||
info: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.info(...args);
|
||||
}
|
||||
},
|
||||
debug: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.debug(...args);
|
||||
}
|
||||
},
|
||||
dir: (...args: unknown[]) => {
|
||||
if (isDevelopment || isLoggerEnabled) {
|
||||
console.dir(...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default logger;
|
|
@ -58,13 +58,14 @@ export const trimUndoneRange = (textAreaRef: React.RefObject<HTMLTextAreaElement
|
|||
};
|
||||
|
||||
/**
|
||||
* Remove the "@" character from the end of the textarea's text if it's present.
|
||||
* This function ensures that the "@" is only removed if it's the last character.
|
||||
* Remove the specified character from the end of the textarea's text if it's present.
|
||||
* This function ensures that the specified character is only removed if it's the last character.
|
||||
*
|
||||
* @param {HTMLTextAreaElement} textarea - The textarea element where text manipulation will occur.
|
||||
* @param {string} charToRemove - The character to remove if it's the last character in the textarea's value.
|
||||
*/
|
||||
export function removeAtSymbolIfLast(textarea: HTMLTextAreaElement) {
|
||||
if (textarea.value.endsWith('@')) {
|
||||
export function removeCharIfLast(textarea: HTMLTextAreaElement, charToRemove: string) {
|
||||
if (textarea.value.endsWith(charToRemove)) {
|
||||
textarea.value = textarea.value.slice(0, -1);
|
||||
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
|
10
client/src/vite-env.d.ts
vendored
Normal file
10
client/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_ENABLE_LOGGER: string;
|
||||
// Add other env variables here
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
|
@ -69,6 +69,7 @@ module.exports = {
|
|||
'text-tertiary': 'var(--text-tertiary)',
|
||||
'surface-primary': 'var(--surface-primary)',
|
||||
'surface-primary-alt': 'var(--surface-primary-alt)',
|
||||
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||
'surface-secondary': 'var(--surface-secondary)',
|
||||
'surface-tertiary': 'var(--surface-tertiary)',
|
||||
'border-light': 'var(--border-light)',
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -29348,7 +29348,7 @@
|
|||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
|
@ -814,10 +814,14 @@ export enum Constants {
|
|||
CONFIG_VERSION = '1.1.4',
|
||||
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
||||
NO_PARENT = '00000000-0000-0000-0000-000000000000',
|
||||
/** Standard value for the initial conversationId before a request is sent */
|
||||
NEW_CONVO = 'new',
|
||||
/** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */
|
||||
ENCODED_DOMAIN_LENGTH = 10,
|
||||
/** Identifier for using current_model in multi-model requests. */
|
||||
CURRENT_MODEL = 'current_model',
|
||||
/** Common divider for text values */
|
||||
COMMON_DIVIDER = '__',
|
||||
}
|
||||
|
||||
export enum LocalStorageKeys {
|
||||
|
|
|
@ -298,6 +298,8 @@ export const useLoginUserMutation = (): UseMutationResult<
|
|||
onMutate: () => {
|
||||
queryClient.removeQueries();
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`);
|
||||
localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_1`);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_MODEL);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||
|
|
|
@ -286,6 +286,8 @@ export type TMessage = z.input<typeof tMessageSchema> & {
|
|||
plugins?: TResPlugin[];
|
||||
content?: TMessageContentParts[];
|
||||
files?: Partial<TFile>[];
|
||||
depth?: number;
|
||||
siblingIndex?: number;
|
||||
};
|
||||
|
||||
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {
|
||||
|
|
|
@ -36,6 +36,9 @@ export type TEndpointOption = {
|
|||
key?: string | null;
|
||||
/* assistant */
|
||||
thread_id?: string;
|
||||
/* multi-response stream */
|
||||
overrideConvoId?: string;
|
||||
overrideUserMessageId?: string;
|
||||
};
|
||||
|
||||
export type TPayload = Partial<TMessage> &
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue