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
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
|
.git
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
@ -45,6 +46,7 @@ api/node_modules/
|
||||||
client/node_modules/
|
client/node_modules/
|
||||||
bower_components/
|
bower_components/
|
||||||
*.d.ts
|
*.d.ts
|
||||||
|
!vite-env.d.ts
|
||||||
|
|
||||||
# Floobits
|
# Floobits
|
||||||
.floo
|
.floo
|
||||||
|
|
|
@ -19,6 +19,10 @@ class BaseClient {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
});
|
});
|
||||||
this.fetch = this.fetch.bind(this);
|
this.fetch = this.fetch.bind(this);
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.skipSaveConvo = false;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.skipSaveUserMessage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions() {
|
setOptions() {
|
||||||
|
@ -84,19 +88,45 @@ class BaseClient {
|
||||||
await stream.processTextStream(onProgress);
|
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 = {}) {
|
async setMessageOptions(opts = {}) {
|
||||||
if (opts && opts.replaceOptions) {
|
if (opts && opts.replaceOptions) {
|
||||||
this.setOptions(opts);
|
this.setOptions(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [overrideConvoId, overrideUserMessageId] = this.processOverideIds();
|
||||||
const { isEdited, isContinued } = opts;
|
const { isEdited, isContinued } = opts;
|
||||||
const user = opts.user ?? null;
|
const user = opts.user ?? null;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
const saveOptions = this.getSaveOptions();
|
const saveOptions = this.getSaveOptions();
|
||||||
this.abortController = opts.abortController ?? new AbortController();
|
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 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 responseMessageId = opts.responseMessageId ?? crypto.randomUUID();
|
||||||
let head = isEdited ? responseMessageId : parentMessageId;
|
let head = isEdited ? responseMessageId : parentMessageId;
|
||||||
this.currentMessages = (await this.loadHistory(conversationId, head)) ?? [];
|
this.currentMessages = (await this.loadHistory(conversationId, head)) ?? [];
|
||||||
|
@ -160,7 +190,7 @@ class BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof opts?.onStart === 'function') {
|
if (typeof opts?.onStart === 'function') {
|
||||||
opts.onStart(userMessage);
|
opts.onStart(userMessage, responseMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -450,7 +480,7 @@ class BaseClient {
|
||||||
this.handleTokenCountMap(tokenCountMap);
|
this.handleTokenCountMap(tokenCountMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEdited) {
|
if (!isEdited && !this.skipSaveUserMessage) {
|
||||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,6 +599,10 @@ class BaseClient {
|
||||||
unfinished: false,
|
unfinished: false,
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.skipSaveConvo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await saveConvo(user, {
|
await saveConvo(user, {
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
endpoint: this.options.endpoint,
|
endpoint: this.options.endpoint,
|
||||||
|
|
|
@ -737,7 +737,7 @@ class GoogleClient extends BaseClient {
|
||||||
|
|
||||||
let clientOptions = { ...parameters, maxRetries: 2 };
|
let clientOptions = { ...parameters, maxRetries: 2 };
|
||||||
|
|
||||||
logger.info('Initialized title client options');
|
logger.debug('Initialized title client options');
|
||||||
|
|
||||||
if (this.project_id) {
|
if (this.project_id) {
|
||||||
clientOptions['authOptions'] = {
|
clientOptions['authOptions'] = {
|
||||||
|
@ -764,7 +764,7 @@ class GoogleClient extends BaseClient {
|
||||||
|
|
||||||
const modelName = clientOptions.modelName ?? clientOptions.model ?? '';
|
const modelName = clientOptions.modelName ?? clientOptions.model ?? '';
|
||||||
if (modelName?.includes('1.5') && !this.project_id) {
|
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} */
|
/** @type {GenerativeModel} */
|
||||||
const client = model;
|
const client = model;
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
|
@ -790,7 +790,7 @@ class GoogleClient extends BaseClient {
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
} else {
|
} else {
|
||||||
logger.info('Beginning titling');
|
logger.debug('Beginning titling');
|
||||||
const safetySettings = _payload.safetySettings;
|
const safetySettings = _payload.safetySettings;
|
||||||
|
|
||||||
const titleResponse = await model.invoke(messages, {
|
const titleResponse = await model.invoke(messages, {
|
||||||
|
@ -840,7 +840,7 @@ class GoogleClient extends BaseClient {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[GoogleClient] There was an issue generating the title', e);
|
logger.error('[GoogleClient] There was an issue generating the title', e);
|
||||||
}
|
}
|
||||||
logger.info(`Title response: ${title}`);
|
logger.debug(`Title response: ${title}`);
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -301,7 +301,10 @@ class PluginsClient extends OpenAIClient {
|
||||||
if (payload) {
|
if (payload) {
|
||||||
this.currentMessages = payload;
|
this.currentMessages = payload;
|
||||||
}
|
}
|
||||||
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
|
||||||
|
if (!this.skipSaveUserMessage) {
|
||||||
|
await this.saveMessageToDatabase(userMessage, saveOptions, user);
|
||||||
|
}
|
||||||
|
|
||||||
if (isEnabled(process.env.CHECK_BALANCE)) {
|
if (isEnabled(process.env.CHECK_BALANCE)) {
|
||||||
await checkBalance({
|
await checkBalance({
|
||||||
|
|
|
@ -576,7 +576,11 @@ describe('BaseClient', () => {
|
||||||
const onStart = jest.fn();
|
const onStart = jest.fn();
|
||||||
const opts = { onStart };
|
const opts = { onStart };
|
||||||
await TestClient.sendMessage('Hello, world!', opts);
|
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 () => {
|
test('saveMessageToDatabase is called with the correct arguments', async () => {
|
||||||
|
|
|
@ -81,7 +81,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
promptTokens,
|
promptTokens,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||||
|
|
||||||
res.on('close', () => {
|
res.on('close', () => {
|
||||||
logger.debug('[AskController] Request closed');
|
logger.debug('[AskController] Request closed');
|
||||||
|
@ -144,7 +144,9 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
await saveMessage({ ...response, user });
|
await saveMessage({ ...response, user });
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveMessage(userMessage);
|
if (!client.skipSaveUserMessage) {
|
||||||
|
await saveMessage(userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
|
if (addTitle && parentMessageId === Constants.NO_PARENT && newConvo) {
|
||||||
addTitle(req, {
|
addTitle(req, {
|
||||||
|
|
|
@ -81,7 +81,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||||
promptTokens,
|
promptTokens,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||||
|
|
||||||
res.on('close', () => {
|
res.on('close', () => {
|
||||||
logger.debug('[EditController] Request closed');
|
logger.debug('[EditController] Request closed');
|
||||||
|
|
|
@ -9,23 +9,28 @@ const { abortRun } = require('./abortRun');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
async function abortMessage(req, res) {
|
async function abortMessage(req, res) {
|
||||||
let { abortKey, conversationId, endpoint } = req.body;
|
let { abortKey, endpoint } = req.body;
|
||||||
|
|
||||||
if (!abortKey && conversationId) {
|
|
||||||
abortKey = conversationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAssistantsEndpoint(endpoint)) {
|
if (isAssistantsEndpoint(endpoint)) {
|
||||||
return await abortRun(req, res);
|
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) {
|
if (!abortControllers.has(abortKey) && !res.headersSent) {
|
||||||
return res.status(204).send({ message: 'Request not found' });
|
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();
|
const finalEvent = await abortController.abortCompletion();
|
||||||
logger.debug('[abortMessage] Aborted request', { abortKey });
|
logger.info('[abortMessage] Aborted request', { abortKey });
|
||||||
abortControllers.delete(abortKey);
|
abortControllers.delete(abortKey);
|
||||||
|
|
||||||
if (res.headersSent && finalEvent) {
|
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 abortController = new AbortController();
|
||||||
const { endpointOption } = req.body;
|
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 });
|
sendMessage(res, { message: userMessage, created: true });
|
||||||
const abortKey = userMessage?.conversationId ?? req.user.id;
|
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 });
|
abortControllers.set(abortKey, { abortController, ...endpointOption });
|
||||||
|
|
||||||
res.on('finish', function () {
|
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 = () => ({
|
const getAbortData = () => ({
|
||||||
sender,
|
sender,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -167,12 +158,23 @@ router.post(
|
||||||
userMessage,
|
userMessage,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
});
|
});
|
||||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
||||||
const { client } = await initializeClient({ req, res, endpointOption });
|
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, {
|
let response = await client.sendMessage(text, {
|
||||||
user,
|
user,
|
||||||
conversationId,
|
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) => {
|
const onChainEnd = (data) => {
|
||||||
let { intermediateSteps: steps } = data;
|
let { intermediateSteps: steps } = data;
|
||||||
plugin.outputs = steps && steps[0].action ? formatSteps(steps) : 'An error occurred.';
|
plugin.outputs = steps && steps[0].action ? formatSteps(steps) : 'An error occurred.';
|
||||||
|
@ -141,12 +126,27 @@ router.post(
|
||||||
userMessage,
|
userMessage,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
});
|
});
|
||||||
const { abortController, onStart } = createAbortController(req, res, getAbortData);
|
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
endpointOption.tools = await validateTools(user, endpointOption.tools);
|
||||||
const { client } = await initializeClient({ req, res, endpointOption });
|
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, {
|
let response = await client.sendMessage(text, {
|
||||||
user,
|
user,
|
||||||
generation,
|
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 { createContext, useContext } from 'react';
|
||||||
import useChatHelpers from '~/hooks/useChatHelpers';
|
import useChatHelpers from '~/hooks/Chat/useChatHelpers';
|
||||||
type TChatContext = ReturnType<typeof useChatHelpers>;
|
type TChatContext = ReturnType<typeof useChatHelpers>;
|
||||||
|
|
||||||
export const ChatContext = createContext<TChatContext>({} as TChatContext);
|
export const ChatContext = createContext<TChatContext>({} as TChatContext);
|
||||||
|
|
|
@ -5,6 +5,7 @@ export * from './ShareContext';
|
||||||
export * from './ToastContext';
|
export * from './ToastContext';
|
||||||
export * from './SearchContext';
|
export * from './SearchContext';
|
||||||
export * from './FileMapContext';
|
export * from './FileMapContext';
|
||||||
|
export * from './AddedChatContext';
|
||||||
export * from './ChatFormContext';
|
export * from './ChatFormContext';
|
||||||
export * from './DashboardContext';
|
export * from './DashboardContext';
|
||||||
export * from './AssistantsContext';
|
export * from './AssistantsContext';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
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 * as InputNumberPrimitive from 'rc-input-number';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
|
@ -11,8 +11,10 @@ import type {
|
||||||
TPlugin,
|
TPlugin,
|
||||||
TMessage,
|
TMessage,
|
||||||
Assistant,
|
Assistant,
|
||||||
|
TResPlugin,
|
||||||
TLoginUser,
|
TLoginUser,
|
||||||
AuthTypeEnum,
|
AuthTypeEnum,
|
||||||
|
TModelsConfig,
|
||||||
TConversation,
|
TConversation,
|
||||||
TStartupConfig,
|
TStartupConfig,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
@ -229,6 +231,8 @@ export type TGenButtonProps = {
|
||||||
|
|
||||||
export type TAskProps = {
|
export type TAskProps = {
|
||||||
text: string;
|
text: string;
|
||||||
|
overrideConvoId?: string;
|
||||||
|
overrideUserMessageId?: string;
|
||||||
parentMessageId?: string | null;
|
parentMessageId?: string | null;
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
messageId?: string | null;
|
messageId?: string | null;
|
||||||
|
@ -241,6 +245,7 @@ export type TOptions = {
|
||||||
isRegenerate?: boolean;
|
isRegenerate?: boolean;
|
||||||
isContinued?: boolean;
|
isContinued?: boolean;
|
||||||
isEdited?: boolean;
|
isEdited?: boolean;
|
||||||
|
overrideMessages?: TMessage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
|
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
|
||||||
|
@ -409,6 +414,29 @@ export type TLoginLayoutContext = {
|
||||||
headerText: string;
|
headerText: string;
|
||||||
setHeaderText: React.Dispatch<React.SetStateAction<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 = {
|
export type TVectorStore = {
|
||||||
_id: string;
|
_id: string;
|
||||||
object: 'vector_store';
|
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 { useParams } from 'react-router-dom';
|
||||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||||
import type { ChatFormValues } from '~/common';
|
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 MessagesView from './Messages/MessagesView';
|
||||||
import { useChatHelpers, useSSE } from '~/hooks';
|
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import Presentation from './Presentation';
|
import Presentation from './Presentation';
|
||||||
import ChatForm from './Input/ChatForm';
|
import ChatForm from './Input/ChatForm';
|
||||||
|
@ -18,8 +18,8 @@ import store from '~/store';
|
||||||
|
|
||||||
function ChatView({ index = 0 }: { index?: number }) {
|
function ChatView({ index = 0 }: { index?: number }) {
|
||||||
const { conversationId } = useParams();
|
const { conversationId } = useParams();
|
||||||
const submissionAtIndex = useRecoilValue(store.submissionByIndex(0));
|
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
|
||||||
useSSE(submissionAtIndex);
|
const addedSubmission = useRecoilValue(store.submissionByIndex(index + 1));
|
||||||
|
|
||||||
const fileMap = useFileMapContext();
|
const fileMap = useFileMapContext();
|
||||||
|
|
||||||
|
@ -32,35 +32,35 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const chatHelpers = useChatHelpers(index, conversationId);
|
const chatHelpers = useChatHelpers(index, conversationId);
|
||||||
|
const addedChatHelpers = useAddedResponse({ rootIndex: index });
|
||||||
|
|
||||||
|
useSSE(rootSubmission, chatHelpers, false);
|
||||||
|
useSSE(addedSubmission, addedChatHelpers, true);
|
||||||
|
|
||||||
const methods = useForm<ChatFormValues>({
|
const methods = useForm<ChatFormValues>({
|
||||||
defaultValues: { text: '' },
|
defaultValues: { text: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatFormProvider
|
<ChatFormProvider {...methods}>
|
||||||
reset={methods.reset}
|
|
||||||
control={methods.control}
|
|
||||||
setValue={methods.setValue}
|
|
||||||
register={methods.register}
|
|
||||||
getValues={methods.getValues}
|
|
||||||
handleSubmit={methods.handleSubmit}
|
|
||||||
>
|
|
||||||
<ChatContext.Provider value={chatHelpers}>
|
<ChatContext.Provider value={chatHelpers}>
|
||||||
<Presentation useSidePanel={true}>
|
<AddedChatContext.Provider value={addedChatHelpers}>
|
||||||
{isLoading && conversationId !== 'new' ? (
|
<Presentation useSidePanel={true}>
|
||||||
<div className="flex h-screen items-center justify-center">
|
{isLoading && conversationId !== 'new' ? (
|
||||||
<Spinner className="opacity-0" />
|
<div className="flex h-screen items-center justify-center">
|
||||||
|
<Spinner className="opacity-0" />
|
||||||
|
</div>
|
||||||
|
) : messagesTree && messagesTree.length !== 0 ? (
|
||||||
|
<MessagesView messagesTree={messagesTree} Header={<Header />} />
|
||||||
|
) : (
|
||||||
|
<Landing Header={<Header />} />
|
||||||
|
)}
|
||||||
|
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
||||||
|
<ChatForm index={index} />
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
) : messagesTree && messagesTree.length !== 0 ? (
|
</Presentation>
|
||||||
<MessagesView messagesTree={messagesTree} Header={<Header />} />
|
</AddedChatContext.Provider>
|
||||||
) : (
|
|
||||||
<Landing Header={<Header />} />
|
|
||||||
)}
|
|
||||||
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
|
||||||
<ChatForm index={index} />
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</Presentation>
|
|
||||||
</ChatContext.Provider>
|
</ChatContext.Provider>
|
||||||
</ChatFormProvider>
|
</ChatFormProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default function Footer({ className }: { className?: string }) {
|
||||||
: '[LibreChat ' +
|
: '[LibreChat ' +
|
||||||
Constants.VERSION +
|
Constants.VERSION +
|
||||||
'](https://librechat.ai) - ' +
|
'](https://librechat.ai) - ' +
|
||||||
localize('com_ui_pay_per_call')
|
localize('com_ui_latest_footer')
|
||||||
).split('|');
|
).split('|');
|
||||||
|
|
||||||
const mainContentRender = mainContentParts.map((text, index) => (
|
const mainContentRender = mainContentParts.map((text, index) => (
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type { ContextType } from '~/common';
|
||||||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||||
import HeaderOptions from './Input/HeaderOptions';
|
import HeaderOptions from './Input/HeaderOptions';
|
||||||
|
import AddMultiConvo from './AddMultiConvo';
|
||||||
import { useMediaQuery } from '~/hooks';
|
import { useMediaQuery } from '~/hooks';
|
||||||
|
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
@ -36,6 +37,7 @@ export default function Header() {
|
||||||
className="pl-0"
|
className="pl-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<AddMultiConvo />
|
||||||
</div>
|
</div>
|
||||||
{!isSmallScreen && (
|
{!isSmallScreen && (
|
||||||
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />
|
<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,
|
isAssistantsEndpoint,
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useChatContext, useAssistantsMapContext, useChatFormContext } from '~/Providers';
|
import {
|
||||||
import { useRequiresKey, useTextarea, useSubmitMessage, useHandleKeyUp } from '~/hooks';
|
useChatContext,
|
||||||
import { useAutoSave } from '~/hooks/Input/useAutoSave';
|
useAddedChatContext,
|
||||||
|
useAssistantsMapContext,
|
||||||
|
useChatFormContext,
|
||||||
|
} from '~/Providers';
|
||||||
|
import {
|
||||||
|
useTextarea,
|
||||||
|
useAutoSave,
|
||||||
|
useRequiresKey,
|
||||||
|
useHandleKeyUp,
|
||||||
|
useSubmitMessage,
|
||||||
|
} from '~/hooks';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
import { cn, removeFocusRings } from '~/utils';
|
import { cn, removeFocusRings } from '~/utils';
|
||||||
|
import TextareaHeader from './TextareaHeader';
|
||||||
import AttachFile from './Files/AttachFile';
|
import AttachFile from './Files/AttachFile';
|
||||||
import AudioRecorder from './AudioRecorder';
|
import AudioRecorder from './AudioRecorder';
|
||||||
import { mainTextareaId } from '~/common';
|
import { mainTextareaId } from '~/common';
|
||||||
|
@ -25,16 +36,19 @@ import store from '~/store';
|
||||||
const ChatForm = ({ index = 0 }) => {
|
const ChatForm = ({ index = 0 }) => {
|
||||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
const SpeechToText = useRecoilValue(store.SpeechToText);
|
const SpeechToText = useRecoilValue(store.SpeechToText);
|
||||||
const TextToSpeech = useRecoilValue(store.TextToSpeech);
|
const TextToSpeech = useRecoilValue(store.TextToSpeech);
|
||||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||||
|
|
||||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||||
|
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
|
||||||
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
||||||
store.showMentionPopoverFamily(index),
|
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({
|
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
submitButtonRef,
|
submitButtonRef,
|
||||||
|
@ -48,9 +62,18 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
filesLoading,
|
filesLoading,
|
||||||
setFilesLoading,
|
setFilesLoading,
|
||||||
|
newConversation,
|
||||||
handleStopGenerating,
|
handleStopGenerating,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
const methods = useChatFormContext();
|
const methods = useChatFormContext();
|
||||||
|
const {
|
||||||
|
addedIndex,
|
||||||
|
generateConversation,
|
||||||
|
conversation: addedConvo,
|
||||||
|
setConversation: setAddedConvo,
|
||||||
|
isSubmitting: isSubmittingAdded,
|
||||||
|
} = useAddedChatContext();
|
||||||
|
const showStopAdded = useRecoilValue(store.showStopButtonByIndex(addedIndex));
|
||||||
|
|
||||||
const { clearDraft } = useAutoSave({
|
const { clearDraft } = useAutoSave({
|
||||||
conversationId: useMemo(() => conversation?.conversationId, [conversation]),
|
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="relative flex h-full flex-1 items-stretch md:flex-col">
|
||||||
<div className="flex w-full items-center">
|
<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 && (
|
{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">
|
<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
|
<FileRow
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
@ -145,7 +183,7 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
endpointType={endpointType}
|
endpointType={endpointType}
|
||||||
disabled={disableInputs}
|
disabled={disableInputs}
|
||||||
/>
|
/>
|
||||||
{isSubmitting && showStopButton ? (
|
{(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
|
||||||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||||
) : (
|
) : (
|
||||||
endpoint && (
|
endpoint && (
|
||||||
|
|
|
@ -1,30 +1,39 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import type { MentionOption } from '~/common';
|
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||||
import { useAssistantsMapContext } from '~/Providers';
|
import { useAssistantsMapContext } from '~/Providers';
|
||||||
import useMentions from '~/hooks/Input/useMentions';
|
import useMentions from '~/hooks/Input/useMentions';
|
||||||
import { useLocalize, useCombobox } from '~/hooks';
|
import { useLocalize, useCombobox } from '~/hooks';
|
||||||
import { removeAtSymbolIfLast } from '~/utils';
|
import { removeCharIfLast } from '~/utils';
|
||||||
import MentionItem from './MentionItem';
|
import MentionItem from './MentionItem';
|
||||||
|
|
||||||
export default function Mention({
|
export default function Mention({
|
||||||
setShowMentionPopover,
|
setShowMentionPopover,
|
||||||
|
newConversation,
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
|
commandChar = '@',
|
||||||
|
placeholder = 'com_ui_mention',
|
||||||
|
includeAssistants = true,
|
||||||
}: {
|
}: {
|
||||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||||
|
newConversation: ConvoGenerator;
|
||||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||||
|
commandChar?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
includeAssistants?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
|
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
|
||||||
useMentions({ assistantMap });
|
useMentions({ assistantMap, includeAssistants });
|
||||||
const { onSelectMention } = useSelectMention({
|
const { onSelectMention } = useSelectMention({
|
||||||
presets,
|
presets,
|
||||||
modelSpecs,
|
modelSpecs,
|
||||||
assistantMap,
|
assistantMap,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
|
newConversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
@ -49,7 +58,7 @@ export default function Mention({
|
||||||
onSelectMention(mention);
|
onSelectMention(mention);
|
||||||
|
|
||||||
if (textAreaRef.current) {
|
if (textAreaRef.current) {
|
||||||
removeAtSymbolIfLast(textAreaRef.current);
|
removeCharIfLast(textAreaRef.current, commandChar);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,7 +114,7 @@ export default function Mention({
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={inputRef}
|
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"
|
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"
|
autoComplete="off"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
|
|
|
@ -1,33 +1,29 @@
|
||||||
export default function StopButton({ stop, setShowStopButton }) {
|
export default function StopButton({ stop, setShowStopButton }) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-0 right-2 top-0 p-1 md:right-3 md:p-2">
|
<div className="absolute bottom-3 right-2 md:bottom-4 md:right-4">
|
||||||
<div className="flex h-full">
|
<button
|
||||||
<div className="flex h-full flex-row items-center justify-center gap-3">
|
type="button"
|
||||||
<button
|
className="border-gizmo-gray-900 rounded-full border-2 p-1 dark:border-gray-200"
|
||||||
type="button"
|
aria-label="Stop generating"
|
||||||
className="border-gizmo-gray-900 rounded-full border-2 p-1 dark:border-gray-200"
|
onClick={(e) => {
|
||||||
aria-label="Stop generating"
|
setShowStopButton(false);
|
||||||
onClick={(e) => {
|
stop(e);
|
||||||
setShowStopButton(false);
|
}}
|
||||||
stop(e);
|
>
|
||||||
}}
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
viewBox="0 0 16 16"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="currentColor"
|
||||||
viewBox="0 0 16 16"
|
className="text-gizmo-gray-900 h-2 w-2 dark:text-gray-200"
|
||||||
fill="currentColor"
|
height="16"
|
||||||
className="text-gizmo-gray-900 h-2 w-2 dark:text-gray-200"
|
width="16"
|
||||||
height="16"
|
>
|
||||||
width="16"
|
<path
|
||||||
>
|
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z"
|
||||||
<path
|
strokeWidth="0"
|
||||||
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z"
|
></path>
|
||||||
strokeWidth="0"
|
</svg>
|
||||||
></path>
|
</button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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 {
|
const {
|
||||||
|
template,
|
||||||
shouldSwitch,
|
shouldSwitch,
|
||||||
isNewModular,
|
isNewModular,
|
||||||
|
newEndpointType,
|
||||||
isCurrentModular,
|
isCurrentModular,
|
||||||
isExistingConversation,
|
isExistingConversation,
|
||||||
newEndpointType,
|
|
||||||
template,
|
|
||||||
} = getConvoSwitchLogic({
|
} = getConvoSwitchLogic({
|
||||||
newEndpoint,
|
newEndpoint,
|
||||||
modularChat,
|
modularChat,
|
||||||
|
@ -63,7 +63,8 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||||
|
if (isExistingConversation && isModular) {
|
||||||
template.endpointType = newEndpointType;
|
template.endpointType = newEndpointType;
|
||||||
|
|
||||||
const currentConvo = getDefaultConversation({
|
const currentConvo = getDefaultConversation({
|
||||||
|
@ -73,10 +74,18 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
newConversation({
|
||||||
|
template: { ...(template as Partial<TConversation>) },
|
||||||
|
keepAddedConvos: isModular,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||||
|
|
|
@ -29,12 +29,12 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
template,
|
||||||
shouldSwitch,
|
shouldSwitch,
|
||||||
isNewModular,
|
isNewModular,
|
||||||
|
newEndpointType,
|
||||||
isCurrentModular,
|
isCurrentModular,
|
||||||
isExistingConversation,
|
isExistingConversation,
|
||||||
newEndpointType,
|
|
||||||
template,
|
|
||||||
} = getConvoSwitchLogic({
|
} = getConvoSwitchLogic({
|
||||||
newEndpoint,
|
newEndpoint,
|
||||||
modularChat,
|
modularChat,
|
||||||
|
@ -42,7 +42,8 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||||
|
if (isExistingConversation && isModular) {
|
||||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||||
|
|
||||||
const currentConvo = getDefaultConversation({
|
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 */
|
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newConversation({ template: { ...(template as Partial<TConversation>) }, preset });
|
newConversation({
|
||||||
|
template: { ...(template as Partial<TConversation>) },
|
||||||
|
preset,
|
||||||
|
keepAddedConvos: isModular,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = useMemo(() => {
|
const selected = useMemo(() => {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
||||||
import type { TEditProps } from '~/common';
|
import type { TEditProps } from '~/common';
|
||||||
|
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||||
import { cn, removeFocusRings } from '~/utils';
|
import { cn, removeFocusRings } from '~/utils';
|
||||||
import { useChatContext } from '~/Providers';
|
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import Container from './Container';
|
import Container from './Container';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
const EditMessage = ({
|
const EditMessage = ({
|
||||||
text,
|
text,
|
||||||
|
@ -17,7 +19,11 @@ const EditMessage = ({
|
||||||
siblingIdx,
|
siblingIdx,
|
||||||
setSiblingIdx,
|
setSiblingIdx,
|
||||||
}: TEditProps) => {
|
}: TEditProps) => {
|
||||||
|
const { addedIndex } = useAddedChatContext();
|
||||||
const { getMessages, setMessages, conversation } = useChatContext();
|
const { getMessages, setMessages, conversation } = useChatContext();
|
||||||
|
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||||
|
store.latestMessageFamily(addedIndex),
|
||||||
|
);
|
||||||
|
|
||||||
const [editedText, setEditedText] = useState<string>(text ?? '');
|
const [editedText, setEditedText] = useState<string>(text ?? '');
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
@ -85,17 +91,28 @@ const EditMessage = ({
|
||||||
text: editedText,
|
text: editedText,
|
||||||
messageId,
|
messageId,
|
||||||
});
|
});
|
||||||
setMessages(
|
|
||||||
messages.map((msg) =>
|
if (message.messageId === latestMultiMessage?.messageId) {
|
||||||
msg.messageId === messageId
|
setLatestMultiMessage({ ...latestMultiMessage, text: editedText });
|
||||||
? {
|
}
|
||||||
...msg,
|
|
||||||
text: editedText,
|
const isInMessages = messages?.some((message) => message?.messageId === messageId);
|
||||||
isEdited: true,
|
if (!isInMessages) {
|
||||||
}
|
message.text = editedText;
|
||||||
: msg,
|
} else {
|
||||||
),
|
setMessages(
|
||||||
);
|
messages.map((msg) =>
|
||||||
|
msg.messageId === messageId
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
text: editedText,
|
||||||
|
isEdited: true,
|
||||||
|
}
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
enterEdit(true);
|
enterEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useAuthContext, useMessageHelpers, useLocalize } from '~/hooks';
|
import { useMessageProcess, useMessageActions } from '~/hooks';
|
||||||
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import { Plugin } from '~/components/Messages/Content';
|
import { Plugin } from '~/components/Messages/Content';
|
||||||
|
@ -10,120 +11,213 @@ import MultiMessage from './MultiMessage';
|
||||||
import HoverButtons from './HoverButtons';
|
import HoverButtons from './HoverButtons';
|
||||||
import SubRow from './SubRow';
|
import SubRow from './SubRow';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function Message(props: TMessageProps) {
|
const MessageContainer = React.memo(
|
||||||
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
|
({ handleScroll, children }: { handleScroll: () => void; children: React.ReactNode }) => {
|
||||||
const { user } = useAuthContext();
|
return (
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
|
||||||
onWheel={handleScroll}
|
onWheel={handleScroll}
|
||||||
onTouchMove={handleScroll}
|
onTouchMove={handleScroll}
|
||||||
>
|
>
|
||||||
<div className="m-auto justify-center p-4 py-2 text-base md:gap-6 ">
|
{children}
|
||||||
<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">
|
</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} />
|
const PlaceholderRow = React.memo(({ isLast, isCard }: { isLast: boolean; isCard?: boolean }) => {
|
||||||
</div>
|
if (!isLast && !isCard) {
|
||||||
</div>
|
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={msg} conversation={conversation} assistant={assistant} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={cn('relative flex w-11/12 flex-col', 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} />}
|
|
||||||
<MessageContent
|
|
||||||
ask={ask}
|
|
||||||
edit={edit}
|
|
||||||
isLast={isLast}
|
|
||||||
text={text ?? ''}
|
|
||||||
message={message}
|
|
||||||
enterEdit={enterEdit}
|
|
||||||
error={!!error}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
unfinished={unfinished ?? false}
|
|
||||||
isCreatedByUser={isCreatedByUser ?? true}
|
|
||||||
siblingIdx={siblingIdx ?? 0}
|
|
||||||
setSiblingIdx={
|
|
||||||
setSiblingIdx ??
|
|
||||||
(() => {
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isLast && isSubmitting ? null : (
|
|
||||||
<SubRow classes="text-xs">
|
|
||||||
<SiblingSwitch
|
|
||||||
siblingIdx={siblingIdx}
|
|
||||||
siblingCount={siblingCount}
|
|
||||||
setSiblingIdx={setSiblingIdx}
|
|
||||||
/>
|
|
||||||
<HoverButtons
|
|
||||||
index={index}
|
|
||||||
isEditing={edit}
|
|
||||||
message={message}
|
|
||||||
enterEdit={enterEdit}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
conversation={conversation ?? null}
|
|
||||||
regenerate={() => regenerateMessage()}
|
|
||||||
copyToClipboard={copyToClipboard}
|
|
||||||
handleContinue={handleContinue}
|
|
||||||
latestMessage={latestMessage}
|
|
||||||
isLast={isLast}
|
|
||||||
/>
|
|
||||||
</SubRow>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
{msg?.plugin && <Plugin plugin={msg?.plugin} />}
|
||||||
|
<MessageContent
|
||||||
|
ask={ask}
|
||||||
|
edit={edit}
|
||||||
|
isLast={isLast}
|
||||||
|
text={msg.text ?? ''}
|
||||||
|
message={msg}
|
||||||
|
enterEdit={enterEdit}
|
||||||
|
error={!!error}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
unfinished={unfinished ?? false}
|
||||||
|
isCreatedByUser={isCreatedByUser ?? true}
|
||||||
|
siblingIdx={siblingIdx ?? 0}
|
||||||
|
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!msg?.children?.length && (isSubmittingFamily || isSubmitting) ? (
|
||||||
|
<PlaceholderRow isLast={isLast} isCard={isCard} />
|
||||||
|
) : (
|
||||||
|
<SubRow classes="text-xs">
|
||||||
|
<SiblingSwitch
|
||||||
|
siblingIdx={siblingIdx}
|
||||||
|
siblingCount={siblingCount}
|
||||||
|
setSiblingIdx={setSiblingIdx}
|
||||||
|
/>
|
||||||
|
<HoverButtons
|
||||||
|
index={index}
|
||||||
|
isEditing={edit}
|
||||||
|
message={msg}
|
||||||
|
enterEdit={enterEdit}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
conversation={conversation ?? null}
|
||||||
|
regenerate={handleRegenerateMessage}
|
||||||
|
copyToClipboard={copyToClipboard}
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
latestMessage={latestMessage}
|
||||||
|
isLast={isLast}
|
||||||
|
/>
|
||||||
|
</SubRow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</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
|
<MultiMessage
|
||||||
key={messageId}
|
key={messageId}
|
||||||
messageId={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 { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TMessage, TPreset, Assistant } from 'librechat-data-provider';
|
import type { TMessage, TPreset, Assistant } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
|
@ -6,7 +6,7 @@ import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
import { getEndpointField, getIconEndpoint } from '~/utils';
|
import { getEndpointField, getIconEndpoint } from '~/utils';
|
||||||
import Icon from '~/components/Endpoints/Icon';
|
import Icon from '~/components/Endpoints/Icon';
|
||||||
|
|
||||||
export default function MessageIcon(
|
function MessageIcon(
|
||||||
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
||||||
assistant?: false | Assistant;
|
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>
|
||||||
</div>
|
</div>
|
||||||
{isLast && isSubmitting ? null : (
|
{isLast && isSubmitting ? (
|
||||||
|
<div className="mt-1 h-[27px] bg-transparent" />
|
||||||
|
) : (
|
||||||
<SubRow classes="text-xs">
|
<SubRow classes="text-xs">
|
||||||
<SiblingSwitch
|
<SiblingSwitch
|
||||||
siblingIdx={siblingIdx}
|
siblingIdx={siblingIdx}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
|
@ -16,9 +16,12 @@ export default function MultiMessage({
|
||||||
}: TMessageProps) {
|
}: TMessageProps) {
|
||||||
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
||||||
|
|
||||||
const setSiblingIdxRev = (value: number) => {
|
const setSiblingIdxRev = useCallback(
|
||||||
setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
|
(value: number) => {
|
||||||
};
|
setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
|
||||||
|
},
|
||||||
|
[messagesTree?.length, setSiblingIdx],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
// 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 { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { IconProps } from '~/common';
|
import { UserIcon } from '~/components/svg';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import MessageEndpointIcon from './MessageEndpointIcon';
|
|
||||||
|
|
||||||
const Icon: React.FC<IconProps> = (props) => {
|
const Icon: React.FC<IconProps> = (props) => {
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
@ -46,4 +47,4 @@ const Icon: React.FC<IconProps> = (props) => {
|
||||||
return <MessageEndpointIcon {...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 { normalizeData } from '~/utils/collection';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
/** Conversations */
|
export type TGenTitleMutation = UseMutationResult<
|
||||||
export const useGenTitleMutation = (): UseMutationResult<
|
|
||||||
t.TGenTitleResponse,
|
t.TGenTitleResponse,
|
||||||
unknown,
|
unknown,
|
||||||
t.TGenTitleRequest,
|
t.TGenTitleRequest,
|
||||||
unknown
|
unknown
|
||||||
> => {
|
>;
|
||||||
|
|
||||||
|
/** Conversations */
|
||||||
|
export const useGenTitleMutation = (): TGenTitleMutation => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation((payload: t.TGenTitleRequest) => dataService.genTitle(payload), {
|
return useMutation((payload: t.TGenTitleRequest) => dataService.genTitle(payload), {
|
||||||
onSuccess: (response, vars) => {
|
onSuccess: (response, vars) => {
|
||||||
|
@ -525,6 +527,8 @@ export const useLogoutUserMutation = (
|
||||||
setDefaultPreset(null);
|
setDefaultPreset(null);
|
||||||
queryClient.removeQueries();
|
queryClient.removeQueries();
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
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_MODEL);
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||||
|
@ -565,6 +569,8 @@ export const useDeleteUserMutation = (
|
||||||
setDefaultPreset(null);
|
setDefaultPreset(null);
|
||||||
queryClient.removeQueries();
|
queryClient.removeQueries();
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
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_MODEL);
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
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 useGetSender } from './useGetSender';
|
||||||
export { default as useDefaultConvo } from './useDefaultConvo';
|
export { default as useDefaultConvo } from './useDefaultConvo';
|
||||||
export { default as useConversation } from './useConversation';
|
export { default as useConversation } from './useConversation';
|
||||||
|
export { default as useGenerateConvo } from './useGenerateConvo';
|
||||||
export { default as useConversations } from './useConversations';
|
export { default as useConversations } from './useConversations';
|
||||||
export { default as useDebouncedInput } from './useDebouncedInput';
|
export { default as useDebouncedInput } from './useDebouncedInput';
|
||||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
|
||||||
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||||
|
@ -9,9 +9,10 @@ import store from '~/store';
|
||||||
const useNavigateToConvo = (index = 0) => {
|
const useNavigateToConvo = (index = 0) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { setConversation } = store.useCreateConversationAtom(index);
|
const clearAllConversations = store.useClearConvoState();
|
||||||
|
const clearAllLatestMessages = store.useClearLatestMessages();
|
||||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
const { setConversation } = store.useCreateConversationAtom(index);
|
||||||
|
|
||||||
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
|
@ -20,7 +21,7 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
}
|
}
|
||||||
setSubmission(null);
|
setSubmission(null);
|
||||||
if (_resetLatestMessage) {
|
if (_resetLatestMessage) {
|
||||||
resetLatestMessage();
|
clearAllLatestMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
let convo = { ...conversation };
|
let convo = { ...conversation };
|
||||||
|
@ -47,6 +48,7 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
models,
|
models,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
clearAllConversations(true);
|
||||||
setConversation(convo);
|
setConversation(convo);
|
||||||
navigate(`/c/${convo.conversationId ?? 'new'}`);
|
navigate(`/c/${convo.conversationId ?? 'new'}`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import filenamify from 'filenamify';
|
import filenamify from 'filenamify';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { QueryKeys, modularEndpoints, isAssistantsEndpoint } from 'librechat-data-provider';
|
|
||||||
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
|
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
|
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
||||||
useDeletePresetMutation,
|
useDeletePresetMutation,
|
||||||
useGetPresetsQuery,
|
useGetPresetsQuery,
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
|
import { cleanupPreset, removeUnavailableTools, getConvoSwitchLogic } from '~/utils';
|
||||||
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||||
import { useChatContext, useToastContext } from '~/Providers';
|
import { useChatContext, useToastContext } from '~/Providers';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
|
@ -124,8 +124,6 @@ export default function usePresets() {
|
||||||
|
|
||||||
const getDefaultConversation = useDefaultConvo();
|
const getDefaultConversation = useDefaultConvo();
|
||||||
|
|
||||||
const { endpoint } = conversation ?? {};
|
|
||||||
|
|
||||||
const importPreset = (jsonPreset: TPreset) => {
|
const importPreset = (jsonPreset: TPreset) => {
|
||||||
createPresetMutation.mutate(
|
createPresetMutation.mutate(
|
||||||
{ ...jsonPreset },
|
{ ...jsonPreset },
|
||||||
|
@ -171,34 +169,38 @@ export default function usePresets() {
|
||||||
|
|
||||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||||
|
|
||||||
const currentEndpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const {
|
||||||
const endpointType = getEndpointField(endpointsConfig, newPreset.endpoint, 'type');
|
shouldSwitch,
|
||||||
const isAssistantSwitch =
|
isNewModular,
|
||||||
isAssistantsEndpoint(newPreset.endpoint) &&
|
newEndpointType,
|
||||||
isAssistantsEndpoint(conversation?.endpoint) &&
|
isCurrentModular,
|
||||||
conversation?.endpoint === newPreset.endpoint;
|
isExistingConversation,
|
||||||
|
} = getConvoSwitchLogic({
|
||||||
|
newEndpoint: newPreset.endpoint ?? '',
|
||||||
|
modularChat,
|
||||||
|
conversation,
|
||||||
|
endpointsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||||
(modularEndpoints.has(endpoint ?? '') ||
|
if (isExistingConversation && isModular) {
|
||||||
modularEndpoints.has(currentEndpointType ?? '') ||
|
|
||||||
isAssistantSwitch) &&
|
|
||||||
(modularEndpoints.has(newPreset?.endpoint ?? '') ||
|
|
||||||
modularEndpoints.has(endpointType ?? '') ||
|
|
||||||
isAssistantSwitch) &&
|
|
||||||
(endpoint === newPreset?.endpoint || modularChat || isAssistantSwitch)
|
|
||||||
) {
|
|
||||||
const currentConvo = getDefaultConversation({
|
const currentConvo = getDefaultConversation({
|
||||||
/* target endpointType is necessary to avoid endpoint mixing */
|
/* target endpointType is necessary to avoid endpoint mixing */
|
||||||
conversation: { ...(conversation ?? {}), endpointType },
|
conversation: { ...(conversation ?? {}), endpointType: newEndpointType },
|
||||||
preset: { ...newPreset, endpointType },
|
preset: { ...newPreset, endpointType: newEndpointType },
|
||||||
});
|
});
|
||||||
|
|
||||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newConversation({ preset: newPreset });
|
newConversation({ preset: newPreset, keepAddedConvos: isModular });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangePreset = (preset: TPreset) => {
|
const onChangePreset = (preset: TPreset) => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './useAutoSave';
|
||||||
export { default as useUserKey } from './useUserKey';
|
export { default as useUserKey } from './useUserKey';
|
||||||
export { default as useDebounce } from './useDebounce';
|
export { default as useDebounce } from './useDebounce';
|
||||||
export { default as useTextarea } from './useTextarea';
|
export { default as useTextarea } from './useTextarea';
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import store from '~/store';
|
|
||||||
|
/** 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.
|
* Utility function to determine if a command should trigger.
|
||||||
|
@ -23,10 +29,6 @@ const shouldTriggerCommand = (
|
||||||
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
|
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
|
||||||
|
|
||||||
const shouldTrigger = isAtStart || isPrecededBySpace;
|
const shouldTrigger = isAtStart || isPrecededBySpace;
|
||||||
if (shouldTrigger) {
|
|
||||||
// Blurring helps prevent the command from firing twice.
|
|
||||||
textAreaRef.current.blur();
|
|
||||||
}
|
|
||||||
return shouldTrigger;
|
return shouldTrigger;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,33 +36,32 @@ const shouldTriggerCommand = (
|
||||||
* Custom hook for handling key up events with command triggers.
|
* Custom hook for handling key up events with command triggers.
|
||||||
*/
|
*/
|
||||||
const useHandleKeyUp = ({
|
const useHandleKeyUp = ({
|
||||||
index,
|
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
|
setShowPlusPopover,
|
||||||
|
setShowMentionPopover,
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
|
||||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
setShowPlusPopover: SetterOrUpdater<boolean>;
|
||||||
|
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||||
}) => {
|
}) => {
|
||||||
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
|
|
||||||
|
|
||||||
const handleAtCommand = useCallback(() => {
|
const handleAtCommand = useCallback(() => {
|
||||||
if (shouldTriggerCommand(textAreaRef, '@')) {
|
if (shouldTriggerCommand(textAreaRef, '@')) {
|
||||||
setShowMentionPopover(true);
|
setShowMentionPopover(true);
|
||||||
}
|
}
|
||||||
}, [textAreaRef, setShowMentionPopover]);
|
}, [textAreaRef, setShowMentionPopover]);
|
||||||
|
|
||||||
// const handlePlusCommand = useCallback(() => {
|
const handlePlusCommand = useCallback(() => {
|
||||||
// if (shouldTriggerCommand(textAreaRef, '+')) {
|
if (shouldTriggerCommand(textAreaRef, '+')) {
|
||||||
// console.log('+ command triggered');
|
setShowPlusPopover(true);
|
||||||
// }
|
}
|
||||||
// }, [textAreaRef]);
|
}, [textAreaRef, setShowPlusPopover]);
|
||||||
|
|
||||||
const commandHandlers = useMemo(
|
const commandHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
'@': handleAtCommand,
|
'@': handleAtCommand,
|
||||||
// '+': handlePlusCommand,
|
'+': handlePlusCommand,
|
||||||
}),
|
}),
|
||||||
[handleAtCommand],
|
[handleAtCommand, handlePlusCommand],
|
||||||
// [handleAtCommand, handlePlusCommand],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,7 +74,7 @@ const useHandleKeyUp = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
if (invalidKeys[event.key]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,12 @@ import {
|
||||||
useGetStartupConfig,
|
useGetStartupConfig,
|
||||||
useGetEndpointsQuery,
|
useGetEndpointsQuery,
|
||||||
} from 'librechat-data-provider/react-query';
|
} 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 { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
import type { MentionOption } from '~/common';
|
import type { MentionOption } from '~/common';
|
||||||
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
|
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: presets } = useGetPresetsQuery();
|
||||||
const { data: modelsConfig } = useGetModelsQuery();
|
const { data: modelsConfig } = useGetModelsQuery();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
@ -85,6 +96,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
);
|
);
|
||||||
|
|
||||||
const options: MentionOption[] = useMemo(() => {
|
const options: MentionOption[] = useMemo(() => {
|
||||||
|
let validEndpoints = endpoints;
|
||||||
|
if (!includeAssistants) {
|
||||||
|
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
|
||||||
|
}
|
||||||
const mentions = [
|
const mentions = [
|
||||||
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
||||||
value: modelSpec.name,
|
value: modelSpec.name,
|
||||||
|
@ -101,7 +116,7 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
}),
|
}),
|
||||||
type: 'modelSpec' as const,
|
type: 'modelSpec' as const,
|
||||||
})),
|
})),
|
||||||
...(interfaceConfig.endpointsMenu ? endpoints : []).map((endpoint) => ({
|
...(interfaceConfig.endpointsMenu ? validEndpoints : []).map((endpoint) => ({
|
||||||
value: endpoint,
|
value: endpoint,
|
||||||
label: alternateName[endpoint] ?? endpoint ?? '',
|
label: alternateName[endpoint] ?? endpoint ?? '',
|
||||||
type: 'endpoint' as const,
|
type: 'endpoint' as const,
|
||||||
|
@ -112,10 +127,10 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
size: 20,
|
size: 20,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
...(endpointsConfig?.[EModelEndpoint.assistants]
|
...(endpointsConfig?.[EModelEndpoint.assistants] && includeAssistants
|
||||||
? assistantListMap[EModelEndpoint.assistants] || []
|
? assistantListMap[EModelEndpoint.assistants] || []
|
||||||
: []),
|
: []),
|
||||||
...(endpointsConfig?.[EModelEndpoint.azureAssistants]
|
...(endpointsConfig?.[EModelEndpoint.azureAssistants] && includeAssistants
|
||||||
? assistantListMap[EModelEndpoint.azureAssistants] || []
|
? assistantListMap[EModelEndpoint.azureAssistants] || []
|
||||||
: []),
|
: []),
|
||||||
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
|
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
|
||||||
|
@ -142,6 +157,7 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
||||||
assistantMap,
|
assistantMap,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
assistantListMap,
|
assistantListMap,
|
||||||
|
includeAssistants,
|
||||||
interfaceConfig.presets,
|
interfaceConfig.presets,
|
||||||
interfaceConfig.endpointsMenu,
|
interfaceConfig.endpointsMenu,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -8,25 +8,26 @@ import type {
|
||||||
TAssistantsMap,
|
TAssistantsMap,
|
||||||
TEndpointsConfig,
|
TEndpointsConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { MentionOption } from '~/common';
|
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||||
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools } from '~/utils';
|
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools } from '~/utils';
|
||||||
import { useDefaultConvo, useNewConvo } from '~/hooks';
|
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
import { useDefaultConvo } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function useSelectMention({
|
export default function useSelectMention({
|
||||||
presets,
|
presets,
|
||||||
modelSpecs,
|
modelSpecs,
|
||||||
endpointsConfig,
|
|
||||||
assistantMap,
|
assistantMap,
|
||||||
|
endpointsConfig,
|
||||||
|
newConversation,
|
||||||
}: {
|
}: {
|
||||||
presets?: TPreset[];
|
presets?: TPreset[];
|
||||||
modelSpecs: TModelSpec[];
|
modelSpecs: TModelSpec[];
|
||||||
endpointsConfig: TEndpointsConfig;
|
|
||||||
assistantMap: TAssistantsMap;
|
assistantMap: TAssistantsMap;
|
||||||
|
newConversation: ConvoGenerator;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
}) {
|
}) {
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const { newConversation } = useNewConvo();
|
|
||||||
const getDefaultConversation = useDefaultConvo();
|
const getDefaultConversation = useDefaultConvo();
|
||||||
const modularChat = useRecoilValue(store.modularChat);
|
const modularChat = useRecoilValue(store.modularChat);
|
||||||
const availableTools = useRecoilValue(store.availableTools);
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
|
@ -45,12 +46,12 @@ export default function useSelectMention({
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
template,
|
||||||
shouldSwitch,
|
shouldSwitch,
|
||||||
isNewModular,
|
isNewModular,
|
||||||
|
newEndpointType,
|
||||||
isCurrentModular,
|
isCurrentModular,
|
||||||
isExistingConversation,
|
isExistingConversation,
|
||||||
newEndpointType,
|
|
||||||
template,
|
|
||||||
} = getConvoSwitchLogic({
|
} = getConvoSwitchLogic({
|
||||||
newEndpoint,
|
newEndpoint,
|
||||||
modularChat,
|
modularChat,
|
||||||
|
@ -58,7 +59,8 @@ export default function useSelectMention({
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||||
|
if (isExistingConversation && isModular) {
|
||||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||||
|
|
||||||
const currentConvo = getDefaultConversation({
|
const currentConvo = getDefaultConversation({
|
||||||
|
@ -68,11 +70,20 @@ export default function useSelectMention({
|
||||||
});
|
});
|
||||||
|
|
||||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newConversation({ template: { ...(template as Partial<TConversation>) }, preset });
|
newConversation({
|
||||||
|
template: { ...(template as Partial<TConversation>) },
|
||||||
|
preset,
|
||||||
|
keepAddedConvos: isModular,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
|
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig],
|
||||||
);
|
);
|
||||||
|
@ -142,12 +153,12 @@ export default function useSelectMention({
|
||||||
const newEndpoint = newPreset.endpoint ?? '';
|
const newEndpoint = newPreset.endpoint ?? '';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
template,
|
||||||
shouldSwitch,
|
shouldSwitch,
|
||||||
isNewModular,
|
isNewModular,
|
||||||
|
newEndpointType,
|
||||||
isCurrentModular,
|
isCurrentModular,
|
||||||
isExistingConversation,
|
isExistingConversation,
|
||||||
newEndpointType,
|
|
||||||
template,
|
|
||||||
} = getConvoSwitchLogic({
|
} = getConvoSwitchLogic({
|
||||||
newEndpoint,
|
newEndpoint,
|
||||||
modularChat,
|
modularChat,
|
||||||
|
@ -155,7 +166,8 @@ export default function useSelectMention({
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
const isModular = isCurrentModular && isNewModular && shouldSwitch;
|
||||||
|
if (isExistingConversation && isModular) {
|
||||||
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||||
|
|
||||||
const currentConvo = getDefaultConversation({
|
const currentConvo = getDefaultConversation({
|
||||||
|
@ -165,19 +177,24 @@ export default function useSelectMention({
|
||||||
});
|
});
|
||||||
|
|
||||||
/* We don't reset the latest message, only when changing settings mid-converstion */
|
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newConversation({ preset: newPreset });
|
newConversation({ preset: newPreset, keepAddedConvos: true });
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
availableTools,
|
|
||||||
conversation,
|
|
||||||
getDefaultConversation,
|
|
||||||
modularChat,
|
modularChat,
|
||||||
|
conversation,
|
||||||
|
availableTools,
|
||||||
newConversation,
|
newConversation,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
|
getDefaultConversation,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export { default as useAvatar } from './useAvatar';
|
export { default as useAvatar } from './useAvatar';
|
||||||
export { default as useProgress } from './useProgress';
|
export { default as useProgress } from './useProgress';
|
||||||
export { default as useSubmitMessage } from './useSubmitMessage';
|
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 useMessageHelpers } from './useMessageHelpers';
|
||||||
export { default as useCopyToClipboard } from './useCopyToClipboard';
|
export { default as useCopyToClipboard } from './useCopyToClipboard';
|
||||||
export { default as useMessageScrolling } from './useMessageScrolling';
|
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 { createAvatar } from '@dicebear/core';
|
||||||
import { initials } from '@dicebear/collection';
|
import { initials } from '@dicebear/collection';
|
||||||
import type { TUser } from 'librechat-data-provider';
|
import type { TUser } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
const avatarCache: Record<string, string> = {};
|
||||||
|
|
||||||
const useAvatar = (user: TUser | undefined) => {
|
const useAvatar = (user: TUser | undefined) => {
|
||||||
const [avatarSrc, setAvatarSrc] = useState('');
|
return useMemo(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (avatarSrc.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user?.avatar) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user?.username) {
|
if (!user?.username) {
|
||||||
return;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateAvatar = async () => {
|
if (user.avatar) {
|
||||||
if (!user) {
|
return user.avatar;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const { username } = user;
|
const { username } = user;
|
||||||
|
|
||||||
const avatar = createAvatar(initials, {
|
if (avatarCache[username]) {
|
||||||
seed: username,
|
return avatarCache[username];
|
||||||
fontFamily: ['Verdana'],
|
}
|
||||||
fontSize: 36,
|
|
||||||
|
const avatar = createAvatar(initials, {
|
||||||
|
seed: username,
|
||||||
|
fontFamily: ['Verdana'],
|
||||||
|
fontSize: 36,
|
||||||
|
});
|
||||||
|
|
||||||
|
let avatarDataUri = '';
|
||||||
|
avatar
|
||||||
|
.toDataUri()
|
||||||
|
.then((dataUri) => {
|
||||||
|
avatarDataUri = dataUri;
|
||||||
|
avatarCache[username] = dataUri; // Store in cache
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to generate avatar:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
return avatarDataUri;
|
||||||
const avatarDataUri = await avatar.toDataUri();
|
}, [user]);
|
||||||
setAvatarSrc(avatarDataUri);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate avatar:', error);
|
|
||||||
setAvatarSrc('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateAvatar();
|
|
||||||
}, [user, avatarSrc.length]);
|
|
||||||
|
|
||||||
return avatarSrc;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useAvatar;
|
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 { useCallback } from 'react';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { useChatContext, useChatFormContext } from '~/Providers';
|
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { replaceSpecialVars } from '~/utils';
|
import { replaceSpecialVars } from '~/utils';
|
||||||
import store from '~/store';
|
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 }) {
|
export default function useSubmitMessage(helpers?: { clearDraft?: () => void }) {
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const methods = useChatFormContext();
|
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 autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||||
|
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
||||||
|
|
||||||
const submitMessage = useCallback(
|
const submitMessage = useCallback(
|
||||||
|
@ -17,11 +29,52 @@ export default function useSubmitMessage(helpers?: { clearDraft?: () => void })
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return console.warn('No data provided to submitMessage');
|
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();
|
methods.reset();
|
||||||
helpers?.clearDraft && helpers.clearDraft();
|
helpers?.clearDraft && helpers.clearDraft();
|
||||||
},
|
},
|
||||||
[ask, methods, helpers],
|
[
|
||||||
|
ask,
|
||||||
|
methods,
|
||||||
|
helpers,
|
||||||
|
addedIndex,
|
||||||
|
addedConvo,
|
||||||
|
setMessages,
|
||||||
|
getMessages,
|
||||||
|
activeConvos,
|
||||||
|
askAdditional,
|
||||||
|
latestMessage,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitPrompt = useCallback(
|
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 { v4 } from 'uuid';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import {
|
import {
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
SSE,
|
SSE,
|
||||||
QueryKeys,
|
|
||||||
Constants,
|
|
||||||
EndpointURLs,
|
|
||||||
createPayload,
|
createPayload,
|
||||||
tPresetSchema,
|
|
||||||
tMessageSchema,
|
|
||||||
LocalStorageKeys,
|
|
||||||
tConvoUpdateSchema,
|
|
||||||
removeNullishValues,
|
removeNullishValues,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type {
|
import type { TSubmission } from 'librechat-data-provider';
|
||||||
TResPlugin,
|
import type { EventHandlerParams } from './useEventHandlers';
|
||||||
TMessage,
|
import type { TResData } from '~/common';
|
||||||
TConversation,
|
|
||||||
TSubmission,
|
|
||||||
ConversationData,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import {
|
|
||||||
addConversation,
|
|
||||||
deleteConversation,
|
|
||||||
updateConversation,
|
|
||||||
getConversationById,
|
|
||||||
} from '~/utils';
|
|
||||||
import { useGenTitleMutation } from '~/data-provider';
|
import { useGenTitleMutation } from '~/data-provider';
|
||||||
import useContentHandler from './useContentHandler';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { useAuthContext } from '../AuthContext';
|
import useEventHandlers from './useEventHandlers';
|
||||||
import useChatHelpers from '../useChatHelpers';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
type TResData = {
|
type ChatHelpers = Pick<
|
||||||
plugin?: TResPlugin;
|
EventHandlerParams,
|
||||||
final?: boolean;
|
| 'setMessages'
|
||||||
initial?: boolean;
|
| 'getMessages'
|
||||||
previousMessages?: TMessage[];
|
| 'setConversation'
|
||||||
requestMessage: TMessage;
|
| 'setIsSubmitting'
|
||||||
responseMessage: TMessage;
|
| 'newConversation'
|
||||||
conversation: TConversation;
|
| 'resetLatestMessage'
|
||||||
conversationId?: string;
|
>;
|
||||||
runMessages?: TMessage[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type TSyncData = {
|
export default function useSSE(
|
||||||
sync: boolean;
|
submission: TSubmission | null,
|
||||||
thread_id: string;
|
chatHelpers: ChatHelpers,
|
||||||
messages?: TMessage[];
|
isAddedRequest = false,
|
||||||
requestMessage: TMessage;
|
runIndex = 0,
|
||||||
responseMessage: TMessage;
|
) {
|
||||||
conversationId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useSSE(submission: TSubmission | null, index = 0) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const genTitle = useGenTitleMutation();
|
const genTitle = useGenTitleMutation();
|
||||||
const setActiveRunId = useSetRecoilState(store.activeRunFamily(index));
|
const setActiveRunId = useSetRecoilState(store.activeRunFamily(runIndex));
|
||||||
|
|
||||||
const { conversationId: paramId } = useParams();
|
|
||||||
const { token, isAuthenticated } = useAuthContext();
|
const { token, isAuthenticated } = useAuthContext();
|
||||||
const [completed, setCompleted] = useState(new Set());
|
const [completed, setCompleted] = useState(new Set());
|
||||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(runIndex));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setMessages,
|
setMessages,
|
||||||
|
@ -75,435 +47,34 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
newConversation,
|
newConversation,
|
||||||
resetLatestMessage,
|
resetLatestMessage,
|
||||||
} = useChatHelpers(index, paramId);
|
} = chatHelpers;
|
||||||
const contentHandler = useContentHandler({ setMessages, getMessages });
|
|
||||||
|
const {
|
||||||
|
syncHandler,
|
||||||
|
finalHandler,
|
||||||
|
errorHandler,
|
||||||
|
messageHandler,
|
||||||
|
contentHandler,
|
||||||
|
createdHandler,
|
||||||
|
abortConversation,
|
||||||
|
} = useEventHandlers({
|
||||||
|
genTitle,
|
||||||
|
setMessages,
|
||||||
|
getMessages,
|
||||||
|
setCompleted,
|
||||||
|
isAddedRequest,
|
||||||
|
setConversation,
|
||||||
|
setIsSubmitting,
|
||||||
|
newConversation,
|
||||||
|
setShowStopButton,
|
||||||
|
resetLatestMessage,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const balanceQuery = useGetUserBalance({
|
const balanceQuery = useGetUserBalance({
|
||||||
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
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(() => {
|
useEffect(() => {
|
||||||
if (submission === null || Object.keys(submission).length === 0) {
|
if (submission === null || Object.keys(submission).length === 0) {
|
||||||
return;
|
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.onopen = () => console.log('connection is opened');
|
||||||
|
|
||||||
events.oncancel = async () => {
|
events.oncancel = async () => {
|
||||||
|
@ -595,9 +156,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setCompleted((prev) => new Set(prev.add(streamKey)));
|
setCompleted((prev) => new Set(prev.add(streamKey)));
|
||||||
|
const latestMessages = getMessages();
|
||||||
|
const conversationId = latestMessages?.[latestMessages?.length - 1]?.conversationId;
|
||||||
return await abortConversation(
|
return await abortConversation(
|
||||||
userMessage?.conversationId ?? submission?.conversationId,
|
conversationId ?? userMessage?.conversationId ?? submission?.conversationId,
|
||||||
submission,
|
submission,
|
||||||
|
latestMessages,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './Assistants';
|
export * from './Assistants';
|
||||||
|
export * from './Chat';
|
||||||
export * from './Config';
|
export * from './Config';
|
||||||
export * from './Conversations';
|
export * from './Conversations';
|
||||||
export * from './Nav';
|
export * from './Nav';
|
||||||
|
@ -19,11 +20,10 @@ export { default as useTimeout } from './useTimeout';
|
||||||
export { default as useNewConvo } from './useNewConvo';
|
export { default as useNewConvo } from './useNewConvo';
|
||||||
export { default as useLocalize } from './useLocalize';
|
export { default as useLocalize } from './useLocalize';
|
||||||
export { default as useMediaQuery } from './useMediaQuery';
|
export { default as useMediaQuery } from './useMediaQuery';
|
||||||
export { default as useChatHelpers } from './useChatHelpers';
|
|
||||||
export { default as useScrollToRef } from './useScrollToRef';
|
export { default as useScrollToRef } from './useScrollToRef';
|
||||||
export { default as useLocalStorage } from './useLocalStorage';
|
export { default as useLocalStorage } from './useLocalStorage';
|
||||||
export { default as useDelayedRender } from './useDelayedRender';
|
export { default as useDelayedRender } from './useDelayedRender';
|
||||||
export { default as useOnClickOutside } from './useOnClickOutside';
|
export { default as useOnClickOutside } from './useOnClickOutside';
|
||||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
|
||||||
export { default as useSpeechToText } from './Input/useSpeechToText';
|
export { default as useSpeechToText } from './Input/useSpeechToText';
|
||||||
export { default as useTextToSpeech } from './Input/useTextToSpeech';
|
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';
|
} from 'librechat-data-provider/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import {
|
import { useRecoilState, useRecoilValue, useSetRecoilState, useRecoilCallback } from 'recoil';
|
||||||
useRecoilState,
|
|
||||||
useRecoilValue,
|
|
||||||
useSetRecoilState,
|
|
||||||
useRecoilCallback,
|
|
||||||
useResetRecoilState,
|
|
||||||
} from 'recoil';
|
|
||||||
import type {
|
import type {
|
||||||
TPreset,
|
TPreset,
|
||||||
TSubmission,
|
TSubmission,
|
||||||
|
@ -38,12 +32,14 @@ import store from '~/store';
|
||||||
const useNewConvo = (index = 0) => {
|
const useNewConvo = (index = 0) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const clearAllConversations = store.useClearConvoState();
|
||||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||||
|
const clearAllLatestMessages = store.useClearLatestMessages();
|
||||||
const { setConversation } = store.useCreateConversationAtom(index);
|
const { setConversation } = store.useCreateConversationAtom(index);
|
||||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||||
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
|
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
|
||||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
|
||||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
|
|
||||||
const modelsQuery = useGetModelsQuery();
|
const modelsQuery = useGetModelsQuery();
|
||||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||||
const assistantsListMap = useAssistantListMap();
|
const assistantsListMap = useAssistantListMap();
|
||||||
|
@ -67,6 +63,7 @@ const useNewConvo = (index = 0) => {
|
||||||
modelsData?: TModelsConfig,
|
modelsData?: TModelsConfig,
|
||||||
buildDefault?: boolean,
|
buildDefault?: boolean,
|
||||||
keepLatestMessage?: boolean,
|
keepLatestMessage?: boolean,
|
||||||
|
keepAddedConvos?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const modelsConfig = modelsData ?? modelsQuery.data;
|
const modelsConfig = modelsData ?? modelsQuery.data;
|
||||||
const { endpoint = null } = conversation;
|
const { endpoint = null } = conversation;
|
||||||
|
@ -139,10 +136,13 @@ const useNewConvo = (index = 0) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!keepAddedConvos) {
|
||||||
|
clearAllConversations(true);
|
||||||
|
}
|
||||||
setConversation(conversation);
|
setConversation(conversation);
|
||||||
setSubmission({} as TSubmission);
|
setSubmission({} as TSubmission);
|
||||||
if (!keepLatestMessage) {
|
if (!keepLatestMessage) {
|
||||||
resetLatestMessage();
|
clearAllLatestMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversation.conversationId === 'new' && !modelsData) {
|
if (conversation.conversationId === 'new' && !modelsData) {
|
||||||
|
@ -171,12 +171,14 @@ const useNewConvo = (index = 0) => {
|
||||||
modelsData,
|
modelsData,
|
||||||
buildDefault = true,
|
buildDefault = true,
|
||||||
keepLatestMessage = false,
|
keepLatestMessage = false,
|
||||||
|
keepAddedConvos = false,
|
||||||
}: {
|
}: {
|
||||||
template?: Partial<TConversation>;
|
template?: Partial<TConversation>;
|
||||||
preset?: Partial<TPreset>;
|
preset?: Partial<TPreset>;
|
||||||
modelsData?: TModelsConfig;
|
modelsData?: TModelsConfig;
|
||||||
buildDefault?: boolean;
|
buildDefault?: boolean;
|
||||||
keepLatestMessage?: boolean;
|
keepLatestMessage?: boolean;
|
||||||
|
keepAddedConvos?: boolean;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
pauseGlobalAudio();
|
pauseGlobalAudio();
|
||||||
|
|
||||||
|
@ -217,7 +219,14 @@ const useNewConvo = (index = 0) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToConversation(conversation, preset, modelsData, buildDefault, keepLatestMessage);
|
switchToConversation(
|
||||||
|
conversation,
|
||||||
|
preset,
|
||||||
|
modelsData,
|
||||||
|
buildDefault,
|
||||||
|
keepLatestMessage,
|
||||||
|
keepAddedConvos,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
pauseGlobalAudio,
|
pauseGlobalAudio,
|
||||||
|
|
|
@ -150,6 +150,7 @@ export default {
|
||||||
com_ui_entries: 'Entries',
|
com_ui_entries: 'Entries',
|
||||||
com_ui_pay_per_call: 'All AI conversations in one place. Pay per call and not per month',
|
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_new_footer: 'All AI conversations in one place.',
|
||||||
|
com_ui_latest_footer: 'Every AI for Everyone.',
|
||||||
com_ui_enter: 'Enter',
|
com_ui_enter: 'Enter',
|
||||||
com_ui_submit: 'Submit',
|
com_ui_submit: 'Submit',
|
||||||
com_ui_none_selected: 'None selected',
|
com_ui_none_selected: 'None selected',
|
||||||
|
@ -198,6 +199,7 @@ export default {
|
||||||
com_ui_fork_visible: 'Visible messages only',
|
com_ui_fork_visible: 'Visible messages only',
|
||||||
com_ui_fork_from_message: 'Select a fork option',
|
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_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_regenerate: 'Regenerate',
|
||||||
com_ui_continue: 'Continue',
|
com_ui_continue: 'Continue',
|
||||||
com_ui_edit: 'Edit',
|
com_ui_edit: 'Edit',
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {
|
import {
|
||||||
atom,
|
atom,
|
||||||
atomFamily,
|
|
||||||
selector,
|
selector,
|
||||||
|
atomFamily,
|
||||||
|
selectorFamily,
|
||||||
useRecoilState,
|
useRecoilState,
|
||||||
useRecoilValue,
|
useRecoilValue,
|
||||||
useSetRecoilState,
|
useSetRecoilState,
|
||||||
|
@ -10,9 +11,53 @@ import {
|
||||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
|
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
|
||||||
import type { TOptionSettings, ExtendedFile } from '~/common';
|
import type { TOptionSettings, ExtendedFile } from '~/common';
|
||||||
import { storeEndpointSettings } from '~/utils';
|
import { storeEndpointSettings, logger } from '~/utils';
|
||||||
import { useEffect } from 'react';
|
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>({
|
const conversationByIndex = atomFamily<TConversation | null, string | number>({
|
||||||
key: 'conversationByIndex',
|
key: 'conversationByIndex',
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -41,7 +86,10 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
|
||||||
}
|
}
|
||||||
|
|
||||||
storeEndpointSettings(newValue);
|
storeEndpointSettings(newValue);
|
||||||
localStorage.setItem(LocalStorageKeys.LAST_CONVO_SETUP, JSON.stringify(newValue));
|
localStorage.setItem(
|
||||||
|
`${LocalStorageKeys.LAST_CONVO_SETUP}_${index}`,
|
||||||
|
JSON.stringify(newValue),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
] as const,
|
] as const,
|
||||||
|
@ -70,11 +118,6 @@ const presetByIndex = atomFamily<TPreset | null, string | number>({
|
||||||
default: null,
|
default: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
|
|
||||||
key: 'submissionByIndex',
|
|
||||||
default: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const textByIndex = atomFamily<string, string | number>({
|
const textByIndex = atomFamily<string, string | number>({
|
||||||
key: 'textByIndex',
|
key: 'textByIndex',
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -125,6 +168,11 @@ const showMentionPopoverFamily = atomFamily<boolean, string | number | null>({
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showPlusPopoverFamily = atomFamily<boolean, string | number | null>({
|
||||||
|
key: 'showPlusPopoverByIndex',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
|
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
|
||||||
key: 'globalAudioURLByIndex',
|
key: 'globalAudioURLByIndex',
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -150,11 +198,6 @@ const audioRunFamily = atomFamily<string | null, string | number | null>({
|
||||||
default: null,
|
default: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
|
|
||||||
key: 'latestMessageByIndex',
|
|
||||||
default: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
function useCreateConversationAtom(key: string | number) {
|
function useCreateConversationAtom(key: string | number) {
|
||||||
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
|
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
|
||||||
const setConversation = useSetRecoilState(conversationByIndex(key));
|
const setConversation = useSetRecoilState(conversationByIndex(key));
|
||||||
|
@ -170,12 +213,17 @@ function useCreateConversationAtom(key: string | number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useClearConvoState() {
|
function useClearConvoState() {
|
||||||
|
/** Clears all active conversations. Pass `true` to skip the first or root conversation */
|
||||||
const clearAllConversations = useRecoilCallback(
|
const clearAllConversations = useRecoilCallback(
|
||||||
({ reset, snapshot }) =>
|
({ reset, snapshot }) =>
|
||||||
async () => {
|
async (skipFirst?: boolean) => {
|
||||||
const conversationKeys = await snapshot.getPromise(conversationKeysAtom);
|
const conversationKeys = await snapshot.getPromise(conversationKeysAtom);
|
||||||
|
|
||||||
for (const conversationKey of conversationKeys) {
|
for (const conversationKey of conversationKeys) {
|
||||||
|
if (skipFirst && conversationKey == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
reset(conversationByIndex(conversationKey));
|
reset(conversationByIndex(conversationKey));
|
||||||
|
|
||||||
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
|
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
|
||||||
|
@ -192,6 +240,64 @@ function useClearConvoState() {
|
||||||
return clearAllConversations;
|
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 {
|
export default {
|
||||||
conversationByIndex,
|
conversationByIndex,
|
||||||
filesByIndex,
|
filesByIndex,
|
||||||
|
@ -207,6 +313,7 @@ export default {
|
||||||
showPopoverFamily,
|
showPopoverFamily,
|
||||||
latestMessageFamily,
|
latestMessageFamily,
|
||||||
allConversationsSelector,
|
allConversationsSelector,
|
||||||
|
conversationByKeySelector,
|
||||||
useClearConvoState,
|
useClearConvoState,
|
||||||
useCreateConversationAtom,
|
useCreateConversationAtom,
|
||||||
showMentionPopoverFamily,
|
showMentionPopoverFamily,
|
||||||
|
@ -215,5 +322,8 @@ export default {
|
||||||
audioRunFamily,
|
audioRunFamily,
|
||||||
globalAudioPlayingFamily,
|
globalAudioPlayingFamily,
|
||||||
globalAudioFetchingFamily,
|
globalAudioFetchingFamily,
|
||||||
|
showPlusPopoverFamily,
|
||||||
activePromptByIndex,
|
activePromptByIndex,
|
||||||
|
useClearSubmissionState,
|
||||||
|
useClearLatestMessages,
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,7 @@ html {
|
||||||
--text-tertiary:var(--gray-500);
|
--text-tertiary:var(--gray-500);
|
||||||
--surface-primary:var(--white);
|
--surface-primary:var(--white);
|
||||||
--surface-primary-alt:var(--white);
|
--surface-primary-alt:var(--white);
|
||||||
|
--surface-primary-contrast:var(--gray-100);
|
||||||
--surface-secondary:var(--gray-50);
|
--surface-secondary:var(--gray-50);
|
||||||
--surface-tertiary:var(--gray-100);
|
--surface-tertiary:var(--gray-100);
|
||||||
--border-light:var(--gray-100);
|
--border-light:var(--gray-100);
|
||||||
|
@ -41,8 +42,9 @@ html {
|
||||||
--text-secondary:var(--gray-300);
|
--text-secondary:var(--gray-300);
|
||||||
--text-tertiary:var(--gray-500);
|
--text-tertiary:var(--gray-500);
|
||||||
--surface-primary:var(--gray-900);
|
--surface-primary:var(--gray-900);
|
||||||
--surface-secondary:var(--gray-800);
|
|
||||||
--surface-primary-alt:var(--gray-850);
|
--surface-primary-alt:var(--gray-850);
|
||||||
|
--surface-primary-contrast:var(--gray-850);
|
||||||
|
--surface-secondary:var(--gray-800);
|
||||||
--surface-tertiary:var(--gray-700);
|
--surface-tertiary:var(--gray-700);
|
||||||
--border-light:var(--gray-700);
|
--border-light:var(--gray-700);
|
||||||
--border-medium-alt:var(--gray-600);
|
--border-medium-alt:var(--gray-600);
|
||||||
|
|
|
@ -1,78 +1,64 @@
|
||||||
import { TFile, TMessage } from 'librechat-data-provider';
|
import { TFile, TMessage } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
type ParentMessage = TMessage & { children: TMessage[]; depth: number };
|
||||||
|
export default function buildTree({
|
||||||
|
messages,
|
||||||
|
fileMap,
|
||||||
|
}: {
|
||||||
|
messages: TMessage[] | null;
|
||||||
|
fileMap?: Record<string, TFile>;
|
||||||
|
}) {
|
||||||
|
if (messages === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageMap: Record<string, ParentMessage> = {};
|
||||||
|
const rootMessages: TMessage[] = [];
|
||||||
|
const childrenCount: Record<string, number> = {};
|
||||||
|
|
||||||
|
messages.forEach((message) => {
|
||||||
|
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) {
|
||||||
|
extendedMessage.files = message.files.map((file) => fileMap[file.file_id ?? ''] ?? file);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageMap[message.messageId] = extendedMessage;
|
||||||
|
|
||||||
|
const parentMessage = messageMap[parentId];
|
||||||
|
if (parentMessage) {
|
||||||
|
parentMessage.children.push(extendedMessage);
|
||||||
|
extendedMessage.depth = parentMessage.depth + 1;
|
||||||
|
} else {
|
||||||
|
rootMessages.push(extendedMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootMessages;
|
||||||
|
}
|
||||||
|
|
||||||
const even =
|
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';
|
'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 =
|
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';
|
'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';
|
||||||
|
|
||||||
export default function buildTree({
|
export function groupIntoList({
|
||||||
messages,
|
messages,
|
||||||
fileMap,
|
}: // fileMap,
|
||||||
groupAll = false,
|
{
|
||||||
}: {
|
|
||||||
messages: TMessage[] | null;
|
messages: TMessage[] | null;
|
||||||
fileMap?: Record<string, TFile>;
|
// fileMap?: Record<string, TFile>;
|
||||||
groupAll?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
if (messages === null) {
|
if (messages === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return messages.map((m, idx) => ({ ...m, bg: idx % 2 === 0 ? even : odd }));
|
||||||
const messageMap: Record<string, TMessage & { children: TMessage[] }> = {};
|
|
||||||
const rootMessages: TMessage[] = [];
|
|
||||||
|
|
||||||
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: [] };
|
|
||||||
|
|
||||||
if (message.files && fileMap) {
|
|
||||||
messageMap[message.messageId].files = message.files.map(
|
|
||||||
(file) => fileMap[file.file_id ?? ''] ?? file,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentMessage = messageMap[message.parentMessageId ?? ''];
|
|
||||||
if (parentMessage) {
|
|
||||||
parentMessage.children.push(messageMap[message.messageId]);
|
|
||||||
} else {
|
|
||||||
rootMessages.push(messageMap[message.messageId]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* Ensures the last selected model stays up to date, as conversation may
|
||||||
* update without updating last convo setup when same endpoint */
|
* update without updating last convo setup when same endpoint */
|
||||||
|
@ -98,13 +100,11 @@ export function updateLastSelectedModel({
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastConversationSetup = JSON.parse(
|
const lastConversationSetup = JSON.parse(localStorage.getItem(firstLocalConvoKey) || '{}');
|
||||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) || '{}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastConversationSetup.endpoint === endpoint) {
|
if (lastConversationSetup.endpoint === endpoint) {
|
||||||
lastConversationSetup.model = model;
|
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) || '{}');
|
const lastSelectedModels = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) || '{}');
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default function getLocalStorageItems() {
|
||||||
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL),
|
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL),
|
||||||
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS),
|
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS),
|
||||||
lastBingSettings: localStorage.getItem(LocalStorageKeys.LAST_BING),
|
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) : {};
|
const lastSelectedModel = items.lastSelectedModel ? JSON.parse(items.lastSelectedModel) : {};
|
||||||
|
|
|
@ -12,6 +12,7 @@ export * from './endpoints';
|
||||||
export * from './sharedLink';
|
export * from './sharedLink';
|
||||||
export * from './promptGroups';
|
export * from './promptGroups';
|
||||||
export { default as cn } from './cn';
|
export { default as cn } from './cn';
|
||||||
|
export { default as logger } from './logger';
|
||||||
export { default as buildTree } from './buildTree';
|
export { default as buildTree } from './buildTree';
|
||||||
export { default as getLoginError } from './getLoginError';
|
export { default as getLoginError } from './getLoginError';
|
||||||
export { default as cleanupPreset } from './cleanupPreset';
|
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.
|
* Remove the specified 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.
|
* 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 {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) {
|
export function removeCharIfLast(textarea: HTMLTextAreaElement, charToRemove: string) {
|
||||||
if (textarea.value.endsWith('@')) {
|
if (textarea.value.endsWith(charToRemove)) {
|
||||||
textarea.value = textarea.value.slice(0, -1);
|
textarea.value = textarea.value.slice(0, -1);
|
||||||
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
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)',
|
'text-tertiary': 'var(--text-tertiary)',
|
||||||
'surface-primary': 'var(--surface-primary)',
|
'surface-primary': 'var(--surface-primary)',
|
||||||
'surface-primary-alt': 'var(--surface-primary-alt)',
|
'surface-primary-alt': 'var(--surface-primary-alt)',
|
||||||
|
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||||
'surface-secondary': 'var(--surface-secondary)',
|
'surface-secondary': 'var(--surface-secondary)',
|
||||||
'surface-tertiary': 'var(--surface-tertiary)',
|
'surface-tertiary': 'var(--surface-tertiary)',
|
||||||
'border-light': 'var(--border-light)',
|
'border-light': 'var(--border-light)',
|
||||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -29348,7 +29348,7 @@
|
||||||
},
|
},
|
||||||
"packages/data-provider": {
|
"packages/data-provider": {
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"description": "data services for librechat apps",
|
"description": "data services for librechat apps",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.es.js",
|
"module": "dist/index.es.js",
|
||||||
|
|
|
@ -814,10 +814,14 @@ export enum Constants {
|
||||||
CONFIG_VERSION = '1.1.4',
|
CONFIG_VERSION = '1.1.4',
|
||||||
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
||||||
NO_PARENT = '00000000-0000-0000-0000-000000000000',
|
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. */
|
/** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */
|
||||||
ENCODED_DOMAIN_LENGTH = 10,
|
ENCODED_DOMAIN_LENGTH = 10,
|
||||||
/** Identifier for using current_model in multi-model requests. */
|
/** Identifier for using current_model in multi-model requests. */
|
||||||
CURRENT_MODEL = 'current_model',
|
CURRENT_MODEL = 'current_model',
|
||||||
|
/** Common divider for text values */
|
||||||
|
COMMON_DIVIDER = '__',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocalStorageKeys {
|
export enum LocalStorageKeys {
|
||||||
|
|
|
@ -298,6 +298,8 @@ export const useLoginUserMutation = (): UseMutationResult<
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
queryClient.removeQueries();
|
queryClient.removeQueries();
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
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_MODEL);
|
||||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||||
|
|
|
@ -286,6 +286,8 @@ export type TMessage = z.input<typeof tMessageSchema> & {
|
||||||
plugins?: TResPlugin[];
|
plugins?: TResPlugin[];
|
||||||
content?: TMessageContentParts[];
|
content?: TMessageContentParts[];
|
||||||
files?: Partial<TFile>[];
|
files?: Partial<TFile>[];
|
||||||
|
depth?: number;
|
||||||
|
siblingIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {
|
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {
|
||||||
|
|
|
@ -36,6 +36,9 @@ export type TEndpointOption = {
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
/* assistant */
|
/* assistant */
|
||||||
thread_id?: string;
|
thread_id?: string;
|
||||||
|
/* multi-response stream */
|
||||||
|
overrideConvoId?: string;
|
||||||
|
overrideUserMessageId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPayload = Partial<TMessage> &
|
export type TPayload = Partial<TMessage> &
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue