🎨 style: settings tab update (#3088)

* style: settings UI  update

* style: update UI

* style: update button style

* fix: scroll settings on mobile

* feat: `?` for settings
This commit is contained in:
Marco Beretta 2024-06-21 15:58:04 +02:00 committed by GitHub
parent 4319c62e66
commit 0424f8fe55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 359 additions and 173 deletions

View file

@ -303,6 +303,7 @@ export type TDangerButtonProps = {
actionTextCode: string;
dataTestIdInitial: string;
dataTestIdConfirm: string;
infoDescriptionCode?: string;
confirmActionTextCode?: string;
};

View file

@ -66,7 +66,7 @@ export default function OptionsPopover({
{presetsDisabled ? null : (
<Button
type="button"
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500"
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
onClick={saveAsPreset}
>
<Save className="mr-1 w-[14px]" />

View file

@ -15,9 +15,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
disableScroll={isSmallScreen}
className={cn(
'overflow-hidden shadow-2xl md:min-h-[373px] md:w-[680px]',
isSmallScreen ? 'top-5 -translate-y-0' : '',
'max-h-[90vh] overflow-auto shadow-2xl md:min-h-[500px] md:w-[680px]',
isSmallScreen ? 'min-h-[200px]' : '',
)}
>
<DialogHeader>
@ -28,7 +29,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root
defaultValue={SettingsTabValues.GENERAL}
className="flex flex-col gap-10 md:flex-row"
className="flex flex-col gap-3 md:flex-row"
orientation="horizontal"
>
<Tabs.List
@ -36,17 +37,18 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
role="tablist"
aria-orientation="horizontal"
className={cn(
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-wrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-800' : '',
isSmallScreen
? 'hide-scrollbar flex flex-row space-x-4 overflow-x-auto'
: 'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-col flex-wrap overflow-auto sm:max-w-none',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.GENERAL}
@ -57,10 +59,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.MESSAGES}
@ -71,10 +73,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.BETA}
@ -85,10 +87,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.SPEECH}
@ -99,10 +101,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.DATA}
@ -113,10 +115,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.ACCOUNT}
@ -126,7 +128,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
{localize('com_nav_setting_account')}
</Tabs.Trigger>
</Tabs.List>
<div className="h-screen max-h-[373px] overflow-auto sm:w-full sm:max-w-none">
<div className="h-auto min-h-[280px] overflow-auto sm:w-full sm:max-w-none">
<General />
<Messages />
<Beta />

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { SettingsTabValues } from 'librechat-data-provider';
import HoverCardSettings from '../HoverCardSettings';
import DeleteAccount from './DeleteAccount';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
@ -25,7 +26,7 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
role="tabpanel"
className="w-full md:min-h-[271px]"
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<Avatar />
</div>
@ -33,7 +34,10 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
<DeleteAccount />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_user_name_display')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
</div>
<Switch
id="UsernameDisplay"
checked={UsernameDisplay}

View file

@ -80,11 +80,8 @@ function Avatar() {
<>
<div className="flex items-center justify-between">
<span>{localize('com_nav_profile_picture')}</span>
<label
htmlFor={'file-upload-avatar'}
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-normal transition-colors hover:bg-gray-100 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-green-500"
>
<FileImage className="mr-1 flex w-[22px] items-center stroke-1" />
<label htmlFor={'file-upload-avatar'} className="btn btn-neutral relative">
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
<span>{localize('com_nav_change_picture')}</span>
<input
id={'file-upload-avatar'}

View file

@ -1,17 +1,11 @@
import React, { useState, useCallback } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogButton,
Input,
} from '~/components/ui';
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '~/components/ui';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { useDeleteUserMutation } from '~/data-provider';
import { Spinner, LockIcon } from '~/components/svg';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLocalize } from '~/hooks';
import DangerButton from '../DangerButton';
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
const localize = useLocalize();
@ -50,16 +44,19 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
<div className="flex items-center justify-between">
<span>{localize('com_nav_delete_account')}</span>
<label>
<DialogButton
<DangerButton
id={'delete-user-account'}
disabled={disabled}
onClick={onClick}
actionTextCode="com_ui_delete"
className={cn(
'btn btn-danger relative border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
'btn relative border-none bg-red-500 text-white hover:bg-red-700 dark:hover:bg-red-700',
)}
>
{localize('com_ui_delete')}
</DialogButton>
confirmClear={false}
infoTextCode={''}
dataTestIdInitial={''}
dataTestIdConfirm={''}
/>
</label>
</div>
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>

View file

@ -11,7 +11,7 @@ function Beta() {
role="tabpanel"
className="w-full md:min-h-[271px]"
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ModularChat />
</div>

View file

@ -1,4 +1,5 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';
@ -20,7 +21,10 @@ export default function LaTeXParsingSwitch({
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}

View file

@ -6,6 +6,7 @@ import { Spinner } from '~/components/svg';
import type { TDangerButtonProps } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import HoverCardSettings from './HoverCardSettings';
const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
const {
@ -20,6 +21,7 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonEle
showText = true,
dataTestIdInitial,
dataTestIdConfirm,
infoDescriptionCode,
confirmActionTextCode = 'com_ui_confirm_action',
} = props;
const localize = useLocalize();
@ -33,14 +35,19 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonEle
return (
<div className="flex items-center justify-between">
{showText && <div> {localize(infoTextCode)} </div>}
{showText && (
<div className={`flex items-center ${infoDescriptionCode ? 'space-x-2' : ''}`}>
<div>{localize(infoTextCode)}</div>
{infoDescriptionCode && <HoverCardSettings side="bottom" text={infoDescriptionCode} />}
</div>
)}
<DialogButton
id={id}
ref={ref}
disabled={disabled}
onClick={onClick}
className={cn(
' btn btn-danger relative min-w-[70px] border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
'btn relative border-none bg-red-500 text-white hover:bg-red-700 dark:hover:bg-red-700',
className,
)}
>

View file

@ -43,7 +43,7 @@ function Data() {
className="w-full md:min-h-[271px]"
ref={dataTabRef}
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ImportConversations />
</div>

View file

@ -45,6 +45,7 @@ export const DeleteCacheButton = ({
id={'delete-cache'}
actionTextCode={'com_ui_delete'}
infoTextCode={'com_nav_delete_cache_storage'}
infoDescriptionCode={'com_nav_info_delete_cache_storage'}
dataTestIdInitial={'delete-cache-initial'}
dataTestIdConfirm={'delete-cache-confirm'}
/>

View file

@ -68,11 +68,8 @@ function ImportConversations() {
return (
<div className="flex items-center justify-between">
<span>{localize('com_ui_import_conversation_info')}</span>
<label
htmlFor={'import-conversations-file'}
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-3 text-xs font-medium transition-colors hover:bg-gray-100 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-green-500"
>
<div>{localize('com_ui_import_conversation_info')}</div>
<label htmlFor={'import-conversations-file'} className="btn btn-neutral relative">
{allowImport ? (
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
) : (

View file

@ -56,6 +56,7 @@ export const RevokeKeysButton = ({
id={'revoke-all-user-keys'}
actionTextCode={'com_ui_revoke'}
infoTextCode={'com_ui_revoke_info'}
infoDescriptionCode={'com_nav_info_revoke'}
dataTestIdInitial={'revoke-all-keys-initial'}
dataTestIdConfirm={'revoke-all-keys-confirm'}
mutation={all ? revokeKeysMutation : revokeKeyMutation}

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks';
import { Dialog, DialogTrigger } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { Dialog, DialogTrigger } from '~/components/ui';
import ArchivedChatsTable from './ArchivedChatsTable';
@ -10,7 +10,6 @@ export default function ArchivedChats() {
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_archived_chats')}</div>
<Dialog>
<DialogTrigger asChild>
<button className="btn btn-neutral relative ">

View file

@ -29,11 +29,12 @@ export const ThemeSelector = ({
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_theme')}</div>
<Dropdown
value={theme}
onChange={onChange}
options={themeOptions}
width={220}
width={180}
position={'left'}
maxHeight="200px"
testId="theme-selector"
@ -64,6 +65,7 @@ export const ClearChatsButton = ({
confirmActionTextCode="com_nav_confirm_clear"
dataTestIdInitial="clear-convos-initial"
dataTestIdConfirm="clear-convos-confirm"
infoDescriptionCode="com_nav_info_clear_all_chats"
onClick={onClick}
/>
);
@ -105,6 +107,7 @@ export const LangSelector = ({
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_language')}</div>
<Dropdown
value={langcode}
onChange={onChange}
@ -153,7 +156,7 @@ function General() {
className="w-full md:min-h-[271px]"
ref={contentRef}
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ThemeSelector theme={theme} onChange={changeTheme} />
</div>

View file

@ -21,6 +21,7 @@ export default function HideSidePanelSwitch({
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_hide_panel')}</div>
<Switch
id="hideSidePanel"
checked={hideSidePanel}

View file

@ -0,0 +1,25 @@
import React from 'react';
import { HoverCard, HoverCardTrigger, HoverCardPortal, HoverCardContent } from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
const HoverCardSettings = ({ side, text }) => {
const localize = useLocalize();
return (
<HoverCard openDelay={500}>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />{' '}
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent side={side} className="z-[999] w-80">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{localize(text)}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
};
export default HoverCardSettings;

View file

@ -1,4 +1,5 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui/Switch';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
@ -20,7 +21,10 @@ export default function SendMessageKeyEnter({
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}

View file

@ -1,4 +1,5 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { ForkOptions } from 'librechat-data-provider';
import { Dropdown, Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
@ -20,12 +21,14 @@ export const ForkSettings = () => {
<>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_ui_fork_change_default')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_fork_change_default" />
</div>
<Dropdown
value={forkSetting}
onChange={setForkSetting}
options={forkOptions}
width={200}
position={'left'}
maxHeight="199px"
testId="fork-setting-dropdown"
@ -46,7 +49,10 @@ export const ForkSettings = () => {
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_ui_fork_split_target_setting')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_fork_split_target_setting" />
</div>
<Switch
id="splitAtTarget"
checked={splitAtTarget}

View file

@ -9,7 +9,7 @@ import SaveDraft from './SaveDraft';
function Messages() {
return (
<Tabs.Content value={SettingsTabValues.MESSAGES} role="tabpanel" className="md: w-full">
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<SendMessageKeyEnter />
</div>

View file

@ -1,5 +1,6 @@
import { useRecoilState } from 'recoil';
import { Switch } from '~/components/ui/Switch';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
@ -20,7 +21,10 @@ 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>
<Switch
id="saveDrafts"
checked={saveDrafts}

View file

@ -1,4 +1,5 @@
import { useRecoilState } from 'recoil';
import HoverCardSettings from '../HoverCardSettings';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import store from '~/store';

View file

@ -10,7 +10,7 @@ export default function ConversationModeSwitch({
}) {
const localize = useLocalize();
const [conversationMode, setConversationMode] = useRecoilState<boolean>(store.conversationMode);
const [advancedMode, setAdvancedMode] = useRecoilState<boolean>(store.advancedMode);
const [advancedMode] = useRecoilState<boolean>(store.advancedMode);
const [textToSpeech] = useRecoilState<boolean>(store.TextToSpeech);
const [, setAutoSendText] = useRecoilState<boolean>(store.autoSendText);
const [, setDecibelValue] = useRecoilState(store.decibelValue);
@ -34,13 +34,6 @@ export default function ConversationModeSwitch({
<strong>{localize('com_nav_conversation_mode')}</strong>
</div>
<div className="flex items-center justify-between">
<label
className="flex h-auto cursor-pointer items-center rounded border border-gray-500/70 bg-transparent px-2 py-1 text-xs font-medium font-normal transition-colors hover:border-gray-500/95 hover:bg-gray-100 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-green-500"
onClick={() => setAdvancedMode(!advancedMode)}
>
<span>{advancedMode ? 'Advanced Mode' : 'Simple Mode'}</span>
</label>
<div className="w-2" />
<Switch
id="ConversationMode"
checked={conversationMode}

View file

@ -22,7 +22,7 @@ export default function EngineSTTDropdown() {
value={endpointSTT}
onChange={handleSelect}
options={endpointOptions}
width={220}
width={180}
position={'left'}
testId="EngineSTTDropdown"
/>

View file

@ -2,8 +2,10 @@ import * as Tabs from '@radix-ui/react-tabs';
import { SettingsTabValues } from 'librechat-data-provider';
import React, { useState, useRef } from 'react';
import { useRecoilState } from 'recoil';
import { useOnClickOutside } from '~/hooks';
import { Lightbulb, Cog } from 'lucide-react';
import { useOnClickOutside, useMediaQuery } from '~/hooks';
import store from '~/store';
import { cn } from '~/utils';
import ConversationModeSwitch from './ConversationModeSwitch';
import {
TextToSpeechSwitch,
@ -22,9 +24,10 @@ import {
} from './STT';
function Speech() {
const [confirmClear, setConfirmClear] = useState(false);
const [advancedMode] = useRecoilState<boolean>(store.advancedMode);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const [advancedMode, setAdvancedMode] = useRecoilState<boolean>(store.advancedMode);
const [autoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
const [confirmClear, setConfirmClear] = useState(false);
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
@ -36,7 +39,48 @@ function Speech() {
className="w-full px-4 md:min-h-[300px]"
ref={contentRef}
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<Tabs.Root
defaultValue={'simple'}
orientation="horizontal"
value={advancedMode ? 'advanced' : 'simple'}
>
<div className="sticky top-0 z-50 bg-white dark:bg-gray-700">
<Tabs.List className="sticky top-0 mb-4 flex justify-center bg-white dark:bg-gray-700">
<Tabs.Trigger
onClick={() => setAdvancedMode(false)}
className={cn(
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
'w-full',
)}
value="simple"
style={{ userSelect: 'none' }}
>
<Lightbulb />
Simple
</Tabs.Trigger>
<Tabs.Trigger
onClick={() => setAdvancedMode(true)}
className={cn(
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
'w-full',
)}
value="advanced"
style={{ userSelect: 'none' }}
>
<Cog />
Advanced
</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.Content value={'simple'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ConversationModeSwitch />
</div>
@ -47,21 +91,6 @@ function Speech() {
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown />
</div>
{advancedMode && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutoTranscribeAudioSwitch />
</div>
)}
{autoTranscribeAudio && advancedMode && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<DecibelSelector />
</div>
)}
{advancedMode && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutoSendTextSwitch />
</div>
)}
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<TextToSpeechSwitch />
@ -75,18 +104,55 @@ function Speech() {
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
{advancedMode && (
</div>
</Tabs.Content>
<Tabs.Content value={'advanced'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ConversationModeSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<SpeechToTextSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutoTranscribeAudioSwitch />
</div>
{autoTranscribeAudio && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<DecibelSelector />
</div>
)}
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutoSendTextSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<TextToSpeechSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<AutomaticPlayback />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<EngineTTSDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<PlaybackRate />
</div>
)}
{advancedMode && (
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<CacheTTSSwitch />
</div>
)}
</div>
</Tabs.Content>
</Tabs.Root>
</Tabs.Content>
);
}

View file

@ -22,7 +22,7 @@ export default function EngineTTSDropdown() {
value={endpointTTS}
onChange={handleSelect}
options={endpointOptions}
width={220}
width={180}
position={'left'}
testId="EngineTTSDropdown"
/>

View file

@ -28,7 +28,6 @@ export default function VoiceDropdown() {
value={voice}
onChange={(value: string) => setVoice(value)}
options={voiceOptions}
width={220}
position={'left'}
testId="VoiceDropdown"
/>

View file

@ -0,0 +1,22 @@
import { cn } from '~/utils/';
export default function CircleHelpIcon({ className = 'icon-md-heavy', size = '1em' }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size}
width={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn(className)}
>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
}

View file

@ -55,3 +55,4 @@ export { default as AssistantIcon } from './AssistantIcon';
export { default as Sparkles } from './Sparkles';
export { default as SpeechIcon } from './SpeechIcon';
export { default as SaveIcon } from './SaveIcon';
export { default as CircleHelpIcon } from './CircleHelpIcon';

View file

@ -35,10 +35,19 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
disableScroll?: boolean;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }
>(({ className, children = true, showCloseButton = true, ...props }, ref) => {
DialogContentProps
>(
(
{ className, children = true, showCloseButton = true, disableScroll = false, ...props },
ref,
) => {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
return (
<DialogPortal>
@ -51,13 +60,14 @@ const DialogContent = React.forwardRef<
isSmallScreen
? 'fixed left-1/2 top-1/2 z-[999] m-auto grid w-11/12 -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-white pb-6'
: '',
disableScroll ? 'overflow-hidden' : '',
className ?? '',
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-6 top-[1.6rem] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 dark:data-[state=open]:bg-gray-800">
<DialogPrimitive.Close className="absolute right-6 top-[1.6rem] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-white dark:focus:ring-offset-gray-700 dark:data-[state=open]:bg-gray-800">
<X className="h-5 w-5 text-black dark:text-white" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@ -65,7 +75,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Content>
</DialogPortal>
);
});
},
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (

View file

@ -52,10 +52,11 @@ const Dropdown: FC<DropdownProps> = ({
<Listbox.Button
data-testid={testId}
className={cn(
'relative inline-flex items-center justify-between rounded-md border-gray-300 bg-white py-2 pl-3 pr-8 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
'relative inline-flex items-center justify-between rounded-md border-gray-50 bg-white py-2 pl-3 pr-8 text-black transition-all duration-100 ease-in-out hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white dark:focus:ring-offset-gray-700',
'w-auto',
className,
)}
aria-label="Select an option"
>
<span className="block truncate">
{label}
@ -70,7 +71,7 @@ const Dropdown: FC<DropdownProps> = ({
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="h-4 w-5 rotate-0 transform text-gray-400 transition-transform duration-300 ease-in-out"
className="h-4 w-5 rotate-0 transform text-black transition-transform duration-300 ease-in-out dark:text-gray-50"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
@ -78,24 +79,46 @@ const Dropdown: FC<DropdownProps> = ({
</Listbox.Button>
<Listbox.Options
className={cn(
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-100 bg-white p-1.5 text-black shadow-lg transition-opacity dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
className,
)}
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
aria-label="List of options"
>
{options.map((item, index) => (
<Listbox.Option
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-6 text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
'duration-50 relative cursor-pointer select-none rounded border-gray-50 bg-white py-2.5 pl-3 pr-2 text-black transition-all ease-in-out hover:bg-gray-100 focus:bg-gray-200 dark:border-gray-50 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:bg-gray-500',
)}
style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<div className="flex w-full items-center justify-between">
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
{selectedValue === (typeof item === 'string' ? item : item.value) && (
<span className="ml-auto pl-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md block group-hover:hidden"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
fill="currentColor"
/>
</svg>
</span>
)}
</div>
</Listbox.Option>
))}
</Listbox.Options>

View file

@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className = '', ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-[19.2px] w-[32px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-500 data-[state=unchecked]:bg-gray-200',
'focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-[20px] w-[32px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-black data-[state=unchecked]:bg-gray-200 dark:data-[state=checked]:bg-green-500 dark:data-[state=unchecked]:bg-gray-500',
className,
)}
{...props}
@ -17,7 +17,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-4 w-4 -translate-x-0.5 rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.45)] transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0',
'pointer-events-none block h-4 w-4 -translate-x-0.5 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>

View file

@ -626,9 +626,24 @@ export default {
com_nav_engine: 'Engine',
com_nav_browser: 'Browser',
com_nav_external: 'External',
com_nav_delete_cache_storage: 'Delete cache storage',
com_nav_delete_cache_storage: 'Delete TTS cache storage',
com_nav_enable_cache_tts: 'Enable cache TTS',
com_nav_voice_select: 'Voice',
com_nav_info_enter_to_send:
'When enabled, pressing `ENTER` will send your message. When disabled, pressing Enter will add a new line, and you\'ll need to press `CTRL + ENTER` to send your message.',
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_fork_change_default:
'`Visible messages only` includes just the direct path to the selected message. `Include related branches` adds branches along the path. `Include all to/from here` includes all connected messages and branches.',
com_nav_info_fork_split_target_setting: 'idk what this does 🤷‍♂️',
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_latex_parsing:
'When enabled, LaTeX code in messages will be rendered as mathematical equations. Disabling this may improve performance if you don\'t need LaTeX rendering.',
com_nav_info_revoke:
'This action will revoke and remove all the API keys that you have provided. You will need to re-enter these credentials to continue using those endpoints.',
com_nav_info_delete_cache_storage:
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
com_nav_setting_general: 'General',
com_nav_setting_beta: 'Beta features',
com_nav_setting_data: 'Data controls',

View file

@ -1057,7 +1057,8 @@ button {
font-weight:500;
line-height:1.25rem;
padding:.5rem .75rem;
pointer-events:auto
pointer-events:auto;
transition: all 0.1s ease-in-out;
}
.custom-btn {
align-items: center;
@ -1068,6 +1069,7 @@ button {
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.5rem 0.75rem;
transition: all 0.1s ease-in-out;
}
.btn:focus {
outline: 2px solid transparent;