mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-19 16:56:12 +01:00
🚀 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:
parent
14ff66b2c3
commit
a5154e1349
20 changed files with 227 additions and 350 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +1,82 @@
|
|||
import { memo } from 'react';
|
||||
import MaximizeChatSpace from './MaximizeChatSpace';
|
||||
import FontSizeSelector from './FontSizeSelector';
|
||||
import SendMessageKeyEnter from './EnterToSend';
|
||||
import CenterChatInput from './CenterChatInput';
|
||||
import ShowCodeSwitch from './ShowCodeSwitch';
|
||||
import { ForkSettings } from './ForkSettings';
|
||||
import ChatDirection from './ChatDirection';
|
||||
import ShowThinking from './ShowThinking';
|
||||
import LaTeXParsing from './LaTeXParsing';
|
||||
import ScrollButton from './ScrollButton';
|
||||
import ModularChat from './ModularChat';
|
||||
import SaveDraft from './SaveDraft';
|
||||
import ToggleSwitch from '../ToggleSwitch';
|
||||
import store from '~/store';
|
||||
|
||||
const toggleSwitchConfigs = [
|
||||
{
|
||||
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() {
|
||||
return (
|
||||
|
|
@ -21,34 +87,17 @@ function Chat() {
|
|||
<div className="pb-3">
|
||||
<ChatDirection />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<CenterChatInput />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<SendMessageKeyEnter />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<MaximizeChatSpace />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<ShowCodeSwitch />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<SaveDraft />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<ScrollButton />
|
||||
</div>
|
||||
{toggleSwitchConfigs.map((config) => (
|
||||
<div key={config.key} className="pb-3">
|
||||
<ToggleSwitch
|
||||
stateAtom={config.stateAtom}
|
||||
localizationKey={config.localizationKey}
|
||||
hoverCardText={config.hoverCardText}
|
||||
switchId={config.switchId}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<ForkSettings />
|
||||
<div className="pb-3">
|
||||
<ModularChat />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<LaTeXParsing />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<ShowThinking />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,16 +4,16 @@ import { Switch } from '~/components/ui';
|
|||
import useLocalize from '~/hooks/useLocalize';
|
||||
import store from '~/store';
|
||||
|
||||
export default function SaveDraft({
|
||||
export default function SaveBadgesState({
|
||||
onCheckedChange,
|
||||
}: {
|
||||
onCheckedChange?: (value: boolean) => void;
|
||||
}) {
|
||||
const [saveDrafts, setSaveDrafts] = useRecoilState<boolean>(store.saveDrafts);
|
||||
const [saveBadgesState, setSaveBadgesState] = useRecoilState<boolean>(store.saveBadgesState);
|
||||
const localize = useLocalize();
|
||||
|
||||
const handleCheckedChange = (value: boolean) => {
|
||||
setSaveDrafts(value);
|
||||
setSaveBadgesState(value);
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(value);
|
||||
}
|
||||
|
|
@ -22,15 +22,15 @@ export default function SaveDraft({
|
|||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{localize('com_nav_save_drafts')}</div>
|
||||
<HoverCardSettings side="bottom" text="com_nav_info_save_draft" />
|
||||
<div>{localize('com_nav_save_badges_state')}</div>
|
||||
<HoverCardSettings side="bottom" text="com_nav_info_save_badges_state" />
|
||||
</div>
|
||||
<Switch
|
||||
id="saveDrafts"
|
||||
checked={saveDrafts}
|
||||
id="saveBadgesState"
|
||||
checked={saveBadgesState}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
className="ml-4"
|
||||
data-testid="saveDrafts"
|
||||
data-testid="saveBadgesState"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,9 +6,34 @@ import HideSidePanelSwitch from './HideSidePanelSwitch';
|
|||
import { ThemeContext, useLocalize } from '~/hooks';
|
||||
import AutoScrollSwitch from './AutoScrollSwitch';
|
||||
import ArchivedChats from './ArchivedChats';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import ToggleSwitch from '../ToggleSwitch';
|
||||
import { Dropdown } from '~/components';
|
||||
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 = ({
|
||||
theme,
|
||||
onChange,
|
||||
|
|
@ -127,15 +152,16 @@ function General() {
|
|||
<div className="pb-3">
|
||||
<LangSelector langcode={langcode} onChange={changeLang} />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<UserMsgMarkdownSwitch />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<AutoScrollSwitch />
|
||||
</div>
|
||||
<div className="pb-3">
|
||||
<HideSidePanelSwitch />
|
||||
</div>
|
||||
{toggleSwitchConfigs.map((config) => (
|
||||
<div key={config.key} className="pb-3">
|
||||
<ToggleSwitch
|
||||
stateAtom={config.stateAtom}
|
||||
localizationKey={config.localizationKey}
|
||||
hoverCardText={config.hoverCardText}
|
||||
switchId={config.switchId}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="pb-3">
|
||||
<ArchivedChats />
|
||||
</div>
|
||||
|
|
|
|||
49
client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx
Normal file
49
client/src/components/Nav/SettingsTabs/ToggleSwitch.tsx
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue