mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +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
|
|
@ -56,7 +56,6 @@ export default function Footer({ className }: { className?: string }) {
|
|||
<React.Fragment key={`main-content-part-${index}`}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
|
||||
a: ({ node: _n, href, children, ...otherProps }) => {
|
||||
return (
|
||||
<a
|
||||
|
|
@ -87,7 +86,7 @@ export default function Footer({ className }: { className?: string }) {
|
|||
<div
|
||||
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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -43,12 +43,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
|
||||
const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]);
|
||||
|
||||
const isSearching = useRecoilValue(store.isSearching);
|
||||
const SpeechToText = useRecoilValue(store.speechToText);
|
||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const chatDirection = useRecoilValue(store.chatDirection);
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const chatDirection = useRecoilValue(store.chatDirection);
|
||||
const isSearching = useRecoilValue(store.isSearching);
|
||||
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
|
||||
|
||||
const [badges, setBadges] = useRecoilState(store.chatBadges);
|
||||
const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges);
|
||||
|
|
@ -190,8 +191,9 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<form
|
||||
onSubmit={methods.handleSubmit(submitMessage)}
|
||||
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',
|
||||
centerFormOnLanding ? 'sm:mb-28' : 'sm:mb-10',
|
||||
)}
|
||||
>
|
||||
<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
|
||||
onClick={handleContainerClick}
|
||||
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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
import { Spinner } from '~/components';
|
||||
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { Endpoint } from '~/common';
|
||||
import { CustomMenu as Menu, CustomMenuItem as MenuItem } from '../CustomMenu';
|
||||
import { useModelSelectorContext } from '../ModelSelectorContext';
|
||||
import { renderEndpointModels } from './EndpointModelItem';
|
||||
import { TooltipAnchor, Spinner } from '~/components';
|
||||
import { filterModels } from '../utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -84,6 +84,18 @@ export function EndpointItem({ endpoint }: EndpointItemProps) {
|
|||
>
|
||||
{endpoint.label}
|
||||
</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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -23,7 +23,7 @@ const HoverCardContent = React.forwardRef<
|
|||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue