mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 10:20:15 +01:00
🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)
* WIP: first pass ModelSpecs * refactor(onSelectEndpoint): use `getConvoSwitchLogic` * feat: introduce iconURL, greeting, frontend fields for conversations/presets/messages * feat: conversation.iconURL & greeting in Landing * feat: conversation.iconURL & greeting in New Chat button * feat: message.iconURL * refactor: ConversationIcon -> ConvoIconURL * WIP: add spec as a conversation field * refactor: useAppStartup, set spec on initial load for new chat, allow undefined spec, add localStorage keys enum, additional type fields for spec * feat: handle `showIconInMenu`, `showIconInHeader`, undefined `iconURL` and no specs on initial load * chore: handle undefined or empty modelSpecs * WIP: first pass, modelSpec schema for custom config * refactor: move default filtered tools definition to ToolService * feat: pass modelSpecs from backend via startupConfig * refactor: modelSpecs config, return and define list * fix: react error and include iconURL in responseMessage * refactor: add iconURL to responseMessage only * refactor: getIconEndpoint * refactor: pass TSpecsConfig * fix(assistants): differentiate compactAssistantSchema, correctly resets shared conversation state with other endpoints * refactor: assistant id prefix localStorage key * refactor: add more LocalStorageKeys and replace hardcoded values * feat: prioritize spec on new chat behavior: last selected modelSpec behavior (localStorage) * feat: first pass, interface config * chore: WIP, todo: add warnings based on config.modelSpecs settings. * feat: enforce modelSpecs if configured * feat: show config file yaml errors * chore: delete unused legacy Plugins component * refactor: set tools to localStorage from recoil store * chore: add stable recoil setter to useEffect deps * refactor: save tools to conversation documents * style(MultiSelectPop): dynamic height, remove unused import * refactor(react-query): use localstorage keys and pass config to useAvailablePluginsQuery * feat(utils): add mapPlugins * refactor(Convo): use conversation.tools if defined, lastSelectedTools if not * refactor: remove unused legacy code using `useSetOptions`, remove conditional flag `isMultiChat` for using legacy settings * refactor(PluginStoreDialog): add exhaustive-deps which are stable react state setters * fix(HeaderOptions): pass `popover` as true * refactor(useSetStorage): use project enums * refactor: use LocalStorageKeys enum * fix: prevent setConversation from setting falsy values in lastSelectedTools * refactor: use map for availableTools state and available Plugins query * refactor(updateLastSelectedModel): organize logic better and add note on purpose * fix(setAgentOption): prevent reseting last model to secondary model for gptPlugins * refactor(buildDefaultConvo): use enum * refactor: remove `useSetStorage` and consolidate areas where conversation state is saved to localStorage * fix: conversations retain tools on refresh * fix(gptPlugins): prevent nullish tools from being saved * chore: delete useServerStream * refactor: move initial plugins logic to useAppStartup * refactor(MultiSelectDropDown): add more pass-in className props * feat: use tools in presets * chore: delete unused usePresetOptions * refactor: new agentOptions default handling * chore: note * feat: add label and custom instructions to agents * chore: remove 'disabled with tools' message * style: move plugins to 2nd column in parameters * fix: TPreset type for agentOptions * fix: interface controls * refactor: add interfaceConfig, use Separator within Switcher * refactor: hide Assistants panel if interface.parameters are disabled * fix(Header): only modelSpecs if list is greater than 0 * refactor: separate MessageIcon logic from useMessageHelpers for better react rule-following * fix(AppService): don't use reserved keyword 'interface' * feat: set existing Icon for custom endpoints through iconURL * fix(ci): tests passing for App Service * docs: refactor custom_config.md for readability and better organization, also include missing values * docs: interface section and re-organize docs * docs: update modelSpecs info * chore: remove unused files * chore: remove unused files * chore: move useSetIndexOptions * chore: remove unused file * chore: move useConversation(s) * chore: move useDefaultConvo * chore: move useNavigateToConvo * refactor: use plugin install hook so it can be used elsewhere * chore: import order * update docs * refactor(OpenAI/Plugins): allow modelLabel as an initial value for chatGptLabel * chore: remove unused EndpointOptionsPopover and hide 'Save as Preset' button if preset UI visibility disabled * feat(loadDefaultInterface): issue warnings based on values * feat: changelog for custom config file * docs: add additional changelog note * fix: prevent unavailable tool selection from preset and update availableTools on Plugin installations * feat: add `filteredTools` option in custom config * chore: changelog * fix(MessageIcon): always overwrite conversation.iconURL in messageSettings * fix(ModelSpecsMenu): icon edge cases * fix(NewChat): dynamic icon * fix(PluginsClient): always include endpoint in responseMessage * fix: always include endpoint and iconURL in responseMessage across different response methods * feat: interchangeable keys for modelSpec enforcing
This commit is contained in:
parent
a5cac03fa4
commit
0e50c07e3f
130 changed files with 3934 additions and 2973 deletions
|
|
@ -655,6 +655,9 @@ class AnthropicClient extends BaseClient {
|
||||||
promptPrefix: this.options.promptPrefix,
|
promptPrefix: this.options.promptPrefix,
|
||||||
modelLabel: this.options.modelLabel,
|
modelLabel: this.options.modelLabel,
|
||||||
resendFiles: this.options.resendFiles,
|
resendFiles: this.options.resendFiles,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
|
greeting: this.options.greeting,
|
||||||
|
spec: this.options.spec,
|
||||||
...this.modelOptions,
|
...this.modelOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -456,6 +456,8 @@ class BaseClient {
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
text: addSpaceIfNeeded(generation) + completion,
|
text: addSpaceIfNeeded(generation) + completion,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
|
endpoint: this.options.endpoint,
|
||||||
...(this.metadata ?? {}),
|
...(this.metadata ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -525,8 +527,19 @@ class BaseClient {
|
||||||
return _messages;
|
return _messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a message to the database.
|
||||||
|
* @param {TMessage} message
|
||||||
|
* @param {Partial<TConversation>} endpointOptions
|
||||||
|
* @param {string | null} user
|
||||||
|
*/
|
||||||
async saveMessageToDatabase(message, endpointOptions, user = null) {
|
async saveMessageToDatabase(message, endpointOptions, user = null) {
|
||||||
await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
|
await saveMessage({
|
||||||
|
...message,
|
||||||
|
endpoint: this.options.endpoint,
|
||||||
|
unfinished: false,
|
||||||
|
user,
|
||||||
|
});
|
||||||
await saveConvo(user, {
|
await saveConvo(user, {
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
endpoint: this.options.endpoint,
|
endpoint: this.options.endpoint,
|
||||||
|
|
|
||||||
|
|
@ -708,6 +708,9 @@ class GoogleClient extends BaseClient {
|
||||||
return {
|
return {
|
||||||
promptPrefix: this.options.promptPrefix,
|
promptPrefix: this.options.promptPrefix,
|
||||||
modelLabel: this.options.modelLabel,
|
modelLabel: this.options.modelLabel,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
|
greeting: this.options.greeting,
|
||||||
|
spec: this.options.spec,
|
||||||
...this.modelOptions,
|
...this.modelOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,9 @@ class OpenAIClient extends BaseClient {
|
||||||
promptPrefix: this.options.promptPrefix,
|
promptPrefix: this.options.promptPrefix,
|
||||||
resendFiles: this.options.resendFiles,
|
resendFiles: this.options.resendFiles,
|
||||||
imageDetail: this.options.imageDetail,
|
imageDetail: this.options.imageDetail,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
|
greeting: this.options.greeting,
|
||||||
|
spec: this.options.spec,
|
||||||
...this.modelOptions,
|
...this.modelOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,12 @@ class PluginsClient extends OpenAIClient {
|
||||||
return {
|
return {
|
||||||
chatGptLabel: this.options.chatGptLabel,
|
chatGptLabel: this.options.chatGptLabel,
|
||||||
promptPrefix: this.options.promptPrefix,
|
promptPrefix: this.options.promptPrefix,
|
||||||
|
tools: this.options.tools,
|
||||||
...this.modelOptions,
|
...this.modelOptions,
|
||||||
agentOptions: this.agentOptions,
|
agentOptions: this.agentOptions,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
|
greeting: this.options.greeting,
|
||||||
|
spec: this.options.spec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,9 +148,11 @@ class PluginsClient extends OpenAIClient {
|
||||||
signal,
|
signal,
|
||||||
pastMessages,
|
pastMessages,
|
||||||
tools: this.tools,
|
tools: this.tools,
|
||||||
currentDateString: this.currentDateString,
|
|
||||||
verbose: this.options.debug,
|
verbose: this.options.debug,
|
||||||
returnIntermediateSteps: true,
|
returnIntermediateSteps: true,
|
||||||
|
customName: this.options.chatGptLabel,
|
||||||
|
currentDateString: this.currentDateString,
|
||||||
|
customInstructions: this.options.promptPrefix,
|
||||||
callbackManager: CallbackManager.fromHandlers({
|
callbackManager: CallbackManager.fromHandlers({
|
||||||
async handleAgentAction(action, runId) {
|
async handleAgentAction(action, runId) {
|
||||||
handleAction(action, runId, onAgentAction);
|
handleAction(action, runId, onAgentAction);
|
||||||
|
|
@ -304,6 +310,8 @@ class PluginsClient extends OpenAIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseMessage = {
|
const responseMessage = {
|
||||||
|
endpoint: EModelEndpoint.gptPlugins,
|
||||||
|
iconURL: this.options.iconURL,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
conversationId,
|
conversationId,
|
||||||
parentMessageId: userMessage.messageId,
|
parentMessageId: userMessage.messageId,
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,18 @@ const initializeCustomAgent = async ({
|
||||||
tools,
|
tools,
|
||||||
model,
|
model,
|
||||||
pastMessages,
|
pastMessages,
|
||||||
|
customName,
|
||||||
|
customInstructions,
|
||||||
currentDateString,
|
currentDateString,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
|
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
|
||||||
|
if (customName) {
|
||||||
|
prompt = `You are "${customName}".\n${prompt}`;
|
||||||
|
}
|
||||||
|
if (customInstructions) {
|
||||||
|
prompt = `${prompt}\n${customInstructions}`;
|
||||||
|
}
|
||||||
|
|
||||||
const chatPrompt = ChatPromptTemplate.fromMessages([
|
const chatPrompt = ChatPromptTemplate.fromMessages([
|
||||||
new SystemMessagePromptTemplate(prompt),
|
new SystemMessagePromptTemplate(prompt),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ const initializeFunctionsAgent = async ({
|
||||||
tools,
|
tools,
|
||||||
model,
|
model,
|
||||||
pastMessages,
|
pastMessages,
|
||||||
|
customName,
|
||||||
|
customInstructions,
|
||||||
currentDateString,
|
currentDateString,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -24,7 +26,13 @@ const initializeFunctionsAgent = async ({
|
||||||
returnMessages: true,
|
returnMessages: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
|
let prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
|
||||||
|
if (customName) {
|
||||||
|
prefix = `You are "${customName}".\n${prefix}`;
|
||||||
|
}
|
||||||
|
if (customInstructions) {
|
||||||
|
prefix = `${prefix}\n${customInstructions}`;
|
||||||
|
}
|
||||||
|
|
||||||
return await initializeAgentExecutorWithOptions(tools, model, {
|
return await initializeAgentExecutorWithOptions(tools, model, {
|
||||||
agentType: 'openai-functions',
|
agentType: 'openai-functions',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
async saveMessage({
|
async saveMessage({
|
||||||
user,
|
user,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
iconURL,
|
||||||
messageId,
|
messageId,
|
||||||
newMessageId,
|
newMessageId,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
@ -35,6 +36,7 @@ module.exports = {
|
||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
user,
|
user,
|
||||||
|
iconURL,
|
||||||
endpoint,
|
endpoint,
|
||||||
messageId: newMessageId || messageId,
|
messageId: newMessageId || messageId,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ module.exports = {
|
||||||
try {
|
try {
|
||||||
const setter = { $set: {} };
|
const setter = { $set: {} };
|
||||||
const update = { presetId, ...preset };
|
const update = { presetId, ...preset };
|
||||||
|
if (preset.tools && Array.isArray(preset.tools)) {
|
||||||
|
update.tools =
|
||||||
|
preset.tools
|
||||||
|
.map((tool) => tool?.pluginKey ?? tool)
|
||||||
|
.filter((toolName) => typeof toolName === 'string') ?? [];
|
||||||
|
}
|
||||||
if (newPresetId) {
|
if (newPresetId) {
|
||||||
update.presetId = newPresetId;
|
update.presetId = newPresetId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,17 @@ const conversationPreset = {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
stop: { type: [{ type: String }], default: undefined },
|
stop: { type: [{ type: String }], default: undefined },
|
||||||
|
/* UI Components */
|
||||||
|
iconURL: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
greeting: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
tools: { type: [{ type: String }], default: undefined },
|
||||||
};
|
};
|
||||||
|
|
||||||
const agentOptions = {
|
const agentOptions = {
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,10 @@ const messageSchema = mongoose.Schema(
|
||||||
thread_id: {
|
thread_id: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
/* frontend components */
|
||||||
|
iconURL: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ timestamps: true },
|
{ timestamps: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ const getAvailablePluginsController = async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {{ filteredTools: string[] }} */
|
||||||
|
const { filteredTools = [] } = req.app.locals;
|
||||||
|
|
||||||
const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');
|
const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');
|
||||||
|
|
||||||
const jsonData = JSON.parse(pluginManifest);
|
const jsonData = JSON.parse(pluginManifest);
|
||||||
|
|
@ -67,7 +70,10 @@ const getAvailablePluginsController = async (req, res) => {
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const plugins = await addOpenAPISpecs(authenticatedPlugins);
|
|
||||||
|
let plugins = await addOpenAPISpecs(authenticatedPlugins);
|
||||||
|
plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));
|
||||||
|
|
||||||
await cache.set(CacheKeys.PLUGINS, plugins);
|
await cache.set(CacheKeys.PLUGINS, plugins);
|
||||||
res.status(200).json(plugins);
|
res.status(200).json(plugins);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ const createAbortController = (req, res, getAbortData) => {
|
||||||
...responseData,
|
...responseData,
|
||||||
conversationId,
|
conversationId,
|
||||||
finish_reason: 'incomplete',
|
finish_reason: 'incomplete',
|
||||||
|
endpoint: endpointOption.endpoint,
|
||||||
|
iconURL: endpointOption.iconURL,
|
||||||
model: endpointOption.modelOptions.model,
|
model: endpointOption.modelOptions.model,
|
||||||
unfinished: false,
|
unfinished: false,
|
||||||
error: false,
|
error: false,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ const anthropic = require('~/server/services/Endpoints/anthropic');
|
||||||
const openAI = require('~/server/services/Endpoints/openAI');
|
const openAI = require('~/server/services/Endpoints/openAI');
|
||||||
const custom = require('~/server/services/Endpoints/custom');
|
const custom = require('~/server/services/Endpoints/custom');
|
||||||
const google = require('~/server/services/Endpoints/google');
|
const google = require('~/server/services/Endpoints/google');
|
||||||
|
const enforceModelSpec = require('./enforceModelSpec');
|
||||||
|
const { handleError } = require('~/server/utils');
|
||||||
|
|
||||||
const buildFunction = {
|
const buildFunction = {
|
||||||
[EModelEndpoint.openAI]: openAI.buildOptions,
|
[EModelEndpoint.openAI]: openAI.buildOptions,
|
||||||
|
|
@ -21,6 +23,31 @@ const buildFunction = {
|
||||||
async function buildEndpointOption(req, res, next) {
|
async function buildEndpointOption(req, res, next) {
|
||||||
const { endpoint, endpointType } = req.body;
|
const { endpoint, endpointType } = req.body;
|
||||||
const parsedBody = parseConvo({ endpoint, endpointType, conversation: req.body });
|
const parsedBody = parseConvo({ endpoint, endpointType, conversation: req.body });
|
||||||
|
|
||||||
|
if (req.app.locals.modelSpecs?.list && req.app.locals.modelSpecs?.enforce) {
|
||||||
|
/** @type {{ list: TModelSpec[] }}*/
|
||||||
|
const { list } = req.app.locals.modelSpecs;
|
||||||
|
const { spec } = parsedBody;
|
||||||
|
|
||||||
|
if (!spec) {
|
||||||
|
return handleError(res, { text: 'No model spec selected' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentModelSpec = list.find((s) => s.name === spec);
|
||||||
|
if (!currentModelSpec) {
|
||||||
|
return handleError(res, { text: 'Invalid model spec' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoint !== currentModelSpec.preset.endpoint) {
|
||||||
|
return handleError(res, { text: 'Model spec mismatch' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidModelSpec = enforceModelSpec(currentModelSpec, parsedBody);
|
||||||
|
if (!isValidModelSpec) {
|
||||||
|
return handleError(res, { text: 'Model spec mismatch' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.body.endpointOption = buildFunction[endpointType ?? endpoint](
|
req.body.endpointOption = buildFunction[endpointType ?? endpoint](
|
||||||
endpoint,
|
endpoint,
|
||||||
parsedBody,
|
parsedBody,
|
||||||
|
|
|
||||||
47
api/server/middleware/enforceModelSpec.js
Normal file
47
api/server/middleware/enforceModelSpec.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const interchangeableKeys = new Map([
|
||||||
|
['chatGptLabel', ['modelLabel']],
|
||||||
|
['modelLabel', ['chatGptLabel']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to enforce the model spec for a conversation
|
||||||
|
* @param {TModelSpec} modelSpec - The model spec to enforce
|
||||||
|
* @param {TConversation} parsedBody - The parsed body of the conversation
|
||||||
|
* @returns {boolean} - Whether the model spec is enforced
|
||||||
|
*/
|
||||||
|
const enforceModelSpec = (modelSpec, parsedBody) => {
|
||||||
|
for (const [key, value] of Object.entries(modelSpec.preset)) {
|
||||||
|
if (key === 'endpoint') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkMatch(key, value, parsedBody)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there is a match for the given key and value in the parsed body
|
||||||
|
* or any of its interchangeable keys.
|
||||||
|
* @param {string} key
|
||||||
|
* @param {any} value
|
||||||
|
* @param {TConversation} parsedBody
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const checkMatch = (key, value, parsedBody) => {
|
||||||
|
if (parsedBody[key] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interchangeableKeys.has(key)) {
|
||||||
|
return interchangeableKeys
|
||||||
|
.get(key)
|
||||||
|
.some((interchangeableKey) => parsedBody[interchangeableKey] === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = enforceModelSpec;
|
||||||
|
|
@ -14,6 +14,7 @@ router.get('/', async function (req, res) {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
/** @type {TStartupConfig} */
|
||||||
const payload = {
|
const payload = {
|
||||||
appTitle: process.env.APP_TITLE || 'LibreChat',
|
appTitle: process.env.APP_TITLE || 'LibreChat',
|
||||||
socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins,
|
socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins,
|
||||||
|
|
@ -44,7 +45,8 @@ router.get('/', async function (req, res) {
|
||||||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
||||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||||
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
||||||
interface: req.app.locals.interface,
|
interface: req.app.locals.interfaceConfig,
|
||||||
|
modelSpecs: req.app.locals.modelSpecs,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof process.env.CUSTOM_FOOTER === 'string') {
|
if (typeof process.env.CUSTOM_FOOTER === 'string') {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
const {
|
const { FileSources, EModelEndpoint, getConfigDefaults } = require('librechat-data-provider');
|
||||||
FileSources,
|
|
||||||
EModelEndpoint,
|
|
||||||
EImageOutputType,
|
|
||||||
defaultSocialLogins,
|
|
||||||
} = require('librechat-data-provider');
|
|
||||||
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
|
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
|
||||||
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
|
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
|
||||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||||
const handleRateLimits = require('./Config/handleRateLimits');
|
const handleRateLimits = require('./Config/handleRateLimits');
|
||||||
|
const { loadDefaultInterface } = require('./start/interface');
|
||||||
const { azureConfigSetup } = require('./start/azureOpenAI');
|
const { azureConfigSetup } = require('./start/azureOpenAI');
|
||||||
const { loadAndFormatTools } = require('./ToolService');
|
const { loadAndFormatTools } = require('./ToolService');
|
||||||
const paths = require('~/config/paths');
|
const paths = require('~/config/paths');
|
||||||
|
|
@ -22,9 +18,12 @@ const paths = require('~/config/paths');
|
||||||
const AppService = async (app) => {
|
const AppService = async (app) => {
|
||||||
/** @type {TCustomConfig}*/
|
/** @type {TCustomConfig}*/
|
||||||
const config = (await loadCustomConfig()) ?? {};
|
const config = (await loadCustomConfig()) ?? {};
|
||||||
|
const configDefaults = getConfigDefaults();
|
||||||
|
|
||||||
|
const filteredTools = config.filteredTools;
|
||||||
|
const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy;
|
||||||
|
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
|
||||||
|
|
||||||
const fileStrategy = config.fileStrategy ?? FileSources.local;
|
|
||||||
const imageOutputType = config?.imageOutputType ?? EImageOutputType.PNG;
|
|
||||||
process.env.CDN_PROVIDER = fileStrategy;
|
process.env.CDN_PROVIDER = fileStrategy;
|
||||||
|
|
||||||
checkVariables();
|
checkVariables();
|
||||||
|
|
@ -37,24 +36,22 @@ const AppService = async (app) => {
|
||||||
/** @type {Record<string, FunctionTool} */
|
/** @type {Record<string, FunctionTool} */
|
||||||
const availableTools = loadAndFormatTools({
|
const availableTools = loadAndFormatTools({
|
||||||
directory: paths.structuredTools,
|
directory: paths.structuredTools,
|
||||||
filter: new Set([
|
adminFilter: filteredTools,
|
||||||
'ChatTool.js',
|
|
||||||
'CodeSherpa.js',
|
|
||||||
'CodeSherpaTools.js',
|
|
||||||
'E2BTools.js',
|
|
||||||
'extractionChain.js',
|
|
||||||
]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const socialLogins = config?.registration?.socialLogins ?? defaultSocialLogins;
|
const socialLogins =
|
||||||
|
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
|
||||||
|
const interfaceConfig = loadDefaultInterface(config, configDefaults);
|
||||||
|
|
||||||
if (!Object.keys(config).length) {
|
if (!Object.keys(config).length) {
|
||||||
app.locals = {
|
app.locals = {
|
||||||
paths,
|
paths,
|
||||||
fileStrategy,
|
fileStrategy,
|
||||||
socialLogins,
|
socialLogins,
|
||||||
|
filteredTools,
|
||||||
availableTools,
|
availableTools,
|
||||||
imageOutputType,
|
imageOutputType,
|
||||||
|
interfaceConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -85,9 +82,11 @@ const AppService = async (app) => {
|
||||||
paths,
|
paths,
|
||||||
socialLogins,
|
socialLogins,
|
||||||
fileStrategy,
|
fileStrategy,
|
||||||
|
filteredTools,
|
||||||
availableTools,
|
availableTools,
|
||||||
imageOutputType,
|
imageOutputType,
|
||||||
interface: config?.interface,
|
interfaceConfig,
|
||||||
|
modelSpecs: config.modelSpecs,
|
||||||
fileConfig: config?.fileConfig,
|
fileConfig: config?.fileConfig,
|
||||||
secureImageLinks: config?.secureImageLinks,
|
secureImageLinks: config?.secureImageLinks,
|
||||||
...endpointLocals,
|
...endpointLocals,
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,16 @@ describe('AppService', () => {
|
||||||
expect(app.locals).toEqual({
|
expect(app.locals).toEqual({
|
||||||
socialLogins: ['testLogin'],
|
socialLogins: ['testLogin'],
|
||||||
fileStrategy: 'testStrategy',
|
fileStrategy: 'testStrategy',
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
privacyPolicy: undefined,
|
||||||
|
termsOfService: undefined,
|
||||||
|
endpointsMenu: true,
|
||||||
|
modelSelect: true,
|
||||||
|
parameters: true,
|
||||||
|
sidePanel: true,
|
||||||
|
presets: true,
|
||||||
|
}),
|
||||||
|
modelSpecs: undefined,
|
||||||
availableTools: {
|
availableTools: {
|
||||||
ExampleTool: {
|
ExampleTool: {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
|
|
@ -109,7 +119,6 @@ describe('AppService', () => {
|
||||||
},
|
},
|
||||||
paths: expect.anything(),
|
paths: expect.anything(),
|
||||||
imageOutputType: expect.any(String),
|
imageOutputType: expect.any(String),
|
||||||
interface: undefined,
|
|
||||||
fileConfig: undefined,
|
fileConfig: undefined,
|
||||||
secureImageLinks: undefined,
|
secureImageLinks: undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -181,7 +190,6 @@ describe('AppService', () => {
|
||||||
|
|
||||||
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
||||||
directory: expect.anything(),
|
directory: expect.anything(),
|
||||||
filter: expect.anything(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(app.locals.availableTools.ExampleTool).toBeDefined();
|
expect(app.locals.availableTools.ExampleTool).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ async function loadCustomConfig() {
|
||||||
i === 0 && i++;
|
i === 0 && i++;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customConfig.reason || customConfig.stack) {
|
||||||
|
i === 0 && logger.error('Config file YAML format is invalid:', customConfig);
|
||||||
|
i === 0 && i++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof customConfig === 'string') {
|
if (typeof customConfig === 'string') {
|
||||||
|
|
@ -84,6 +90,10 @@ Please specify a correct \`imageOutputType\` value (case-sensitive).
|
||||||
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
|
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.data.modelSpecs) {
|
||||||
|
customConfig.modelSpecs = result.data.modelSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
return customConfig;
|
return customConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
const buildOptions = (endpoint, parsedBody) => {
|
const buildOptions = (endpoint, parsedBody) => {
|
||||||
const { modelLabel, promptPrefix, resendFiles, ...rest } = parsedBody;
|
const { modelLabel, promptPrefix, resendFiles, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
endpoint,
|
endpoint,
|
||||||
modelLabel,
|
modelLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
resendFiles,
|
resendFiles,
|
||||||
|
iconURL,
|
||||||
|
greeting,
|
||||||
|
spec,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
const buildOptions = (endpoint, parsedBody) => {
|
const buildOptions = (endpoint, parsedBody) => {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { promptPrefix, assistant_id, ...rest } = parsedBody;
|
const { promptPrefix, assistant_id, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
endpoint,
|
endpoint,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
assistant_id,
|
assistant_id,
|
||||||
|
iconURL,
|
||||||
|
greeting,
|
||||||
|
spec,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const buildOptions = (endpoint, parsedBody, endpointType) => {
|
const buildOptions = (endpoint, parsedBody, endpointType) => {
|
||||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, ...rest } = parsedBody;
|
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, iconURL, greeting, spec, ...rest } =
|
||||||
|
parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
endpoint,
|
endpoint,
|
||||||
endpointType,
|
endpointType,
|
||||||
|
|
@ -7,6 +8,9 @@ const buildOptions = (endpoint, parsedBody, endpointType) => {
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
resendFiles,
|
resendFiles,
|
||||||
imageDetail,
|
imageDetail,
|
||||||
|
iconURL,
|
||||||
|
greeting,
|
||||||
|
spec,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
const buildOptions = (endpoint, parsedBody) => {
|
const buildOptions = (endpoint, parsedBody) => {
|
||||||
const { examples, modelLabel, promptPrefix, ...rest } = parsedBody;
|
const { examples, modelLabel, promptPrefix, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
examples,
|
examples,
|
||||||
endpoint,
|
endpoint,
|
||||||
modelLabel,
|
modelLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
|
iconURL,
|
||||||
|
greeting,
|
||||||
|
spec,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,24 @@ const buildOptions = (endpoint, parsedBody) => {
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
agentOptions,
|
agentOptions,
|
||||||
tools,
|
tools,
|
||||||
model,
|
iconURL,
|
||||||
temperature,
|
greeting,
|
||||||
top_p,
|
spec,
|
||||||
presence_penalty,
|
...modelOptions
|
||||||
frequency_penalty,
|
|
||||||
} = parsedBody;
|
} = parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
endpoint,
|
endpoint,
|
||||||
tools: tools.map((tool) => tool.pluginKey) ?? [],
|
tools:
|
||||||
|
tools
|
||||||
|
.map((tool) => tool?.pluginKey ?? tool)
|
||||||
|
.filter((toolName) => typeof toolName === 'string') ?? [],
|
||||||
chatGptLabel,
|
chatGptLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
agentOptions,
|
agentOptions,
|
||||||
modelOptions: {
|
iconURL,
|
||||||
model,
|
greeting,
|
||||||
temperature,
|
spec,
|
||||||
top_p,
|
modelOptions,
|
||||||
presence_penalty,
|
|
||||||
frequency_penalty,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return endpointOption;
|
return endpointOption;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
const buildOptions = (endpoint, parsedBody) => {
|
const buildOptions = (endpoint, parsedBody) => {
|
||||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, ...rest } = parsedBody;
|
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, iconURL, greeting, spec, ...rest } =
|
||||||
|
parsedBody;
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
endpoint,
|
endpoint,
|
||||||
chatGptLabel,
|
chatGptLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
resendFiles,
|
resendFiles,
|
||||||
imageDetail,
|
imageDetail,
|
||||||
|
iconURL,
|
||||||
|
greeting,
|
||||||
|
spec,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@ const { redactMessage } = require('~/config/parsers');
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
const filteredTools = new Set([
|
||||||
|
'ChatTool.js',
|
||||||
|
'CodeSherpa.js',
|
||||||
|
'CodeSherpaTools.js',
|
||||||
|
'E2BTools.js',
|
||||||
|
'extractionChain.js',
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and formats tools from the specified tool directory.
|
* Loads and formats tools from the specified tool directory.
|
||||||
*
|
*
|
||||||
|
|
@ -30,10 +38,11 @@ const { logger } = require('~/config');
|
||||||
*
|
*
|
||||||
* @param {object} params - The parameters for the function.
|
* @param {object} params - The parameters for the function.
|
||||||
* @param {string} params.directory - The directory path where the tools are located.
|
* @param {string} params.directory - The directory path where the tools are located.
|
||||||
* @param {Set<string>} [params.filter=new Set()] - A set of filenames to exclude from loading.
|
* @param {Array<string>} [params.adminFilter=[]] - Array of admin-defined tool keys to exclude from loading.
|
||||||
* @returns {Record<string, FunctionTool>} An object mapping each tool's plugin key to its instance.
|
* @returns {Record<string, FunctionTool>} An object mapping each tool's plugin key to its instance.
|
||||||
*/
|
*/
|
||||||
function loadAndFormatTools({ directory, filter = new Set() }) {
|
function loadAndFormatTools({ directory, adminFilter = [] }) {
|
||||||
|
const filter = new Set([...adminFilter, ...filteredTools]);
|
||||||
const tools = [];
|
const tools = [];
|
||||||
/* Structured Tools Directory */
|
/* Structured Tools Directory */
|
||||||
const files = fs.readdirSync(directory);
|
const files = fs.readdirSync(directory);
|
||||||
|
|
|
||||||
74
api/server/services/start/interface.js
Normal file
74
api/server/services/start/interface.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the default interface object.
|
||||||
|
* @param {TCustomConfig | undefined} config - The loaded custom configuration.
|
||||||
|
* @param {TConfigDefaults} configDefaults - The custom configuration default values.
|
||||||
|
* @returns {TCustomConfig['interface']} The default interface object.
|
||||||
|
*/
|
||||||
|
function loadDefaultInterface(config, configDefaults) {
|
||||||
|
const { interface: interfaceConfig } = config ?? {};
|
||||||
|
const { interface: defaults } = configDefaults;
|
||||||
|
const hasModelSpecs = config?.modelSpecs?.list?.length > 0;
|
||||||
|
|
||||||
|
const loadedInterface = {
|
||||||
|
endpointsMenu:
|
||||||
|
interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu),
|
||||||
|
modelSelect: interfaceConfig?.modelSelect ?? (hasModelSpecs ? false : defaults.modelSelect),
|
||||||
|
parameters: interfaceConfig?.parameters ?? (hasModelSpecs ? false : defaults.parameters),
|
||||||
|
presets: interfaceConfig?.presets ?? (hasModelSpecs ? false : defaults.presets),
|
||||||
|
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
||||||
|
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
||||||
|
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||||
|
};
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const logSettings = () => {
|
||||||
|
// log interface object and model specs object (without list) for reference
|
||||||
|
logger.warn(`\`interface\` settings:\n${JSON.stringify(loadedInterface, null, 2)}`);
|
||||||
|
logger.warn(
|
||||||
|
`\`modelSpecs\` settings:\n${JSON.stringify(
|
||||||
|
{ ...(config?.modelSpecs ?? {}), list: undefined },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs.
|
||||||
|
if (config?.modelSpecs?.prioritize && loadedInterface.presets) {
|
||||||
|
logger.warn(
|
||||||
|
'Note: Prioritizing model specs can conflict with default presets if a default preset is set. It\'s recommended to disable presets from the interface or disable use of a default preset.',
|
||||||
|
);
|
||||||
|
i === 0 && i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn about config.modelSpecs.enforce if true and if any of these, endpointsMenu, modelSelect, presets, or parameters are enabled, that enforcing model specs can conflict with these options.
|
||||||
|
if (
|
||||||
|
config?.modelSpecs?.enforce &&
|
||||||
|
(loadedInterface.endpointsMenu ||
|
||||||
|
loadedInterface.modelSelect ||
|
||||||
|
loadedInterface.presets ||
|
||||||
|
loadedInterface.parameters)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
'Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It\'s recommended to disable these options from the interface or disable enforcing model specs.',
|
||||||
|
);
|
||||||
|
i === 0 && i++;
|
||||||
|
}
|
||||||
|
// warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior.
|
||||||
|
if (config?.modelSpecs?.enforce && !config?.modelSpecs?.prioritize) {
|
||||||
|
logger.warn(
|
||||||
|
'Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It\'s recommended to enable prioritizing model specs if enforcing them.',
|
||||||
|
);
|
||||||
|
i === 0 && i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
logSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadDefaultInterface };
|
||||||
|
|
@ -300,6 +300,18 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TStartupConfig
|
||||||
|
* @typedef {import('librechat-data-provider').TStartupConfig} TStartupConfig
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TConfigDefaults
|
||||||
|
* @typedef {import('librechat-data-provider').TConfigDefaults} TConfigDefaults
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports TPlugin
|
* @exports TPlugin
|
||||||
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||||
|
|
@ -342,6 +354,18 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TConversation
|
||||||
|
* @typedef {import('librechat-data-provider').TConversation} TConversation
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TModelSpec
|
||||||
|
* @typedef {import('librechat-data-provider').TModelSpec} TModelSpec
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports TPlugin
|
* @exports TPlugin
|
||||||
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ function loadYaml(filepath) {
|
||||||
let fileContents = fs.readFileSync(filepath, 'utf8');
|
let fileContents = fs.readFileSync(filepath, 'utf8');
|
||||||
return yaml.load(fileContents);
|
return yaml.load(fileContents);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error(e);
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,24 @@ import { FileSources } from 'librechat-data-provider';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import type {
|
import type {
|
||||||
TSetOption as SetOption,
|
|
||||||
TConversation,
|
|
||||||
TMessage,
|
|
||||||
TPreset,
|
|
||||||
TLoginUser,
|
|
||||||
TUser,
|
TUser,
|
||||||
EModelEndpoint,
|
|
||||||
Action,
|
Action,
|
||||||
|
TPreset,
|
||||||
|
TPlugin,
|
||||||
|
TMessage,
|
||||||
|
TLoginUser,
|
||||||
AuthTypeEnum,
|
AuthTypeEnum,
|
||||||
|
TConversation,
|
||||||
|
EModelEndpoint,
|
||||||
AuthorizationTypeEnum,
|
AuthorizationTypeEnum,
|
||||||
|
TSetOption as SetOption,
|
||||||
TokenExchangeMethodEnum,
|
TokenExchangeMethodEnum,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export type TPluginMap = Record<string, TPlugin>;
|
||||||
|
|
||||||
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
||||||
|
|
||||||
export type LastSelectedModels = Record<EModelEndpoint, string>;
|
export type LastSelectedModels = Record<EModelEndpoint, string>;
|
||||||
|
|
@ -32,6 +35,16 @@ export enum IconContext {
|
||||||
message = 'message',
|
message = 'message',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IconMapProps = {
|
||||||
|
className?: string;
|
||||||
|
iconURL?: string;
|
||||||
|
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
||||||
|
endpoint?: string | null;
|
||||||
|
assistantName?: string;
|
||||||
|
avatar?: string;
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type NavLink = {
|
export type NavLink = {
|
||||||
title: string;
|
title: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
import { getConfigDefaults } from 'librechat-data-provider';
|
||||||
|
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type { ContextType } from '~/common';
|
import type { ContextType } from '~/common';
|
||||||
import { EndpointsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||||
import HeaderOptions from './Input/HeaderOptions';
|
import HeaderOptions from './Input/HeaderOptions';
|
||||||
|
|
||||||
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const { navVisible } = useOutletContext<ContextType>();
|
const { navVisible } = useOutletContext<ContextType>();
|
||||||
|
const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]);
|
||||||
|
const interfaceConfig = useMemo(
|
||||||
|
() => startupConfig?.interface ?? defaultInterface,
|
||||||
|
[startupConfig],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold dark:bg-gray-800 dark:text-white">
|
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold dark:bg-gray-800 dark:text-white">
|
||||||
<div className="hide-scrollbar flex items-center gap-2 overflow-x-auto">
|
<div className="hide-scrollbar flex items-center gap-2 overflow-x-auto">
|
||||||
{!navVisible && <HeaderNewChat />}
|
{!navVisible && <HeaderNewChat />}
|
||||||
<EndpointsMenu />
|
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
|
||||||
<HeaderOptions />
|
{modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||||
<PresetsMenu />
|
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
||||||
|
{interfaceConfig.presets && <PresetsMenu />}
|
||||||
</div>
|
</div>
|
||||||
{/* Empty div for spacing */}
|
{/* Empty div for spacing */}
|
||||||
<div />
|
<div />
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Settings2 } from 'lucide-react';
|
||||||
import { Root, Anchor } from '@radix-ui/react-popover';
|
import { Root, Anchor } from '@radix-ui/react-popover';
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
||||||
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||||
import { ModelSelect } from '~/components/Input/ModelSelect';
|
import { ModelSelect } from '~/components/Input/ModelSelect';
|
||||||
import { PluginStoreDialog } from '~/components';
|
import { PluginStoreDialog } from '~/components';
|
||||||
|
|
@ -15,7 +15,11 @@ import { Button } from '~/components/ui';
|
||||||
import { cn, cardStyle } from '~/utils/';
|
import { cn, cardStyle } from '~/utils/';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function HeaderOptions() {
|
export default function HeaderOptions({
|
||||||
|
interfaceConfig,
|
||||||
|
}: {
|
||||||
|
interfaceConfig?: Partial<TInterfaceConfig>;
|
||||||
|
}) {
|
||||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
||||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
||||||
store.showPluginStoreDialog,
|
store.showPluginStoreDialog,
|
||||||
|
|
@ -70,13 +74,15 @@ export default function HeaderOptions() {
|
||||||
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
|
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
|
||||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
||||||
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
||||||
|
{interfaceConfig?.modelSelect && (
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
isMultiChat={true}
|
|
||||||
showAbove={false}
|
showAbove={false}
|
||||||
|
popover={true}
|
||||||
/>
|
/>
|
||||||
{!noSettings[endpoint] && (
|
)}
|
||||||
|
{!noSettings[endpoint] && interfaceConfig?.parameters && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -90,22 +96,25 @@ export default function HeaderOptions() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{interfaceConfig?.parameters && (
|
||||||
<OptionsPopover
|
<OptionsPopover
|
||||||
visible={showPopover}
|
visible={showPopover}
|
||||||
saveAsPreset={saveAsPreset}
|
saveAsPreset={saveAsPreset}
|
||||||
closePopover={() => setShowPopover(false)}
|
presetsDisabled={!interfaceConfig?.presets}
|
||||||
PopoverButtons={<PopoverButtons />}
|
PopoverButtons={<PopoverButtons />}
|
||||||
|
closePopover={() => setShowPopover(false)}
|
||||||
>
|
>
|
||||||
<div className="px-4 py-4">
|
<div className="px-4 py-4">
|
||||||
<EndpointSettings
|
<EndpointSettings
|
||||||
className="[&::-webkit-scrollbar]:w-2"
|
className="[&::-webkit-scrollbar]:w-2"
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
isMultiChat={true}
|
|
||||||
/>
|
/>
|
||||||
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
||||||
</div>
|
</div>
|
||||||
</OptionsPopover>
|
</OptionsPopover>
|
||||||
|
)}
|
||||||
|
{interfaceConfig?.presets && (
|
||||||
<SaveAsPresetDialog
|
<SaveAsPresetDialog
|
||||||
open={saveAsDialogShow}
|
open={saveAsDialogShow}
|
||||||
onOpenChange={setSaveAsDialogShow}
|
onOpenChange={setSaveAsDialogShow}
|
||||||
|
|
@ -115,10 +124,13 @@ export default function HeaderOptions() {
|
||||||
}) as TPreset
|
}) as TPreset
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{interfaceConfig?.parameters && (
|
||||||
<PluginStoreDialog
|
<PluginStoreDialog
|
||||||
isOpen={showPluginStoreDialog}
|
isOpen={showPluginStoreDialog}
|
||||||
setIsOpen={setShowPluginStoreDialog}
|
setIsOpen={setShowPluginStoreDialog}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type TOptionsPopoverProps = {
|
||||||
saveAsPreset: () => void;
|
saveAsPreset: () => void;
|
||||||
closePopover: () => void;
|
closePopover: () => void;
|
||||||
PopoverButtons: ReactNode;
|
PopoverButtons: ReactNode;
|
||||||
|
presetsDisabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function OptionsPopover({
|
export default function OptionsPopover({
|
||||||
|
|
@ -22,6 +23,7 @@ export default function OptionsPopover({
|
||||||
saveAsPreset,
|
saveAsPreset,
|
||||||
closePopover,
|
closePopover,
|
||||||
PopoverButtons,
|
PopoverButtons,
|
||||||
|
presetsDisabled,
|
||||||
}: TOptionsPopoverProps) {
|
}: TOptionsPopoverProps) {
|
||||||
const popoverRef = useRef(null);
|
const popoverRef = useRef(null);
|
||||||
useOnClickOutside(
|
useOnClickOutside(
|
||||||
|
|
@ -61,6 +63,7 @@ export default function OptionsPopover({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
||||||
|
{presetsDisabled ? null : (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-auto w-[150px] justify-start rounded-md border-2 border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500"
|
className="h-auto w-[150px] justify-start rounded-md border-2 border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500"
|
||||||
|
|
@ -69,6 +72,7 @@ export default function OptionsPopover({
|
||||||
<Save className="mr-1 w-[14px]" />
|
<Save className="mr-1 w-[14px]" />
|
||||||
{localize('com_endpoint_save_as_preset')}
|
{localize('com_endpoint_save_as_preset')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{PopoverButtons}
|
{PopoverButtons}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,22 @@ import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
||||||
|
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
import { icons } from './Menus/Endpoints/Icons';
|
import { icons } from './Menus/Endpoints/Icons';
|
||||||
import { BirthdayIcon } from '~/components/svg';
|
import { BirthdayIcon } from '~/components/svg';
|
||||||
import { getEndpointField } from '~/utils';
|
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
export default function Landing({ Header }: { Header?: ReactNode }) {
|
export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
let { endpoint } = conversation ?? {};
|
let { endpoint = '' } = conversation ?? {};
|
||||||
const { assistant_id = null } = conversation ?? {};
|
const { assistant_id = null } = conversation ?? {};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -27,9 +28,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
endpoint = EModelEndpoint.openAI;
|
endpoint = EModelEndpoint.openAI;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const iconURL = conversation?.iconURL;
|
||||||
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
|
||||||
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
|
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
||||||
const Icon = icons[iconKey];
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
|
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
|
||||||
|
|
@ -51,6 +54,15 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
<div className="absolute left-0 right-0">{Header && Header}</div>
|
<div className="absolute left-0 right-0">{Header && Header}</div>
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<div className="relative mb-3 h-[72px] w-[72px]">
|
<div className="relative mb-3 h-[72px] w-[72px]">
|
||||||
|
{iconURL && iconURL.includes('http') ? (
|
||||||
|
<ConvoIconURL
|
||||||
|
preset={conversation}
|
||||||
|
endpointIconURL={endpointIconURL}
|
||||||
|
assistantName={assistantName}
|
||||||
|
assistantAvatar={avatar}
|
||||||
|
context="landing"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{endpoint &&
|
{endpoint &&
|
||||||
Icon &&
|
Icon &&
|
||||||
|
|
@ -58,12 +70,13 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
size: 41,
|
size: 41,
|
||||||
context: 'landing',
|
context: 'landing',
|
||||||
className: 'h-2/3 w-2/3',
|
className: 'h-2/3 w-2/3',
|
||||||
endpoint: endpoint,
|
iconURL: endpointIconURL,
|
||||||
iconURL: iconURL,
|
|
||||||
assistantName,
|
assistantName,
|
||||||
|
endpoint,
|
||||||
avatar,
|
avatar,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
{(startupConfig?.showBirthdayIcon ?? false) && (
|
{(startupConfig?.showBirthdayIcon ?? false) && (
|
||||||
<BirthdayIcon className="absolute bottom-12 right-5" />
|
<BirthdayIcon className="absolute bottom-12 right-5" />
|
||||||
|
|
@ -88,8 +101,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-5 text-2xl font-medium dark:text-white">
|
<div className="mb-5 text-2xl font-medium dark:text-white">
|
||||||
{endpoint === EModelEndpoint.assistants
|
{endpoint === EModelEndpoint.assistants
|
||||||
? localize('com_nav_welcome_assistant')
|
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
|
||||||
: localize('com_nav_welcome_message')}
|
: conversation?.greeting ?? localize('com_nav_welcome_message')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import type { IconMapProps } from '~/common';
|
||||||
import {
|
import {
|
||||||
MinimalPlugin,
|
MinimalPlugin,
|
||||||
GPTIcon,
|
GPTIcon,
|
||||||
|
|
@ -23,17 +24,7 @@ export const icons = {
|
||||||
[EModelEndpoint.google]: GoogleMinimalIcon,
|
[EModelEndpoint.google]: GoogleMinimalIcon,
|
||||||
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
|
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
|
||||||
[EModelEndpoint.custom]: CustomMinimalIcon,
|
[EModelEndpoint.custom]: CustomMinimalIcon,
|
||||||
[EModelEndpoint.assistants]: ({
|
[EModelEndpoint.assistants]: ({ className = '', assistantName, avatar, size }: IconMapProps) => {
|
||||||
className = '',
|
|
||||||
assistantName,
|
|
||||||
avatar,
|
|
||||||
size,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
assistantName?: string;
|
|
||||||
avatar?: string;
|
|
||||||
size?: number;
|
|
||||||
}) => {
|
|
||||||
if (assistantName && avatar) {
|
if (assistantName && avatar) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { EModelEndpoint, modularEndpoints } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TPreset, TConversation } from 'librechat-data-provider';
|
import type { TConversation } from 'librechat-data-provider';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { cn, getConvoSwitchLogic, getEndpointField, getIconKey } from '~/utils';
|
||||||
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
|
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
|
||||||
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
|
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
|
||||||
import { cn, getEndpointField } from '~/utils';
|
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import { icons } from './Icons';
|
import { icons } from './Icons';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -43,38 +43,25 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
const onSelectEndpoint = (newEndpoint: EModelEndpoint) => {
|
const onSelectEndpoint = (newEndpoint: EModelEndpoint) => {
|
||||||
if (!newEndpoint) {
|
if (!newEndpoint) {
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
if (!expiryTime) {
|
if (!expiryTime) {
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentEndpoint = conversation?.endpoint;
|
const {
|
||||||
const template: Partial<TPreset> = {
|
shouldSwitch,
|
||||||
...conversation,
|
isNewModular,
|
||||||
endpoint: newEndpoint,
|
isCurrentModular,
|
||||||
conversationId: 'new',
|
isExistingConversation,
|
||||||
};
|
newEndpointType,
|
||||||
const isAssistantSwitch =
|
template,
|
||||||
newEndpoint === EModelEndpoint.assistants &&
|
} = getConvoSwitchLogic({
|
||||||
currentEndpoint === EModelEndpoint.assistants &&
|
newEndpoint,
|
||||||
currentEndpoint === newEndpoint;
|
modularChat,
|
||||||
|
conversation,
|
||||||
const { conversationId } = conversation ?? {};
|
endpointsConfig,
|
||||||
const isExistingConversation = conversationId && conversationId !== 'new';
|
});
|
||||||
const currentEndpointType =
|
|
||||||
getEndpointField(endpointsConfig, currentEndpoint, 'type') ?? currentEndpoint;
|
|
||||||
const newEndpointType = getEndpointField(endpointsConfig, newEndpoint, 'type') ?? newEndpoint;
|
|
||||||
|
|
||||||
const hasEndpoint = modularEndpoints.has(currentEndpoint ?? '');
|
|
||||||
const hasCurrentEndpointType = modularEndpoints.has(currentEndpointType ?? '');
|
|
||||||
const isCurrentModular = hasEndpoint || hasCurrentEndpointType || isAssistantSwitch;
|
|
||||||
|
|
||||||
const hasNewEndpoint = modularEndpoints.has(newEndpoint ?? '');
|
|
||||||
const hasNewEndpointType = modularEndpoints.has(newEndpointType ?? '');
|
|
||||||
const isNewModular = hasNewEndpoint || hasNewEndpointType || isAssistantSwitch;
|
|
||||||
|
|
||||||
const endpointsMatch = currentEndpoint === newEndpoint;
|
|
||||||
const shouldSwitch = endpointsMatch || modularChat || isAssistantSwitch;
|
|
||||||
|
|
||||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||||
template.endpointType = newEndpointType;
|
template.endpointType = newEndpointType;
|
||||||
|
|
@ -90,11 +77,10 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType });
|
||||||
const Icon = icons[iconKey];
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
49
client/src/components/Chat/Menus/Models/MenuButton.tsx
Normal file
49
client/src/components/Chat/Menus/Models/MenuButton.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Trigger } from '@radix-ui/react-popover';
|
||||||
|
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import SpecIcon from './SpecIcon';
|
||||||
|
|
||||||
|
export default function MenuButton({
|
||||||
|
selected,
|
||||||
|
primaryText = '',
|
||||||
|
secondaryText = '',
|
||||||
|
endpointsConfig,
|
||||||
|
}: {
|
||||||
|
selected?: TModelSpec;
|
||||||
|
primaryText?: string;
|
||||||
|
secondaryText?: string;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
return (
|
||||||
|
<Trigger asChild>
|
||||||
|
<div
|
||||||
|
className="group flex cursor-pointer items-center gap-1 rounded-xl px-3 py-2 text-lg font-medium hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700"
|
||||||
|
// type="button"
|
||||||
|
>
|
||||||
|
{selected && selected.showIconInHeader && (
|
||||||
|
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{!selected ? localize('com_ui_none_selected') : primaryText}{' '}
|
||||||
|
{!!secondaryText && <span className="text-token-text-secondary">{secondaryText}</span>}
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
fill="none"
|
||||||
|
className="text-token-text-tertiary"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
client/src/components/Chat/Menus/Models/ModelSpec.tsx
Normal file
130
client/src/components/Chat/Menus/Models/ModelSpec.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Settings } from 'lucide-react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
|
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
|
||||||
|
import { useLocalize, useUserKey } from '~/hooks';
|
||||||
|
import { cn, getEndpointField } from '~/utils';
|
||||||
|
import SpecIcon from './SpecIcon';
|
||||||
|
|
||||||
|
type MenuItemProps = {
|
||||||
|
title: string;
|
||||||
|
spec: TModelSpec;
|
||||||
|
selected: boolean;
|
||||||
|
description?: string;
|
||||||
|
userProvidesKey: boolean;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
|
onClick?: () => void;
|
||||||
|
// iconPath: string;
|
||||||
|
// hoverContent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuItem: FC<MenuItemProps> = ({
|
||||||
|
title,
|
||||||
|
spec,
|
||||||
|
selected,
|
||||||
|
description,
|
||||||
|
userProvidesKey,
|
||||||
|
endpointsConfig,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { endpoint } = spec.preset;
|
||||||
|
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const { getExpiry } = useUserKey(endpoint ?? '');
|
||||||
|
const localize = useLocalize();
|
||||||
|
const expiryTime = getExpiry();
|
||||||
|
|
||||||
|
const clickHandler = () => {
|
||||||
|
if (!expiryTime) {
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpointType = useMemo(
|
||||||
|
() => spec.preset.endpointType ?? getEndpointField(endpointsConfig, endpoint, 'type'),
|
||||||
|
[spec, endpointsConfig, endpoint],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showIconInMenu = true } = spec;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
role="menuitem"
|
||||||
|
className="group m-1.5 flex cursor-pointer gap-2 rounded px-1 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||||
|
tabIndex={-1}
|
||||||
|
{...rest}
|
||||||
|
onClick={clickHandler}
|
||||||
|
>
|
||||||
|
<div className="flex grow items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{showIconInMenu && <SpecIcon currentSpec={spec} endpointsConfig={endpointsConfig} />}
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
<div className="text-token-text-tertiary">{description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{userProvidesKey ? (
|
||||||
|
<div className="text-token-text-primary" key={`set-key-${endpoint}`}>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'invisible flex gap-x-1 group-hover:visible',
|
||||||
|
selected ? 'visible' : '',
|
||||||
|
expiryTime
|
||||||
|
? 'w-full rounded-lg p-2 hover:bg-gray-200 dark:hover:bg-gray-900'
|
||||||
|
: '',
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cn('invisible group-hover:visible', expiryTime ? 'text-xs' : '')}>
|
||||||
|
{localize('com_endpoint_config_key')}
|
||||||
|
</div>
|
||||||
|
<Settings className={cn(expiryTime ? 'icon-sm' : 'icon-md stroke-1')} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{selected && (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="icon-md block"
|
||||||
|
// className="icon-md block group-hover:hidden"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{userProvidesKey && (
|
||||||
|
<SetKeyDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
endpoint={endpoint ?? ''}
|
||||||
|
endpointType={endpointType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuItem;
|
||||||
44
client/src/components/Chat/Menus/Models/ModelSpecs.tsx
Normal file
44
client/src/components/Chat/Menus/Models/ModelSpecs.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { Close } from '@radix-ui/react-popover';
|
||||||
|
import { AuthType } from 'librechat-data-provider';
|
||||||
|
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
|
import MenuSeparator from '~/components/Chat/Menus/UI/MenuSeparator';
|
||||||
|
import ModelSpec from './ModelSpec';
|
||||||
|
|
||||||
|
const ModelSpecs: FC<{
|
||||||
|
specs?: TModelSpec[];
|
||||||
|
selected?: TModelSpec;
|
||||||
|
setSelected?: (spec: TModelSpec) => void;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
|
}> = ({ specs = [], selected, setSelected = () => ({}), endpointsConfig }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{specs &&
|
||||||
|
specs.map((spec, i) => {
|
||||||
|
if (!spec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Close asChild key={`spec-${spec.name}`}>
|
||||||
|
<div key={`spec-${spec.name}`}>
|
||||||
|
<ModelSpec
|
||||||
|
spec={spec}
|
||||||
|
title={spec.label}
|
||||||
|
key={`spec-item-${spec.name}`}
|
||||||
|
description={spec.description}
|
||||||
|
onClick={() => setSelected(spec)}
|
||||||
|
data-testid={`spec-item-${spec.name}`}
|
||||||
|
selected={selected?.name === spec.name}
|
||||||
|
userProvidesKey={spec.authType === AuthType.USER_PROVIDED}
|
||||||
|
endpointsConfig={endpointsConfig}
|
||||||
|
/>
|
||||||
|
{i !== specs.length - 1 && <MenuSeparator />}
|
||||||
|
</div>
|
||||||
|
</Close>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelSpecs;
|
||||||
106
client/src/components/Chat/Menus/Models/ModelSpecsMenu.tsx
Normal file
106
client/src/components/Chat/Menus/Models/ModelSpecsMenu.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import { Content, Portal, Root } from '@radix-ui/react-popover';
|
||||||
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import type { TModelSpec, TConversation, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
|
import { getConvoSwitchLogic, getModelSpecIconURL } from '~/utils';
|
||||||
|
import { useDefaultConvo, useNewConvo } from '~/hooks';
|
||||||
|
import { useChatContext } from '~/Providers';
|
||||||
|
import MenuButton from './MenuButton';
|
||||||
|
import ModelSpecs from './ModelSpecs';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs: TModelSpec[] }) {
|
||||||
|
const { conversation } = useChatContext();
|
||||||
|
const { newConversation } = useNewConvo();
|
||||||
|
|
||||||
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
|
const modularChat = useRecoilValue(store.modularChat);
|
||||||
|
const getDefaultConversation = useDefaultConvo();
|
||||||
|
|
||||||
|
const onSelectSpec = (spec: TModelSpec) => {
|
||||||
|
const { preset } = spec;
|
||||||
|
preset.iconURL = getModelSpecIconURL(spec);
|
||||||
|
preset.spec = spec.name;
|
||||||
|
const { endpoint: newEndpoint } = preset;
|
||||||
|
if (!newEndpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldSwitch,
|
||||||
|
isNewModular,
|
||||||
|
isCurrentModular,
|
||||||
|
isExistingConversation,
|
||||||
|
newEndpointType,
|
||||||
|
template,
|
||||||
|
} = getConvoSwitchLogic({
|
||||||
|
newEndpoint,
|
||||||
|
modularChat,
|
||||||
|
conversation,
|
||||||
|
endpointsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||||
|
template.endpointType = newEndpointType as EModelEndpoint | undefined;
|
||||||
|
|
||||||
|
const currentConvo = getDefaultConversation({
|
||||||
|
/* target endpointType is necessary to avoid endpoint mixing */
|
||||||
|
conversation: { ...(conversation ?? {}), endpointType: template.endpointType },
|
||||||
|
preset: template,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* We don't reset the latest message, only when changing settings mid-converstion */
|
||||||
|
newConversation({ template: currentConvo, preset, keepLatestMessage: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newConversation({ template: { ...(template as Partial<TConversation>) }, preset });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selected = useMemo(() => {
|
||||||
|
const spec = modelSpecs?.find((spec) => spec.name === conversation?.spec);
|
||||||
|
if (!spec) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return spec;
|
||||||
|
}, [modelSpecs, conversation?.spec]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<MenuButton
|
||||||
|
primaryText={selected?.label ?? ''}
|
||||||
|
selected={selected}
|
||||||
|
endpointsConfig={endpointsConfig}
|
||||||
|
/>
|
||||||
|
<Portal>
|
||||||
|
{modelSpecs && modelSpecs?.length && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: '0px',
|
||||||
|
top: '0px',
|
||||||
|
transform: 'translate3d(268px, 50px, 0px)',
|
||||||
|
minWidth: 'max-content',
|
||||||
|
zIndex: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Content
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
|
||||||
|
>
|
||||||
|
<ModelSpecs
|
||||||
|
specs={modelSpecs}
|
||||||
|
selected={selected}
|
||||||
|
setSelected={onSelectSpec}
|
||||||
|
endpointsConfig={endpointsConfig}
|
||||||
|
/>
|
||||||
|
</Content>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Portal>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
client/src/components/Chat/Menus/Models/SpecIcon.tsx
Normal file
50
client/src/components/Chat/Menus/Models/SpecIcon.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
|
import type { IconMapProps } from '~/common';
|
||||||
|
import { getModelSpecIconURL, getIconKey, getEndpointField } from '~/utils';
|
||||||
|
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
|
|
||||||
|
interface SpecIconProps {
|
||||||
|
currentSpec: TModelSpec;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) => {
|
||||||
|
const iconURL = getModelSpecIconURL(currentSpec);
|
||||||
|
const { endpoint } = currentSpec.preset;
|
||||||
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
|
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
||||||
|
let Icon: (props: IconMapProps) => React.JSX.Element;
|
||||||
|
|
||||||
|
if (!iconURL?.includes('http')) {
|
||||||
|
Icon = icons[iconKey] ?? icons.unknown;
|
||||||
|
} else {
|
||||||
|
Icon = iconURL
|
||||||
|
? () => (
|
||||||
|
<div
|
||||||
|
className="icon-xl mr-1 shrink-0 overflow-hidden rounded-full "
|
||||||
|
style={{ width: '20', height: '20' }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={iconURL}
|
||||||
|
alt={currentSpec.name}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: icons[endpoint ?? ''] ?? icons.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
size={20}
|
||||||
|
endpoint={endpoint}
|
||||||
|
context="menu-item"
|
||||||
|
iconURL={endpointIconURL}
|
||||||
|
className="icon-lg mr-1 shrink-0 dark:text-white"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpecIcon;
|
||||||
46
client/src/components/Chat/Menus/Models/fakeData.ts
Normal file
46
client/src/components/Chat/Menus/Models/fakeData.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { EModelEndpoint, AuthType } from 'librechat-data-provider';
|
||||||
|
import type { TModelSpec } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export const data: TModelSpec[] = [
|
||||||
|
{
|
||||||
|
name: 'commander_01',
|
||||||
|
label: 'Commander in Chief',
|
||||||
|
description:
|
||||||
|
'Salute your president, soldier! Salute your president, soldier! Salute your president, soldier!',
|
||||||
|
iconURL: 'https://i.kym-cdn.com/entries/icons/facebook/000/017/252/2f0.jpg',
|
||||||
|
// iconURL: EModelEndpoint.openAI,
|
||||||
|
preset: {
|
||||||
|
endpoint: 'Ollama',
|
||||||
|
greeting: 'My fellow Americans,',
|
||||||
|
// 'endpointType': EModelEndpoint.custom,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
// 'imageDetail': 'auto',
|
||||||
|
model: 'command-r',
|
||||||
|
presence_penalty: 0,
|
||||||
|
promptPrefix: null,
|
||||||
|
resendFiles: false,
|
||||||
|
temperature: 0.8,
|
||||||
|
top_p: 0.5,
|
||||||
|
},
|
||||||
|
authType: AuthType.SYSTEM_DEFINED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vision_pro',
|
||||||
|
label: 'Vision Pro',
|
||||||
|
description:
|
||||||
|
'Salute your president, soldier! Salute your president, soldier! Salute your president, soldier!',
|
||||||
|
// iconURL: 'https://i.ytimg.com/vi/SaneSRqePVY/maxresdefault.jpg',
|
||||||
|
iconURL: EModelEndpoint.openAI, // Allow using project-included icons
|
||||||
|
preset: {
|
||||||
|
chatGptLabel: 'Vision Helper',
|
||||||
|
greeting: 'What\'s up!!',
|
||||||
|
endpoint: EModelEndpoint.openAI,
|
||||||
|
model: 'gpt-4-turbo',
|
||||||
|
promptPrefix:
|
||||||
|
'Examine images closely to understand its style, colors, composition, and other elements. Then, craft a detailed prompt to that closely resemble the original. Your focus is on accuracy in replicating the style, colors, techniques, and details of the original image in written form. Your prompt must be excruciatingly detailed as it will be given to an image generating AI for image generation. \n',
|
||||||
|
temperature: 0.8,
|
||||||
|
top_p: 1,
|
||||||
|
},
|
||||||
|
authType: AuthType.SYSTEM_DEFINED,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -105,7 +105,6 @@ const EditPresetDialog = ({
|
||||||
conversation={preset}
|
conversation={preset}
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
isPreset={true}
|
isPreset={true}
|
||||||
isMultiChat={true}
|
|
||||||
className="h-full md:mb-4 md:h-[440px]"
|
className="h-full md:mb-4 md:h-[440px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { Flipper, Flipped } from 'react-flip-toolkit';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
|
import { getPresetTitle, getEndpointField, getIconKey } from '~/utils';
|
||||||
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
||||||
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
||||||
|
import { Dialog, DialogTrigger, Label } from '~/components/ui';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||||
import { getPresetTitle, getEndpointField } from '~/utils';
|
|
||||||
import { Dialog, DialogTrigger, Label } from '~/components/ui/';
|
|
||||||
import { MenuSeparator, MenuItem } from '../UI';
|
import { MenuSeparator, MenuItem } from '../UI';
|
||||||
import { icons } from '../Endpoints/Icons';
|
import { icons } from '../Endpoints/Icons';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
@ -115,9 +115,7 @@ const PresetItems: FC<{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconKey = getEndpointField(endpointsConfig, preset.endpoint, 'type')
|
const iconKey = getIconKey({ endpoint: preset.endpoint, endpointsConfig });
|
||||||
? 'unknown'
|
|
||||||
: preset.endpointType ?? preset.endpoint ?? 'unknown';
|
|
||||||
const Icon = icons[iconKey];
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as EndpointsMenu } from './EndpointsMenu';
|
|
||||||
export { default as PresetsMenu } from './PresetsMenu';
|
export { default as PresetsMenu } from './PresetsMenu';
|
||||||
|
export { default as EndpointsMenu } from './EndpointsMenu';
|
||||||
export { default as HeaderNewChat } from './HeaderNewChat';
|
export { default as HeaderNewChat } from './HeaderNewChat';
|
||||||
|
export { default as ModelSpecsMenu } from './Models/ModelSpecsMenu';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useAuthContext, useMessageHelpers, useLocalize } from '~/hooks';
|
import { useAuthContext, useMessageHelpers, useLocalize } from '~/hooks';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
|
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import { Plugin } from '~/components/Messages/Content';
|
import { Plugin } from '~/components/Messages/Content';
|
||||||
import MessageContent from './Content/MessageContent';
|
import MessageContent from './Content/MessageContent';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
|
|
@ -18,7 +19,6 @@ export default function Message(props: TMessageProps) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ask,
|
ask,
|
||||||
icon,
|
|
||||||
edit,
|
edit,
|
||||||
isLast,
|
isLast,
|
||||||
enterEdit,
|
enterEdit,
|
||||||
|
|
@ -60,11 +60,7 @@ export default function Message(props: TMessageProps) {
|
||||||
<div>
|
<div>
|
||||||
<div className="pt-0.5">
|
<div className="pt-0.5">
|
||||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||||
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
|
<Icon message={message} conversation={conversation} />
|
||||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
58
client/src/components/Chat/Messages/MessageIcon.tsx
Normal file
58
client/src/components/Chat/Messages/MessageIcon.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import type { TMessage, TPreset, Assistant } from 'librechat-data-provider';
|
||||||
|
import type { TMessageProps } from '~/common';
|
||||||
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
|
import { getEndpointField, getIconEndpoint } from '~/utils';
|
||||||
|
import Icon from '~/components/Endpoints/Icon';
|
||||||
|
|
||||||
|
export default function MessageIcon(
|
||||||
|
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
||||||
|
assistant?: false | Assistant;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
|
const { message, conversation, assistant } = props;
|
||||||
|
|
||||||
|
const assistantName = assistant ? (assistant.name as string | undefined) : '';
|
||||||
|
const assistantAvatar = assistant ? (assistant.metadata?.avatar as string | undefined) : '';
|
||||||
|
|
||||||
|
const messageSettings = useMemo(
|
||||||
|
() => ({
|
||||||
|
...(conversation ?? {}),
|
||||||
|
...({
|
||||||
|
...message,
|
||||||
|
iconURL: message?.iconURL ?? '',
|
||||||
|
} as TMessage),
|
||||||
|
}),
|
||||||
|
[conversation, message],
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconURL = messageSettings?.iconURL;
|
||||||
|
let endpoint = messageSettings?.endpoint;
|
||||||
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
|
|
||||||
|
if (!message?.isCreatedByUser && iconURL && iconURL.includes('http')) {
|
||||||
|
return (
|
||||||
|
<ConvoIconURL
|
||||||
|
preset={messageSettings as typeof messageSettings & TPreset}
|
||||||
|
context="message"
|
||||||
|
assistantAvatar={assistantAvatar}
|
||||||
|
endpointIconURL={endpointIconURL}
|
||||||
|
assistantName={assistantName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...messageSettings}
|
||||||
|
endpoint={endpoint}
|
||||||
|
iconURL={!assistant ? endpointIconURL : assistantAvatar}
|
||||||
|
model={message?.model ?? conversation?.model}
|
||||||
|
assistantName={assistantName}
|
||||||
|
size={28.8}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import ContentParts from './Content/ContentParts';
|
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
|
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||||
|
import ContentParts from './Content/ContentParts';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
import { useMessageHelpers } from '~/hooks';
|
import { useMessageHelpers } from '~/hooks';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
|
@ -14,7 +15,6 @@ export default function Message(props: TMessageProps) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ask,
|
ask,
|
||||||
icon,
|
|
||||||
edit,
|
edit,
|
||||||
isLast,
|
isLast,
|
||||||
enterEdit,
|
enterEdit,
|
||||||
|
|
@ -47,11 +47,7 @@ export default function Message(props: TMessageProps) {
|
||||||
<div>
|
<div>
|
||||||
<div className="pt-0.5">
|
<div className="pt-0.5">
|
||||||
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||||
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
|
<Icon message={message} conversation={conversation} assistant={assistant} />
|
||||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { FileSources } from 'librechat-data-provider';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
|
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile } from '~/common';
|
||||||
import { useDragHelpers, useSetFilesToDelete } from '~/hooks';
|
import { useDragHelpers, useSetFilesToDelete } from '~/hooks';
|
||||||
import DragDropOverlay from './Input/Files/DragDropOverlay';
|
import DragDropOverlay from './Input/Files/DragDropOverlay';
|
||||||
|
|
@ -8,6 +9,8 @@ import { useDeleteFilesMutation } from '~/data-provider';
|
||||||
import { SidePanel } from '~/components/SidePanel';
|
import { SidePanel } from '~/components/SidePanel';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
export default function Presentation({
|
export default function Presentation({
|
||||||
children,
|
children,
|
||||||
useSidePanel = false,
|
useSidePanel = false,
|
||||||
|
|
@ -17,9 +20,16 @@ export default function Presentation({
|
||||||
panel?: React.ReactNode;
|
panel?: React.ReactNode;
|
||||||
useSidePanel?: boolean;
|
useSidePanel?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||||
const { isOver, canDrop, drop } = useDragHelpers();
|
const interfaceConfig = useMemo(
|
||||||
|
() => startupConfig?.interface ?? defaultInterface,
|
||||||
|
[startupConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const setFilesToDelete = useSetFilesToDelete();
|
const setFilesToDelete = useSetFilesToDelete();
|
||||||
|
const { isOver, canDrop, drop } = useDragHelpers();
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteFilesMutation({
|
const { mutateAsync } = useDeleteFilesMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('Temporary Files deleted');
|
console.log('Temporary Files deleted');
|
||||||
|
|
@ -31,7 +41,7 @@ export default function Presentation({
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filesToDelete = localStorage.getItem('filesToDelete');
|
const filesToDelete = localStorage.getItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||||
const map = JSON.parse(filesToDelete ?? '{}') as Record<string, ExtendedFile>;
|
const map = JSON.parse(filesToDelete ?? '{}') as Record<string, ExtendedFile>;
|
||||||
const files = Object.values(map)
|
const files = Object.values(map)
|
||||||
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
|
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
|
||||||
|
|
@ -69,7 +79,7 @@ export default function Presentation({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useSidePanel && !hideSidePanel) {
|
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={drop}
|
ref={drop}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useState, useRef, useMemo } from 'react';
|
import { useState, useRef, useMemo } from 'react';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
||||||
import { useConversations, useNavigateToConvo } from '~/hooks';
|
import { MinimalIcon, ConvoIconURL } from '~/components/Endpoints';
|
||||||
import { useUpdateConversationMutation } from '~/data-provider';
|
import { useUpdateConversationMutation } from '~/data-provider';
|
||||||
import { MinimalIcon } from '~/components/Endpoints';
|
import { useConversations, useNavigateToConvo } from '~/hooks';
|
||||||
|
import { getEndpointField, getIconEndpoint } from '~/utils';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
import { getEndpointField } from '~/utils';
|
|
||||||
import RenameButton from './RenameButton';
|
import RenameButton from './RenameButton';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -51,11 +51,15 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
if (conversation?.endpoint === EModelEndpoint.gptPlugins) {
|
if (conversation?.endpoint === EModelEndpoint.gptPlugins) {
|
||||||
let lastSelectedTools = [];
|
let lastSelectedTools = [];
|
||||||
try {
|
try {
|
||||||
lastSelectedTools = JSON.parse(localStorage.getItem('lastSelectedTools') ?? '') ?? [];
|
lastSelectedTools =
|
||||||
|
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error(e);
|
// console.error(e);
|
||||||
}
|
}
|
||||||
navigateToConvo({ ...conversation, tools: lastSelectedTools });
|
navigateToConvo({
|
||||||
|
...conversation,
|
||||||
|
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
navigateToConvo(conversation);
|
navigateToConvo(conversation);
|
||||||
}
|
}
|
||||||
|
|
@ -95,11 +99,26 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const icon = MinimalIcon({
|
const iconURL = conversation.iconURL ?? '';
|
||||||
|
let endpoint = conversation.endpoint;
|
||||||
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
|
|
||||||
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||||
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
|
|
||||||
|
let icon: React.ReactNode | null = null;
|
||||||
|
if (iconURL && iconURL.includes('http')) {
|
||||||
|
icon = ConvoIconURL({
|
||||||
|
preset: conversation,
|
||||||
|
context: 'menu-item',
|
||||||
|
endpointIconURL,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
icon = MinimalIcon({
|
||||||
size: 20,
|
size: 20,
|
||||||
iconURL: getEndpointField(endpointsConfig, conversation.endpoint, 'iconURL'),
|
iconURL: endpointIconURL,
|
||||||
endpoint: conversation.endpoint,
|
endpoint,
|
||||||
endpointType: conversation.endpointType,
|
endpointType,
|
||||||
model: conversation.model,
|
model: conversation.model,
|
||||||
error: false,
|
error: false,
|
||||||
className: 'mr-0',
|
className: 'mr-0',
|
||||||
|
|
@ -108,6 +127,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
modelLabel: undefined,
|
modelLabel: undefined,
|
||||||
jailbreak: undefined,
|
jailbreak: undefined,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyEvent) => {
|
const handleKeyDown = (e: KeyEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default function AlternativeSettings({
|
||||||
setOption,
|
setOption,
|
||||||
isPreset = false,
|
isPreset = false,
|
||||||
className = '',
|
className = '',
|
||||||
}: TSettingsProps & { isMultiChat?: boolean }) {
|
}: TSettingsProps) {
|
||||||
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||||
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
|
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
78
client/src/components/Endpoints/ConvoIconURL.tsx
Normal file
78
client/src/components/Endpoints/ConvoIconURL.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react';
|
||||||
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
|
import type { IconMapProps } from '~/common';
|
||||||
|
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
|
|
||||||
|
interface ConvoIconURLProps {
|
||||||
|
preset: TPreset | null;
|
||||||
|
endpointIconURL?: string;
|
||||||
|
assistantName?: string;
|
||||||
|
context?: 'landing' | 'menu-item' | 'nav' | 'message';
|
||||||
|
assistantAvatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classMap = {
|
||||||
|
'menu-item': 'relative flex h-full items-center justify-center overflow-hidden rounded-full',
|
||||||
|
message: 'icon-md',
|
||||||
|
default: 'icon-xl relative flex h-full overflow-hidden rounded-full',
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleMap = {
|
||||||
|
'menu-item': { width: '20px', height: '20px' },
|
||||||
|
default: { width: '100%', height: '100%' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleImageMap = {
|
||||||
|
default: { width: '100%', height: '100%' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
|
||||||
|
preset,
|
||||||
|
endpointIconURL,
|
||||||
|
assistantAvatar,
|
||||||
|
assistantName,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const { iconURL = '' } = preset ?? {};
|
||||||
|
let Icon: (
|
||||||
|
props: IconMapProps & {
|
||||||
|
context?: string;
|
||||||
|
iconURL?: string;
|
||||||
|
},
|
||||||
|
) => React.JSX.Element;
|
||||||
|
|
||||||
|
if (!iconURL?.includes('http')) {
|
||||||
|
Icon = icons[iconURL] ?? icons.unknown;
|
||||||
|
} else {
|
||||||
|
Icon = () => (
|
||||||
|
<div
|
||||||
|
className={classMap[context ?? 'default'] ?? classMap.default}
|
||||||
|
style={styleMap[context ?? 'default'] ?? styleMap.default}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={iconURL}
|
||||||
|
alt={preset?.chatGptLabel ?? preset?.modelLabel ?? ''}
|
||||||
|
style={styleImageMap[context ?? 'default'] ?? styleImageMap.default}
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Icon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
|
||||||
|
<Icon
|
||||||
|
size={41}
|
||||||
|
context={context}
|
||||||
|
className="h-2/3 w-2/3"
|
||||||
|
iconURL={endpointIconURL}
|
||||||
|
assistantName={assistantName}
|
||||||
|
avatar={assistantAvatar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvoIconURL;
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { Save } from 'lucide-react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
// import { EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import { cn, removeFocusOutlines } from '~/utils';
|
|
||||||
// import PopoverButtons from './PopoverButtons';
|
|
||||||
import { CrossIcon } from '~/components/svg';
|
|
||||||
import { Button } from '~/components/ui';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
type TEndpointOptionsPopoverProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
visible: boolean;
|
|
||||||
// endpoint: EModelEndpoint;
|
|
||||||
saveAsPreset: () => void;
|
|
||||||
closePopover: () => void;
|
|
||||||
PopoverButtons: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EndpointOptionsPopover({
|
|
||||||
children,
|
|
||||||
// endpoint,
|
|
||||||
visible,
|
|
||||||
saveAsPreset,
|
|
||||||
closePopover,
|
|
||||||
PopoverButtons,
|
|
||||||
}: TEndpointOptionsPopoverProps) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const cardStyle =
|
|
||||||
'shadow-xl rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'endpointOptionsPopover-container absolute bottom-[-10px] z-0 flex w-full flex-col items-center md:px-4',
|
|
||||||
visible ? ' show' : '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
cardStyle,
|
|
||||||
'border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-white px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center bg-gray-100 px-2 py-2 dark:bg-gray-800/60">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
|
||||||
onClick={saveAsPreset}
|
|
||||||
>
|
|
||||||
<Save className="mr-1 w-[14px]" />
|
|
||||||
{localize('com_endpoint_save_as_preset')}
|
|
||||||
</Button>
|
|
||||||
{PopoverButtons}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
'ml-auto h-auto bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white',
|
|
||||||
removeFocusOutlines,
|
|
||||||
)}
|
|
||||||
onClick={closePopover}
|
|
||||||
>
|
|
||||||
<CrossIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -11,15 +11,14 @@ export default function Settings({
|
||||||
setOption,
|
setOption,
|
||||||
isPreset = false,
|
isPreset = false,
|
||||||
className = '',
|
className = '',
|
||||||
isMultiChat = false,
|
}: TSettingsProps) {
|
||||||
}: TSettingsProps & { isMultiChat?: boolean }) {
|
|
||||||
const modelsQuery = useGetModelsQuery();
|
const modelsQuery = useGetModelsQuery();
|
||||||
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||||
if (!conversation?.endpoint || currentSettingsView !== SettingsViews.default) {
|
if (!conversation?.endpoint || currentSettingsView !== SettingsViews.default) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { settings, multiViewSettings } = getSettings(isMultiChat);
|
const { settings, multiViewSettings } = getSettings();
|
||||||
const { endpoint: _endpoint, endpointType } = conversation;
|
const { endpoint: _endpoint, endpointType } = conversation;
|
||||||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
||||||
const endpoint = endpointType ?? _endpoint;
|
const endpoint = endpointType ?? _endpoint;
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,15 @@ import { cn } from '~/utils';
|
||||||
const Icon: React.FC<IconProps> = (props) => {
|
const Icon: React.FC<IconProps> = (props) => {
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const {
|
const {
|
||||||
size = 30,
|
|
||||||
isCreatedByUser,
|
|
||||||
button,
|
|
||||||
model = '',
|
|
||||||
endpoint,
|
|
||||||
error,
|
error,
|
||||||
|
button,
|
||||||
|
iconURL,
|
||||||
|
endpoint,
|
||||||
jailbreak,
|
jailbreak,
|
||||||
|
size = 30,
|
||||||
|
model = '',
|
||||||
assistantName,
|
assistantName,
|
||||||
|
isCreatedByUser,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const avatarSrc = useAvatar(user);
|
const avatarSrc = useAvatar(user);
|
||||||
|
|
@ -167,9 +168,13 @@ const Icon: React.FC<IconProps> = (props) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { icon, bg, name } =
|
let { icon, bg, name } =
|
||||||
endpoint && endpointIcons[endpoint] ? endpointIcons[endpoint] : endpointIcons.default;
|
endpoint && endpointIcons[endpoint] ? endpointIcons[endpoint] : endpointIcons.default;
|
||||||
|
|
||||||
|
if (iconURL && endpointIcons[iconURL]) {
|
||||||
|
({ icon, bg, name } = endpointIcons[iconURL]);
|
||||||
|
}
|
||||||
|
|
||||||
if (endpoint === EModelEndpoint.assistants) {
|
if (endpoint === EModelEndpoint.assistants) {
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,10 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { icon, name } = endpointIcons[endpoint] ?? endpointIcons.default;
|
let { icon, name } = endpointIcons[endpoint] ?? endpointIcons.default;
|
||||||
|
if (props.iconURL && endpointIcons[props.iconURL]) {
|
||||||
|
({ icon, name } = endpointIcons[props.iconURL]);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import Settings from '../Google';
|
|
||||||
import Examples from '../Examples';
|
|
||||||
import { useSetOptions } from '~/hooks';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function GoogleView({ conversation, models, isPreset = false }) {
|
|
||||||
const optionSettings = useRecoilValue(store.optionSettings);
|
|
||||||
const { setOption, setExample, addExample, removeExample } = useSetOptions(
|
|
||||||
isPreset ? conversation : null,
|
|
||||||
);
|
|
||||||
if (!conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { examples } = conversation;
|
|
||||||
const { showExamples, isCodeChat } = optionSettings;
|
|
||||||
return showExamples && !isCodeChat ? (
|
|
||||||
<Examples
|
|
||||||
examples={examples ?? []}
|
|
||||||
setExample={setExample}
|
|
||||||
addExample={addExample}
|
|
||||||
removeExample={removeExample}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Settings conversation={conversation} setOption={setOption} models={models} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { useChatContext } from '~/Providers';
|
||||||
|
|
||||||
export default function PluginsView({ conversation, models, isPreset = false }) {
|
export default function PluginsView({ conversation, models, isPreset = false }) {
|
||||||
const { showAgentSettings } = useChatContext();
|
const { showAgentSettings } = useChatContext();
|
||||||
const { setOption, setAgentOption } = useSetIndexOptions(isPreset ? conversation : null);
|
const { setOption, setTools, setAgentOption, checkPluginSelection } = useSetIndexOptions(
|
||||||
|
isPreset ? conversation : null,
|
||||||
|
);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +15,12 @@ export default function PluginsView({ conversation, models, isPreset = false })
|
||||||
return showAgentSettings ? (
|
return showAgentSettings ? (
|
||||||
<AgentSettings conversation={conversation} setOption={setAgentOption} models={models} />
|
<AgentSettings conversation={conversation} setOption={setAgentOption} models={models} />
|
||||||
) : (
|
) : (
|
||||||
<Settings conversation={conversation} setOption={setOption} models={models} />
|
<Settings
|
||||||
|
conversation={conversation}
|
||||||
|
setOption={setOption}
|
||||||
|
setTools={setTools}
|
||||||
|
checkPluginSelection={checkPluginSelection}
|
||||||
|
models={models}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import Settings from '../Plugins';
|
|
||||||
import AgentSettings from '../AgentSettings';
|
|
||||||
import { useSetOptions } from '~/hooks';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function PluginsView({ conversation, models, isPreset = false }) {
|
|
||||||
const showAgentSettings = useRecoilValue(store.showAgentSettings);
|
|
||||||
const { setOption, setAgentOption } = useSetOptions(isPreset ? conversation : null);
|
|
||||||
if (!conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return showAgentSettings ? (
|
|
||||||
<AgentSettings conversation={conversation} setOption={setAgentOption} models={models} />
|
|
||||||
) : (
|
|
||||||
<Settings conversation={conversation} setOption={setOption} models={models} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
export { default as Google } from './Google';
|
|
||||||
export { default as Plugins } from './Plugins';
|
|
||||||
export { default as GoogleSettings } from './GoogleSettings';
|
export { default as GoogleSettings } from './GoogleSettings';
|
||||||
export { default as PluginSettings } from './PluginSettings';
|
export { default as PluginSettings } from './PluginSettings';
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
endpoint,
|
endpoint,
|
||||||
endpointType,
|
endpointType,
|
||||||
model,
|
model,
|
||||||
|
modelLabel,
|
||||||
chatGptLabel,
|
chatGptLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
temperature,
|
temperature,
|
||||||
|
|
@ -41,32 +42,33 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
resendFiles,
|
resendFiles,
|
||||||
imageDetail,
|
imageDetail,
|
||||||
} = conversation ?? {};
|
} = conversation ?? {};
|
||||||
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput({
|
|
||||||
|
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput<string | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'chatGptLabel',
|
optionKey: 'chatGptLabel',
|
||||||
initialValue: chatGptLabel,
|
initialValue: modelLabel ?? chatGptLabel,
|
||||||
});
|
});
|
||||||
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput({
|
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput<string | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'promptPrefix',
|
optionKey: 'promptPrefix',
|
||||||
initialValue: promptPrefix,
|
initialValue: promptPrefix,
|
||||||
});
|
});
|
||||||
const [setTemperature, temperatureValue] = useDebouncedInput({
|
const [setTemperature, temperatureValue] = useDebouncedInput<number | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'temperature',
|
optionKey: 'temperature',
|
||||||
initialValue: temperature,
|
initialValue: temperature,
|
||||||
});
|
});
|
||||||
const [setTopP, topPValue] = useDebouncedInput({
|
const [setTopP, topPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'top_p',
|
optionKey: 'top_p',
|
||||||
initialValue: topP,
|
initialValue: topP,
|
||||||
});
|
});
|
||||||
const [setFreqP, freqPValue] = useDebouncedInput({
|
const [setFreqP, freqPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'frequency_penalty',
|
optionKey: 'frequency_penalty',
|
||||||
initialValue: freqP,
|
initialValue: freqP,
|
||||||
});
|
});
|
||||||
const [setPresP, presPValue] = useDebouncedInput({
|
const [setPresP, presPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
setOption,
|
setOption,
|
||||||
optionKey: 'presence_penalty',
|
optionKey: 'presence_penalty',
|
||||||
initialValue: presP,
|
initialValue: presP,
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,105 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
|
import type { TModelSelectProps } from '~/common';
|
||||||
import {
|
import {
|
||||||
SelectDropDown,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Slider,
|
Slider,
|
||||||
InputNumber,
|
|
||||||
HoverCard,
|
HoverCard,
|
||||||
|
InputNumber,
|
||||||
|
SelectDropDown,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '~/components';
|
MultiSelectDropDown,
|
||||||
|
} from '~/components/ui';
|
||||||
|
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils';
|
||||||
|
import { useLocalize, useDebouncedInput } from '~/hooks';
|
||||||
|
import { processPlugins, selectPlugins } from '~/utils';
|
||||||
import OptionHover from './OptionHover';
|
import OptionHover from './OptionHover';
|
||||||
import type { TModelSelectProps } from '~/common';
|
|
||||||
import { ESide } from '~/common';
|
import { ESide } from '~/common';
|
||||||
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
import store from '~/store';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
|
export default function Settings({
|
||||||
|
conversation,
|
||||||
|
setOption,
|
||||||
|
setTools,
|
||||||
|
checkPluginSelection,
|
||||||
|
models,
|
||||||
|
readonly,
|
||||||
|
}: TModelSelectProps & {
|
||||||
|
setTools: (newValue: string, remove?: boolean | undefined) => void;
|
||||||
|
checkPluginSelection: (value: string) => boolean;
|
||||||
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
if (!conversation) {
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
return null;
|
const { data: allPlugins } = useAvailablePluginsQuery({
|
||||||
|
select: selectPlugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationTools: TPlugin[] = useMemo(() => {
|
||||||
|
if (!conversation?.tools) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
return processPlugins(conversation.tools, allPlugins?.map);
|
||||||
|
}, [conversation, allPlugins]);
|
||||||
|
|
||||||
|
const availablePlugins = useMemo(() => {
|
||||||
|
if (!availableTools) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(availableTools);
|
||||||
|
}, [availableTools]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
model,
|
model,
|
||||||
|
modelLabel,
|
||||||
chatGptLabel,
|
chatGptLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
temperature,
|
temperature,
|
||||||
top_p: topP,
|
top_p: topP,
|
||||||
frequency_penalty: freqP,
|
frequency_penalty: freqP,
|
||||||
presence_penalty: presP,
|
presence_penalty: presP,
|
||||||
tools,
|
} = conversation ?? {};
|
||||||
} = conversation;
|
|
||||||
|
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput<string | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'chatGptLabel',
|
||||||
|
initialValue: modelLabel ?? chatGptLabel,
|
||||||
|
});
|
||||||
|
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput<string | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'promptPrefix',
|
||||||
|
initialValue: promptPrefix,
|
||||||
|
});
|
||||||
|
const [setTemperature, temperatureValue] = useDebouncedInput<number | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'temperature',
|
||||||
|
initialValue: temperature,
|
||||||
|
});
|
||||||
|
const [setTopP, topPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'top_p',
|
||||||
|
initialValue: topP,
|
||||||
|
});
|
||||||
|
const [setFreqP, freqPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'frequency_penalty',
|
||||||
|
initialValue: freqP,
|
||||||
|
});
|
||||||
|
const [setPresP, presPValue] = useDebouncedInput<number | null | undefined>({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'presence_penalty',
|
||||||
|
initialValue: presP,
|
||||||
|
});
|
||||||
|
|
||||||
const setModel = setOption('model');
|
const setModel = setOption('model');
|
||||||
const setChatGptLabel = setOption('chatGptLabel');
|
|
||||||
const setPromptPrefix = setOption('promptPrefix');
|
|
||||||
const setTemperature = setOption('temperature');
|
|
||||||
const setTopP = setOption('top_p');
|
|
||||||
const setFreqP = setOption('frequency_penalty');
|
|
||||||
const setPresP = setOption('presence_penalty');
|
|
||||||
|
|
||||||
const toolsSelected = tools && tools.length > 0;
|
if (!conversation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-5 gap-6">
|
<div className="grid grid-cols-5 gap-6">
|
||||||
|
|
@ -58,21 +119,14 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_custom_name')}{' '}
|
{localize('com_endpoint_custom_name')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||||
({localize('com_endpoint_default_empty')} |{' '}
|
|
||||||
{localize('com_endpoint_disabled_with_tools')})
|
|
||||||
</small>
|
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="chatGptLabel"
|
id="chatGptLabel"
|
||||||
disabled={readonly || toolsSelected}
|
disabled={readonly}
|
||||||
value={chatGptLabel || ''}
|
value={chatGptLabelValue || ''}
|
||||||
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
|
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
|
||||||
placeholder={
|
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
||||||
toolsSelected
|
|
||||||
? localize('com_endpoint_disabled_with_tools_placeholder')
|
|
||||||
: localize('com_endpoint_openai_custom_name_placeholder')
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||||
|
|
@ -83,21 +137,16 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||||
{localize('com_endpoint_prompt_prefix')}{' '}
|
{localize('com_endpoint_prompt_prefix')}{' '}
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||||
({localize('com_endpoint_default_empty')} |{' '}
|
|
||||||
{localize('com_endpoint_disabled_with_tools')})
|
|
||||||
</small>
|
|
||||||
</Label>
|
</Label>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
id="promptPrefix"
|
id="promptPrefix"
|
||||||
disabled={readonly || toolsSelected}
|
disabled={readonly}
|
||||||
value={promptPrefix || ''}
|
value={promptPrefixValue || ''}
|
||||||
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
|
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
|
||||||
placeholder={
|
placeholder={localize(
|
||||||
toolsSelected
|
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
|
||||||
? localize('com_endpoint_disabled_with_tools_placeholder')
|
)}
|
||||||
: localize('com_endpoint_plug_set_custom_instructions_for_gpt_placeholder')
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ',
|
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ',
|
||||||
|
|
@ -107,6 +156,20 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
<div className="col-span-5 flex flex-col items-center justify-start gap-6 px-3 sm:col-span-2">
|
||||||
|
<MultiSelectDropDown
|
||||||
|
showAbove={false}
|
||||||
|
showLabel={false}
|
||||||
|
setSelected={setTools}
|
||||||
|
value={conversationTools}
|
||||||
|
optionValueKey="pluginKey"
|
||||||
|
availableValues={availablePlugins}
|
||||||
|
isSelected={checkPluginSelection}
|
||||||
|
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
||||||
|
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusOutlines)}
|
||||||
|
optionsClassName="w-full max-h-[275px] dark:bg-gray-700 z-10 border dark:border-gray-600"
|
||||||
|
containerClassName="flex w-full resize-none border border-transparent"
|
||||||
|
labelClassName="dark:text-white"
|
||||||
|
/>
|
||||||
<HoverCard openDelay={300}>
|
<HoverCard openDelay={300}>
|
||||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
@ -119,7 +182,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="temp-int"
|
id="temp-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={temperature}
|
value={temperatureValue}
|
||||||
onChange={(value) => setTemperature(Number(value))}
|
onChange={(value) => setTemperature(Number(value))}
|
||||||
max={2}
|
max={2}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -136,7 +199,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[temperature ?? 0.8]}
|
value={[temperatureValue ?? 0.8]}
|
||||||
onValueChange={(value) => setTemperature(value[0])}
|
onValueChange={(value) => setTemperature(value[0])}
|
||||||
doubleClickHandler={() => setTemperature(0.8)}
|
doubleClickHandler={() => setTemperature(0.8)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
@ -159,7 +222,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="top-p-int"
|
id="top-p-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={topP}
|
value={topPValue}
|
||||||
onChange={(value) => setTopP(Number(value))}
|
onChange={(value) => setTopP(Number(value))}
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -176,7 +239,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[topP ?? 1]}
|
value={[topPValue ?? 1]}
|
||||||
onValueChange={(value) => setTopP(value[0])}
|
onValueChange={(value) => setTopP(value[0])}
|
||||||
doubleClickHandler={() => setTopP(1)}
|
doubleClickHandler={() => setTopP(1)}
|
||||||
max={1}
|
max={1}
|
||||||
|
|
@ -200,7 +263,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="freq-penalty-int"
|
id="freq-penalty-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={freqP}
|
value={freqPValue}
|
||||||
onChange={(value) => setFreqP(Number(value))}
|
onChange={(value) => setFreqP(Number(value))}
|
||||||
max={2}
|
max={2}
|
||||||
min={-2}
|
min={-2}
|
||||||
|
|
@ -217,7 +280,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[freqP ?? 0]}
|
value={[freqPValue ?? 0]}
|
||||||
onValueChange={(value) => setFreqP(value[0])}
|
onValueChange={(value) => setFreqP(value[0])}
|
||||||
doubleClickHandler={() => setFreqP(0)}
|
doubleClickHandler={() => setFreqP(0)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
@ -241,7 +304,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="pres-penalty-int"
|
id="pres-penalty-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={presP}
|
value={presPValue}
|
||||||
onChange={(value) => setPresP(Number(value))}
|
onChange={(value) => setPresP(Number(value))}
|
||||||
max={2}
|
max={2}
|
||||||
min={-2}
|
min={-2}
|
||||||
|
|
@ -258,7 +321,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[presP ?? 0]}
|
value={[presPValue ?? 0]}
|
||||||
onValueChange={(value) => setPresP(value[0])}
|
onValueChange={(value) => setPresP(value[0])}
|
||||||
doubleClickHandler={() => setPresP(0)}
|
doubleClickHandler={() => setPresP(0)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { TModelSelectProps, TBaseSettingsProps, TModels } from '~/common';
|
import type { TModelSelectProps } from '~/common';
|
||||||
import { Google, Plugins, GoogleSettings, PluginSettings } from './MultiView';
|
import { GoogleSettings, PluginSettings } from './MultiView';
|
||||||
import AssistantsSettings from './Assistants';
|
import AssistantsSettings from './Assistants';
|
||||||
import AnthropicSettings from './Anthropic';
|
import AnthropicSettings from './Anthropic';
|
||||||
import BingAISettings from './BingAI';
|
import BingAISettings from './BingAI';
|
||||||
|
|
@ -16,19 +16,7 @@ const settings: { [key: string]: FC<TModelSelectProps> } = {
|
||||||
[EModelEndpoint.anthropic]: AnthropicSettings,
|
[EModelEndpoint.anthropic]: AnthropicSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiViewSettings: { [key: string]: FC<TBaseSettingsProps & TModels> } = {
|
export const getSettings = () => {
|
||||||
[EModelEndpoint.google]: Google,
|
|
||||||
[EModelEndpoint.gptPlugins]: Plugins,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSettings = (isMultiChat = false) => {
|
|
||||||
if (!isMultiChat) {
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
multiViewSettings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
multiViewSettings: {
|
multiViewSettings: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export { default as Icon } from './Icon';
|
export { default as Icon } from './Icon';
|
||||||
export { default as MinimalIcon } from './MinimalIcon';
|
export { default as MinimalIcon } from './MinimalIcon';
|
||||||
|
export { default as ConvoIconURL } from './ConvoIconURL';
|
||||||
export { default as EndpointSettings } from './EndpointSettings';
|
export { default as EndpointSettings } from './EndpointSettings';
|
||||||
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
||||||
export { default as AlternativeSettings } from './AlternativeSettings';
|
export { default as AlternativeSettings } from './AlternativeSettings';
|
||||||
export { default as EndpointOptionsPopover } from './EndpointOptionsPopover';
|
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
|
||||||
import { useMessageHandler, useMediaQuery, useGenerations } from '~/hooks';
|
|
||||||
import { cn } from '~/utils';
|
|
||||||
import Regenerate from './Regenerate';
|
|
||||||
import Continue from './Continue';
|
|
||||||
import Stop from './Stop';
|
|
||||||
|
|
||||||
type GenerationButtonsProps = {
|
|
||||||
endpoint: string;
|
|
||||||
showPopover: boolean;
|
|
||||||
opacityClass: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function GenerationButtons({
|
|
||||||
endpoint,
|
|
||||||
showPopover,
|
|
||||||
opacityClass,
|
|
||||||
}: GenerationButtonsProps) {
|
|
||||||
const {
|
|
||||||
messages,
|
|
||||||
isSubmitting,
|
|
||||||
latestMessage,
|
|
||||||
handleContinue,
|
|
||||||
handleRegenerate,
|
|
||||||
handleStopGenerating,
|
|
||||||
} = useMessageHandler();
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
|
||||||
const { continueSupported, regenerateEnabled } = useGenerations({
|
|
||||||
endpoint,
|
|
||||||
message: latestMessage as TMessage,
|
|
||||||
isSubmitting,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [userStopped, setUserStopped] = useState(false);
|
|
||||||
|
|
||||||
const handleStop = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setUserStopped(true);
|
|
||||||
handleStopGenerating(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
if (userStopped) {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
setUserStopped(false);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
};
|
|
||||||
}, [userStopped]);
|
|
||||||
|
|
||||||
if (isSmallScreen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let button: React.ReactNode = null;
|
|
||||||
|
|
||||||
if (isSubmitting) {
|
|
||||||
button = <Stop onClick={handleStop} />;
|
|
||||||
} else if (userStopped || continueSupported) {
|
|
||||||
button = <Continue onClick={handleContinue} />;
|
|
||||||
} else if (messages && messages.length > 0 && regenerateEnabled) {
|
|
||||||
button = <Regenerate onClick={handleRegenerate} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute bottom-4 right-0 z-[62]">
|
|
||||||
<div className="grow" />
|
|
||||||
<div className="flex items-center md:items-end">
|
|
||||||
<div
|
|
||||||
className={cn('option-buttons', showPopover ? '' : opacityClass)}
|
|
||||||
data-projection-id="173"
|
|
||||||
>
|
|
||||||
{button}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default as GenerationButtons } from './GenerationButtons';
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TConversation } from 'librechat-data-provider';
|
import type { TConversation } from 'librechat-data-provider';
|
||||||
import type { TSetOption } from '~/common';
|
import type { TSetOption } from '~/common';
|
||||||
import { options, multiChatOptions } from './options';
|
import { multiChatOptions } from './options';
|
||||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
|
||||||
|
|
||||||
type TGoogleProps = {
|
type TGoogleProps = {
|
||||||
showExamples: boolean;
|
showExamples: boolean;
|
||||||
|
|
@ -12,14 +12,14 @@ type TSelectProps = {
|
||||||
conversation: TConversation | null;
|
conversation: TConversation | null;
|
||||||
setOption: TSetOption;
|
setOption: TSetOption;
|
||||||
extraProps?: TGoogleProps;
|
extraProps?: TGoogleProps;
|
||||||
isMultiChat?: boolean;
|
|
||||||
showAbove?: boolean;
|
showAbove?: boolean;
|
||||||
|
popover?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ModelSelect({
|
export default function ModelSelect({
|
||||||
conversation,
|
conversation,
|
||||||
setOption,
|
setOption,
|
||||||
isMultiChat = false,
|
popover = false,
|
||||||
showAbove = true,
|
showAbove = true,
|
||||||
}: TSelectProps) {
|
}: TSelectProps) {
|
||||||
const modelsQuery = useGetModelsQuery();
|
const modelsQuery = useGetModelsQuery();
|
||||||
|
|
@ -32,7 +32,7 @@ export default function ModelSelect({
|
||||||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
||||||
const endpoint = endpointType ?? _endpoint;
|
const endpoint = endpointType ?? _endpoint;
|
||||||
|
|
||||||
const OptionComponent = isMultiChat ? multiChatOptions[endpoint] : options[endpoint];
|
const OptionComponent = multiChatOptions[endpoint];
|
||||||
|
|
||||||
if (!OptionComponent) {
|
if (!OptionComponent) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -44,7 +44,7 @@ export default function ModelSelect({
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
models={models}
|
models={models}
|
||||||
showAbove={showAbove}
|
showAbove={showAbove}
|
||||||
popover={isMultiChat}
|
popover={popover}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
|
||||||
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
|
||||||
import type { TModelSelectProps } from '~/common';
|
|
||||||
import { SelectDropDown, MultiSelectDropDown, SelectDropDownPop, Button } from '~/components/ui';
|
|
||||||
import { useSetOptions, useAuthContext, useMediaQuery, useLocalize } from '~/hooks';
|
|
||||||
import { cn, cardStyle } from '~/utils/';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
const pluginStore: TPlugin = {
|
|
||||||
name: 'Plugin store',
|
|
||||||
pluginKey: 'pluginStore',
|
|
||||||
isButton: true,
|
|
||||||
description: '',
|
|
||||||
icon: '',
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Plugins({
|
|
||||||
conversation,
|
|
||||||
setOption,
|
|
||||||
models,
|
|
||||||
showAbove,
|
|
||||||
popover = false,
|
|
||||||
}: TModelSelectProps) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const { data: allPlugins } = useAvailablePluginsQuery();
|
|
||||||
const [visible, setVisibility] = useState<boolean>(true);
|
|
||||||
const [availableTools, setAvailableTools] = useRecoilState(store.availableTools);
|
|
||||||
const { checkPluginSelection, setTools } = useSetOptions();
|
|
||||||
const { user } = useAuthContext();
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 640px)');
|
|
||||||
const Menu = popover ? SelectDropDownPop : SelectDropDown;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSmallScreen) {
|
|
||||||
setVisibility(false);
|
|
||||||
}
|
|
||||||
}, [isSmallScreen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allPlugins) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.plugins || user.plugins.length === 0) {
|
|
||||||
setAvailableTools([pluginStore]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools = [...user.plugins]
|
|
||||||
.map((el) => allPlugins.find((plugin: TPlugin) => plugin.pluginKey === el))
|
|
||||||
.filter((el): el is TPlugin => el !== undefined);
|
|
||||||
|
|
||||||
/* Filter Last Selected Tools */
|
|
||||||
const localStorageItem = localStorage.getItem('lastSelectedTools');
|
|
||||||
if (!localStorageItem) {
|
|
||||||
return setAvailableTools([...tools, pluginStore]);
|
|
||||||
}
|
|
||||||
const lastSelectedTools = JSON.parse(localStorageItem);
|
|
||||||
const filteredTools = lastSelectedTools.filter((tool: TPlugin) =>
|
|
||||||
tools.some((existingTool) => existingTool.pluginKey === tool.pluginKey),
|
|
||||||
);
|
|
||||||
localStorage.setItem('lastSelectedTools', JSON.stringify(filteredTools));
|
|
||||||
|
|
||||||
setAvailableTools([...tools, pluginStore]);
|
|
||||||
// setAvailableTools is a recoil state setter, so it's safe to use it in useEffect
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [allPlugins, user]);
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
cardStyle,
|
|
||||||
'z-40 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 hover:bg-white focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-700',
|
|
||||||
)}
|
|
||||||
onClick={() => setVisibility((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
!visible ? 'rotate-180 transform' : '',
|
|
||||||
'w-4 text-gray-600 dark:text-white',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Menu
|
|
||||||
value={conversation.model ?? ''}
|
|
||||||
setValue={setOption('model')}
|
|
||||||
availableValues={models}
|
|
||||||
showAbove={showAbove}
|
|
||||||
className={cn(cardStyle, 'z-40 flex w-64 min-w-60 sm:w-48', visible ? '' : 'hidden')}
|
|
||||||
/>
|
|
||||||
<MultiSelectDropDown
|
|
||||||
value={conversation.tools || []}
|
|
||||||
isSelected={checkPluginSelection}
|
|
||||||
setSelected={setTools}
|
|
||||||
availableValues={availableTools}
|
|
||||||
optionValueKey="pluginKey"
|
|
||||||
showAbove={showAbove}
|
|
||||||
className={cn(cardStyle, 'z-50 w-64 min-w-60 sm:w-48', visible ? '' : 'hidden')}
|
|
||||||
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import type { TModelSelectProps } from '~/common';
|
import type { TModelSelectProps } from '~/common';
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
MultiSelectPop,
|
||||||
SelectDropDown,
|
SelectDropDown,
|
||||||
SelectDropDownPop,
|
SelectDropDownPop,
|
||||||
MultiSelectDropDown,
|
MultiSelectDropDown,
|
||||||
MultiSelectPop,
|
|
||||||
Button,
|
|
||||||
} from '~/components/ui';
|
} from '~/components/ui';
|
||||||
import { useSetIndexOptions, useAuthContext, useMediaQuery, useLocalize } from '~/hooks';
|
import { useSetIndexOptions, useAuthContext, useMediaQuery, useLocalize } from '~/hooks';
|
||||||
import { cn, cardStyle } from '~/utils/';
|
import { cn, cardStyle, selectPlugins, processPlugins } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const pluginStore: TPlugin = {
|
|
||||||
name: 'Plugin store',
|
|
||||||
pluginKey: 'pluginStore',
|
|
||||||
isButton: true,
|
|
||||||
description: '',
|
|
||||||
icon: '',
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PluginsByIndex({
|
export default function PluginsByIndex({
|
||||||
conversation,
|
conversation,
|
||||||
setOption,
|
setOption,
|
||||||
|
|
@ -33,12 +23,16 @@ export default function PluginsByIndex({
|
||||||
popover = false,
|
popover = false,
|
||||||
}: TModelSelectProps) {
|
}: TModelSelectProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { data: allPlugins } = useAvailablePluginsQuery();
|
|
||||||
const [visible, setVisibility] = useState<boolean>(true);
|
|
||||||
const [availableTools, setAvailableTools] = useRecoilState(store.availableTools);
|
|
||||||
const { checkPluginSelection, setTools } = useSetIndexOptions();
|
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
const [visible, setVisibility] = useState<boolean>(true);
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 640px)');
|
const isSmallScreen = useMediaQuery('(max-width: 640px)');
|
||||||
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
|
const { checkPluginSelection, setTools } = useSetIndexOptions();
|
||||||
|
|
||||||
|
const { data: allPlugins } = useAvailablePluginsQuery({
|
||||||
|
enabled: !!user?.plugins,
|
||||||
|
select: selectPlugins,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
|
|
@ -46,39 +40,20 @@ export default function PluginsByIndex({
|
||||||
}
|
}
|
||||||
}, [isSmallScreen]);
|
}, [isSmallScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
const conversationTools: TPlugin[] = useMemo(() => {
|
||||||
if (!user) {
|
if (!conversation?.tools) {
|
||||||
return;
|
return [];
|
||||||
|
}
|
||||||
|
return processPlugins(conversation.tools, allPlugins?.map);
|
||||||
|
}, [conversation, allPlugins]);
|
||||||
|
|
||||||
|
const availablePlugins = useMemo(() => {
|
||||||
|
if (!availableTools) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allPlugins) {
|
return Object.values(availableTools);
|
||||||
return;
|
}, [availableTools]);
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.plugins || user.plugins.length === 0) {
|
|
||||||
setAvailableTools([pluginStore]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools = [...user.plugins]
|
|
||||||
.map((el) => allPlugins.find((plugin: TPlugin) => plugin.pluginKey === el))
|
|
||||||
.filter((el): el is TPlugin => el !== undefined);
|
|
||||||
|
|
||||||
/* Filter Last Selected Tools */
|
|
||||||
const localStorageItem = localStorage.getItem('lastSelectedTools');
|
|
||||||
if (!localStorageItem) {
|
|
||||||
return setAvailableTools([...tools, pluginStore]);
|
|
||||||
}
|
|
||||||
const lastSelectedTools = JSON.parse(localStorageItem);
|
|
||||||
const filteredTools = lastSelectedTools.filter((tool: TPlugin) =>
|
|
||||||
tools.some((existingTool) => existingTool.pluginKey === tool.pluginKey),
|
|
||||||
);
|
|
||||||
localStorage.setItem('lastSelectedTools', JSON.stringify(filteredTools));
|
|
||||||
|
|
||||||
setAvailableTools([...tools, pluginStore]);
|
|
||||||
// setAvailableTools is a recoil state setter, so it's safe to use it in useEffect
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [allPlugins, user]);
|
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -112,15 +87,19 @@ export default function PluginsByIndex({
|
||||||
availableValues={models}
|
availableValues={models}
|
||||||
showAbove={showAbove}
|
showAbove={showAbove}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
|
className={cn(
|
||||||
|
cardStyle,
|
||||||
|
'z-50 flex h-[40px] w-48 min-w-48 flex-none items-center justify-center px-4 hover:cursor-pointer',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<PluginsMenu
|
<PluginsMenu
|
||||||
value={conversation.tools || []}
|
|
||||||
isSelected={checkPluginSelection}
|
|
||||||
setSelected={setTools}
|
|
||||||
availableValues={availableTools}
|
|
||||||
optionValueKey="pluginKey"
|
|
||||||
showAbove={false}
|
showAbove={false}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
|
setSelected={setTools}
|
||||||
|
value={conversationTools}
|
||||||
|
optionValueKey="pluginKey"
|
||||||
|
availableValues={availablePlugins}
|
||||||
|
isSelected={checkPluginSelection}
|
||||||
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import type { FC } from 'react';
|
||||||
import OpenAI from './OpenAI';
|
import OpenAI from './OpenAI';
|
||||||
import BingAI from './BingAI';
|
import BingAI from './BingAI';
|
||||||
import Google from './Google';
|
import Google from './Google';
|
||||||
import Plugins from './Plugins';
|
|
||||||
import ChatGPT from './ChatGPT';
|
import ChatGPT from './ChatGPT';
|
||||||
import Anthropic from './Anthropic';
|
import Anthropic from './Anthropic';
|
||||||
import PluginsByIndex from './PluginsByIndex';
|
import PluginsByIndex from './PluginsByIndex';
|
||||||
|
|
@ -16,7 +15,6 @@ export const options: { [key: string]: FC<TModelSelectProps> } = {
|
||||||
[EModelEndpoint.azureOpenAI]: OpenAI,
|
[EModelEndpoint.azureOpenAI]: OpenAI,
|
||||||
[EModelEndpoint.bingAI]: BingAI,
|
[EModelEndpoint.bingAI]: BingAI,
|
||||||
[EModelEndpoint.google]: Google,
|
[EModelEndpoint.google]: Google,
|
||||||
[EModelEndpoint.gptPlugins]: Plugins,
|
|
||||||
[EModelEndpoint.anthropic]: Anthropic,
|
[EModelEndpoint.anthropic]: Anthropic,
|
||||||
[EModelEndpoint.chatGPTBrowser]: ChatGPT,
|
[EModelEndpoint.chatGPTBrowser]: ChatGPT,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import { useGetConversationByIdQuery } from 'librechat-data-provider/react-query';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useSetRecoilState, useRecoilState, useRecoilValue } from 'recoil';
|
|
||||||
import copy from 'copy-to-clipboard';
|
|
||||||
import { SubRow, Plugin, MessageContent } from './Content';
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
|
||||||
import MultiMessage from './MultiMessage';
|
|
||||||
import HoverButtons from './HoverButtons';
|
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
|
||||||
import { Icon } from '~/components/Endpoints';
|
|
||||||
import { useMessageHandler, useConversation } from '~/hooks';
|
|
||||||
import type { TMessageProps } from '~/common';
|
|
||||||
import { cn } from '~/utils';
|
|
||||||
import store from '~/store';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function Message(props: TMessageProps) {
|
|
||||||
const {
|
|
||||||
conversation,
|
|
||||||
message,
|
|
||||||
scrollToBottom,
|
|
||||||
currentEditId,
|
|
||||||
setCurrentEditId,
|
|
||||||
siblingIdx,
|
|
||||||
siblingCount,
|
|
||||||
setSiblingIdx,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const setLatestMessage = useSetRecoilState(store.latestMessage);
|
|
||||||
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScroll);
|
|
||||||
const { isSubmitting, ask, regenerate, handleContinue } = useMessageHandler();
|
|
||||||
const { switchToConversation } = useConversation();
|
|
||||||
const { conversationId } = useParams();
|
|
||||||
const isSearching = useRecoilValue(store.isSearching);
|
|
||||||
|
|
||||||
const {
|
|
||||||
text,
|
|
||||||
children,
|
|
||||||
messageId = null,
|
|
||||||
searchResult,
|
|
||||||
isCreatedByUser,
|
|
||||||
error,
|
|
||||||
unfinished,
|
|
||||||
} = message ?? {};
|
|
||||||
|
|
||||||
const isLast = !children?.length;
|
|
||||||
const edit = messageId === currentEditId;
|
|
||||||
const getConversationQuery = useGetConversationByIdQuery(message?.conversationId ?? '', {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const autoScroll = useRecoilValue(store.autoScroll);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSubmitting && scrollToBottom && !abortScroll) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
}, [isSubmitting, text, scrollToBottom, abortScroll]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollToBottom && autoScroll && !isSearching && conversationId !== 'new') {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
}, [autoScroll, conversationId, scrollToBottom, isSearching]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
} else if (isLast) {
|
|
||||||
setLatestMessage({ ...message });
|
|
||||||
}
|
|
||||||
}, [isLast, message, setLatestMessage]);
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enterEdit = (cancel?: boolean) =>
|
|
||||||
setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId);
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (isSubmitting) {
|
|
||||||
setAbortScroll(true);
|
|
||||||
} else {
|
|
||||||
setAbortScroll(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const commonClasses =
|
|
||||||
'w-full border-b text-gray-800 group border-black/10 dark:border-gray-800/50 dark:text-gray-200';
|
|
||||||
const uniqueClasses = isCreatedByUser
|
|
||||||
? 'bg-white dark:bg-gray-800 dark:text-gray-20'
|
|
||||||
: 'bg-gray-50 dark:bg-gray-700 dark:text-gray-100';
|
|
||||||
|
|
||||||
const messageProps = {
|
|
||||||
className: cn(commonClasses, uniqueClasses),
|
|
||||||
titleclass: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = Icon({
|
|
||||||
...conversation,
|
|
||||||
...message,
|
|
||||||
model: message?.model ?? conversation?.model,
|
|
||||||
size: 36,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (message?.bg && searchResult) {
|
|
||||||
messageProps.className = message?.bg?.split('hover')[0];
|
|
||||||
messageProps.titleclass = message?.bg?.split(messageProps.className)[1] + ' cursor-pointer';
|
|
||||||
}
|
|
||||||
|
|
||||||
const regenerateMessage = () => {
|
|
||||||
if (!isSubmitting && !isCreatedByUser) {
|
|
||||||
regenerate(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = (setIsCopied: React.Dispatch<React.SetStateAction<boolean>>) => {
|
|
||||||
setIsCopied(true);
|
|
||||||
copy(text ?? '');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickSearchResult = async () => {
|
|
||||||
if (!searchResult) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await getConversationQuery.refetch({
|
|
||||||
queryKey: [message?.conversationId],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('getConversationQuery response.data:', response.data);
|
|
||||||
|
|
||||||
if (response.data) {
|
|
||||||
switchToConversation(response.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div {...messageProps} onWheel={handleScroll} onTouchMove={handleScroll}>
|
|
||||||
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-4 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
|
||||||
<div className="relative flex h-[40px] w-[40px] flex-col items-end text-right text-xs md:text-sm">
|
|
||||||
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
|
|
||||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
|
||||||
) : (
|
|
||||||
icon
|
|
||||||
)}
|
|
||||||
<div className="sibling-switch invisible absolute left-0 top-2 -ml-4 flex -translate-x-full items-center justify-center gap-1 text-xs group-hover:visible">
|
|
||||||
<SiblingSwitch
|
|
||||||
siblingIdx={siblingIdx}
|
|
||||||
siblingCount={siblingCount}
|
|
||||||
setSiblingIdx={setSiblingIdx}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 md:gap-3 lg:w-[calc(100%-115px)]">
|
|
||||||
{searchResult && (
|
|
||||||
<SubRow
|
|
||||||
classes={messageProps.titleclass + ' rounded'}
|
|
||||||
subclasses="switch-result pl-2 pb-2"
|
|
||||||
onClick={clickSearchResult}
|
|
||||||
>
|
|
||||||
<strong>{`${message?.title} | ${message?.sender}`}</strong>
|
|
||||||
</SubRow>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-grow flex-col gap-3">
|
|
||||||
{/* Legacy Plugins */}
|
|
||||||
{message?.plugin && <Plugin plugin={message?.plugin} />}
|
|
||||||
<MessageContent
|
|
||||||
ask={ask}
|
|
||||||
edit={edit}
|
|
||||||
isLast={isLast}
|
|
||||||
text={text ?? ''}
|
|
||||||
message={message}
|
|
||||||
enterEdit={enterEdit}
|
|
||||||
error={!!(error && !searchResult)}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
unfinished={unfinished ?? false}
|
|
||||||
isCreatedByUser={isCreatedByUser ?? true}
|
|
||||||
siblingIdx={siblingIdx ?? 0}
|
|
||||||
setSiblingIdx={
|
|
||||||
setSiblingIdx ??
|
|
||||||
(() => {
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<HoverButtons
|
|
||||||
isEditing={edit}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
message={message}
|
|
||||||
conversation={conversation ?? null}
|
|
||||||
enterEdit={enterEdit}
|
|
||||||
regenerate={() => regenerateMessage()}
|
|
||||||
handleContinue={handleContinue}
|
|
||||||
copyToClipboard={copyToClipboard}
|
|
||||||
/>
|
|
||||||
<SubRow subclasses="switch-container">
|
|
||||||
<SiblingSwitch
|
|
||||||
siblingIdx={siblingIdx}
|
|
||||||
siblingCount={siblingCount}
|
|
||||||
setSiblingIdx={setSiblingIdx}
|
|
||||||
/>
|
|
||||||
</SubRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MultiMessage
|
|
||||||
messageId={messageId}
|
|
||||||
conversation={conversation}
|
|
||||||
messagesTree={children ?? []}
|
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
currentEditId={currentEditId}
|
|
||||||
setCurrentEditId={setCurrentEditId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import type { TMessageProps } from '~/common';
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
|
||||||
import Message from './Message';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function MultiMessage({
|
|
||||||
messageId,
|
|
||||||
conversation,
|
|
||||||
messagesTree,
|
|
||||||
scrollToBottom,
|
|
||||||
currentEditId,
|
|
||||||
setCurrentEditId,
|
|
||||||
isSearchView,
|
|
||||||
}: TMessageProps) {
|
|
||||||
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
|
||||||
|
|
||||||
const setSiblingIdxRev = (value: number) => {
|
|
||||||
setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
|
||||||
setSiblingIdx(0);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [messagesTree?.length]);
|
|
||||||
|
|
||||||
// if (!messageList?.length) return null;
|
|
||||||
if (!(messagesTree && messagesTree?.length)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siblingIdx >= messagesTree?.length) {
|
|
||||||
setSiblingIdx(0);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = messagesTree[messagesTree.length - siblingIdx - 1];
|
|
||||||
if (isSearchView) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{messagesTree
|
|
||||||
? messagesTree.map((message) => (
|
|
||||||
<Message
|
|
||||||
key={message.messageId}
|
|
||||||
conversation={conversation}
|
|
||||||
message={message}
|
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
currentEditId={currentEditId}
|
|
||||||
setCurrentEditId={null}
|
|
||||||
siblingIdx={1}
|
|
||||||
siblingCount={1}
|
|
||||||
setSiblingIdx={null}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Message
|
|
||||||
key={message.messageId}
|
|
||||||
conversation={conversation}
|
|
||||||
message={message}
|
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
currentEditId={currentEditId}
|
|
||||||
setCurrentEditId={setCurrentEditId}
|
|
||||||
siblingIdx={messagesTree.length - siblingIdx - 1}
|
|
||||||
siblingCount={messagesTree.length}
|
|
||||||
setSiblingIdx={setSiblingIdxRev}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import type { TMessageProps } from '~/common';
|
|
||||||
|
|
||||||
type TSiblingSwitchProps = Pick<TMessageProps, 'siblingIdx' | 'siblingCount' | 'setSiblingIdx'>;
|
|
||||||
|
|
||||||
export default function SiblingSwitch({
|
|
||||||
siblingIdx,
|
|
||||||
siblingCount,
|
|
||||||
setSiblingIdx,
|
|
||||||
}: TSiblingSwitchProps) {
|
|
||||||
if (siblingIdx === undefined) {
|
|
||||||
return null;
|
|
||||||
} else if (siblingCount === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previous = () => {
|
|
||||||
setSiblingIdx && setSiblingIdx(siblingIdx - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
setSiblingIdx && setSiblingIdx(siblingIdx + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return siblingCount > 1 ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="disabled:text-gray-300 dark:text-white dark:disabled:text-gray-400"
|
|
||||||
onClick={previous}
|
|
||||||
disabled={siblingIdx == 0}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="h-3 w-3"
|
|
||||||
height="1em"
|
|
||||||
width="1em"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<polyline points="15 18 9 12 15 6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<span className="flex-shrink-0 flex-grow">
|
|
||||||
{siblingIdx + 1} / {siblingCount}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className="disabled:text-gray-300 dark:text-white dark:disabled:text-gray-400"
|
|
||||||
onClick={next}
|
|
||||||
disabled={siblingIdx == siblingCount - 1}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="h-3 w-3"
|
|
||||||
height="1em"
|
|
||||||
width="1em"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<polyline points="9 18 15 12 9 6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +1,36 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
||||||
import { useLocalize, useNewConvo, useLocalStorage } from '~/hooks';
|
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
||||||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
|
import { useLocalize, useNewConvo } from '~/hooks';
|
||||||
import { NewChatIcon } from '~/components/svg';
|
import { NewChatIcon } from '~/components/svg';
|
||||||
import { getEndpointField } from '~/utils';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function NewChat({
|
export default function NewChat({
|
||||||
|
index = 0,
|
||||||
toggleNav,
|
toggleNav,
|
||||||
subHeaders,
|
subHeaders,
|
||||||
}: {
|
}: {
|
||||||
|
index?: number;
|
||||||
toggleNav: () => void;
|
toggleNav: () => void;
|
||||||
subHeaders?: React.ReactNode;
|
subHeaders?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { newConversation: newConvo } = useNewConvo();
|
/** Note: this component needs an explicit index passed if using more than one */
|
||||||
|
const { newConversation: newConvo } = useNewConvo(index);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const [convo] = useLocalStorage('lastConversationSetup', { endpoint: EModelEndpoint.openAI });
|
const { conversation } = store.useCreateConversationAtom(index);
|
||||||
const { endpoint } = convo;
|
let { endpoint = '' } = conversation ?? {};
|
||||||
|
const iconURL = conversation?.iconURL ?? '';
|
||||||
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
|
|
||||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||||
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType, endpointIconURL });
|
||||||
const Icon = icons[iconKey];
|
const Icon = icons[iconKey];
|
||||||
|
|
||||||
const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
|
@ -47,6 +54,9 @@ export default function NewChat({
|
||||||
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium hover:bg-gray-200 dark:hover:bg-gray-700"
|
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<div className="h-7 w-7 flex-shrink-0">
|
<div className="h-7 w-7 flex-shrink-0">
|
||||||
|
{iconURL && iconURL.includes('http') ? (
|
||||||
|
<ConvoIconURL preset={conversation} endpointIconURL={iconURL} context="nav" />
|
||||||
|
) : (
|
||||||
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
||||||
{endpoint &&
|
{endpoint &&
|
||||||
Icon &&
|
Icon &&
|
||||||
|
|
@ -54,10 +64,12 @@ export default function NewChat({
|
||||||
size: 41,
|
size: 41,
|
||||||
context: 'nav',
|
context: 'nav',
|
||||||
className: 'h-2/3 w-2/3',
|
className: 'h-2/3 w-2/3',
|
||||||
endpoint: endpoint,
|
endpoint,
|
||||||
iconURL: iconURL,
|
endpointType,
|
||||||
|
iconURL: endpointIconURL,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-token-text-primary grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<div className="text-token-text-primary grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||||
{localize('com_ui_new_chat')}
|
{localize('com_ui_new_chat')}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import { Dialog } from '@headlessui/react';
|
import { Dialog } from '@headlessui/react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||||
useAvailablePluginsQuery,
|
import type { TError, TPlugin, TPluginAction } from 'librechat-data-provider';
|
||||||
useUpdateUserPluginsMutation,
|
|
||||||
} from 'librechat-data-provider/react-query';
|
|
||||||
import type { TError, TPluginAction } from 'librechat-data-provider';
|
|
||||||
import type { TPluginStoreDialogProps } from '~/common/types';
|
import type { TPluginStoreDialogProps } from '~/common/types';
|
||||||
import { useLocalize, usePluginDialogHelpers, useSetIndexOptions, useAuthContext } from '~/hooks';
|
import {
|
||||||
|
usePluginDialogHelpers,
|
||||||
|
useSetIndexOptions,
|
||||||
|
usePluginInstall,
|
||||||
|
useAuthContext,
|
||||||
|
useLocalize,
|
||||||
|
} from '~/hooks';
|
||||||
import PluginPagination from './PluginPagination';
|
import PluginPagination from './PluginPagination';
|
||||||
import PluginStoreItem from './PluginStoreItem';
|
import PluginStoreItem from './PluginStoreItem';
|
||||||
import PluginAuthForm from './PluginAuthForm';
|
import PluginAuthForm from './PluginAuthForm';
|
||||||
|
|
@ -16,7 +19,6 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const { data: availablePlugins } = useAvailablePluginsQuery();
|
const { data: availablePlugins } = useAvailablePluginsQuery();
|
||||||
const updateUserPlugins = useUpdateUserPluginsMutation();
|
|
||||||
const { setTools } = useSetIndexOptions();
|
const { setTools } = useSetIndexOptions();
|
||||||
|
|
||||||
const [userPlugins, setUserPlugins] = useState<string[]>([]);
|
const [userPlugins, setUserPlugins] = useState<string[]>([]);
|
||||||
|
|
@ -44,7 +46,8 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
setSelectedPlugin,
|
setSelectedPlugin,
|
||||||
} = usePluginDialogHelpers();
|
} = usePluginDialogHelpers();
|
||||||
|
|
||||||
const handleInstallError = (error: TError) => {
|
const handleInstallError = useCallback(
|
||||||
|
(error: TError) => {
|
||||||
setError(true);
|
setError(true);
|
||||||
if (error.response?.data?.message) {
|
if (error.response?.data?.message) {
|
||||||
setErrorMessage(error.response?.data?.message);
|
setErrorMessage(error.response?.data?.message);
|
||||||
|
|
@ -53,41 +56,39 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
},
|
||||||
|
[setError, setErrorMessage],
|
||||||
|
);
|
||||||
|
|
||||||
const handleInstall = (pluginAction: TPluginAction) => {
|
const { installPlugin, uninstallPlugin } = usePluginInstall({
|
||||||
updateUserPlugins.mutate(pluginAction, {
|
onInstallError: handleInstallError,
|
||||||
onError: (error: unknown) => {
|
onUninstallError: handleInstallError,
|
||||||
handleInstallError(error as TError);
|
onUninstallSuccess: (_data, variables) => {
|
||||||
|
setTools(variables.pluginKey, true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleInstall = (pluginAction: TPluginAction, plugin?: TPlugin) => {
|
||||||
|
if (!plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installPlugin(pluginAction, plugin);
|
||||||
setShowPluginAuthForm(false);
|
setShowPluginAuthForm(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPluginUninstall = (plugin: string) => {
|
|
||||||
updateUserPlugins.mutate(
|
|
||||||
{ pluginKey: plugin, action: 'uninstall', auth: null },
|
|
||||||
{
|
|
||||||
onError: (error: unknown) => {
|
|
||||||
handleInstallError(error as TError);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
setTools(plugin, true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPluginInstall = (pluginKey: string) => {
|
const onPluginInstall = (pluginKey: string) => {
|
||||||
const getAvailablePluginFromKey = availablePlugins?.find((p) => p.pluginKey === pluginKey);
|
const plugin = availablePlugins?.find((p) => p.pluginKey === pluginKey);
|
||||||
setSelectedPlugin(getAvailablePluginFromKey);
|
if (!plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedPlugin(plugin);
|
||||||
|
|
||||||
const { authConfig, authenticated } = getAvailablePluginFromKey ?? {};
|
const { authConfig, authenticated } = plugin ?? {};
|
||||||
|
|
||||||
if (authConfig && authConfig.length > 0 && !authenticated) {
|
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||||
setShowPluginAuthForm(true);
|
setShowPluginAuthForm(true);
|
||||||
} else {
|
} else {
|
||||||
handleInstall({ pluginKey, action: 'install', auth: null });
|
handleInstall({ pluginKey, action: 'install', auth: null }, plugin);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -107,10 +108,17 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
setSearchChanged(false);
|
setSearchChanged(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
// Disabled due to state setters erroneously being flagged as dependencies
|
availablePlugins,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
itemsPerPage,
|
||||||
}, [availablePlugins, itemsPerPage, user, searchValue, filteredPlugins, searchChanged]);
|
user,
|
||||||
|
searchValue,
|
||||||
|
filteredPlugins,
|
||||||
|
searchChanged,
|
||||||
|
setMaxPage,
|
||||||
|
setCurrentPage,
|
||||||
|
setSearchChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
@ -165,7 +173,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
<div className="p-4 sm:p-6 sm:pt-4">
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
<PluginAuthForm
|
<PluginAuthForm
|
||||||
plugin={selectedPlugin}
|
plugin={selectedPlugin}
|
||||||
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
onSubmit={(action: TPluginAction) => handleInstall(action, selectedPlugin)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -197,7 +205,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
isInstalled={userPlugins.includes(plugin.pluginKey)}
|
isInstalled={userPlugins.includes(plugin.pluginKey)}
|
||||||
onInstall={() => onPluginInstall(plugin.pluginKey)}
|
onInstall={() => onPluginInstall(plugin.pluginKey)}
|
||||||
onUninstall={() => onPluginUninstall(plugin.pluginKey)}
|
onUninstall={() => uninstallPlugin(plugin.pluginKey)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { Combobox } from '~/components/ui';
|
import { Combobox } from '~/components/ui';
|
||||||
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
import { EModelEndpoint, defaultOrderQuery, LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import type { SwitcherProps } from '~/common';
|
import type { SwitcherProps } from '~/common';
|
||||||
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
|
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
|
|
@ -25,7 +25,9 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
|
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
|
||||||
const assistant_id =
|
const assistant_id =
|
||||||
localStorage.getItem(`assistant_id__${index}`) ?? assistants[0]?.id ?? '';
|
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`) ??
|
||||||
|
assistants[0]?.id ??
|
||||||
|
'';
|
||||||
const assistant = assistantMap?.[assistant_id];
|
const assistant = assistantMap?.[assistant_id];
|
||||||
|
|
||||||
if (!assistant) {
|
if (!assistant) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
|
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
|
||||||
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
||||||
import { useGetEndpointsQuery, useUserKeyQuery } from 'librechat-data-provider/react-query';
|
import {
|
||||||
|
useGetEndpointsQuery,
|
||||||
|
useGetStartupConfig,
|
||||||
|
useUserKeyQuery,
|
||||||
|
} from 'librechat-data-provider/react-query';
|
||||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||||
import { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider';
|
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||||
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
||||||
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
|
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
|
||||||
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
||||||
import { useMediaQuery, useLocalStorage } from '~/hooks';
|
import { useMediaQuery, useLocalStorage } from '~/hooks';
|
||||||
import { Separator } from '~/components/ui/Separator';
|
|
||||||
import NavToggle from '~/components/Nav/NavToggle';
|
import NavToggle from '~/components/Nav/NavToggle';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import Switcher from './Switcher';
|
import Switcher from './Switcher';
|
||||||
|
|
@ -23,6 +27,7 @@ interface SidePanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultMinSize = 20;
|
const defaultMinSize = 20;
|
||||||
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
const SidePanel = ({
|
const SidePanel = ({
|
||||||
defaultLayout = [97, 3],
|
defaultLayout = [97, 3],
|
||||||
|
|
@ -38,6 +43,12 @@ const SidePanel = ({
|
||||||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const interfaceConfig = useMemo(
|
||||||
|
() => startupConfig?.interface ?? defaultInterface,
|
||||||
|
[startupConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const { endpoint } = conversation ?? {};
|
const { endpoint } = conversation ?? {};
|
||||||
|
|
@ -69,7 +80,7 @@ const SidePanel = ({
|
||||||
panelRef.current?.collapse();
|
panelRef.current?.collapse();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const Links = useSideNavLinks({ hidePanel, assistants, keyProvided, endpoint });
|
const Links = useSideNavLinks({ hidePanel, assistants, keyProvided, endpoint, interfaceConfig });
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const throttledSaveLayout = useCallback(
|
const throttledSaveLayout = useCallback(
|
||||||
|
|
@ -181,6 +192,7 @@ const SidePanel = ({
|
||||||
: 'opacity-100',
|
: 'opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{interfaceConfig.modelSelect && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-white dark:bg-gray-850',
|
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-white dark:bg-gray-850',
|
||||||
|
|
@ -192,8 +204,8 @@ const SidePanel = ({
|
||||||
endpointKeyProvided={keyProvided}
|
endpointKeyProvided={keyProvided}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
/>
|
/>
|
||||||
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<Nav
|
<Nav
|
||||||
resize={panelRef.current?.resize}
|
resize={panelRef.current?.resize}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { SwitcherProps } from '~/common';
|
import type { SwitcherProps } from '~/common';
|
||||||
|
import { Separator } from '~/components/ui/Separator';
|
||||||
import AssistantSwitcher from './AssistantSwitcher';
|
import AssistantSwitcher from './AssistantSwitcher';
|
||||||
import ModelSwitcher from './ModelSwitcher';
|
import ModelSwitcher from './ModelSwitcher';
|
||||||
|
|
||||||
export default function Switcher(props: SwitcherProps) {
|
export default function Switcher(props: SwitcherProps) {
|
||||||
if (props.endpoint === EModelEndpoint.assistants && props.endpointKeyProvided) {
|
if (props.endpoint === EModelEndpoint.assistants && props.endpointKeyProvided) {
|
||||||
return <AssistantSwitcher {...props} />;
|
return (
|
||||||
|
<>
|
||||||
|
<AssistantSwitcher {...props} />
|
||||||
|
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
} else if (props.endpoint === EModelEndpoint.assistants) {
|
} else if (props.endpoint === EModelEndpoint.assistants) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ModelSwitcher {...props} />;
|
return (
|
||||||
|
<>
|
||||||
|
<ModelSwitcher {...props} />
|
||||||
|
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export type TMultiSelectDropDownProps = {
|
||||||
showAbove?: boolean;
|
showAbove?: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
optionsClassName?: string;
|
||||||
|
labelClassName?: string;
|
||||||
isSelected: (value: string) => boolean;
|
isSelected: (value: string) => boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
|
|
@ -31,6 +33,8 @@ function MultiSelectDropDown({
|
||||||
showAbove = false,
|
showAbove = false,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
optionsClassName = '',
|
||||||
|
labelClassName = '',
|
||||||
isSelected,
|
isSelected,
|
||||||
className,
|
className,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
|
|
@ -72,7 +76,7 @@ function MultiSelectDropDown({
|
||||||
<>
|
<>
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
|
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
|
||||||
className ?? '',
|
className ?? '',
|
||||||
)}
|
)}
|
||||||
id={excludeIds[0]}
|
id={excludeIds[0]}
|
||||||
|
|
@ -82,7 +86,7 @@ function MultiSelectDropDown({
|
||||||
{' '}
|
{' '}
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<Listbox.Label
|
<Listbox.Label
|
||||||
className="block text-xs text-gray-700 dark:text-gray-500"
|
className={cn('block text-xs text-gray-700 dark:text-gray-500', labelClassName)}
|
||||||
id={excludeIds[1]}
|
id={excludeIds[1]}
|
||||||
data-headlessui-state=""
|
data-headlessui-state=""
|
||||||
>
|
>
|
||||||
|
|
@ -153,6 +157,7 @@ function MultiSelectDropDown({
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
|
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
|
||||||
|
optionsClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{searchRender}
|
{searchRender}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import { Wrench } from 'lucide-react';
|
import { Wrench } from 'lucide-react';
|
||||||
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
|
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
|
|
@ -115,7 +114,7 @@ function MultiSelectPop({
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="center"
|
align="center"
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 max-h-60 min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
|
'mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
|
||||||
hasSearchRender && 'relative',
|
hasSearchRender && 'relative',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
|
|
@ -264,10 +265,10 @@ export const useLogoutUserMutation = (
|
||||||
onMutate: (...args) => {
|
onMutate: (...args) => {
|
||||||
setDefaultPreset(null);
|
setDefaultPreset(null);
|
||||||
queryClient.removeQueries();
|
queryClient.removeQueries();
|
||||||
localStorage.removeItem('lastConversationSetup');
|
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
||||||
localStorage.removeItem('lastSelectedModel');
|
localStorage.removeItem(LocalStorageKeys.LAST_MODEL);
|
||||||
localStorage.removeItem('lastSelectedTools');
|
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||||
localStorage.removeItem('filesToDelete');
|
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||||
// localStorage.removeItem('lastAssistant');
|
// localStorage.removeItem('lastAssistant');
|
||||||
options?.onMutate?.(...args);
|
options?.onMutate?.(...args);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
||||||
import type { TConversation, TPreset } from 'librechat-data-provider';
|
import type { TConversation, TPreset } from 'librechat-data-provider';
|
||||||
|
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||||
import { useListAssistantsQuery } from '~/data-provider';
|
import { useListAssistantsQuery } from '~/data-provider';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
|
||||||
import { mapAssistants } from '~/utils';
|
import { mapAssistants } from '~/utils';
|
||||||
|
|
||||||
export default function useSelectAssistant() {
|
export default function useSelectAssistant() {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { default as useConfigOverride } from './useConfigOverride';
|
export { default as useAppStartup } from './useAppStartup';
|
||||||
|
|
|
||||||
101
client/src/hooks/Config/useAppStartup.ts
Normal file
101
client/src/hooks/Config/useAppStartup.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
|
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
|
||||||
|
import { data as modelSpecs } from '~/components/Chat/Menus/Models/fakeData';
|
||||||
|
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
|
||||||
|
import useConfigOverride from './useConfigOverride';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
const pluginStore: TPlugin = {
|
||||||
|
name: 'Plugin store',
|
||||||
|
pluginKey: 'pluginStore',
|
||||||
|
isButton: true,
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
|
authConfig: [],
|
||||||
|
authenticated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useAppStartup({
|
||||||
|
startupConfig,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
startupConfig?: TStartupConfig;
|
||||||
|
user?: TUser;
|
||||||
|
}) {
|
||||||
|
useConfigOverride();
|
||||||
|
const setAvailableTools = useSetRecoilState(store.availableTools);
|
||||||
|
const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
||||||
|
const { data: allPlugins } = useAvailablePluginsQuery({
|
||||||
|
enabled: !!user?.plugins,
|
||||||
|
select: selectPlugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Set the app title */
|
||||||
|
useEffect(() => {
|
||||||
|
if (startupConfig?.appTitle) {
|
||||||
|
document.title = startupConfig.appTitle;
|
||||||
|
localStorage.setItem(LocalStorageKeys.APP_TITLE, startupConfig.appTitle);
|
||||||
|
}
|
||||||
|
}, [startupConfig]);
|
||||||
|
|
||||||
|
/** Set the default spec's preset as default */
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultPreset && defaultPreset.spec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelSpecs || !modelSpecs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSpec = modelSpecs.find((spec) => spec.default);
|
||||||
|
|
||||||
|
if (!defaultSpec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultPreset({
|
||||||
|
...defaultSpec.preset,
|
||||||
|
iconURL: defaultSpec.iconURL,
|
||||||
|
spec: defaultSpec.name,
|
||||||
|
});
|
||||||
|
}, [defaultPreset, setDefaultPreset]);
|
||||||
|
|
||||||
|
/** Set the available Plugins */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allPlugins) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.plugins || user.plugins.length === 0) {
|
||||||
|
setAvailableTools({ pluginStore });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = [...user.plugins]
|
||||||
|
.map((el) => allPlugins.map[el])
|
||||||
|
.filter((el): el is TPlugin => el !== undefined);
|
||||||
|
|
||||||
|
/* Filter Last Selected Tools */
|
||||||
|
const localStorageItem = localStorage.getItem(LocalStorageKeys.LAST_TOOLS);
|
||||||
|
if (!localStorageItem) {
|
||||||
|
return setAvailableTools({ pluginStore, ...mapPlugins(tools) });
|
||||||
|
}
|
||||||
|
const lastSelectedTools = processPlugins(JSON.parse(localStorageItem) ?? [], allPlugins.map);
|
||||||
|
const filteredTools = lastSelectedTools
|
||||||
|
.filter((tool: TPlugin) =>
|
||||||
|
tools.some((existingTool) => existingTool.pluginKey === tool.pluginKey),
|
||||||
|
)
|
||||||
|
.filter((tool: TPlugin) => !!tool);
|
||||||
|
localStorage.setItem(LocalStorageKeys.LAST_TOOLS, JSON.stringify(filteredTools));
|
||||||
|
|
||||||
|
setAvailableTools({ pluginStore, ...mapPlugins(tools) });
|
||||||
|
}, [allPlugins, user, setAvailableTools]);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
export { default as usePresets } from './usePresets';
|
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 useConversation } from './useConversation';
|
||||||
|
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 useSetIndexOptions } from './useSetIndexOptions';
|
||||||
export { default as useParameterEffects } from './useParameterEffects';
|
export { default as useParameterEffects } from './useParameterEffects';
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import type {
|
||||||
TEndpointsConfig,
|
TEndpointsConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||||
import useOriginNavigate from './useOriginNavigate';
|
import useOriginNavigate from '../useOriginNavigate';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const useConversation = () => {
|
const useConversation = () => {
|
||||||
|
|
@ -3,17 +3,14 @@ import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
import { QueryKeys } 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';
|
||||||
import useOriginNavigate from './useOriginNavigate';
|
import useOriginNavigate from '../useOriginNavigate';
|
||||||
import useSetStorage from './useSetStorage';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const useNavigateToConvo = (index = 0) => {
|
const useNavigateToConvo = (index = 0) => {
|
||||||
const setStorage = useSetStorage();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useOriginNavigate();
|
const navigate = useOriginNavigate();
|
||||||
const { setConversation } = store.useCreateConversationAtom(index);
|
const { setConversation } = store.useCreateConversationAtom(index);
|
||||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||||
// const setConversation = useSetRecoilState(store.conversationByIndex(index));
|
|
||||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||||
|
|
||||||
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
||||||
|
|
@ -50,7 +47,6 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
models,
|
models,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setStorage(convo);
|
|
||||||
setConversation(convo);
|
setConversation(convo);
|
||||||
navigate(convo?.conversationId);
|
navigate(convo?.conversationId);
|
||||||
};
|
};
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { TPreset } from 'librechat-data-provider';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import type { TPreset, TPlugin } from 'librechat-data-provider';
|
||||||
import type { TSetOptionsPayload, TSetExample, TSetOption } from '~/common';
|
import type { TSetOptionsPayload, TSetExample, TSetOption } from '~/common';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import { cleanupPreset } from '~/utils';
|
import { cleanupPreset } from '~/utils';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
type TUsePresetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload | boolean;
|
type TUsePresetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload | boolean;
|
||||||
|
|
||||||
const usePresetOptions: TUsePresetOptions = (_preset) => {
|
const usePresetIndexOptions: TUsePresetOptions = (_preset) => {
|
||||||
|
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
|
||||||
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
const { preset, setPreset } = useChatContext();
|
const { preset, setPreset } = useChatContext();
|
||||||
|
|
||||||
if (!_preset) {
|
if (!_preset) {
|
||||||
|
|
@ -99,9 +103,52 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkPluginSelection: (value: string) => boolean = () => false;
|
function checkPluginSelection(value: string) {
|
||||||
const setTools: (newValue: string) => void = () => {
|
if (!preset?.tools) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return preset.tools.find((el) => {
|
||||||
|
if (typeof el === 'string') {
|
||||||
|
return el === value;
|
||||||
|
}
|
||||||
|
return el.pluginKey === value;
|
||||||
|
})
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTools: (newValue: string, remove?: boolean) => void = (newValue, remove) => {
|
||||||
|
if (newValue === 'pluginStore') {
|
||||||
|
setShowPluginStoreDialog(true);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = {};
|
||||||
|
const current =
|
||||||
|
preset?.tools
|
||||||
|
?.map((tool: string | TPlugin) => {
|
||||||
|
if (typeof tool === 'string') {
|
||||||
|
return availableTools[tool];
|
||||||
|
}
|
||||||
|
return tool;
|
||||||
|
})
|
||||||
|
?.filter((el) => !!el) || [];
|
||||||
|
const isSelected = checkPluginSelection(newValue);
|
||||||
|
const tool = availableTools[newValue];
|
||||||
|
if (isSelected || remove) {
|
||||||
|
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
|
||||||
|
} else {
|
||||||
|
update['tools'] = [...current, tool];
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreset((prevState) =>
|
||||||
|
cleanupPreset({
|
||||||
|
preset: {
|
||||||
|
...prevState,
|
||||||
|
...update,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -116,4 +163,4 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePresetOptions;
|
export default usePresetIndexOptions;
|
||||||
|
|
@ -11,9 +11,9 @@ import {
|
||||||
useDeletePresetMutation,
|
useDeletePresetMutation,
|
||||||
useGetPresetsQuery,
|
useGetPresetsQuery,
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
|
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
|
||||||
|
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||||
import { useChatContext, useToastContext } from '~/Providers';
|
import { useChatContext, useToastContext } from '~/Providers';
|
||||||
import { cleanupPreset, getEndpointField } from '~/utils';
|
|
||||||
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
|
@ -28,6 +28,7 @@ export default function usePresets() {
|
||||||
const { user, isAuthenticated } = useAuthContext();
|
const { user, isAuthenticated } = useAuthContext();
|
||||||
|
|
||||||
const modularChat = useRecoilValue(store.modularChat);
|
const modularChat = useRecoilValue(store.modularChat);
|
||||||
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
|
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
|
||||||
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
||||||
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
|
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
|
||||||
|
|
@ -151,11 +152,13 @@ export default function usePresets() {
|
||||||
importPreset(jsonPreset);
|
importPreset(jsonPreset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectPreset = (newPreset: TPreset) => {
|
const onSelectPreset = (_newPreset: TPreset) => {
|
||||||
if (!newPreset) {
|
if (!_newPreset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newPreset = removeUnavailableTools(_newPreset, availableTools);
|
||||||
|
|
||||||
const toastTitle = newPreset.title
|
const toastTitle = newPreset.title
|
||||||
? `"${newPreset.title}"`
|
? `"${newPreset.title}"`
|
||||||
: localize('com_endpoint_preset_title');
|
: localize('com_endpoint_preset_title');
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import { TPreset, TPlugin, TConversation, tConvoUpdateSchema } from 'librechat-data-provider';
|
||||||
TPreset,
|
|
||||||
TPlugin,
|
|
||||||
tConvoUpdateSchema,
|
|
||||||
EModelEndpoint,
|
|
||||||
TConversation,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
|
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
|
||||||
import usePresetIndexOptions from './usePresetIndexOptions';
|
import usePresetIndexOptions from './usePresetIndexOptions';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import useLocalStorage from './useLocalStorage';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
|
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
|
||||||
|
|
@ -18,11 +11,6 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
||||||
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
|
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
|
||||||
const availableTools = useRecoilValue(store.availableTools);
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
const { conversation, setConversation } = useChatContext();
|
const { conversation, setConversation } = useChatContext();
|
||||||
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
|
|
||||||
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {
|
|
||||||
primaryModel: '',
|
|
||||||
secondaryModel: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = usePresetIndexOptions(preset);
|
const result = usePresetIndexOptions(preset);
|
||||||
|
|
||||||
|
|
@ -31,16 +19,10 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setOption: TSetOption = (param) => (newValue) => {
|
const setOption: TSetOption = (param) => (newValue) => {
|
||||||
const { endpoint } = conversation ?? {};
|
|
||||||
const update = {};
|
const update = {};
|
||||||
update[param] = newValue;
|
update[param] = newValue;
|
||||||
|
|
||||||
if (param === 'model' && endpoint) {
|
if (param === 'presetOverride') {
|
||||||
const lastModelUpdate = { ...lastModel, [endpoint]: newValue };
|
|
||||||
setLastModel(lastModelUpdate);
|
|
||||||
} else if (param === 'jailbreak' && endpoint) {
|
|
||||||
setLastBingSettings({ ...lastBingSettings, jailbreak: newValue });
|
|
||||||
} else if (param === 'presetOverride') {
|
|
||||||
const currentOverride = conversation?.presetOverride || {};
|
const currentOverride = conversation?.presetOverride || {};
|
||||||
update['presetOverride'] = {
|
update['presetOverride'] = {
|
||||||
...currentOverride,
|
...currentOverride,
|
||||||
|
|
@ -116,7 +98,14 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
||||||
if (!conversation?.tools) {
|
if (!conversation?.tools) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return conversation.tools.find((el) => el.pluginKey === value) ? true : false;
|
return conversation.tools.find((el) => {
|
||||||
|
if (typeof el === 'string') {
|
||||||
|
return el === value;
|
||||||
|
}
|
||||||
|
return el.pluginKey === value;
|
||||||
|
})
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAgentOption: TSetOption = (param) => (newValue) => {
|
const setAgentOption: TSetOption = (param) => (newValue) => {
|
||||||
|
|
@ -124,12 +113,7 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
||||||
const convo = JSON.parse(editableConvo);
|
const convo = JSON.parse(editableConvo);
|
||||||
const { agentOptions } = convo;
|
const { agentOptions } = convo;
|
||||||
agentOptions[param] = newValue;
|
agentOptions[param] = newValue;
|
||||||
console.log('agentOptions', agentOptions, param, newValue);
|
|
||||||
if (param === 'model' && typeof newValue === 'string') {
|
|
||||||
const lastModelUpdate = { ...lastModel, [EModelEndpoint.gptPlugins]: newValue };
|
|
||||||
lastModelUpdate.secondaryModel = newValue;
|
|
||||||
setLastModel(lastModelUpdate);
|
|
||||||
}
|
|
||||||
setConversation(
|
setConversation(
|
||||||
(prevState) =>
|
(prevState) =>
|
||||||
tConvoUpdateSchema.parse({
|
tConvoUpdateSchema.parse({
|
||||||
|
|
@ -146,17 +130,23 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = {};
|
const update = {};
|
||||||
const current = conversation?.tools || [];
|
const current =
|
||||||
|
conversation?.tools
|
||||||
|
?.map((tool: string | TPlugin) => {
|
||||||
|
if (typeof tool === 'string') {
|
||||||
|
return availableTools[tool];
|
||||||
|
}
|
||||||
|
return tool;
|
||||||
|
})
|
||||||
|
?.filter((el) => !!el) || [];
|
||||||
const isSelected = checkPluginSelection(newValue);
|
const isSelected = checkPluginSelection(newValue);
|
||||||
const tool =
|
const tool = availableTools[newValue];
|
||||||
availableTools[availableTools.findIndex((el: TPlugin) => el.pluginKey === newValue)];
|
|
||||||
if (isSelected || remove) {
|
if (isSelected || remove) {
|
||||||
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
|
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
|
||||||
} else {
|
} else {
|
||||||
update['tools'] = [...current, tool];
|
update['tools'] = [...current, tool];
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
|
|
||||||
setConversation(
|
setConversation(
|
||||||
(prevState) =>
|
(prevState) =>
|
||||||
tConvoUpdateSchema.parse({
|
tConvoUpdateSchema.parse({
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
|
|
||||||
export default function useSetFilesToDelete() {
|
export default function useSetFilesToDelete() {
|
||||||
const setFilesToDelete = (files: Record<string, unknown>) =>
|
const setFilesToDelete = (files: Record<string, unknown>) =>
|
||||||
localStorage.setItem('filesToDelete', JSON.stringify(files));
|
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify(files));
|
||||||
return setFilesToDelete;
|
return setFilesToDelete;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
import { EModelEndpoint, ContentTypes } from 'librechat-data-provider';
|
import { EModelEndpoint, ContentTypes } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
import Icon from '~/components/Endpoints/Icon';
|
|
||||||
import { getEndpointField } from '~/utils';
|
|
||||||
|
|
||||||
export default function useMessageHelpers(props: TMessageProps) {
|
export default function useMessageHelpers(props: TMessageProps) {
|
||||||
const latestText = useRef<string | number>('');
|
const latestText = useRef<string | number>('');
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
|
||||||
const { message, currentEditId, setCurrentEditId } = props;
|
const { message, currentEditId, setCurrentEditId } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -62,18 +57,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
const assistant =
|
const assistant =
|
||||||
conversation?.endpoint === EModelEndpoint.assistants && assistantMap?.[message?.model ?? ''];
|
conversation?.endpoint === EModelEndpoint.assistants && assistantMap?.[message?.model ?? ''];
|
||||||
|
|
||||||
const iconEndpoint = message?.endpoint ?? conversation?.endpoint;
|
|
||||||
const icon = Icon({
|
|
||||||
...conversation,
|
|
||||||
...(message as TMessage),
|
|
||||||
iconURL: !assistant
|
|
||||||
? getEndpointField(endpointsConfig, iconEndpoint, 'iconURL')
|
|
||||||
: (assistant?.metadata?.avatar as string | undefined) ?? '',
|
|
||||||
model: message?.model ?? conversation?.model,
|
|
||||||
assistantName: assistant ? (assistant.name as string | undefined) : '',
|
|
||||||
size: 28.8,
|
|
||||||
});
|
|
||||||
|
|
||||||
const regenerateMessage = () => {
|
const regenerateMessage = () => {
|
||||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
if ((isSubmitting && isCreatedByUser) || !message) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -105,7 +88,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ask,
|
ask,
|
||||||
icon,
|
|
||||||
edit,
|
edit,
|
||||||
isLast,
|
isLast,
|
||||||
assistant,
|
assistant,
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
// Settings2,
|
// Settings2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TConfig } from 'librechat-data-provider';
|
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
|
||||||
import type { NavLink } from '~/common';
|
import type { NavLink } from '~/common';
|
||||||
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
||||||
// import Parameters from '~/components/SidePanel/Parameters/Panel';
|
// import Parameters from '~/components/SidePanel/Parameters/Panel';
|
||||||
|
|
@ -16,11 +16,13 @@ export default function useSideNavLinks({
|
||||||
assistants,
|
assistants,
|
||||||
keyProvided,
|
keyProvided,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
interfaceConfig,
|
||||||
}: {
|
}: {
|
||||||
hidePanel: () => void;
|
hidePanel: () => void;
|
||||||
assistants?: TConfig | null;
|
assistants?: TConfig | null;
|
||||||
keyProvided: boolean;
|
keyProvided: boolean;
|
||||||
endpoint?: EModelEndpoint | null;
|
endpoint?: EModelEndpoint | null;
|
||||||
|
interfaceConfig: Partial<TInterfaceConfig>;
|
||||||
}) {
|
}) {
|
||||||
const Links = useMemo(() => {
|
const Links = useMemo(() => {
|
||||||
const links: NavLink[] = [];
|
const links: NavLink[] = [];
|
||||||
|
|
@ -37,7 +39,8 @@ export default function useSideNavLinks({
|
||||||
endpoint === EModelEndpoint.assistants &&
|
endpoint === EModelEndpoint.assistants &&
|
||||||
assistants &&
|
assistants &&
|
||||||
assistants.disableBuilder !== true &&
|
assistants.disableBuilder !== true &&
|
||||||
keyProvided
|
keyProvided &&
|
||||||
|
interfaceConfig.parameters
|
||||||
) {
|
) {
|
||||||
links.push({
|
links.push({
|
||||||
title: 'com_sidepanel_assistant_builder',
|
title: 'com_sidepanel_assistant_builder',
|
||||||
|
|
@ -65,7 +68,7 @@ export default function useSideNavLinks({
|
||||||
});
|
});
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
}, [assistants, keyProvided, hidePanel, endpoint]);
|
}, [assistants, keyProvided, hidePanel, endpoint, interfaceConfig.parameters]);
|
||||||
|
|
||||||
return Links;
|
return Links;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
export { default as usePluginInstall } from './usePluginInstall';
|
||||||
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';
|
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';
|
||||||
|
|
|
||||||
77
client/src/hooks/Plugins/usePluginInstall.ts
Normal file
77
client/src/hooks/Plugins/usePluginInstall.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// hooks/Plugins/usePluginInstall.ts
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import type {
|
||||||
|
TError,
|
||||||
|
TUser,
|
||||||
|
TUpdateUserPlugins,
|
||||||
|
TPlugin,
|
||||||
|
TPluginAction,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
interface PluginStoreHandlers {
|
||||||
|
onInstallError?: (error: TError) => void;
|
||||||
|
onUninstallError?: (error: TError) => void;
|
||||||
|
onInstallSuccess?: (data: TUser, variables: TUpdateUserPlugins, context: unknown) => void;
|
||||||
|
onUninstallSuccess?: (data: TUser, variables: TUpdateUserPlugins, context: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function usePluginInstall(handlers: PluginStoreHandlers = {}) {
|
||||||
|
const setAvailableTools = useSetRecoilState(store.availableTools);
|
||||||
|
const { onInstallError, onInstallSuccess, onUninstallError, onUninstallSuccess } = handlers;
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
|
||||||
|
const installPlugin = useCallback(
|
||||||
|
(pluginAction: TPluginAction, plugin: TPlugin) => {
|
||||||
|
updateUserPlugins.mutate(pluginAction, {
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
if (onInstallError) {
|
||||||
|
onInstallError(error as TError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (...rest) => {
|
||||||
|
setAvailableTools((prev) => {
|
||||||
|
return { ...prev, [plugin.pluginKey]: plugin };
|
||||||
|
});
|
||||||
|
if (onInstallSuccess) {
|
||||||
|
onInstallSuccess(...rest);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateUserPlugins, onInstallError, onInstallSuccess, setAvailableTools],
|
||||||
|
);
|
||||||
|
|
||||||
|
const uninstallPlugin = useCallback(
|
||||||
|
(plugin: string) => {
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{ pluginKey: plugin, action: 'uninstall', auth: null },
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
if (onUninstallError) {
|
||||||
|
onUninstallError(error as TError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (...rest) => {
|
||||||
|
setAvailableTools((prev) => {
|
||||||
|
const newAvailableTools = { ...prev };
|
||||||
|
delete newAvailableTools[plugin];
|
||||||
|
return newAvailableTools;
|
||||||
|
});
|
||||||
|
if (onUninstallSuccess) {
|
||||||
|
onUninstallSuccess(...rest);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[updateUserPlugins, onUninstallError, onUninstallSuccess, setAvailableTools],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
installPlugin,
|
||||||
|
uninstallPlugin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,13 @@ import {
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
SSE,
|
SSE,
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
EndpointURLs,
|
|
||||||
Constants,
|
Constants,
|
||||||
|
EndpointURLs,
|
||||||
createPayload,
|
createPayload,
|
||||||
tPresetSchema,
|
tPresetSchema,
|
||||||
tMessageSchema,
|
tMessageSchema,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
LocalStorageKeys,
|
||||||
tConvoUpdateSchema,
|
tConvoUpdateSchema,
|
||||||
removeNullishValues,
|
removeNullishValues,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
|
|
@ -34,7 +35,6 @@ import { useGenTitleMutation } from '~/data-provider';
|
||||||
import useContentHandler from './useContentHandler';
|
import useContentHandler from './useContentHandler';
|
||||||
import { useAuthContext } from '../AuthContext';
|
import { useAuthContext } from '../AuthContext';
|
||||||
import useChatHelpers from '../useChatHelpers';
|
import useChatHelpers from '../useChatHelpers';
|
||||||
import useSetStorage from '../useSetStorage';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
type TResData = {
|
type TResData = {
|
||||||
|
|
@ -59,7 +59,6 @@ type TSyncData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useSSE(submission: TSubmission | null, index = 0) {
|
export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
const setStorage = useSetStorage();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const genTitle = useGenTitleMutation();
|
const genTitle = useGenTitleMutation();
|
||||||
|
|
||||||
|
|
@ -165,13 +164,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
...convoUpdate,
|
...convoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
setStorage(update);
|
|
||||||
return update;
|
return update;
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
},
|
},
|
||||||
[setMessages, setConversation, setStorage, genTitle, queryClient, setIsSubmitting],
|
[setMessages, setConversation, genTitle, queryClient, setIsSubmitting],
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncHandler = useCallback(
|
const syncHandler = useCallback(
|
||||||
|
|
@ -208,7 +206,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
messages: [requestMessage.messageId, responseMessage.messageId],
|
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||||
}) as TConversation;
|
}) as TConversation;
|
||||||
|
|
||||||
setStorage(update);
|
|
||||||
return update;
|
return update;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -227,7 +224,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
|
|
||||||
resetLatestMessage();
|
resetLatestMessage();
|
||||||
},
|
},
|
||||||
[setMessages, setConversation, setStorage, queryClient, setShowStopButton, resetLatestMessage],
|
[setMessages, setConversation, queryClient, setShowStopButton, resetLatestMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const createdHandler = useCallback(
|
const createdHandler = useCallback(
|
||||||
|
|
@ -273,7 +270,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
title,
|
title,
|
||||||
}) as TConversation;
|
}) as TConversation;
|
||||||
|
|
||||||
setStorage(update);
|
|
||||||
return update;
|
return update;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -289,7 +285,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
});
|
});
|
||||||
resetLatestMessage();
|
resetLatestMessage();
|
||||||
},
|
},
|
||||||
[setMessages, setConversation, setStorage, queryClient, resetLatestMessage],
|
[setMessages, setConversation, queryClient, resetLatestMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalHandler = useCallback(
|
const finalHandler = useCallback(
|
||||||
|
|
@ -336,21 +332,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
update.model = prevState.model;
|
update.model = prevState.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStorage(update);
|
|
||||||
return update;
|
return update;
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
},
|
},
|
||||||
[
|
[genTitle, queryClient, setMessages, setConversation, setIsSubmitting, setShowStopButton],
|
||||||
setMessages,
|
|
||||||
setConversation,
|
|
||||||
setStorage,
|
|
||||||
genTitle,
|
|
||||||
queryClient,
|
|
||||||
setIsSubmitting,
|
|
||||||
setShowStopButton,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorHandler = useCallback(
|
const errorHandler = useCallback(
|
||||||
|
|
@ -430,8 +417,9 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
async (conversationId = '', submission: TSubmission) => {
|
async (conversationId = '', submission: TSubmission) => {
|
||||||
let runAbortKey = '';
|
let runAbortKey = '';
|
||||||
try {
|
try {
|
||||||
const conversation = (JSON.parse(localStorage.getItem('lastConversationSetup') ?? '') ??
|
const conversation = (JSON.parse(
|
||||||
{}) as TConversation;
|
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) ?? '',
|
||||||
|
) ?? {}) as TConversation;
|
||||||
const { conversationId, messages } = conversation;
|
const { conversationId, messages } = conversation;
|
||||||
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
|
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,11 @@ 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 useSetOptions } from './useSetOptions';
|
|
||||||
export { default as useSetStorage } from './useSetStorage';
|
|
||||||
export { default as useChatHelpers } from './useChatHelpers';
|
export { default as useChatHelpers } from './useChatHelpers';
|
||||||
export { default as useGenerations } from './useGenerations';
|
export { default as useGenerations } from './useGenerations';
|
||||||
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 useConversation } from './useConversation';
|
|
||||||
export { default as useDefaultConvo } from './useDefaultConvo';
|
|
||||||
export { default as useServerStream } from './useServerStream';
|
|
||||||
export { default as useConversations } from './useConversations';
|
|
||||||
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 useMessageHandler } from './useMessageHandler';
|
|
||||||
export { default as useOriginNavigate } from './useOriginNavigate';
|
export { default as useOriginNavigate } from './useOriginNavigate';
|
||||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
|
||||||
export { default as useSetIndexOptions } from './useSetIndexOptions';
|
|
||||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
|
||||||
unfinished: false,
|
unfinished: false,
|
||||||
isCreatedByUser: false,
|
isCreatedByUser: false,
|
||||||
isEdited: isEditOrContinue,
|
isEdited: isEditOrContinue,
|
||||||
|
iconURL: convo.iconURL,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue