🚀 feat: enhance UI components and refactor settings (#6625)

* 🚀 feat: Add Save Badges State functionality to chat settings

* 🚀 feat: Remove individual chat setting components and introduce a reusable ToggleSwitch component

* 🚀 feat: Replace Switches with reusable ToggleSwitch component in General settings; style: improved HoverCard

* 🚀 feat: Refactor ChatForm and Footer components for improved layout and state management

* 🚀 feat: Add deprecation warning for GPT Plugins endpoint

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-04-01 09:15:41 +02:00 committed by GitHub
parent 14ff66b2c3
commit a5154e1349
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 227 additions and 350 deletions

View file

@ -56,7 +56,6 @@ export default function Footer({ className }: { className?: string }) {
<React.Fragment key={`main-content-part-${index}`}> <React.Fragment key={`main-content-part-${index}`}>
<ReactMarkdown <ReactMarkdown
components={{ components={{
a: ({ node: _n, href, children, ...otherProps }) => { a: ({ node: _n, href, children, ...otherProps }) => {
return ( return (
<a <a
@ -87,7 +86,7 @@ export default function Footer({ className }: { className?: string }) {
<div <div
className={ className={
className ?? className ??
'relative hidden items-center justify-center gap-2 px-2 py-2 text-center text-xs text-text-primary sm:flex md:px-[60px]' 'absolute bottom-0 left-0 right-0 hidden items-center justify-center gap-2 px-2 py-2 text-center text-xs text-text-primary sm:flex md:px-[60px]'
} }
role="contentinfo" role="contentinfo"
> >

View file

@ -43,12 +43,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false); const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]); const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]);
const isSearching = useRecoilValue(store.isSearching);
const SpeechToText = useRecoilValue(store.speechToText); const SpeechToText = useRecoilValue(store.speechToText);
const TextToSpeech = useRecoilValue(store.textToSpeech); const TextToSpeech = useRecoilValue(store.textToSpeech);
const chatDirection = useRecoilValue(store.chatDirection);
const automaticPlayback = useRecoilValue(store.automaticPlayback); const automaticPlayback = useRecoilValue(store.automaticPlayback);
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
const chatDirection = useRecoilValue(store.chatDirection); const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
const isSearching = useRecoilValue(store.isSearching);
const [badges, setBadges] = useRecoilState(store.chatBadges); const [badges, setBadges] = useRecoilState(store.chatBadges);
const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges); const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges);
@ -190,8 +191,9 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
<form <form
onSubmit={methods.handleSubmit(submitMessage)} onSubmit={methods.handleSubmit(submitMessage)}
className={cn( className={cn(
'mx-auto flex flex-row gap-3 transition-all duration-200 sm:mb-2 sm:px-2', 'mx-auto flex flex-row gap-3 transition-all duration-200 sm:px-2',
maximizeChatSpace ? 'w-full max-w-full' : 'md:max-w-3xl xl:max-w-4xl', maximizeChatSpace ? 'w-full max-w-full' : 'md:max-w-3xl xl:max-w-4xl',
centerFormOnLanding ? 'sm:mb-28' : 'sm:mb-10',
)} )}
> >
<div className="relative flex h-full flex-1 items-stretch md:flex-col"> <div className="relative flex h-full flex-1 items-stretch md:flex-col">
@ -217,7 +219,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
<div <div
onClick={handleContainerClick} onClick={handleContainerClick}
className={cn( className={cn(
'relative flex w-full flex-grow flex-col overflow-hidden rounded-t-3xl border border-border-light bg-surface-chat pb-4 text-text-primary transition-all duration-200 sm:rounded-3xl sm:pb-0', 'relative flex w-full flex-grow flex-col overflow-hidden rounded-t-3xl border border-border-medium bg-surface-chat pb-4 text-text-primary transition-all duration-200 sm:rounded-3xl sm:pb-0',
isTextAreaFocused ? 'shadow-lg' : 'shadow-md', isTextAreaFocused ? 'shadow-lg' : 'shadow-md',
)} )}
> >

View file

@ -1,11 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SettingsIcon } from 'lucide-react'; import { SettingsIcon } from 'lucide-react';
import { Spinner } from '~/components';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { Endpoint } from '~/common'; import type { Endpoint } from '~/common';
import { CustomMenu as Menu, CustomMenuItem as MenuItem } from '../CustomMenu'; import { CustomMenu as Menu, CustomMenuItem as MenuItem } from '../CustomMenu';
import { useModelSelectorContext } from '../ModelSelectorContext'; import { useModelSelectorContext } from '../ModelSelectorContext';
import { renderEndpointModels } from './EndpointModelItem'; import { renderEndpointModels } from './EndpointModelItem';
import { TooltipAnchor, Spinner } from '~/components';
import { filterModels } from '../utils'; import { filterModels } from '../utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -84,6 +84,18 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
> >
{endpoint.label} {endpoint.label}
</span> </span>
{/* TODO: remove this after deprecation */}
{endpoint.value === 'gptPlugins' && (
<TooltipAnchor
description={localize('com_endpoint_deprecated_info')}
aria-label={localize('com_endpoint_deprecated_info_a11y')}
render={
<span className="ml-2 rounded bg-amber-600/70 px-2 py-0.5 text-xs font-semibold text-white">
{localize('com_endpoint_deprecated')}
</span>
}
/>
)}
</div> </div>
); );

View file

@ -1,34 +0,0 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui/Switch';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
export default function CenterChatInput({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [centerFormOnLanding, setcenterFormOnLanding] = useRecoilState(store.centerFormOnLanding);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setcenterFormOnLanding(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_center_chat_input')}</div>
<Switch
id="centerFormOnLanding"
checked={centerFormOnLanding}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="centerFormOnLanding"
/>
</div>
);
}

View file

@ -1,16 +1,82 @@
import { memo } from 'react'; import { memo } from 'react';
import MaximizeChatSpace from './MaximizeChatSpace';
import FontSizeSelector from './FontSizeSelector'; import FontSizeSelector from './FontSizeSelector';
import SendMessageKeyEnter from './EnterToSend';
import CenterChatInput from './CenterChatInput';
import ShowCodeSwitch from './ShowCodeSwitch';
import { ForkSettings } from './ForkSettings'; import { ForkSettings } from './ForkSettings';
import ChatDirection from './ChatDirection'; import ChatDirection from './ChatDirection';
import ShowThinking from './ShowThinking'; import ToggleSwitch from '../ToggleSwitch';
import LaTeXParsing from './LaTeXParsing'; import store from '~/store';
import ScrollButton from './ScrollButton';
import ModularChat from './ModularChat'; const toggleSwitchConfigs = [
import SaveDraft from './SaveDraft'; {
stateAtom: store.enterToSend,
localizationKey: 'com_nav_enter_to_send',
switchId: 'enterToSend',
hoverCardText: 'com_nav_info_enter_to_send',
key: 'enterToSend',
},
{
stateAtom: store.maximizeChatSpace,
localizationKey: 'com_nav_maximize_chat_space',
switchId: 'maximizeChatSpace',
hoverCardText: undefined,
key: 'maximizeChatSpace',
},
{
stateAtom: store.centerFormOnLanding,
localizationKey: 'com_nav_center_chat_input',
switchId: 'centerFormOnLanding',
hoverCardText: undefined,
key: 'centerFormOnLanding',
},
{
stateAtom: store.showThinking,
localizationKey: 'com_nav_show_thinking',
switchId: 'showThinking',
hoverCardText: undefined,
key: 'showThinking',
},
{
stateAtom: store.showCode,
localizationKey: 'com_nav_show_code',
switchId: 'showCode',
hoverCardText: undefined,
key: 'showCode',
},
{
stateAtom: store.LaTeXParsing,
localizationKey: 'com_nav_latex_parsing',
switchId: 'latexParsing',
hoverCardText: 'com_nav_info_latex_parsing',
key: 'latexParsing',
},
{
stateAtom: store.saveDrafts,
localizationKey: 'com_nav_save_drafts',
switchId: 'saveDrafts',
hoverCardText: 'com_nav_info_save_draft',
key: 'saveDrafts',
},
{
stateAtom: store.showScrollButton,
localizationKey: 'com_nav_scroll_button',
switchId: 'showScrollButton',
hoverCardText: undefined,
key: 'showScrollButton',
},
{
stateAtom: store.saveBadgesState,
localizationKey: 'com_nav_save_badges_state',
switchId: 'showBadges',
hoverCardText: 'com_nav_info_save_badges_state',
key: 'showBadges',
},
{
stateAtom: store.modularChat,
localizationKey: 'com_nav_modular_chat',
switchId: 'modularChat',
hoverCardText: undefined,
key: 'modularChat',
},
];
function Chat() { function Chat() {
return ( return (
@ -21,34 +87,17 @@ function Chat() {
<div className="pb-3"> <div className="pb-3">
<ChatDirection /> <ChatDirection />
</div> </div>
<div className="pb-3"> {toggleSwitchConfigs.map((config) => (
<CenterChatInput /> <div key={config.key} className="pb-3">
</div> <ToggleSwitch
<div className="pb-3"> stateAtom={config.stateAtom}
<SendMessageKeyEnter /> localizationKey={config.localizationKey}
</div> hoverCardText={config.hoverCardText}
<div className="pb-3"> switchId={config.switchId}
<MaximizeChatSpace /> />
</div>
<div className="pb-3">
<ShowCodeSwitch />
</div>
<div className="pb-3">
<SaveDraft />
</div>
<div className="pb-3">
<ScrollButton />
</div> </div>
))}
<ForkSettings /> <ForkSettings />
<div className="pb-3">
<ModularChat />
</div>
<div className="pb-3">
<LaTeXParsing />
</div>
<div className="pb-3">
<ShowThinking />
</div>
</div> </div>
); );
} }

View file

@ -1,37 +0,0 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui/Switch';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
export default function SendMessageKeyEnter({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [enterToSend, setEnterToSend] = useRecoilState<boolean>(store.enterToSend);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setEnterToSend(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_enter_to_send')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_enter_to_send" />
</div>
<Switch
id="enterToSend"
checked={enterToSend}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="enterToSend"
/>
</div>
);
}

View file

@ -1,37 +0,0 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function LaTeXParsingSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [LaTeXParsing, setLaTeXParsing] = useRecoilState<boolean>(store.LaTeXParsing);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setLaTeXParsing(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_latex_parsing')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_latex_parsing" />
</div>
<Switch
id="LaTeXParsing"
checked={LaTeXParsing}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="LaTeXParsing"
/>
</div>
);
}

View file

@ -1,37 +0,0 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui/Switch';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
export default function MaximizeChatSpace({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [maximizeChatSpace, setmaximizeChatSpace] = useRecoilState<boolean>(
store.maximizeChatSpace,
);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setmaximizeChatSpace(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_maximize_chat_space')}</div>
</div>
<Switch
id="maximizeChatSpace"
checked={maximizeChatSpace}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="maximizeChatSpace"
/>
</div>
);
}

View file

@ -1,33 +0,0 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function ModularChatSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [modularChat, setModularChat] = useRecoilState<boolean>(store.modularChat);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setModularChat(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div> {localize('com_nav_modular_chat')} </div>
<Switch
id="modularChat"
checked={modularChat}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="modularChat"
/>
</div>
);
}

View file

@ -4,16 +4,16 @@ import { Switch } from '~/components/ui';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
import store from '~/store'; import store from '~/store';
export default function SaveDraft({ export default function SaveBadgesState({
onCheckedChange, onCheckedChange,
}: { }: {
onCheckedChange?: (value: boolean) => void; onCheckedChange?: (value: boolean) => void;
}) { }) {
const [saveDrafts, setSaveDrafts] = useRecoilState<boolean>(store.saveDrafts); const [saveBadgesState, setSaveBadgesState] = useRecoilState<boolean>(store.saveBadgesState);
const localize = useLocalize(); const localize = useLocalize();
const handleCheckedChange = (value: boolean) => { const handleCheckedChange = (value: boolean) => {
setSaveDrafts(value); setSaveBadgesState(value);
if (onCheckedChange) { if (onCheckedChange) {
onCheckedChange(value); onCheckedChange(value);
} }
@ -22,15 +22,15 @@ export default function SaveDraft({
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div>{localize('com_nav_save_drafts')}</div> <div>{localize('com_nav_save_badges_state')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_save_draft" /> <HoverCardSettings side="bottom" text="com_nav_info_save_badges_state" />
</div> </div>
<Switch <Switch
id="saveDrafts" id="saveBadgesState"
checked={saveDrafts} checked={saveBadgesState}
onCheckedChange={handleCheckedChange} onCheckedChange={handleCheckedChange}
className="ml-4" className="ml-4"
data-testid="saveDrafts" data-testid="saveBadgesState"
/> />
</div> </div>
); );

View file

@ -1,35 +0,0 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui/Switch';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
export default function ScrollButton({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [showScrollButton, setShowScrollButton] = useRecoilState<boolean>(store.showScrollButton);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setShowScrollButton(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_scroll_button')}</div>
</div>
<Switch
id="scrollButton"
checked={showScrollButton}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="scrollButton"
/>
</div>
);
}

View file

@ -1,34 +0,0 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function ShowCodeSwitch({
onCheckedChange,
}: {
onCheckedChange?: (value: boolean) => void;
}) {
const [showCode, setShowCode] = useRecoilState<boolean>(store.showCode);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setShowCode(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div> {localize('com_nav_show_code')} </div>
<Switch
id="showCode"
checked={showCode}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="showCode"
/>
</div>
);
}

View file

@ -1,38 +0,0 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, fireEvent } from 'test/layout-test-utils';
import AutoScrollSwitch from './AutoScrollSwitch';
import { RecoilRoot } from 'recoil';
describe('AutoScrollSwitch', () => {
/**
* Mock function to set the auto-scroll state.
*/
let mockSetAutoScroll: jest.Mock<void, [boolean]> | ((value: boolean) => void) | undefined;
beforeEach(() => {
mockSetAutoScroll = jest.fn();
});
it('renders correctly', () => {
const { getByTestId } = render(
<RecoilRoot>
<AutoScrollSwitch />
</RecoilRoot>,
);
expect(getByTestId('autoScroll')).toBeInTheDocument();
});
it('calls onCheckedChange when the switch is toggled', () => {
const { getByTestId } = render(
<RecoilRoot>
<AutoScrollSwitch onCheckedChange={mockSetAutoScroll} />
</RecoilRoot>,
);
const switchElement = getByTestId('autoScroll');
fireEvent.click(switchElement);
expect(mockSetAutoScroll).toHaveBeenCalledWith(true);
});
});

View file

@ -6,9 +6,34 @@ import HideSidePanelSwitch from './HideSidePanelSwitch';
import { ThemeContext, useLocalize } from '~/hooks'; import { ThemeContext, useLocalize } from '~/hooks';
import AutoScrollSwitch from './AutoScrollSwitch'; import AutoScrollSwitch from './AutoScrollSwitch';
import ArchivedChats from './ArchivedChats'; import ArchivedChats from './ArchivedChats';
import { Dropdown } from '~/components/ui'; import ToggleSwitch from '../ToggleSwitch';
import { Dropdown } from '~/components';
import store from '~/store'; import store from '~/store';
const toggleSwitchConfigs = [
{
stateAtom: store.enableUserMsgMarkdown,
localizationKey: 'com_nav_user_msg_markdown',
switchId: 'enableUserMsgMarkdown',
hoverCardText: undefined,
key: 'enableUserMsgMarkdown',
},
{
stateAtom: store.autoScroll,
localizationKey: 'com_nav_auto_scroll',
switchId: 'autoScroll',
hoverCardText: undefined,
key: 'autoScroll',
},
{
stateAtom: store.hideSidePanel,
localizationKey: 'com_nav_hide_panel',
switchId: 'hideSidePanel',
hoverCardText: undefined,
key: 'hideSidePanel',
},
];
export const ThemeSelector = ({ export const ThemeSelector = ({
theme, theme,
onChange, onChange,
@ -127,15 +152,16 @@ function General() {
<div className="pb-3"> <div className="pb-3">
<LangSelector langcode={langcode} onChange={changeLang} /> <LangSelector langcode={langcode} onChange={changeLang} />
</div> </div>
<div className="pb-3"> {toggleSwitchConfigs.map((config) => (
<UserMsgMarkdownSwitch /> <div key={config.key} className="pb-3">
</div> <ToggleSwitch
<div className="pb-3"> stateAtom={config.stateAtom}
<AutoScrollSwitch /> localizationKey={config.localizationKey}
</div> hoverCardText={config.hoverCardText}
<div className="pb-3"> switchId={config.switchId}
<HideSidePanelSwitch /> />
</div> </div>
))}
<div className="pb-3"> <div className="pb-3">
<ArchivedChats /> <ArchivedChats />
</div> </div>

View file

@ -0,0 +1,49 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from './HoverCardSettings';
import useLocalize from '~/hooks/useLocalize';
import { Switch } from '~/components/ui';
import { RecoilState } from 'recoil';
interface ToggleSwitchProps {
stateAtom: RecoilState<boolean>;
localizationKey: string;
hoverCardText?: string;
switchId: string;
onCheckedChange?: (value: boolean) => void;
}
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
stateAtom,
localizationKey,
hoverCardText,
switchId,
onCheckedChange,
}) => {
const [switchState, setSwitchState] = useRecoilState<boolean>(stateAtom);
const localize = useLocalize();
const handleCheckedChange = (value: boolean) => {
setSwitchState(value);
if (onCheckedChange) {
onCheckedChange(value);
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize(localizationKey as any)}</div>
{hoverCardText && <HoverCardSettings side="bottom" text={hoverCardText} />}
</div>
<Switch
id={switchId}
checked={switchState}
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid={switchId}
/>
</div>
);
};
export default ToggleSwitch;

View file

@ -23,7 +23,7 @@ const HoverCardContent = React.forwardRef<
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'z-50 w-64 rounded-md border border-none bg-surface-tertiary p-4 shadow-md outline-none animate-in fade-in-0', 'z-50 w-64 origin-[--radix-hover-card-content-transform-origin] rounded-xl border border-border-light bg-surface-secondary p-4 text-text-primary shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className, className,
)} )}
{...props} {...props}

View file

@ -1,3 +1,4 @@
import { useRecoilCallback } from 'recoil';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MessageCircleDashed, Box } from 'lucide-react'; import { MessageCircleDashed, Box } from 'lucide-react';
import type { BadgeItem } from '~/common'; import type { BadgeItem } from '~/common';
@ -33,3 +34,14 @@ export default function useChatBadges(): BadgeItem[] {
isAvailable: activeBadgeIds.has(cfg.id), isAvailable: activeBadgeIds.has(cfg.id),
})); }));
} }
export function useResetChatBadges() {
return useRecoilCallback(
({ reset }) =>
() => {
badgeConfig.forEach(({ atom }) => reset(atom));
reset(store.chatBadges);
},
[],
);
}

View file

@ -28,6 +28,7 @@ import {
} from '~/utils'; } from '~/utils';
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import useAssistantListMap from './Assistants/useAssistantListMap'; import useAssistantListMap from './Assistants/useAssistantListMap';
import { useResetChatBadges } from './useChatBadges';
import { usePauseGlobalAudio } from './Audio'; import { usePauseGlobalAudio } from './Audio';
import { mainTextareaId } from '~/common'; import { mainTextareaId } from '~/common';
import store from '~/store'; import store from '~/store';
@ -38,8 +39,8 @@ const useNewConvo = (index = 0) => {
const clearAllConversations = store.useClearConvoState(); const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset); const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index); const { setConversation } = store.useCreateConversationAtom(index);
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const saveBadgesState = useRecoilValue<boolean>(store.saveBadgesState);
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`); const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index)); const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
@ -196,8 +197,8 @@ const useNewConvo = (index = 0) => {
keepAddedConvos?: boolean; keepAddedConvos?: boolean;
} = {}) { } = {}) {
pauseGlobalAudio(); pauseGlobalAudio();
if (isTemporary) { if (!saveBadgesState) {
setIsTemporary(false); useResetChatBadges();
} }
const templateConvoId = _template.conversationId ?? ''; const templateConvoId = _template.conversationId ?? '';

View file

@ -356,6 +356,7 @@
"com_nav_info_save_draft": "When enabled, the text and attachments you enter in the chat form will be automatically saved locally as drafts. These drafts will be available even if you reload the page or switch to a different conversation. Drafts are stored locally on your device and are deleted once the message is sent.", "com_nav_info_save_draft": "When enabled, the text and attachments you enter in the chat form will be automatically saved locally as drafts. These drafts will be available even if you reload the page or switch to a different conversation. Drafts are stored locally on your device and are deleted once the message is sent.",
"com_nav_info_show_thinking": "When enabled, the chat will display the thinking dropdowns open by default, allowing you to view the AI's reasoning in real-time. When disabled, the thinking dropdowns will remain closed by default for a cleaner and more streamlined interface", "com_nav_info_show_thinking": "When enabled, the chat will display the thinking dropdowns open by default, allowing you to view the AI's reasoning in real-time. When disabled, the thinking dropdowns will remain closed by default for a cleaner and more streamlined interface",
"com_nav_info_user_name_display": "When enabled, the username of the sender will be shown above each message you send. When disabled, you will only see \"You\" above your messages.", "com_nav_info_user_name_display": "When enabled, the username of the sender will be shown above each message you send. When disabled, you will only see \"You\" above your messages.",
"com_nav_info_save_badges_state": "When enabled, the state of the chat badges will be saved. This means that if you create a new chat, the badges will remain in the same state as the previous chat. If you disable this option, the badges will reset to their default state every time you create a new chat",
"com_nav_lang_arabic": "العربية", "com_nav_lang_arabic": "العربية",
"com_nav_lang_auto": "Auto detect", "com_nav_lang_auto": "Auto detect",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro", "com_nav_lang_brazilian_portuguese": "Português Brasileiro",
@ -402,6 +403,7 @@
"com_nav_plus_command_description": "Toggle command \"+\" for adding a multi-response setting", "com_nav_plus_command_description": "Toggle command \"+\" for adding a multi-response setting",
"com_nav_profile_picture": "Profile Picture", "com_nav_profile_picture": "Profile Picture",
"com_nav_save_drafts": "Save drafts locally", "com_nav_save_drafts": "Save drafts locally",
"com_nav_save_badges_state": "Save badges state",
"com_nav_scroll_button": "Scroll to the end button", "com_nav_scroll_button": "Scroll to the end button",
"com_nav_search_placeholder": "Search messages", "com_nav_search_placeholder": "Search messages",
"com_nav_send_message": "Send message", "com_nav_send_message": "Send message",
@ -850,6 +852,15 @@
"com_ui_write": "Writing", "com_ui_write": "Writing",
"com_ui_yes": "Yes", "com_ui_yes": "Yes",
"com_ui_zoom": "Zoom", "com_ui_zoom": "Zoom",
"com_ui_save_badge_changes": "Save badge changes?",
"com_ui_late_night": "Happy late night",
"com_ui_weekend_morning": "Happy weekend",
"com_ui_good_morning": "Good morning",
"com_ui_good_afternoon": "Good afternoon",
"com_ui_good_evening": "Good evening",
"com_endpoint_deprecated": "Deprecated",
"com_endpoint_deprecated_info": "This endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_endpoint_deprecated_info_a11y": "The plugin endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_user_message": "You", "com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
} }

View file

@ -38,6 +38,7 @@ const localStorageAtoms = {
splitAtTarget: atomWithLocalStorage('splitAtTarget', false), splitAtTarget: atomWithLocalStorage('splitAtTarget', false),
rememberDefaultFork: atomWithLocalStorage(LocalStorageKeys.REMEMBER_FORK_OPTION, false), rememberDefaultFork: atomWithLocalStorage(LocalStorageKeys.REMEMBER_FORK_OPTION, false),
showThinking: atomWithLocalStorage('showThinking', false), showThinking: atomWithLocalStorage('showThinking', false),
saveBadgesState: atomWithLocalStorage('saveBadgesState', false),
// Beta features settings // Beta features settings
modularChat: atomWithLocalStorage('modularChat', true), modularChat: atomWithLocalStorage('modularChat', true),