mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +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}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useRecoilCallback } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MessageCircleDashed, Box } from 'lucide-react';
|
||||
import type { BadgeItem } from '~/common';
|
||||
|
|
@ -33,3 +34,14 @@ export default function useChatBadges(): BadgeItem[] {
|
|||
isAvailable: activeBadgeIds.has(cfg.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export function useResetChatBadges() {
|
||||
return useRecoilCallback(
|
||||
({ reset }) =>
|
||||
() => {
|
||||
badgeConfig.forEach(({ atom }) => reset(atom));
|
||||
reset(store.chatBadges);
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from '~/utils';
|
||||
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import useAssistantListMap from './Assistants/useAssistantListMap';
|
||||
import { useResetChatBadges } from './useChatBadges';
|
||||
import { usePauseGlobalAudio } from './Audio';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import store from '~/store';
|
||||
|
|
@ -38,8 +39,8 @@ const useNewConvo = (index = 0) => {
|
|||
const clearAllConversations = store.useClearConvoState();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
|
||||
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
||||
const saveBadgesState = useRecoilValue<boolean>(store.saveBadgesState);
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
|
||||
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
|
@ -196,8 +197,8 @@ const useNewConvo = (index = 0) => {
|
|||
keepAddedConvos?: boolean;
|
||||
} = {}) {
|
||||
pauseGlobalAudio();
|
||||
if (isTemporary) {
|
||||
setIsTemporary(false);
|
||||
if (!saveBadgesState) {
|
||||
useResetChatBadges();
|
||||
}
|
||||
|
||||
const templateConvoId = _template.conversationId ?? '';
|
||||
|
|
|
|||
|
|
@ -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_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_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_auto": "Auto detect",
|
||||
"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_profile_picture": "Profile Picture",
|
||||
"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_search_placeholder": "Search messages",
|
||||
"com_nav_send_message": "Send message",
|
||||
|
|
@ -850,6 +852,15 @@
|
|||
"com_ui_write": "Writing",
|
||||
"com_ui_yes": "Yes",
|
||||
"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_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ const localStorageAtoms = {
|
|||
splitAtTarget: atomWithLocalStorage('splitAtTarget', false),
|
||||
rememberDefaultFork: atomWithLocalStorage(LocalStorageKeys.REMEMBER_FORK_OPTION, false),
|
||||
showThinking: atomWithLocalStorage('showThinking', false),
|
||||
saveBadgesState: atomWithLocalStorage('saveBadgesState', false),
|
||||
|
||||
// Beta features settings
|
||||
modularChat: atomWithLocalStorage('modularChat', true),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue