mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02: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,
|
||||
modelLabel: this.options.modelLabel,
|
||||
resendFiles: this.options.resendFiles,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -456,6 +456,8 @@ class BaseClient {
|
|||
sender: this.sender,
|
||||
text: addSpaceIfNeeded(generation) + completion,
|
||||
promptTokens,
|
||||
iconURL: this.options.iconURL,
|
||||
endpoint: this.options.endpoint,
|
||||
...(this.metadata ?? {}),
|
||||
};
|
||||
|
||||
|
@ -525,8 +527,19 @@ class BaseClient {
|
|||
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) {
|
||||
await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
|
||||
await saveMessage({
|
||||
...message,
|
||||
endpoint: this.options.endpoint,
|
||||
unfinished: false,
|
||||
user,
|
||||
});
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: this.options.endpoint,
|
||||
|
|
|
@ -708,6 +708,9 @@ class GoogleClient extends BaseClient {
|
|||
return {
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
modelLabel: this.options.modelLabel,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -381,6 +381,9 @@ class OpenAIClient extends BaseClient {
|
|||
promptPrefix: this.options.promptPrefix,
|
||||
resendFiles: this.options.resendFiles,
|
||||
imageDetail: this.options.imageDetail,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
...this.modelOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -42,8 +42,12 @@ class PluginsClient extends OpenAIClient {
|
|||
return {
|
||||
chatGptLabel: this.options.chatGptLabel,
|
||||
promptPrefix: this.options.promptPrefix,
|
||||
tools: this.options.tools,
|
||||
...this.modelOptions,
|
||||
agentOptions: this.agentOptions,
|
||||
iconURL: this.options.iconURL,
|
||||
greeting: this.options.greeting,
|
||||
spec: this.options.spec,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -144,9 +148,11 @@ class PluginsClient extends OpenAIClient {
|
|||
signal,
|
||||
pastMessages,
|
||||
tools: this.tools,
|
||||
currentDateString: this.currentDateString,
|
||||
verbose: this.options.debug,
|
||||
returnIntermediateSteps: true,
|
||||
customName: this.options.chatGptLabel,
|
||||
currentDateString: this.currentDateString,
|
||||
customInstructions: this.options.promptPrefix,
|
||||
callbackManager: CallbackManager.fromHandlers({
|
||||
async handleAgentAction(action, runId) {
|
||||
handleAction(action, runId, onAgentAction);
|
||||
|
@ -304,6 +310,8 @@ class PluginsClient extends OpenAIClient {
|
|||
}
|
||||
|
||||
const responseMessage = {
|
||||
endpoint: EModelEndpoint.gptPlugins,
|
||||
iconURL: this.options.iconURL,
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
|
|
|
@ -13,10 +13,18 @@ const initializeCustomAgent = async ({
|
|||
tools,
|
||||
model,
|
||||
pastMessages,
|
||||
customName,
|
||||
customInstructions,
|
||||
currentDateString,
|
||||
...rest
|
||||
}) => {
|
||||
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([
|
||||
new SystemMessagePromptTemplate(prompt),
|
||||
|
|
|
@ -10,6 +10,8 @@ const initializeFunctionsAgent = async ({
|
|||
tools,
|
||||
model,
|
||||
pastMessages,
|
||||
customName,
|
||||
customInstructions,
|
||||
currentDateString,
|
||||
...rest
|
||||
}) => {
|
||||
|
@ -24,7 +26,13 @@ const initializeFunctionsAgent = async ({
|
|||
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, {
|
||||
agentType: 'openai-functions',
|
||||
|
|
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
async saveMessage({
|
||||
user,
|
||||
endpoint,
|
||||
iconURL,
|
||||
messageId,
|
||||
newMessageId,
|
||||
conversationId,
|
||||
|
@ -35,6 +36,7 @@ module.exports = {
|
|||
|
||||
const update = {
|
||||
user,
|
||||
iconURL,
|
||||
endpoint,
|
||||
messageId: newMessageId || messageId,
|
||||
conversationId,
|
||||
|
|
|
@ -39,6 +39,12 @@ module.exports = {
|
|||
try {
|
||||
const setter = { $set: {} };
|
||||
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) {
|
||||
update.presetId = newPresetId;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,17 @@ const conversationPreset = {
|
|||
type: String,
|
||||
},
|
||||
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 = {
|
||||
|
|
|
@ -110,6 +110,10 @@ const messageSchema = mongoose.Schema(
|
|||
thread_id: {
|
||||
type: String,
|
||||
},
|
||||
/* frontend components */
|
||||
iconURL: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
|
|
@ -55,6 +55,9 @@ const getAvailablePluginsController = async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
/** @type {{ filteredTools: string[] }} */
|
||||
const { filteredTools = [] } = req.app.locals;
|
||||
|
||||
const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');
|
||||
|
||||
const jsonData = JSON.parse(pluginManifest);
|
||||
|
@ -67,7 +70,10 @@ const getAvailablePluginsController = async (req, res) => {
|
|||
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);
|
||||
res.status(200).json(plugins);
|
||||
} catch (error) {
|
||||
|
|
|
@ -73,6 +73,8 @@ const createAbortController = (req, res, getAbortData) => {
|
|||
...responseData,
|
||||
conversationId,
|
||||
finish_reason: 'incomplete',
|
||||
endpoint: endpointOption.endpoint,
|
||||
iconURL: endpointOption.iconURL,
|
||||
model: endpointOption.modelOptions.model,
|
||||
unfinished: false,
|
||||
error: false,
|
||||
|
|
|
@ -7,6 +7,8 @@ const anthropic = require('~/server/services/Endpoints/anthropic');
|
|||
const openAI = require('~/server/services/Endpoints/openAI');
|
||||
const custom = require('~/server/services/Endpoints/custom');
|
||||
const google = require('~/server/services/Endpoints/google');
|
||||
const enforceModelSpec = require('./enforceModelSpec');
|
||||
const { handleError } = require('~/server/utils');
|
||||
|
||||
const buildFunction = {
|
||||
[EModelEndpoint.openAI]: openAI.buildOptions,
|
||||
|
@ -21,6 +23,31 @@ const buildFunction = {
|
|||
async function buildEndpointOption(req, res, next) {
|
||||
const { endpoint, endpointType } = 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](
|
||||
endpoint,
|
||||
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 {
|
||||
/** @type {TStartupConfig} */
|
||||
const payload = {
|
||||
appTitle: process.env.APP_TITLE || 'LibreChat',
|
||||
socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins,
|
||||
|
@ -44,7 +45,8 @@ router.get('/', async function (req, res) {
|
|||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||
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') {
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
const {
|
||||
FileSources,
|
||||
EModelEndpoint,
|
||||
EImageOutputType,
|
||||
defaultSocialLogins,
|
||||
} = require('librechat-data-provider');
|
||||
const { FileSources, EModelEndpoint, getConfigDefaults } = require('librechat-data-provider');
|
||||
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
|
||||
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
|
||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||
const handleRateLimits = require('./Config/handleRateLimits');
|
||||
const { loadDefaultInterface } = require('./start/interface');
|
||||
const { azureConfigSetup } = require('./start/azureOpenAI');
|
||||
const { loadAndFormatTools } = require('./ToolService');
|
||||
const paths = require('~/config/paths');
|
||||
|
@ -22,9 +18,12 @@ const paths = require('~/config/paths');
|
|||
const AppService = async (app) => {
|
||||
/** @type {TCustomConfig}*/
|
||||
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;
|
||||
|
||||
checkVariables();
|
||||
|
@ -37,24 +36,22 @@ const AppService = async (app) => {
|
|||
/** @type {Record<string, FunctionTool} */
|
||||
const availableTools = loadAndFormatTools({
|
||||
directory: paths.structuredTools,
|
||||
filter: new Set([
|
||||
'ChatTool.js',
|
||||
'CodeSherpa.js',
|
||||
'CodeSherpaTools.js',
|
||||
'E2BTools.js',
|
||||
'extractionChain.js',
|
||||
]),
|
||||
adminFilter: filteredTools,
|
||||
});
|
||||
|
||||
const socialLogins = config?.registration?.socialLogins ?? defaultSocialLogins;
|
||||
const socialLogins =
|
||||
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
|
||||
const interfaceConfig = loadDefaultInterface(config, configDefaults);
|
||||
|
||||
if (!Object.keys(config).length) {
|
||||
app.locals = {
|
||||
paths,
|
||||
fileStrategy,
|
||||
socialLogins,
|
||||
filteredTools,
|
||||
availableTools,
|
||||
imageOutputType,
|
||||
interfaceConfig,
|
||||
};
|
||||
|
||||
return;
|
||||
|
@ -85,9 +82,11 @@ const AppService = async (app) => {
|
|||
paths,
|
||||
socialLogins,
|
||||
fileStrategy,
|
||||
filteredTools,
|
||||
availableTools,
|
||||
imageOutputType,
|
||||
interface: config?.interface,
|
||||
interfaceConfig,
|
||||
modelSpecs: config.modelSpecs,
|
||||
fileConfig: config?.fileConfig,
|
||||
secureImageLinks: config?.secureImageLinks,
|
||||
...endpointLocals,
|
||||
|
|
|
@ -93,6 +93,16 @@ describe('AppService', () => {
|
|||
expect(app.locals).toEqual({
|
||||
socialLogins: ['testLogin'],
|
||||
fileStrategy: 'testStrategy',
|
||||
interfaceConfig: expect.objectContaining({
|
||||
privacyPolicy: undefined,
|
||||
termsOfService: undefined,
|
||||
endpointsMenu: true,
|
||||
modelSelect: true,
|
||||
parameters: true,
|
||||
sidePanel: true,
|
||||
presets: true,
|
||||
}),
|
||||
modelSpecs: undefined,
|
||||
availableTools: {
|
||||
ExampleTool: {
|
||||
type: 'function',
|
||||
|
@ -109,7 +119,6 @@ describe('AppService', () => {
|
|||
},
|
||||
paths: expect.anything(),
|
||||
imageOutputType: expect.any(String),
|
||||
interface: undefined,
|
||||
fileConfig: undefined,
|
||||
secureImageLinks: undefined,
|
||||
});
|
||||
|
@ -181,7 +190,6 @@ describe('AppService', () => {
|
|||
|
||||
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
||||
directory: expect.anything(),
|
||||
filter: expect.anything(),
|
||||
});
|
||||
|
||||
expect(app.locals.availableTools.ExampleTool).toBeDefined();
|
||||
|
|
|
@ -42,6 +42,12 @@ async function loadCustomConfig() {
|
|||
i === 0 && i++;
|
||||
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') {
|
||||
|
@ -84,6 +90,10 @@ Please specify a correct \`imageOutputType\` value (case-sensitive).
|
|||
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
|
||||
}
|
||||
|
||||
if (result.data.modelSpecs) {
|
||||
customConfig.modelSpecs = result.data.modelSpecs;
|
||||
}
|
||||
|
||||
return customConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const { modelLabel, promptPrefix, resendFiles, ...rest } = parsedBody;
|
||||
const { modelLabel, promptPrefix, resendFiles, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||
const endpointOption = {
|
||||
endpoint,
|
||||
modelLabel,
|
||||
promptPrefix,
|
||||
resendFiles,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions: {
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const buildOptions = (endpoint, parsedBody) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { promptPrefix, assistant_id, ...rest } = parsedBody;
|
||||
const { promptPrefix, assistant_id, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||
const endpointOption = {
|
||||
endpoint,
|
||||
promptPrefix,
|
||||
assistant_id,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions: {
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const buildOptions = (endpoint, parsedBody, endpointType) => {
|
||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, ...rest } = parsedBody;
|
||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, iconURL, greeting, spec, ...rest } =
|
||||
parsedBody;
|
||||
const endpointOption = {
|
||||
endpoint,
|
||||
endpointType,
|
||||
|
@ -7,6 +8,9 @@ const buildOptions = (endpoint, parsedBody, endpointType) => {
|
|||
promptPrefix,
|
||||
resendFiles,
|
||||
imageDetail,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions: {
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const { examples, modelLabel, promptPrefix, ...rest } = parsedBody;
|
||||
const { examples, modelLabel, promptPrefix, iconURL, greeting, spec, ...rest } = parsedBody;
|
||||
const endpointOption = {
|
||||
examples,
|
||||
endpoint,
|
||||
modelLabel,
|
||||
promptPrefix,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions: {
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -4,25 +4,24 @@ const buildOptions = (endpoint, parsedBody) => {
|
|||
promptPrefix,
|
||||
agentOptions,
|
||||
tools,
|
||||
model,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
...modelOptions
|
||||
} = parsedBody;
|
||||
const endpointOption = {
|
||||
endpoint,
|
||||
tools: tools.map((tool) => tool.pluginKey) ?? [],
|
||||
tools:
|
||||
tools
|
||||
.map((tool) => tool?.pluginKey ?? tool)
|
||||
.filter((toolName) => typeof toolName === 'string') ?? [],
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
agentOptions,
|
||||
modelOptions: {
|
||||
model,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
},
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions,
|
||||
};
|
||||
|
||||
return endpointOption;
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
const buildOptions = (endpoint, parsedBody) => {
|
||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, ...rest } = parsedBody;
|
||||
const { chatGptLabel, promptPrefix, resendFiles, imageDetail, iconURL, greeting, spec, ...rest } =
|
||||
parsedBody;
|
||||
const endpointOption = {
|
||||
endpoint,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
resendFiles,
|
||||
imageDetail,
|
||||
iconURL,
|
||||
greeting,
|
||||
spec,
|
||||
modelOptions: {
|
||||
...rest,
|
||||
},
|
||||
|
|
|
@ -20,6 +20,14 @@ const { redactMessage } = require('~/config/parsers');
|
|||
const { sleep } = require('~/server/utils');
|
||||
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.
|
||||
*
|
||||
|
@ -30,10 +38,11 @@ const { logger } = require('~/config');
|
|||
*
|
||||
* @param {object} params - The parameters for the function.
|
||||
* @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.
|
||||
*/
|
||||
function loadAndFormatTools({ directory, filter = new Set() }) {
|
||||
function loadAndFormatTools({ directory, adminFilter = [] }) {
|
||||
const filter = new Set([...adminFilter, ...filteredTools]);
|
||||
const tools = [];
|
||||
/* Structured Tools 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TStartupConfig
|
||||
* @typedef {import('librechat-data-provider').TStartupConfig} TStartupConfig
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TConfigDefaults
|
||||
* @typedef {import('librechat-data-provider').TConfigDefaults} TConfigDefaults
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TPlugin
|
||||
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||
|
@ -342,6 +354,18 @@
|
|||
* @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
|
||||
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||
|
|
|
@ -6,7 +6,7 @@ function loadYaml(filepath) {
|
|||
let fileContents = fs.readFileSync(filepath, 'utf8');
|
||||
return yaml.load(fileContents);
|
||||
} 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 { SetterOrUpdater } from 'recoil';
|
||||
import type {
|
||||
TSetOption as SetOption,
|
||||
TConversation,
|
||||
TMessage,
|
||||
TPreset,
|
||||
TLoginUser,
|
||||
TUser,
|
||||
EModelEndpoint,
|
||||
Action,
|
||||
TPreset,
|
||||
TPlugin,
|
||||
TMessage,
|
||||
TLoginUser,
|
||||
AuthTypeEnum,
|
||||
TConversation,
|
||||
EModelEndpoint,
|
||||
AuthorizationTypeEnum,
|
||||
TSetOption as SetOption,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type TPluginMap = Record<string, TPlugin>;
|
||||
|
||||
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
||||
|
||||
export type LastSelectedModels = Record<EModelEndpoint, string>;
|
||||
|
@ -32,6 +35,16 @@ export enum IconContext {
|
|||
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 = {
|
||||
title: string;
|
||||
label?: string;
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
import { useMemo } from 'react';
|
||||
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 { EndpointsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
|
||||
import HeaderOptions from './Input/HeaderOptions';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export default function Header() {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { navVisible } = useOutletContext<ContextType>();
|
||||
const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]);
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
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="hide-scrollbar flex items-center gap-2 overflow-x-auto">
|
||||
{!navVisible && <HeaderNewChat />}
|
||||
<EndpointsMenu />
|
||||
<HeaderOptions />
|
||||
<PresetsMenu />
|
||||
{interfaceConfig.endpointsMenu && <EndpointsMenu />}
|
||||
{modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
|
||||
{<HeaderOptions interfaceConfig={interfaceConfig} />}
|
||||
{interfaceConfig.presets && <PresetsMenu />}
|
||||
</div>
|
||||
{/* Empty div for spacing */}
|
||||
<div />
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Settings2 } from 'lucide-react';
|
|||
import { Root, Anchor } from '@radix-ui/react-popover';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
||||
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||
import { ModelSelect } from '~/components/Input/ModelSelect';
|
||||
import { PluginStoreDialog } from '~/components';
|
||||
|
@ -15,7 +15,11 @@ import { Button } from '~/components/ui';
|
|||
import { cn, cardStyle } from '~/utils/';
|
||||
import store from '~/store';
|
||||
|
||||
export default function HeaderOptions() {
|
||||
export default function HeaderOptions({
|
||||
interfaceConfig,
|
||||
}: {
|
||||
interfaceConfig?: Partial<TInterfaceConfig>;
|
||||
}) {
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState<boolean>(false);
|
||||
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
|
||||
store.showPluginStoreDialog,
|
||||
|
@ -70,13 +74,15 @@ export default function HeaderOptions() {
|
|||
<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">
|
||||
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
||||
<ModelSelect
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
isMultiChat={true}
|
||||
showAbove={false}
|
||||
/>
|
||||
{!noSettings[endpoint] && (
|
||||
{interfaceConfig?.modelSelect && (
|
||||
<ModelSelect
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
showAbove={false}
|
||||
popover={true}
|
||||
/>
|
||||
)}
|
||||
{!noSettings[endpoint] && interfaceConfig?.parameters && (
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
|
@ -90,35 +96,41 @@ export default function HeaderOptions() {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<OptionsPopover
|
||||
visible={showPopover}
|
||||
saveAsPreset={saveAsPreset}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
PopoverButtons={<PopoverButtons />}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<EndpointSettings
|
||||
className="[&::-webkit-scrollbar]:w-2"
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
isMultiChat={true}
|
||||
/>
|
||||
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
||||
</div>
|
||||
</OptionsPopover>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={
|
||||
tPresetUpdateSchema.parse({
|
||||
...conversation,
|
||||
}) as TPreset
|
||||
}
|
||||
/>
|
||||
<PluginStoreDialog
|
||||
isOpen={showPluginStoreDialog}
|
||||
setIsOpen={setShowPluginStoreDialog}
|
||||
/>
|
||||
{interfaceConfig?.parameters && (
|
||||
<OptionsPopover
|
||||
visible={showPopover}
|
||||
saveAsPreset={saveAsPreset}
|
||||
presetsDisabled={!interfaceConfig?.presets}
|
||||
PopoverButtons={<PopoverButtons />}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<EndpointSettings
|
||||
className="[&::-webkit-scrollbar]:w-2"
|
||||
conversation={conversation}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<AlternativeSettings conversation={conversation} setOption={setOption} />
|
||||
</div>
|
||||
</OptionsPopover>
|
||||
)}
|
||||
{interfaceConfig?.presets && (
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={
|
||||
tPresetUpdateSchema.parse({
|
||||
...conversation,
|
||||
}) as TPreset
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{interfaceConfig?.parameters && (
|
||||
<PluginStoreDialog
|
||||
isOpen={showPluginStoreDialog}
|
||||
setIsOpen={setShowPluginStoreDialog}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Anchor>
|
||||
|
|
|
@ -13,6 +13,7 @@ type TOptionsPopoverProps = {
|
|||
saveAsPreset: () => void;
|
||||
closePopover: () => void;
|
||||
PopoverButtons: ReactNode;
|
||||
presetsDisabled: boolean;
|
||||
};
|
||||
|
||||
export default function OptionsPopover({
|
||||
|
@ -22,6 +23,7 @@ export default function OptionsPopover({
|
|||
saveAsPreset,
|
||||
closePopover,
|
||||
PopoverButtons,
|
||||
presetsDisabled,
|
||||
}: TOptionsPopoverProps) {
|
||||
const popoverRef = useRef(null);
|
||||
useOnClickOutside(
|
||||
|
@ -61,14 +63,16 @@ export default function OptionsPopover({
|
|||
)}
|
||||
>
|
||||
<div className="flex w-full items-center bg-gray-50 px-2 py-2 dark:bg-gray-700">
|
||||
<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"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</Button>
|
||||
{presetsDisabled ? null : (
|
||||
<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"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
{localize('com_endpoint_save_as_preset')}
|
||||
</Button>
|
||||
)}
|
||||
{PopoverButtons}
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
@ -2,21 +2,22 @@ import { EModelEndpoint } from 'librechat-data-provider';
|
|||
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
||||
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { icons } from './Menus/Endpoints/Icons';
|
||||
import { BirthdayIcon } from '~/components/svg';
|
||||
import { getEndpointField } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||
const { conversation } = useChatContext();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const localize = useLocalize();
|
||||
|
||||
let { endpoint } = conversation ?? {};
|
||||
let { endpoint = '' } = conversation ?? {};
|
||||
const { assistant_id = null } = conversation ?? {};
|
||||
|
||||
if (
|
||||
|
@ -27,9 +28,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
|||
endpoint = EModelEndpoint.openAI;
|
||||
}
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
||||
const iconURL = conversation?.iconURL;
|
||||
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||
|
||||
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
||||
const Icon = icons[iconKey];
|
||||
|
||||
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
|
||||
|
@ -51,19 +54,29 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
|||
<div className="absolute left-0 right-0">{Header && Header}</div>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="relative mb-3 h-[72px] w-[72px]">
|
||||
<div className={className}>
|
||||
{endpoint &&
|
||||
Icon &&
|
||||
Icon({
|
||||
size: 41,
|
||||
context: 'landing',
|
||||
className: 'h-2/3 w-2/3',
|
||||
endpoint: endpoint,
|
||||
iconURL: iconURL,
|
||||
assistantName,
|
||||
avatar,
|
||||
})}
|
||||
</div>
|
||||
{iconURL && iconURL.includes('http') ? (
|
||||
<ConvoIconURL
|
||||
preset={conversation}
|
||||
endpointIconURL={endpointIconURL}
|
||||
assistantName={assistantName}
|
||||
assistantAvatar={avatar}
|
||||
context="landing"
|
||||
/>
|
||||
) : (
|
||||
<div className={className}>
|
||||
{endpoint &&
|
||||
Icon &&
|
||||
Icon({
|
||||
size: 41,
|
||||
context: 'landing',
|
||||
className: 'h-2/3 w-2/3',
|
||||
iconURL: endpointIconURL,
|
||||
assistantName,
|
||||
endpoint,
|
||||
avatar,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<TooltipTrigger>
|
||||
{(startupConfig?.showBirthdayIcon ?? false) && (
|
||||
<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">
|
||||
{endpoint === EModelEndpoint.assistants
|
||||
? localize('com_nav_welcome_assistant')
|
||||
: localize('com_nav_welcome_message')}
|
||||
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
|
||||
: conversation?.greeting ?? localize('com_nav_welcome_message')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { IconMapProps } from '~/common';
|
||||
import {
|
||||
MinimalPlugin,
|
||||
GPTIcon,
|
||||
|
@ -23,17 +24,7 @@ export const icons = {
|
|||
[EModelEndpoint.google]: GoogleMinimalIcon,
|
||||
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
|
||||
[EModelEndpoint.custom]: CustomMinimalIcon,
|
||||
[EModelEndpoint.assistants]: ({
|
||||
className = '',
|
||||
assistantName,
|
||||
avatar,
|
||||
size,
|
||||
}: {
|
||||
className?: string;
|
||||
assistantName?: string;
|
||||
avatar?: string;
|
||||
size?: number;
|
||||
}) => {
|
||||
[EModelEndpoint.assistants]: ({ className = '', assistantName, avatar, size }: IconMapProps) => {
|
||||
if (assistantName && avatar) {
|
||||
return (
|
||||
<img
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { useState } from 'react';
|
||||
import { Settings } from 'lucide-react';
|
||||
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 type { TPreset, TConversation } from 'librechat-data-provider';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import type { FC } from 'react';
|
||||
import { cn, getConvoSwitchLogic, getEndpointField, getIconKey } from '~/utils';
|
||||
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
|
||||
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
|
||||
import { cn, getEndpointField } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { icons } from './Icons';
|
||||
import store from '~/store';
|
||||
|
@ -43,58 +43,44 @@ const MenuItem: FC<MenuItemProps> = ({
|
|||
const onSelectEndpoint = (newEndpoint: EModelEndpoint) => {
|
||||
if (!newEndpoint) {
|
||||
return;
|
||||
} else {
|
||||
if (!expiryTime) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
const currentEndpoint = conversation?.endpoint;
|
||||
const template: Partial<TPreset> = {
|
||||
...conversation,
|
||||
endpoint: newEndpoint,
|
||||
conversationId: 'new',
|
||||
};
|
||||
const isAssistantSwitch =
|
||||
newEndpoint === EModelEndpoint.assistants &&
|
||||
currentEndpoint === EModelEndpoint.assistants &&
|
||||
currentEndpoint === newEndpoint;
|
||||
|
||||
const { conversationId } = conversation ?? {};
|
||||
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) {
|
||||
template.endpointType = newEndpointType;
|
||||
|
||||
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: currentConvo, keepLatestMessage: true });
|
||||
return;
|
||||
}
|
||||
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
||||
}
|
||||
|
||||
if (!expiryTime) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
const {
|
||||
shouldSwitch,
|
||||
isNewModular,
|
||||
isCurrentModular,
|
||||
isExistingConversation,
|
||||
newEndpointType,
|
||||
template,
|
||||
} = getConvoSwitchLogic({
|
||||
newEndpoint,
|
||||
modularChat,
|
||||
conversation,
|
||||
endpointsConfig,
|
||||
});
|
||||
|
||||
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
|
||||
template.endpointType = newEndpointType;
|
||||
|
||||
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: currentConvo, keepLatestMessage: true });
|
||||
return;
|
||||
}
|
||||
newConversation({ template: { ...(template as Partial<TConversation>) } });
|
||||
};
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType });
|
||||
const Icon = icons[iconKey];
|
||||
|
||||
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}
|
||||
setOption={setOption}
|
||||
isPreset={true}
|
||||
isMultiChat={true}
|
||||
className="h-full md:mb-4 md:h-[440px]"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,11 +4,11 @@ import { Flipper, Flipped } from 'react-flip-toolkit';
|
|||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { FC } from 'react';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { getPresetTitle, getEndpointField, getIconKey } from '~/utils';
|
||||
import FileUpload from '~/components/Chat/Input/Files/FileUpload';
|
||||
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
||||
import { Dialog, DialogTrigger, Label } from '~/components/ui';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import { getPresetTitle, getEndpointField } from '~/utils';
|
||||
import { Dialog, DialogTrigger, Label } from '~/components/ui/';
|
||||
import { MenuSeparator, MenuItem } from '../UI';
|
||||
import { icons } from '../Endpoints/Icons';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
@ -115,9 +115,7 @@ const PresetItems: FC<{
|
|||
return null;
|
||||
}
|
||||
|
||||
const iconKey = getEndpointField(endpointsConfig, preset.endpoint, 'type')
|
||||
? 'unknown'
|
||||
: preset.endpointType ?? preset.endpoint ?? 'unknown';
|
||||
const iconKey = getIconKey({ endpoint: preset.endpoint, endpointsConfig });
|
||||
const Icon = icons[iconKey];
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as EndpointsMenu } from './EndpointsMenu';
|
||||
export { default as PresetsMenu } from './PresetsMenu';
|
||||
export { default as EndpointsMenu } from './EndpointsMenu';
|
||||
export { default as HeaderNewChat } from './HeaderNewChat';
|
||||
export { default as ModelSpecsMenu } from './Models/ModelSpecsMenu';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useAuthContext, useMessageHelpers, useLocalize } from '~/hooks';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import MessageContent from './Content/MessageContent';
|
||||
import SiblingSwitch from './SiblingSwitch';
|
||||
|
@ -18,7 +19,6 @@ export default function Message(props: TMessageProps) {
|
|||
|
||||
const {
|
||||
ask,
|
||||
icon,
|
||||
edit,
|
||||
isLast,
|
||||
enterEdit,
|
||||
|
@ -60,11 +60,7 @@ export default function Message(props: TMessageProps) {
|
|||
<div>
|
||||
<div className="pt-0.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
|
||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<Icon message={message} conversation={conversation} />
|
||||
</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 Icon from '~/components/Chat/Messages/MessageIcon';
|
||||
import ContentParts from './Content/ContentParts';
|
||||
import SiblingSwitch from './SiblingSwitch';
|
||||
import { useMessageHelpers } from '~/hooks';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
|
@ -14,7 +15,6 @@ export default function Message(props: TMessageProps) {
|
|||
|
||||
const {
|
||||
ask,
|
||||
icon,
|
||||
edit,
|
||||
isLast,
|
||||
enterEdit,
|
||||
|
@ -47,11 +47,7 @@ export default function Message(props: TMessageProps) {
|
|||
<div>
|
||||
<div className="pt-0.5">
|
||||
<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) ? (
|
||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<Icon message={message} conversation={conversation} assistant={assistant} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
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 { useDragHelpers, useSetFilesToDelete } from '~/hooks';
|
||||
import DragDropOverlay from './Input/Files/DragDropOverlay';
|
||||
|
@ -8,6 +9,8 @@ import { useDeleteFilesMutation } from '~/data-provider';
|
|||
import { SidePanel } from '~/components/SidePanel';
|
||||
import store from '~/store';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export default function Presentation({
|
||||
children,
|
||||
useSidePanel = false,
|
||||
|
@ -17,9 +20,16 @@ export default function Presentation({
|
|||
panel?: React.ReactNode;
|
||||
useSidePanel?: boolean;
|
||||
}) {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
||||
const { isOver, canDrop, drop } = useDragHelpers();
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
const { isOver, canDrop, drop } = useDragHelpers();
|
||||
|
||||
const { mutateAsync } = useDeleteFilesMutation({
|
||||
onSuccess: () => {
|
||||
console.log('Temporary Files deleted');
|
||||
|
@ -31,7 +41,7 @@ export default function Presentation({
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
const filesToDelete = localStorage.getItem('filesToDelete');
|
||||
const filesToDelete = localStorage.getItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||
const map = JSON.parse(filesToDelete ?? '{}') as Record<string, ExtendedFile>;
|
||||
const files = Object.values(map)
|
||||
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
|
||||
|
@ -69,7 +79,7 @@ export default function Presentation({
|
|||
</div>
|
||||
);
|
||||
|
||||
if (useSidePanel && !hideSidePanel) {
|
||||
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel) {
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useParams } from 'react-router-dom';
|
||||
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 type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
||||
import { useConversations, useNavigateToConvo } from '~/hooks';
|
||||
import { MinimalIcon, ConvoIconURL } from '~/components/Endpoints';
|
||||
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 { useToastContext } from '~/Providers';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import { getEndpointField } from '~/utils';
|
||||
import RenameButton from './RenameButton';
|
||||
import store from '~/store';
|
||||
|
||||
|
@ -51,11 +51,15 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
|||
if (conversation?.endpoint === EModelEndpoint.gptPlugins) {
|
||||
let lastSelectedTools = [];
|
||||
try {
|
||||
lastSelectedTools = JSON.parse(localStorage.getItem('lastSelectedTools') ?? '') ?? [];
|
||||
lastSelectedTools =
|
||||
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
}
|
||||
navigateToConvo({ ...conversation, tools: lastSelectedTools });
|
||||
navigateToConvo({
|
||||
...conversation,
|
||||
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
|
||||
});
|
||||
} else {
|
||||
navigateToConvo(conversation);
|
||||
}
|
||||
|
@ -95,19 +99,35 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
|||
);
|
||||
};
|
||||
|
||||
const icon = MinimalIcon({
|
||||
size: 20,
|
||||
iconURL: getEndpointField(endpointsConfig, conversation.endpoint, 'iconURL'),
|
||||
endpoint: conversation.endpoint,
|
||||
endpointType: conversation.endpointType,
|
||||
model: conversation.model,
|
||||
error: false,
|
||||
className: 'mr-0',
|
||||
isCreatedByUser: false,
|
||||
chatGptLabel: undefined,
|
||||
modelLabel: undefined,
|
||||
jailbreak: undefined,
|
||||
});
|
||||
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,
|
||||
iconURL: endpointIconURL,
|
||||
endpoint,
|
||||
endpointType,
|
||||
model: conversation.model,
|
||||
error: false,
|
||||
className: 'mr-0',
|
||||
isCreatedByUser: false,
|
||||
chatGptLabel: undefined,
|
||||
modelLabel: undefined,
|
||||
jailbreak: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
|
|
@ -10,7 +10,7 @@ export default function AlternativeSettings({
|
|||
setOption,
|
||||
isPreset = false,
|
||||
className = '',
|
||||
}: TSettingsProps & { isMultiChat?: boolean }) {
|
||||
}: TSettingsProps) {
|
||||
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||
if (!conversation?.endpoint || currentSettingsView === SettingsViews.default) {
|
||||
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,
|
||||
isPreset = false,
|
||||
className = '',
|
||||
isMultiChat = false,
|
||||
}: TSettingsProps & { isMultiChat?: boolean }) {
|
||||
}: TSettingsProps) {
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const currentSettingsView = useRecoilValue(store.currentSettingsView);
|
||||
if (!conversation?.endpoint || currentSettingsView !== SettingsViews.default) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { settings, multiViewSettings } = getSettings(isMultiChat);
|
||||
const { settings, multiViewSettings } = getSettings();
|
||||
const { endpoint: _endpoint, endpointType } = conversation;
|
||||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
|
|
|
@ -21,14 +21,15 @@ import { cn } from '~/utils';
|
|||
const Icon: React.FC<IconProps> = (props) => {
|
||||
const { user } = useAuthContext();
|
||||
const {
|
||||
size = 30,
|
||||
isCreatedByUser,
|
||||
button,
|
||||
model = '',
|
||||
endpoint,
|
||||
error,
|
||||
button,
|
||||
iconURL,
|
||||
endpoint,
|
||||
jailbreak,
|
||||
size = 30,
|
||||
model = '',
|
||||
assistantName,
|
||||
isCreatedByUser,
|
||||
} = props;
|
||||
|
||||
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;
|
||||
|
||||
if (iconURL && endpointIcons[iconURL]) {
|
||||
({ icon, bg, name } = endpointIcons[iconURL]);
|
||||
}
|
||||
|
||||
if (endpoint === EModelEndpoint.assistants) {
|
||||
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 (
|
||||
<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 }) {
|
||||
const { showAgentSettings } = useChatContext();
|
||||
const { setOption, setAgentOption } = useSetIndexOptions(isPreset ? conversation : null);
|
||||
const { setOption, setTools, setAgentOption, checkPluginSelection } = useSetIndexOptions(
|
||||
isPreset ? conversation : null,
|
||||
);
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
@ -13,6 +15,12 @@ export default function PluginsView({ conversation, models, isPreset = false })
|
|||
return showAgentSettings ? (
|
||||
<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 PluginSettings } from './PluginSettings';
|
||||
|
|
|
@ -32,6 +32,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
endpoint,
|
||||
endpointType,
|
||||
model,
|
||||
modelLabel,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
|
@ -41,32 +42,33 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
resendFiles,
|
||||
imageDetail,
|
||||
} = conversation ?? {};
|
||||
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput({
|
||||
|
||||
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput<string | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'chatGptLabel',
|
||||
initialValue: chatGptLabel,
|
||||
initialValue: modelLabel ?? chatGptLabel,
|
||||
});
|
||||
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput({
|
||||
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput<string | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'promptPrefix',
|
||||
initialValue: promptPrefix,
|
||||
});
|
||||
const [setTemperature, temperatureValue] = useDebouncedInput({
|
||||
const [setTemperature, temperatureValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'temperature',
|
||||
initialValue: temperature,
|
||||
});
|
||||
const [setTopP, topPValue] = useDebouncedInput({
|
||||
const [setTopP, topPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'top_p',
|
||||
initialValue: topP,
|
||||
});
|
||||
const [setFreqP, freqPValue] = useDebouncedInput({
|
||||
const [setFreqP, freqPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'frequency_penalty',
|
||||
initialValue: freqP,
|
||||
});
|
||||
const [setPresP, presPValue] = useDebouncedInput({
|
||||
const [setPresP, presPValue] = useDebouncedInput<number | null | undefined>({
|
||||
setOption,
|
||||
optionKey: 'presence_penalty',
|
||||
initialValue: presP,
|
||||
|
|
|
@ -1,44 +1,105 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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 {
|
||||
SelectDropDown,
|
||||
Input,
|
||||
Label,
|
||||
Slider,
|
||||
InputNumber,
|
||||
HoverCard,
|
||||
InputNumber,
|
||||
SelectDropDown,
|
||||
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 type { TModelSelectProps } from '~/common';
|
||||
import { ESide } from '~/common';
|
||||
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
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();
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
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 {
|
||||
model,
|
||||
modelLabel,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
top_p: topP,
|
||||
frequency_penalty: freqP,
|
||||
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 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 (
|
||||
<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">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_custom_name')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_empty')} |{' '}
|
||||
{localize('com_endpoint_disabled_with_tools')})
|
||||
</small>
|
||||
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
disabled={readonly || toolsSelected}
|
||||
value={chatGptLabel || ''}
|
||||
disabled={readonly}
|
||||
value={chatGptLabelValue || ''}
|
||||
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
|
||||
placeholder={
|
||||
toolsSelected
|
||||
? localize('com_endpoint_disabled_with_tools_placeholder')
|
||||
: localize('com_endpoint_openai_custom_name_placeholder')
|
||||
}
|
||||
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
|
@ -83,21 +137,16 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
{localize('com_endpoint_prompt_prefix')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default_empty')} |{' '}
|
||||
{localize('com_endpoint_disabled_with_tools')})
|
||||
</small>
|
||||
<small className="opacity-40">{localize('com_endpoint_default_empty')}</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly || toolsSelected}
|
||||
value={promptPrefix || ''}
|
||||
disabled={readonly}
|
||||
value={promptPrefixValue || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
|
||||
placeholder={
|
||||
toolsSelected
|
||||
? localize('com_endpoint_disabled_with_tools_placeholder')
|
||||
: localize('com_endpoint_plug_set_custom_instructions_for_gpt_placeholder')
|
||||
}
|
||||
placeholder={localize(
|
||||
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
|
||||
)}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'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 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}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
|
@ -119,7 +182,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
value={temperatureValue}
|
||||
onChange={(value) => setTemperature(Number(value))}
|
||||
max={2}
|
||||
min={0}
|
||||
|
@ -136,7 +199,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature ?? 0.8]}
|
||||
value={[temperatureValue ?? 0.8]}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(0.8)}
|
||||
max={2}
|
||||
|
@ -159,7 +222,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
value={topPValue}
|
||||
onChange={(value) => setTopP(Number(value))}
|
||||
max={1}
|
||||
min={0}
|
||||
|
@ -176,7 +239,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP ?? 1]}
|
||||
value={[topPValue ?? 1]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
|
@ -200,7 +263,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
disabled={readonly}
|
||||
value={freqP}
|
||||
value={freqPValue}
|
||||
onChange={(value) => setFreqP(Number(value))}
|
||||
max={2}
|
||||
min={-2}
|
||||
|
@ -217,7 +280,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[freqP ?? 0]}
|
||||
value={[freqPValue ?? 0]}
|
||||
onValueChange={(value) => setFreqP(value[0])}
|
||||
doubleClickHandler={() => setFreqP(0)}
|
||||
max={2}
|
||||
|
@ -241,7 +304,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
disabled={readonly}
|
||||
value={presP}
|
||||
value={presPValue}
|
||||
onChange={(value) => setPresP(Number(value))}
|
||||
max={2}
|
||||
min={-2}
|
||||
|
@ -258,7 +321,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
|||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[presP ?? 0]}
|
||||
value={[presPValue ?? 0]}
|
||||
onValueChange={(value) => setPresP(value[0])}
|
||||
doubleClickHandler={() => setPresP(0)}
|
||||
max={2}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { FC } from 'react';
|
||||
import type { TModelSelectProps, TBaseSettingsProps, TModels } from '~/common';
|
||||
import { Google, Plugins, GoogleSettings, PluginSettings } from './MultiView';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { GoogleSettings, PluginSettings } from './MultiView';
|
||||
import AssistantsSettings from './Assistants';
|
||||
import AnthropicSettings from './Anthropic';
|
||||
import BingAISettings from './BingAI';
|
||||
|
@ -16,19 +16,7 @@ const settings: { [key: string]: FC<TModelSelectProps> } = {
|
|||
[EModelEndpoint.anthropic]: AnthropicSettings,
|
||||
};
|
||||
|
||||
const multiViewSettings: { [key: string]: FC<TBaseSettingsProps & TModels> } = {
|
||||
[EModelEndpoint.google]: Google,
|
||||
[EModelEndpoint.gptPlugins]: Plugins,
|
||||
};
|
||||
|
||||
export const getSettings = (isMultiChat = false) => {
|
||||
if (!isMultiChat) {
|
||||
return {
|
||||
settings,
|
||||
multiViewSettings,
|
||||
};
|
||||
}
|
||||
|
||||
export const getSettings = () => {
|
||||
return {
|
||||
settings,
|
||||
multiViewSettings: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export { default as Icon } from './Icon';
|
||||
export { default as MinimalIcon } from './MinimalIcon';
|
||||
export { default as ConvoIconURL } from './ConvoIconURL';
|
||||
export { default as EndpointSettings } from './EndpointSettings';
|
||||
export { default as SaveAsPresetDialog } from './SaveAsPresetDialog';
|
||||
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 { TSetOption } from '~/common';
|
||||
import { options, multiChatOptions } from './options';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import { multiChatOptions } from './options';
|
||||
|
||||
type TGoogleProps = {
|
||||
showExamples: boolean;
|
||||
|
@ -12,14 +12,14 @@ type TSelectProps = {
|
|||
conversation: TConversation | null;
|
||||
setOption: TSetOption;
|
||||
extraProps?: TGoogleProps;
|
||||
isMultiChat?: boolean;
|
||||
showAbove?: boolean;
|
||||
popover?: boolean;
|
||||
};
|
||||
|
||||
export default function ModelSelect({
|
||||
conversation,
|
||||
setOption,
|
||||
isMultiChat = false,
|
||||
popover = false,
|
||||
showAbove = true,
|
||||
}: TSelectProps) {
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
|
@ -32,7 +32,7 @@ export default function ModelSelect({
|
|||
const models = modelsQuery?.data?.[_endpoint] ?? [];
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
|
||||
const OptionComponent = isMultiChat ? multiChatOptions[endpoint] : options[endpoint];
|
||||
const OptionComponent = multiChatOptions[endpoint];
|
||||
|
||||
if (!OptionComponent) {
|
||||
return null;
|
||||
|
@ -44,7 +44,7 @@ export default function ModelSelect({
|
|||
setOption={setOption}
|
||||
models={models}
|
||||
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 { useState, useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import {
|
||||
Button,
|
||||
MultiSelectPop,
|
||||
SelectDropDown,
|
||||
SelectDropDownPop,
|
||||
MultiSelectDropDown,
|
||||
MultiSelectPop,
|
||||
Button,
|
||||
} from '~/components/ui';
|
||||
import { useSetIndexOptions, useAuthContext, useMediaQuery, useLocalize } from '~/hooks';
|
||||
import { cn, cardStyle } from '~/utils/';
|
||||
import { cn, cardStyle, selectPlugins, processPlugins } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const pluginStore: TPlugin = {
|
||||
name: 'Plugin store',
|
||||
pluginKey: 'pluginStore',
|
||||
isButton: true,
|
||||
description: '',
|
||||
icon: '',
|
||||
authConfig: [],
|
||||
authenticated: false,
|
||||
};
|
||||
|
||||
export default function PluginsByIndex({
|
||||
conversation,
|
||||
setOption,
|
||||
|
@ -33,12 +23,16 @@ export default function PluginsByIndex({
|
|||
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 } = useSetIndexOptions();
|
||||
const { user } = useAuthContext();
|
||||
const [visible, setVisibility] = useState<boolean>(true);
|
||||
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(() => {
|
||||
if (isSmallScreen) {
|
||||
|
@ -46,39 +40,20 @@ export default function PluginsByIndex({
|
|||
}
|
||||
}, [isSmallScreen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
const conversationTools: TPlugin[] = useMemo(() => {
|
||||
if (!conversation?.tools) {
|
||||
return [];
|
||||
}
|
||||
return processPlugins(conversation.tools, allPlugins?.map);
|
||||
}, [conversation, allPlugins]);
|
||||
|
||||
const availablePlugins = useMemo(() => {
|
||||
if (!availableTools) {
|
||||
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]);
|
||||
return Object.values(availableTools);
|
||||
}, [availableTools]);
|
||||
|
||||
if (!conversation) {
|
||||
return null;
|
||||
|
@ -112,15 +87,19 @@ export default function PluginsByIndex({
|
|||
availableValues={models}
|
||||
showAbove={showAbove}
|
||||
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
|
||||
value={conversation.tools || []}
|
||||
isSelected={checkPluginSelection}
|
||||
setSelected={setTools}
|
||||
availableValues={availableTools}
|
||||
optionValueKey="pluginKey"
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
setSelected={setTools}
|
||||
value={conversationTools}
|
||||
optionValueKey="pluginKey"
|
||||
availableValues={availablePlugins}
|
||||
isSelected={checkPluginSelection}
|
||||
searchPlaceholder={localize('com_ui_select_search_plugin')}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { FC } from 'react';
|
|||
import OpenAI from './OpenAI';
|
||||
import BingAI from './BingAI';
|
||||
import Google from './Google';
|
||||
import Plugins from './Plugins';
|
||||
import ChatGPT from './ChatGPT';
|
||||
import Anthropic from './Anthropic';
|
||||
import PluginsByIndex from './PluginsByIndex';
|
||||
|
@ -16,7 +15,6 @@ export const options: { [key: string]: FC<TModelSelectProps> } = {
|
|||
[EModelEndpoint.azureOpenAI]: OpenAI,
|
||||
[EModelEndpoint.bingAI]: BingAI,
|
||||
[EModelEndpoint.google]: Google,
|
||||
[EModelEndpoint.gptPlugins]: Plugins,
|
||||
[EModelEndpoint.anthropic]: Anthropic,
|
||||
[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 { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
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 ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { NewChatIcon } from '~/components/svg';
|
||||
import { getEndpointField } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
export default function NewChat({
|
||||
index = 0,
|
||||
toggleNav,
|
||||
subHeaders,
|
||||
}: {
|
||||
index?: number;
|
||||
toggleNav: () => void;
|
||||
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 localize = useLocalize();
|
||||
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const [convo] = useLocalStorage('lastConversationSetup', { endpoint: EModelEndpoint.openAI });
|
||||
const { endpoint } = convo;
|
||||
const { conversation } = store.useCreateConversationAtom(index);
|
||||
let { endpoint = '' } = conversation ?? {};
|
||||
const iconURL = conversation?.iconURL ?? '';
|
||||
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const iconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
|
||||
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType, endpointIconURL });
|
||||
const Icon = icons[iconKey];
|
||||
|
||||
const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
|
@ -47,17 +54,22 @@ 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"
|
||||
>
|
||||
<div className="h-7 w-7 flex-shrink-0">
|
||||
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black dark:bg-white">
|
||||
{endpoint &&
|
||||
Icon &&
|
||||
Icon({
|
||||
size: 41,
|
||||
context: 'nav',
|
||||
className: 'h-2/3 w-2/3',
|
||||
endpoint: endpoint,
|
||||
iconURL: iconURL,
|
||||
})}
|
||||
</div>
|
||||
{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">
|
||||
{endpoint &&
|
||||
Icon &&
|
||||
Icon({
|
||||
size: 41,
|
||||
context: 'nav',
|
||||
className: 'h-2/3 w-2/3',
|
||||
endpoint,
|
||||
endpointType,
|
||||
iconURL: endpointIconURL,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-token-text-primary grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||
{localize('com_ui_new_chat')}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { Search, X } from 'lucide-react';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
useAvailablePluginsQuery,
|
||||
useUpdateUserPluginsMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import type { TError, TPluginAction } from 'librechat-data-provider';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TError, TPlugin, TPluginAction } from 'librechat-data-provider';
|
||||
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 PluginStoreItem from './PluginStoreItem';
|
||||
import PluginAuthForm from './PluginAuthForm';
|
||||
|
@ -16,7 +19,6 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
|||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { data: availablePlugins } = useAvailablePluginsQuery();
|
||||
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||
const { setTools } = useSetIndexOptions();
|
||||
|
||||
const [userPlugins, setUserPlugins] = useState<string[]>([]);
|
||||
|
@ -44,50 +46,49 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
|||
setSelectedPlugin,
|
||||
} = usePluginDialogHelpers();
|
||||
|
||||
const handleInstallError = (error: TError) => {
|
||||
setError(true);
|
||||
if (error.response?.data?.message) {
|
||||
setErrorMessage(error.response?.data?.message);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setError(false);
|
||||
setErrorMessage('');
|
||||
}, 5000);
|
||||
};
|
||||
const handleInstallError = useCallback(
|
||||
(error: TError) => {
|
||||
setError(true);
|
||||
if (error.response?.data?.message) {
|
||||
setErrorMessage(error.response?.data?.message);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setError(false);
|
||||
setErrorMessage('');
|
||||
}, 5000);
|
||||
},
|
||||
[setError, setErrorMessage],
|
||||
);
|
||||
|
||||
const handleInstall = (pluginAction: TPluginAction) => {
|
||||
updateUserPlugins.mutate(pluginAction, {
|
||||
onError: (error: unknown) => {
|
||||
handleInstallError(error as TError);
|
||||
},
|
||||
});
|
||||
const { installPlugin, uninstallPlugin } = usePluginInstall({
|
||||
onInstallError: handleInstallError,
|
||||
onUninstallError: handleInstallError,
|
||||
onUninstallSuccess: (_data, variables) => {
|
||||
setTools(variables.pluginKey, true);
|
||||
},
|
||||
});
|
||||
|
||||
const handleInstall = (pluginAction: TPluginAction, plugin?: TPlugin) => {
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
installPlugin(pluginAction, plugin);
|
||||
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 getAvailablePluginFromKey = availablePlugins?.find((p) => p.pluginKey === pluginKey);
|
||||
setSelectedPlugin(getAvailablePluginFromKey);
|
||||
const plugin = availablePlugins?.find((p) => p.pluginKey === pluginKey);
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
setSelectedPlugin(plugin);
|
||||
|
||||
const { authConfig, authenticated } = getAvailablePluginFromKey ?? {};
|
||||
const { authConfig, authenticated } = plugin ?? {};
|
||||
|
||||
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||
setShowPluginAuthForm(true);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled due to state setters erroneously being flagged as dependencies
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [availablePlugins, itemsPerPage, user, searchValue, filteredPlugins, searchChanged]);
|
||||
}, [
|
||||
availablePlugins,
|
||||
itemsPerPage,
|
||||
user,
|
||||
searchValue,
|
||||
filteredPlugins,
|
||||
searchChanged,
|
||||
setMaxPage,
|
||||
setCurrentPage,
|
||||
setSearchChanged,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -165,7 +173,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
|||
<div className="p-4 sm:p-6 sm:pt-4">
|
||||
<PluginAuthForm
|
||||
plugin={selectedPlugin}
|
||||
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
||||
onSubmit={(action: TPluginAction) => handleInstall(action, selectedPlugin)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -197,7 +205,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
|
|||
plugin={plugin}
|
||||
isInstalled={userPlugins.includes(plugin.pluginKey)}
|
||||
onInstall={() => onPluginInstall(plugin.pluginKey)}
|
||||
onUninstall={() => onPluginUninstall(plugin.pluginKey)}
|
||||
onUninstall={() => uninstallPlugin(plugin.pluginKey)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
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 { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
|
@ -25,7 +25,9 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
|
|||
useEffect(() => {
|
||||
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
|
||||
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];
|
||||
|
||||
if (!assistant) {
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
|
||||
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 { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
||||
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
|
||||
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
||||
import { useMediaQuery, useLocalStorage } from '~/hooks';
|
||||
import { Separator } from '~/components/ui/Separator';
|
||||
import NavToggle from '~/components/Nav/NavToggle';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import Switcher from './Switcher';
|
||||
|
@ -23,6 +27,7 @@ interface SidePanelProps {
|
|||
}
|
||||
|
||||
const defaultMinSize = 20;
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
const SidePanel = ({
|
||||
defaultLayout = [97, 3],
|
||||
|
@ -38,6 +43,12 @@ const SidePanel = ({
|
|||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const { conversation } = useChatContext();
|
||||
const { endpoint } = conversation ?? {};
|
||||
|
@ -69,7 +80,7 @@ const SidePanel = ({
|
|||
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
|
||||
const throttledSaveLayout = useCallback(
|
||||
|
@ -181,19 +192,20 @@ const SidePanel = ({
|
|||
: 'opacity-100',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
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',
|
||||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
<Switcher
|
||||
isCollapsed={isCollapsed}
|
||||
endpointKeyProvided={keyProvided}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||
</div>
|
||||
{interfaceConfig.modelSelect && (
|
||||
<div
|
||||
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',
|
||||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
<Switcher
|
||||
isCollapsed={isCollapsed}
|
||||
endpointKeyProvided={keyProvided}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Nav
|
||||
resize={panelRef.current?.resize}
|
||||
isCollapsed={isCollapsed}
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { SwitcherProps } from '~/common';
|
||||
import { Separator } from '~/components/ui/Separator';
|
||||
import AssistantSwitcher from './AssistantSwitcher';
|
||||
import ModelSwitcher from './ModelSwitcher';
|
||||
|
||||
export default function Switcher(props: SwitcherProps) {
|
||||
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) {
|
||||
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;
|
||||
showLabel?: boolean;
|
||||
containerClassName?: string;
|
||||
optionsClassName?: string;
|
||||
labelClassName?: string;
|
||||
isSelected: (value: string) => boolean;
|
||||
className?: string;
|
||||
searchPlaceholder?: string;
|
||||
|
@ -31,6 +33,8 @@ function MultiSelectDropDown({
|
|||
showAbove = false,
|
||||
showLabel = true,
|
||||
containerClassName,
|
||||
optionsClassName = '',
|
||||
labelClassName = '',
|
||||
isSelected,
|
||||
className,
|
||||
searchPlaceholder,
|
||||
|
@ -72,7 +76,7 @@ function MultiSelectDropDown({
|
|||
<>
|
||||
<Listbox.Button
|
||||
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 ?? '',
|
||||
)}
|
||||
id={excludeIds[0]}
|
||||
|
@ -82,7 +86,7 @@ function MultiSelectDropDown({
|
|||
{' '}
|
||||
{showLabel && (
|
||||
<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]}
|
||||
data-headlessui-state=""
|
||||
>
|
||||
|
@ -153,6 +157,7 @@ function MultiSelectDropDown({
|
|||
ref={menuRef}
|
||||
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%]',
|
||||
optionsClassName,
|
||||
)}
|
||||
>
|
||||
{searchRender}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { Wrench } from 'lucide-react';
|
||||
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
|
@ -115,7 +114,7 @@ function MultiSelectPop({
|
|||
side="bottom"
|
||||
align="center"
|
||||
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',
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
@ -264,10 +265,10 @@ export const useLogoutUserMutation = (
|
|||
onMutate: (...args) => {
|
||||
setDefaultPreset(null);
|
||||
queryClient.removeQueries();
|
||||
localStorage.removeItem('lastConversationSetup');
|
||||
localStorage.removeItem('lastSelectedModel');
|
||||
localStorage.removeItem('lastSelectedTools');
|
||||
localStorage.removeItem('filesToDelete');
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_MODEL);
|
||||
localStorage.removeItem(LocalStorageKeys.LAST_TOOLS);
|
||||
localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE);
|
||||
// localStorage.removeItem('lastAssistant');
|
||||
options?.onMutate?.(...args);
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback } from 'react';
|
||||
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
||||
import type { TConversation, TPreset } from 'librechat-data-provider';
|
||||
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||
import { useListAssistantsQuery } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers/ChatContext';
|
||||
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
||||
import { mapAssistants } from '~/utils';
|
||||
|
||||
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 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 useNavigateToConvo } from './useNavigateToConvo';
|
||||
export { default as useSetIndexOptions } from './useSetIndexOptions';
|
||||
export { default as useParameterEffects } from './useParameterEffects';
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||
import useOriginNavigate from './useOriginNavigate';
|
||||
import useOriginNavigate from '../useOriginNavigate';
|
||||
import store from '~/store';
|
||||
|
||||
const useConversation = () => {
|
|
@ -3,17 +3,14 @@ import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
|||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||
import useOriginNavigate from './useOriginNavigate';
|
||||
import useSetStorage from './useSetStorage';
|
||||
import useOriginNavigate from '../useOriginNavigate';
|
||||
import store from '~/store';
|
||||
|
||||
const useNavigateToConvo = (index = 0) => {
|
||||
const setStorage = useSetStorage();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useOriginNavigate();
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
// const setConversation = useSetRecoilState(store.conversationByIndex(index));
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
|
||||
const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => {
|
||||
|
@ -50,7 +47,6 @@ const useNavigateToConvo = (index = 0) => {
|
|||
models,
|
||||
});
|
||||
}
|
||||
setStorage(convo);
|
||||
setConversation(convo);
|
||||
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 { useChatContext } from '~/Providers/ChatContext';
|
||||
import { cleanupPreset } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
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();
|
||||
|
||||
if (!_preset) {
|
||||
|
@ -99,9 +103,52 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
|
|||
);
|
||||
};
|
||||
|
||||
const checkPluginSelection: (value: string) => boolean = () => false;
|
||||
const setTools: (newValue: string) => void = () => {
|
||||
return;
|
||||
function checkPluginSelection(value: string) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -116,4 +163,4 @@ const usePresetOptions: TUsePresetOptions = (_preset) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default usePresetOptions;
|
||||
export default usePresetIndexOptions;
|
|
@ -11,9 +11,9 @@ import {
|
|||
useDeletePresetMutation,
|
||||
useGetPresetsQuery,
|
||||
} from '~/data-provider';
|
||||
import { cleanupPreset, getEndpointField, removeUnavailableTools } from '~/utils';
|
||||
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { cleanupPreset, getEndpointField } from '~/utils';
|
||||
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
|
@ -28,6 +28,7 @@ export default function usePresets() {
|
|||
const { user, isAuthenticated } = useAuthContext();
|
||||
|
||||
const modularChat = useRecoilValue(store.modularChat);
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
|
||||
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
||||
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
|
||||
|
@ -151,11 +152,13 @@ export default function usePresets() {
|
|||
importPreset(jsonPreset);
|
||||
};
|
||||
|
||||
const onSelectPreset = (newPreset: TPreset) => {
|
||||
if (!newPreset) {
|
||||
const onSelectPreset = (_newPreset: TPreset) => {
|
||||
if (!_newPreset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPreset = removeUnavailableTools(_newPreset, availableTools);
|
||||
|
||||
const toastTitle = newPreset.title
|
||||
? `"${newPreset.title}"`
|
||||
: localize('com_endpoint_preset_title');
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
TPreset,
|
||||
TPlugin,
|
||||
tConvoUpdateSchema,
|
||||
EModelEndpoint,
|
||||
TConversation,
|
||||
} from 'librechat-data-provider';
|
||||
import { TPreset, TPlugin, TConversation, tConvoUpdateSchema } from 'librechat-data-provider';
|
||||
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
|
||||
import usePresetIndexOptions from './usePresetIndexOptions';
|
||||
import { useChatContext } from '~/Providers/ChatContext';
|
||||
import useLocalStorage from './useLocalStorage';
|
||||
import store from '~/store';
|
||||
|
||||
type TUseSetOptions = (preset?: TPreset | boolean | null) => TSetOptionsPayload;
|
||||
|
@ -18,11 +11,6 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
const setShowPluginStoreDialog = useSetRecoilState(store.showPluginStoreDialog);
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
const { conversation, setConversation } = useChatContext();
|
||||
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
|
||||
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {
|
||||
primaryModel: '',
|
||||
secondaryModel: '',
|
||||
});
|
||||
|
||||
const result = usePresetIndexOptions(preset);
|
||||
|
||||
|
@ -31,16 +19,10 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
}
|
||||
|
||||
const setOption: TSetOption = (param) => (newValue) => {
|
||||
const { endpoint } = conversation ?? {};
|
||||
const update = {};
|
||||
update[param] = newValue;
|
||||
|
||||
if (param === 'model' && endpoint) {
|
||||
const lastModelUpdate = { ...lastModel, [endpoint]: newValue };
|
||||
setLastModel(lastModelUpdate);
|
||||
} else if (param === 'jailbreak' && endpoint) {
|
||||
setLastBingSettings({ ...lastBingSettings, jailbreak: newValue });
|
||||
} else if (param === 'presetOverride') {
|
||||
if (param === 'presetOverride') {
|
||||
const currentOverride = conversation?.presetOverride || {};
|
||||
update['presetOverride'] = {
|
||||
...currentOverride,
|
||||
|
@ -116,7 +98,14 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
if (!conversation?.tools) {
|
||||
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) => {
|
||||
|
@ -124,12 +113,7 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
const convo = JSON.parse(editableConvo);
|
||||
const { agentOptions } = convo;
|
||||
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(
|
||||
(prevState) =>
|
||||
tConvoUpdateSchema.parse({
|
||||
|
@ -146,17 +130,23 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
|
|||
}
|
||||
|
||||
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 tool =
|
||||
availableTools[availableTools.findIndex((el: TPlugin) => el.pluginKey === newValue)];
|
||||
const tool = availableTools[newValue];
|
||||
if (isSelected || remove) {
|
||||
update['tools'] = current.filter((el) => el.pluginKey !== newValue);
|
||||
} else {
|
||||
update['tools'] = [...current, tool];
|
||||
}
|
||||
|
||||
localStorage.setItem('lastSelectedTools', JSON.stringify(update['tools']));
|
||||
setConversation(
|
||||
(prevState) =>
|
||||
tConvoUpdateSchema.parse({
|
|
@ -1,5 +1,7 @@
|
|||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
|
||||
export default function useSetFilesToDelete() {
|
||||
const setFilesToDelete = (files: Record<string, unknown>) =>
|
||||
localStorage.setItem('filesToDelete', JSON.stringify(files));
|
||||
localStorage.setItem(LocalStorageKeys.FILES_TO_DELETE, JSON.stringify(files));
|
||||
return setFilesToDelete;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import copy from 'copy-to-clipboard';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
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 { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
import { getEndpointField } from '~/utils';
|
||||
|
||||
export default function useMessageHelpers(props: TMessageProps) {
|
||||
const latestText = useRef<string | number>('');
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { message, currentEditId, setCurrentEditId } = props;
|
||||
|
||||
const {
|
||||
|
@ -62,18 +57,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
const assistant =
|
||||
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 = () => {
|
||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
||||
return;
|
||||
|
@ -105,7 +88,6 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
|
||||
return {
|
||||
ask,
|
||||
icon,
|
||||
edit,
|
||||
isLast,
|
||||
assistant,
|
|
@ -4,7 +4,7 @@ import {
|
|||
// Settings2,
|
||||
} from 'lucide-react';
|
||||
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 PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
||||
// import Parameters from '~/components/SidePanel/Parameters/Panel';
|
||||
|
@ -16,11 +16,13 @@ export default function useSideNavLinks({
|
|||
assistants,
|
||||
keyProvided,
|
||||
endpoint,
|
||||
interfaceConfig,
|
||||
}: {
|
||||
hidePanel: () => void;
|
||||
assistants?: TConfig | null;
|
||||
keyProvided: boolean;
|
||||
endpoint?: EModelEndpoint | null;
|
||||
interfaceConfig: Partial<TInterfaceConfig>;
|
||||
}) {
|
||||
const Links = useMemo(() => {
|
||||
const links: NavLink[] = [];
|
||||
|
@ -37,7 +39,8 @@ export default function useSideNavLinks({
|
|||
endpoint === EModelEndpoint.assistants &&
|
||||
assistants &&
|
||||
assistants.disableBuilder !== true &&
|
||||
keyProvided
|
||||
keyProvided &&
|
||||
interfaceConfig.parameters
|
||||
) {
|
||||
links.push({
|
||||
title: 'com_sidepanel_assistant_builder',
|
||||
|
@ -65,7 +68,7 @@ export default function useSideNavLinks({
|
|||
});
|
||||
|
||||
return links;
|
||||
}, [assistants, keyProvided, hidePanel, endpoint]);
|
||||
}, [assistants, keyProvided, hidePanel, endpoint, interfaceConfig.parameters]);
|
||||
|
||||
return Links;
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { default as usePluginInstall } from './usePluginInstall';
|
||||
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 */
|
||||
SSE,
|
||||
QueryKeys,
|
||||
EndpointURLs,
|
||||
Constants,
|
||||
EndpointURLs,
|
||||
createPayload,
|
||||
tPresetSchema,
|
||||
tMessageSchema,
|
||||
EModelEndpoint,
|
||||
LocalStorageKeys,
|
||||
tConvoUpdateSchema,
|
||||
removeNullishValues,
|
||||
} from 'librechat-data-provider';
|
||||
|
@ -34,7 +35,6 @@ import { useGenTitleMutation } from '~/data-provider';
|
|||
import useContentHandler from './useContentHandler';
|
||||
import { useAuthContext } from '../AuthContext';
|
||||
import useChatHelpers from '../useChatHelpers';
|
||||
import useSetStorage from '../useSetStorage';
|
||||
import store from '~/store';
|
||||
|
||||
type TResData = {
|
||||
|
@ -59,7 +59,6 @@ type TSyncData = {
|
|||
};
|
||||
|
||||
export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||
const setStorage = useSetStorage();
|
||||
const queryClient = useQueryClient();
|
||||
const genTitle = useGenTitleMutation();
|
||||
|
||||
|
@ -165,13 +164,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
...convoUpdate,
|
||||
};
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[setMessages, setConversation, setStorage, genTitle, queryClient, setIsSubmitting],
|
||||
[setMessages, setConversation, genTitle, queryClient, setIsSubmitting],
|
||||
);
|
||||
|
||||
const syncHandler = useCallback(
|
||||
|
@ -208,7 +206,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
messages: [requestMessage.messageId, responseMessage.messageId],
|
||||
}) as TConversation;
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
|
@ -227,7 +224,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
|
||||
resetLatestMessage();
|
||||
},
|
||||
[setMessages, setConversation, setStorage, queryClient, setShowStopButton, resetLatestMessage],
|
||||
[setMessages, setConversation, queryClient, setShowStopButton, resetLatestMessage],
|
||||
);
|
||||
|
||||
const createdHandler = useCallback(
|
||||
|
@ -273,7 +270,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
title,
|
||||
}) as TConversation;
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
|
@ -289,7 +285,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
});
|
||||
resetLatestMessage();
|
||||
},
|
||||
[setMessages, setConversation, setStorage, queryClient, resetLatestMessage],
|
||||
[setMessages, setConversation, queryClient, resetLatestMessage],
|
||||
);
|
||||
|
||||
const finalHandler = useCallback(
|
||||
|
@ -336,21 +332,12 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
update.model = prevState.model;
|
||||
}
|
||||
|
||||
setStorage(update);
|
||||
return update;
|
||||
});
|
||||
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
[
|
||||
setMessages,
|
||||
setConversation,
|
||||
setStorage,
|
||||
genTitle,
|
||||
queryClient,
|
||||
setIsSubmitting,
|
||||
setShowStopButton,
|
||||
],
|
||||
[genTitle, queryClient, setMessages, setConversation, setIsSubmitting, setShowStopButton],
|
||||
);
|
||||
|
||||
const errorHandler = useCallback(
|
||||
|
@ -430,8 +417,9 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
|||
async (conversationId = '', submission: TSubmission) => {
|
||||
let runAbortKey = '';
|
||||
try {
|
||||
const conversation = (JSON.parse(localStorage.getItem('lastConversationSetup') ?? '') ??
|
||||
{}) as TConversation;
|
||||
const conversation = (JSON.parse(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) ?? '',
|
||||
) ?? {}) as TConversation;
|
||||
const { conversationId, messages } = conversation;
|
||||
runAbortKey = `${conversationId}:${messages?.[messages.length - 1]}`;
|
||||
} catch (error) {
|
||||
|
|
|
@ -17,20 +17,11 @@ export { default as useTimeout } from './useTimeout';
|
|||
export { default as useNewConvo } from './useNewConvo';
|
||||
export { default as useLocalize } from './useLocalize';
|
||||
export { default as useMediaQuery } from './useMediaQuery';
|
||||
export { default as useSetOptions } from './useSetOptions';
|
||||
export { default as useSetStorage } from './useSetStorage';
|
||||
export { default as useChatHelpers } from './useChatHelpers';
|
||||
export { default as useGenerations } from './useGenerations';
|
||||
export { default as useScrollToRef } from './useScrollToRef';
|
||||
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 useOnClickOutside } from './useOnClickOutside';
|
||||
export { default as useMessageHandler } from './useMessageHandler';
|
||||
export { default as useOriginNavigate } from './useOriginNavigate';
|
||||
export { default as useNavigateToConvo } from './useNavigateToConvo';
|
||||
export { default as useSetIndexOptions } from './useSetIndexOptions';
|
||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||
|
|
|
@ -211,6 +211,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
|
|||
unfinished: false,
|
||||
isCreatedByUser: false,
|
||||
isEdited: isEditOrContinue,
|
||||
iconURL: convo.iconURL,
|
||||
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