mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-24 11:16:12 +01:00
🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)
* WIP: first pass ModelSpecs * refactor(onSelectEndpoint): use `getConvoSwitchLogic` * feat: introduce iconURL, greeting, frontend fields for conversations/presets/messages * feat: conversation.iconURL & greeting in Landing * feat: conversation.iconURL & greeting in New Chat button * feat: message.iconURL * refactor: ConversationIcon -> ConvoIconURL * WIP: add spec as a conversation field * refactor: useAppStartup, set spec on initial load for new chat, allow undefined spec, add localStorage keys enum, additional type fields for spec * feat: handle `showIconInMenu`, `showIconInHeader`, undefined `iconURL` and no specs on initial load * chore: handle undefined or empty modelSpecs * WIP: first pass, modelSpec schema for custom config * refactor: move default filtered tools definition to ToolService * feat: pass modelSpecs from backend via startupConfig * refactor: modelSpecs config, return and define list * fix: react error and include iconURL in responseMessage * refactor: add iconURL to responseMessage only * refactor: getIconEndpoint * refactor: pass TSpecsConfig * fix(assistants): differentiate compactAssistantSchema, correctly resets shared conversation state with other endpoints * refactor: assistant id prefix localStorage key * refactor: add more LocalStorageKeys and replace hardcoded values * feat: prioritize spec on new chat behavior: last selected modelSpec behavior (localStorage) * feat: first pass, interface config * chore: WIP, todo: add warnings based on config.modelSpecs settings. * feat: enforce modelSpecs if configured * feat: show config file yaml errors * chore: delete unused legacy Plugins component * refactor: set tools to localStorage from recoil store * chore: add stable recoil setter to useEffect deps * refactor: save tools to conversation documents * style(MultiSelectPop): dynamic height, remove unused import * refactor(react-query): use localstorage keys and pass config to useAvailablePluginsQuery * feat(utils): add mapPlugins * refactor(Convo): use conversation.tools if defined, lastSelectedTools if not * refactor: remove unused legacy code using `useSetOptions`, remove conditional flag `isMultiChat` for using legacy settings * refactor(PluginStoreDialog): add exhaustive-deps which are stable react state setters * fix(HeaderOptions): pass `popover` as true * refactor(useSetStorage): use project enums * refactor: use LocalStorageKeys enum * fix: prevent setConversation from setting falsy values in lastSelectedTools * refactor: use map for availableTools state and available Plugins query * refactor(updateLastSelectedModel): organize logic better and add note on purpose * fix(setAgentOption): prevent reseting last model to secondary model for gptPlugins * refactor(buildDefaultConvo): use enum * refactor: remove `useSetStorage` and consolidate areas where conversation state is saved to localStorage * fix: conversations retain tools on refresh * fix(gptPlugins): prevent nullish tools from being saved * chore: delete useServerStream * refactor: move initial plugins logic to useAppStartup * refactor(MultiSelectDropDown): add more pass-in className props * feat: use tools in presets * chore: delete unused usePresetOptions * refactor: new agentOptions default handling * chore: note * feat: add label and custom instructions to agents * chore: remove 'disabled with tools' message * style: move plugins to 2nd column in parameters * fix: TPreset type for agentOptions * fix: interface controls * refactor: add interfaceConfig, use Separator within Switcher * refactor: hide Assistants panel if interface.parameters are disabled * fix(Header): only modelSpecs if list is greater than 0 * refactor: separate MessageIcon logic from useMessageHelpers for better react rule-following * fix(AppService): don't use reserved keyword 'interface' * feat: set existing Icon for custom endpoints through iconURL * fix(ci): tests passing for App Service * docs: refactor custom_config.md for readability and better organization, also include missing values * docs: interface section and re-organize docs * docs: update modelSpecs info * chore: remove unused files * chore: remove unused files * chore: move useSetIndexOptions * chore: remove unused file * chore: move useConversation(s) * chore: move useDefaultConvo * chore: move useNavigateToConvo * refactor: use plugin install hook so it can be used elsewhere * chore: import order * update docs * refactor(OpenAI/Plugins): allow modelLabel as an initial value for chatGptLabel * chore: remove unused EndpointOptionsPopover and hide 'Save as Preset' button if preset UI visibility disabled * feat(loadDefaultInterface): issue warnings based on values * feat: changelog for custom config file * docs: add additional changelog note * fix: prevent unavailable tool selection from preset and update availableTools on Plugin installations * feat: add `filteredTools` option in custom config * chore: changelog * fix(MessageIcon): always overwrite conversation.iconURL in messageSettings * fix(ModelSpecsMenu): icon edge cases * fix(NewChat): dynamic icon * fix(PluginsClient): always include endpoint in responseMessage * fix: always include endpoint and iconURL in responseMessage across different response methods * feat: interchangeable keys for modelSpec enforcing
This commit is contained in:
parent
a5cac03fa4
commit
0e50c07e3f
130 changed files with 3934 additions and 2973 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue