mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
⌨️ a11y(Settings): Improved Keyboard Navigation & Consistent Styling (#3975)
* feat: settings tba accessible * refactor: cleanup unused code * refactor: improve accessibility and user experience in ChatDirection component * style: focus ring primary class * improve a11y of avatar dialog * style: a11y improvements for Settings * style: focus ring primary class in OriginalDialog component --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
1a1e6850a3
commit
d6c0121b19
17 changed files with 507 additions and 513 deletions
|
|
@ -70,13 +70,13 @@ export default function ConvoOptions({
|
||||||
id="conversation-menu-button"
|
id="conversation-menu-button"
|
||||||
aria-label={localize('com_nav_convo_menu_options')}
|
aria-label={localize('com_nav_convo_menu_options')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 rounded-md border-none p-0 text-sm font-medium transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 rounded-md border-none p-0 text-sm font-medium ring-ring-primary transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
isActiveConvo === true
|
isActiveConvo === true
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
|
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true}/>
|
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
|
||||||
</Ariakit.MenuButton>
|
</Ariakit.MenuButton>
|
||||||
}
|
}
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as React from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
import { MessageSquare, Command } from 'lucide-react';
|
import { MessageSquare, Command } from 'lucide-react';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
|
|
@ -11,10 +12,45 @@ import { cn } from '~/utils';
|
||||||
export default function Settings({ open, onOpenChange }: TDialogProps) {
|
export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
const tabs = [
|
||||||
|
SettingsTabValues.GENERAL,
|
||||||
|
SettingsTabValues.CHAT,
|
||||||
|
SettingsTabValues.BETA,
|
||||||
|
SettingsTabValues.COMMANDS,
|
||||||
|
SettingsTabValues.SPEECH,
|
||||||
|
SettingsTabValues.DATA,
|
||||||
|
SettingsTabValues.ACCOUNT,
|
||||||
|
];
|
||||||
|
const currentIndex = tabs.indexOf(activeTab);
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveTab(tabs[0]);
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveTab(tabs[tabs.length - 1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={open}>
|
<Transition appear show={open}>
|
||||||
<Dialog as="div" className="relative z-50 focus:outline-none" onClose={onOpenChange}>
|
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
enter="ease-out duration-200"
|
enter="ease-out duration-200"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
|
|
@ -77,127 +113,93 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
|
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
|
||||||
<Tabs.Root
|
<Tabs.Root
|
||||||
defaultValue={SettingsTabValues.GENERAL}
|
value={activeTab}
|
||||||
|
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
|
||||||
className="flex flex-col gap-10 md:flex-row"
|
className="flex flex-col gap-10 md:flex-row"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
>
|
>
|
||||||
<Tabs.List
|
<Tabs.List
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
role="tablist"
|
|
||||||
aria-orientation="horizontal"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
||||||
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
|
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
|
||||||
)}
|
)}
|
||||||
style={{ outline: 'none' }}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<Tabs.Trigger
|
{[
|
||||||
tabIndex={0}
|
{
|
||||||
className={cn(
|
value: SettingsTabValues.GENERAL,
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
icon: <GearIcon />,
|
||||||
isSmallScreen
|
label: 'com_nav_setting_general',
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
},
|
||||||
: 'bg-surface-tertiary-alt',
|
{
|
||||||
)}
|
value: SettingsTabValues.CHAT,
|
||||||
value={SettingsTabValues.GENERAL}
|
icon: <MessageSquare className="icon-sm" />,
|
||||||
style={{ userSelect: 'none' }}
|
label: 'com_nav_setting_chat',
|
||||||
>
|
},
|
||||||
<GearIcon />
|
{
|
||||||
{localize('com_nav_setting_general')}
|
value: SettingsTabValues.BETA,
|
||||||
</Tabs.Trigger>
|
icon: <ExperimentIcon />,
|
||||||
<Tabs.Trigger
|
label: 'com_nav_setting_beta',
|
||||||
tabIndex={0}
|
},
|
||||||
className={cn(
|
{
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
value: SettingsTabValues.COMMANDS,
|
||||||
isSmallScreen
|
icon: <Command className="icon-sm" />,
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
label: 'com_nav_commands',
|
||||||
: 'bg-surface-tertiary-alt',
|
},
|
||||||
)}
|
{
|
||||||
value={SettingsTabValues.CHAT}
|
value: SettingsTabValues.SPEECH,
|
||||||
style={{ userSelect: 'none' }}
|
icon: <SpeechIcon className="icon-sm" />,
|
||||||
>
|
label: 'com_nav_setting_speech',
|
||||||
<MessageSquare className="icon-sm" />
|
},
|
||||||
{localize('com_nav_setting_chat')}
|
{
|
||||||
</Tabs.Trigger>
|
value: SettingsTabValues.DATA,
|
||||||
<Tabs.Trigger
|
icon: <DataIcon />,
|
||||||
tabIndex={0}
|
label: 'com_nav_setting_data',
|
||||||
className={cn(
|
},
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
{
|
||||||
isSmallScreen
|
value: SettingsTabValues.ACCOUNT,
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
icon: <UserIcon />,
|
||||||
: 'bg-surface-tertiary-alt',
|
label: 'com_nav_setting_account',
|
||||||
)}
|
},
|
||||||
value={SettingsTabValues.BETA}
|
].map(({ value, icon, label }) => (
|
||||||
style={{ userSelect: 'none' }}
|
<Tabs.Trigger
|
||||||
>
|
key={value}
|
||||||
<ExperimentIcon />
|
className={cn(
|
||||||
{localize('com_nav_setting_beta')}
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
||||||
</Tabs.Trigger>
|
isSmallScreen
|
||||||
<Tabs.Trigger
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
tabIndex={0}
|
: 'bg-surface-tertiary-alt',
|
||||||
className={cn(
|
)}
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
value={value}
|
||||||
isSmallScreen
|
>
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
|
{icon}
|
||||||
: 'bg-surface-tertiary-alt',
|
{localize(label)}
|
||||||
)}
|
</Tabs.Trigger>
|
||||||
value={SettingsTabValues.COMMANDS}
|
))}
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
<Command className="icon-sm" />
|
|
||||||
{localize('com_nav_commands')}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger
|
|
||||||
tabIndex={0}
|
|
||||||
className={cn(
|
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
|
||||||
isSmallScreen
|
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
|
||||||
: 'bg-surface-tertiary-alt',
|
|
||||||
)}
|
|
||||||
value={SettingsTabValues.SPEECH}
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
<SpeechIcon className="icon-sm" />
|
|
||||||
{localize('com_nav_setting_speech')}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger
|
|
||||||
tabIndex={0}
|
|
||||||
className={cn(
|
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
|
||||||
isSmallScreen
|
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
|
||||||
: 'bg-surface-tertiary-alt',
|
|
||||||
)}
|
|
||||||
value={SettingsTabValues.DATA}
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
<DataIcon />
|
|
||||||
{localize('com_nav_setting_data')}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger
|
|
||||||
tabIndex={0}
|
|
||||||
className={cn(
|
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
|
||||||
isSmallScreen
|
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
|
||||||
: 'bg-surface-tertiary-alt',
|
|
||||||
)}
|
|
||||||
value={SettingsTabValues.ACCOUNT}
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
<UserIcon />
|
|
||||||
{localize('com_nav_setting_account')}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
||||||
<General />
|
<Tabs.Content value={SettingsTabValues.GENERAL}>
|
||||||
<Chat />
|
<General />
|
||||||
<Beta />
|
</Tabs.Content>
|
||||||
<Commands />
|
<Tabs.Content value={SettingsTabValues.CHAT}>
|
||||||
<Speech />
|
<Chat />
|
||||||
<Data />
|
</Tabs.Content>
|
||||||
<Account />
|
<Tabs.Content value={SettingsTabValues.BETA}>
|
||||||
|
<Beta />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value={SettingsTabValues.COMMANDS}>
|
||||||
|
<Commands />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value={SettingsTabValues.SPEECH}>
|
||||||
|
<Speech />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value={SettingsTabValues.DATA}>
|
||||||
|
<Data />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value={SettingsTabValues.ACCOUNT}>
|
||||||
|
<Account />
|
||||||
|
</Tabs.Content>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import HoverCardSettings from '../HoverCardSettings';
|
import HoverCardSettings from '../HoverCardSettings';
|
||||||
import DeleteAccount from './DeleteAccount';
|
import DeleteAccount from './DeleteAccount';
|
||||||
import { Switch } from '~/components/ui';
|
import { Switch } from '~/components/ui';
|
||||||
|
|
@ -21,33 +19,27 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
value={SettingsTabValues.ACCOUNT}
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
role="tabpanel"
|
<Avatar />
|
||||||
className="w-full md:min-h-[271px]"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<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}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4 mt-2"
|
|
||||||
data-testid="UsernameDisplay"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<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}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
className="ml-4 mt-2"
|
||||||
|
data-testid="UsernameDisplay"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@ import { useSetRecoilState } from 'recoil';
|
||||||
import AvatarEditor from 'react-avatar-editor';
|
import AvatarEditor from 'react-avatar-editor';
|
||||||
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
||||||
import type { TUser } from 'librechat-data-provider';
|
import type { TUser } from 'librechat-data-provider';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Slider } from '~/components/ui';
|
import {
|
||||||
|
OGDialog,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogTitle,
|
||||||
|
OGDialogTrigger,
|
||||||
|
Slider,
|
||||||
|
} from '~/components/ui';
|
||||||
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
|
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
|
|
@ -20,6 +27,7 @@ function Avatar() {
|
||||||
const [rotation, setRotation] = useState<number>(0);
|
const [rotation, setRotation] = useState<number>(0);
|
||||||
const editorRef = useRef<AvatarEditor | null>(null);
|
const editorRef = useRef<AvatarEditor | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const openButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
|
|
@ -31,8 +39,8 @@ function Avatar() {
|
||||||
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
|
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
showToast({ message: localize('com_ui_upload_success') });
|
showToast({ message: localize('com_ui_upload_success') });
|
||||||
setDialogOpen(false);
|
|
||||||
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
|
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
|
||||||
|
openButtonRef.current?.click();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
@ -102,113 +110,114 @@ function Avatar() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<OGDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
resetImage();
|
||||||
|
setTimeout(() => {
|
||||||
|
openButtonRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{localize('com_nav_profile_picture')}</span>
|
<span>{localize('com_nav_profile_picture')}</span>
|
||||||
<button onClick={() => setDialogOpen(true)} className="btn btn-neutral relative">
|
<OGDialogTrigger ref={openButtonRef} className="btn btn-neutral relative">
|
||||||
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
|
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
|
||||||
<span>{localize('com_nav_change_picture')}</span>
|
<span>{localize('com_nav_change_picture')}</span>
|
||||||
</button>
|
</OGDialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<OGDialogContent
|
||||||
open={isDialogOpen}
|
className={cn('bg-surface-tertiary text-text-primary shadow-2xl md:h-auto md:w-[450px]')}
|
||||||
onOpenChange={(open) => {
|
style={{ borderRadius: '12px' }}
|
||||||
setDialogOpen(open);
|
|
||||||
if (!open) {
|
|
||||||
resetImage();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DialogContent
|
<OGDialogHeader>
|
||||||
className={cn('shadow-2xl dark:bg-gray-700 dark:text-white md:h-auto md:w-[450px]')}
|
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
style={{ borderRadius: '12px' }}
|
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
||||||
>
|
</OGDialogTitle>
|
||||||
<DialogHeader>
|
</OGDialogHeader>
|
||||||
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
|
<div className="flex flex-col items-center justify-center">
|
||||||
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
{image ? (
|
||||||
</DialogTitle>
|
<>
|
||||||
</DialogHeader>
|
<div className="relative overflow-hidden rounded-full">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<AvatarEditor
|
||||||
{image ? (
|
ref={editorRef}
|
||||||
<>
|
image={image}
|
||||||
<div className="relative overflow-hidden rounded-full">
|
width={250}
|
||||||
<AvatarEditor
|
height={250}
|
||||||
ref={editorRef}
|
border={0}
|
||||||
image={image}
|
borderRadius={125}
|
||||||
width={250}
|
color={[255, 255, 255, 0.6]}
|
||||||
height={250}
|
scale={scale}
|
||||||
border={0}
|
rotate={rotation}
|
||||||
borderRadius={125}
|
|
||||||
color={[255, 255, 255, 0.6]}
|
|
||||||
scale={scale}
|
|
||||||
rotate={rotation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex w-full flex-col items-center space-y-4">
|
|
||||||
<div className="flex w-full items-center justify-center space-x-4">
|
|
||||||
<span className="text-sm">Zoom:</span>
|
|
||||||
<Slider
|
|
||||||
value={[scale]}
|
|
||||||
min={1}
|
|
||||||
max={5}
|
|
||||||
step={0.001}
|
|
||||||
onValueChange={handleScaleChange}
|
|
||||||
className="w-2/3 max-w-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleRotate}
|
|
||||||
className="rounded-full bg-gray-200 p-2 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500"
|
|
||||||
>
|
|
||||||
<RotateCw className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
|
|
||||||
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
|
|
||||||
)}
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<Spinner className="icon-sm mr-2" />
|
|
||||||
) : (
|
|
||||||
<Upload className="mr-2 h-5 w-5" />
|
|
||||||
)}
|
|
||||||
{localize('com_ui_upload')}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex h-64 w-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
>
|
|
||||||
<FileImage className="mb-4 h-12 w-12 text-gray-400" />
|
|
||||||
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{localize('com_ui_drag_drop')}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={openFileDialog}
|
|
||||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
|
|
||||||
>
|
|
||||||
{localize('com_ui_select_file')}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
accept=".png, .jpg, .jpeg"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="mt-4 flex w-full flex-col items-center space-y-4">
|
||||||
</div>
|
<div className="flex w-full items-center justify-center space-x-4">
|
||||||
</DialogContent>
|
<span className="text-sm">Zoom:</span>
|
||||||
</Dialog>
|
<Slider
|
||||||
</>
|
value={[scale]}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
step={0.001}
|
||||||
|
onValueChange={handleScaleChange}
|
||||||
|
className="w-2/3 max-w-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleRotate}
|
||||||
|
className="rounded-full bg-gray-200 p-2 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500"
|
||||||
|
>
|
||||||
|
<RotateCw className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
|
||||||
|
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
|
||||||
|
)}
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<Spinner className="icon-sm mr-2" />
|
||||||
|
) : (
|
||||||
|
<Upload className="mr-2 h-5 w-5" />
|
||||||
|
)}
|
||||||
|
{localize('com_ui_upload')}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex h-64 w-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
>
|
||||||
|
<FileImage className="mb-4 h-12 w-12 text-gray-400" />
|
||||||
|
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{localize('com_ui_drag_drop')}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={openFileDialog}
|
||||||
|
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
|
||||||
|
>
|
||||||
|
{localize('com_ui_select_file')}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept=".png, .jpg, .jpeg"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,13 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import CodeArtifacts from './CodeArtifacts';
|
import CodeArtifacts from './CodeArtifacts';
|
||||||
|
|
||||||
function Beta() {
|
function Beta() {
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
value={SettingsTabValues.BETA}
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
role="tabpanel"
|
<CodeArtifacts />
|
||||||
className="w-full md:min-h-[271px]"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<CodeArtifacts />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import FontSizeSelector from './FontSizeSelector';
|
import FontSizeSelector from './FontSizeSelector';
|
||||||
import SendMessageKeyEnter from './EnterToSend';
|
import SendMessageKeyEnter from './EnterToSend';
|
||||||
import ShowCodeSwitch from './ShowCodeSwitch';
|
import ShowCodeSwitch from './ShowCodeSwitch';
|
||||||
|
|
@ -12,32 +10,30 @@ import SaveDraft from './SaveDraft';
|
||||||
|
|
||||||
function Chat() {
|
function Chat() {
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={SettingsTabValues.CHAT} role="tabpanel" className="md: w-full">
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
<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">
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<FontSizeSelector />
|
||||||
<FontSizeSelector />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<ChatDirection />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<SendMessageKeyEnter />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<ShowCodeSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<SaveDraft />
|
|
||||||
</div>
|
|
||||||
<ForkSettings />
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<ModularChat />
|
|
||||||
</div>
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<LaTeXParsing />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
|
<ChatDirection />
|
||||||
|
</div>
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
|
<SendMessageKeyEnter />
|
||||||
|
</div>
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
|
<ShowCodeSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
|
<SaveDraft />
|
||||||
|
</div>
|
||||||
|
<ForkSettings />
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<ModularChat />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<LaTeXParsing />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,22 @@ const ChatDirection = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span>{localize('com_nav_chat_direction')}</span>
|
<span id="chat-direction-label">{localize('com_nav_chat_direction')}</span>
|
||||||
</div>
|
</div>
|
||||||
<label
|
<button
|
||||||
onClick={toggleChatDirection}
|
onClick={toggleChatDirection}
|
||||||
data-testid="chatDirection"
|
data-testid="chatDirection"
|
||||||
className="btn btn-neutral relative"
|
className="btn btn-neutral relative ring-ring-primary"
|
||||||
style={{ userSelect: 'none' }}
|
aria-labelledby="chat-direction-label chat-direction-status"
|
||||||
|
aria-pressed={direction === 'RTL'}
|
||||||
>
|
>
|
||||||
{direction.toLowerCase()}
|
<span aria-hidden="true">{direction.toLowerCase()}</span>
|
||||||
</label>
|
<span id="chat-direction-status" className="sr-only">
|
||||||
|
{direction === 'LTR'
|
||||||
|
? localize('chat_direction_left_to_right')
|
||||||
|
: localize('chat_direction_right_to_left')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import { SettingsTabValues, PermissionTypes, Permissions } from 'librechat-data-provider';
|
|
||||||
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
|
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
|
||||||
import { useLocalize, useHasAccess } from '~/hooks';
|
import { useLocalize, useHasAccess } from '~/hooks';
|
||||||
import SlashCommandSwitch from './SlashCommandSwitch';
|
import SlashCommandSwitch from './SlashCommandSwitch';
|
||||||
|
|
@ -21,35 +20,29 @@ function Commands() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<div className="space-y-4 p-1">
|
||||||
value={SettingsTabValues.COMMANDS}
|
<div className="flex items-center gap-2">
|
||||||
role="tabpanel"
|
<h3 className="text-lg font-medium text-text-primary">
|
||||||
className="w-full md:min-h-[271px]"
|
{localize('com_nav_chat_commands')}
|
||||||
>
|
</h3>
|
||||||
<div className="space-y-4">
|
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="text-lg font-medium text-text-primary">
|
|
||||||
{localize('com_nav_chat_commands')}
|
|
||||||
</h3>
|
|
||||||
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<AtCommandSwitch />
|
|
||||||
</div>
|
|
||||||
{hasAccessToMultiConvo === true && (
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<PlusCommandSwitch />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasAccessToPrompts === true && (
|
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
|
||||||
<SlashCommandSwitch />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<AtCommandSwitch />
|
||||||
|
</div>
|
||||||
|
{hasAccessToMultiConvo === true && (
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<PlusCommandSwitch />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasAccessToPrompts === true && (
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<SlashCommandSwitch />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import { useConversation, useConversations, useOnClickOutside } from '~/hooks';
|
import { useConversation, useConversations, useOnClickOutside } from '~/hooks';
|
||||||
import { RevokeKeysButton } from './RevokeKeysButton';
|
import { RevokeKeysButton } from './RevokeKeysButton';
|
||||||
import { DeleteCacheButton } from './DeleteCacheButton';
|
import { DeleteCacheButton } from './DeleteCacheButton';
|
||||||
|
|
@ -37,35 +35,28 @@ function Data() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
value={SettingsTabValues.DATA}
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
role="tabpanel"
|
<ImportConversations />
|
||||||
className="w-full md:min-h-[271px]"
|
|
||||||
ref={dataTabRef}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<SharedLinks />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<RevokeKeysButton all={true} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<DeleteCacheButton />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<ClearChatsButton
|
|
||||||
confirmClear={confirmClearConvos}
|
|
||||||
onClick={clearConvos}
|
|
||||||
showText={true}
|
|
||||||
mutation={clearConvosMutation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<SharedLinks />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<RevokeKeysButton all={true} />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<DeleteCacheButton />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<ClearChatsButton
|
||||||
|
confirmClear={confirmClearConvos}
|
||||||
|
onClick={clearConvos}
|
||||||
|
showText={true}
|
||||||
|
mutation={clearConvosMutation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { Import } from 'lucide-react';
|
import { Import } from 'lucide-react';
|
||||||
import type { TError } from 'librechat-data-provider';
|
import type { TError } from 'librechat-data-provider';
|
||||||
import { useUploadConversationsMutation } from '~/data-provider';
|
import { useUploadConversationsMutation } from '~/data-provider';
|
||||||
|
|
@ -9,6 +9,7 @@ import { cn } from '~/utils';
|
||||||
|
|
||||||
function ImportConversations() {
|
function ImportConversations() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const [, setErrors] = useState<string[]>([]);
|
const [, setErrors] = useState<string[]>([]);
|
||||||
|
|
@ -26,7 +27,7 @@ function ImportConversations() {
|
||||||
console.error('Error: ', error);
|
console.error('Error: ', error);
|
||||||
setAllowImport(true);
|
setAllowImport(true);
|
||||||
setError(
|
setError(
|
||||||
(error as TError)?.response?.data?.message ?? 'An error occurred while uploading the file.',
|
(error as TError).response?.data?.message ?? 'An error occurred while uploading the file.',
|
||||||
);
|
);
|
||||||
if (error?.toString().includes('Unsupported import type')) {
|
if (error?.toString().includes('Unsupported import type')) {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -44,13 +45,12 @@ function ImportConversations() {
|
||||||
|
|
||||||
const startUpload = async (file: File) => {
|
const startUpload = async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file, encodeURIComponent(file?.name || 'File'));
|
formData.append('file', file, encodeURIComponent(file.name || 'File'));
|
||||||
|
|
||||||
uploadFile.mutate(formData);
|
uploadFile.mutate(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFiles = async (_file: File) => {
|
const handleFiles = async (_file: File) => {
|
||||||
/* Process files */
|
|
||||||
try {
|
try {
|
||||||
await startUpload(_file);
|
await startUpload(_file);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -59,33 +59,49 @@ function ImportConversations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (event) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
handleFiles(file);
|
handleFiles(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleImportClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_ui_import_conversation_info')}</div>
|
<div>{localize('com_ui_import_conversation_info')}</div>
|
||||||
<label htmlFor={'import-conversations-file'} className="btn btn-neutral relative">
|
<button
|
||||||
|
onClick={handleImportClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={!allowImport}
|
||||||
|
aria-label={localize('com_ui_import_conversation')}
|
||||||
|
className="btn btn-neutral relative"
|
||||||
|
>
|
||||||
{allowImport ? (
|
{allowImport ? (
|
||||||
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
||||||
) : (
|
) : (
|
||||||
<Spinner className="mr-1 w-4" />
|
<Spinner className="mr-1 w-4" />
|
||||||
)}
|
)}
|
||||||
<span>{localize('com_ui_import_conversation')}</span>
|
<span>{localize('com_ui_import_conversation')}</span>
|
||||||
<input
|
</button>
|
||||||
id={'import-conversations-file'}
|
<input
|
||||||
disabled={!allowImport}
|
ref={fileInputRef}
|
||||||
value=""
|
type="file"
|
||||||
type="file"
|
className={cn('hidden')}
|
||||||
className={cn('hidden')}
|
accept=".json"
|
||||||
accept=".json"
|
onChange={handleFileChange}
|
||||||
onChange={handleFileChange}
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export default function AutoScrollSwitch({
|
||||||
id="autoScroll"
|
id="autoScroll"
|
||||||
checked={autoScroll}
|
checked={autoScroll}
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4 mt-2"
|
className="ml-4 mt-2 ring-ring-primary"
|
||||||
data-testid="autoScroll"
|
data-testid="autoScroll"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import React, { useContext, useCallback, useRef } from 'react';
|
import React, { useContext, useCallback, useRef } from 'react';
|
||||||
import type { TDangerButtonProps } from '~/common';
|
import type { TDangerButtonProps } from '~/common';
|
||||||
import { ThemeContext, useLocalize } from '~/hooks';
|
import { ThemeContext, useLocalize } from '~/hooks';
|
||||||
|
|
@ -151,32 +149,25 @@ function General() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
value={SettingsTabValues.GENERAL}
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
role="tabpanel"
|
<ThemeSelector theme={theme} onChange={changeTheme} />
|
||||||
className="w-full md:min-h-[271px]"
|
|
||||||
ref={contentRef}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<ThemeSelector theme={theme} onChange={changeTheme} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<LangSelector langcode={langcode} onChange={changeLang} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<AutoScrollSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<HideSidePanelSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
<ArchivedChats />
|
|
||||||
</div>
|
|
||||||
{/* <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<LangSelector langcode={langcode} onChange={changeLang} />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<AutoScrollSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<HideSidePanelSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<ArchivedChats />
|
||||||
|
</div>
|
||||||
|
{/* <div className="border-b pb-3 last-of-type:border-b-0 border-border-medium">
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
import { Lightbulb, Cog } from 'lucide-react';
|
import { Lightbulb, Cog } from 'lucide-react';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
|
||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
|
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
|
||||||
import {
|
import {
|
||||||
|
|
@ -141,130 +140,123 @@ function Speech() {
|
||||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content
|
<Tabs.Root
|
||||||
value={SettingsTabValues.SPEECH}
|
defaultValue={'simple'}
|
||||||
role="tabpanel"
|
orientation="horizontal"
|
||||||
className="w-full md:min-h-[271px]"
|
value={advancedMode ? 'advanced' : 'simple'}
|
||||||
ref={contentRef}
|
|
||||||
>
|
>
|
||||||
<Tabs.Root
|
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
|
||||||
defaultValue={'simple'}
|
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
|
||||||
orientation="horizontal"
|
<Tabs.Trigger
|
||||||
value={advancedMode ? 'advanced' : 'simple'}
|
onClick={() => setAdvancedMode(false)}
|
||||||
>
|
className={cn(
|
||||||
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
|
'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',
|
||||||
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
|
isSmallScreen
|
||||||
<Tabs.Trigger
|
? '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'
|
||||||
onClick={() => setAdvancedMode(false)}
|
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
||||||
className={cn(
|
'w-full',
|
||||||
'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
|
value="simple"
|
||||||
? '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'
|
style={{ userSelect: 'none' }}
|
||||||
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
>
|
||||||
'w-full',
|
<Lightbulb />
|
||||||
)}
|
Simple
|
||||||
value="simple"
|
</Tabs.Trigger>
|
||||||
style={{ userSelect: 'none' }}
|
<Tabs.Trigger
|
||||||
>
|
onClick={() => setAdvancedMode(true)}
|
||||||
<Lightbulb />
|
className={cn(
|
||||||
Simple
|
'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',
|
||||||
</Tabs.Trigger>
|
isSmallScreen
|
||||||
<Tabs.Trigger
|
? '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'
|
||||||
onClick={() => setAdvancedMode(true)}
|
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
||||||
className={cn(
|
'w-full',
|
||||||
'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
|
value="advanced"
|
||||||
? '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'
|
style={{ userSelect: 'none' }}
|
||||||
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
>
|
||||||
'w-full',
|
<Cog />
|
||||||
)}
|
Advanced
|
||||||
value="advanced"
|
</Tabs.Trigger>
|
||||||
style={{ userSelect: 'none' }}
|
</Tabs.List>
|
||||||
>
|
</div>
|
||||||
<Cog />
|
|
||||||
Advanced
|
<Tabs.Content value={'simple'}>
|
||||||
</Tabs.Trigger>
|
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
||||||
</Tabs.List>
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<SpeechToTextSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<LanguageSTTDropdown />
|
||||||
|
</div>
|
||||||
|
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<TextToSpeechSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<VoiceDropdown />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value={'simple'}>
|
<Tabs.Content value={'advanced'}>
|
||||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
<SpeechToTextSwitch />
|
<ConversationModeSwitch />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<LanguageSTTDropdown />
|
|
||||||
</div>
|
|
||||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<TextToSpeechSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineTTSDropdown external={ttsExternal} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<VoiceDropdown />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
<Tabs.Content value={'advanced'}>
|
<SpeechToTextSwitch />
|
||||||
<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 last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<SpeechToTextSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<LanguageSTTDropdown />
|
|
||||||
</div>
|
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<AutoTranscribeAudioSwitch />
|
|
||||||
</div>
|
|
||||||
{autoTranscribeAudio && (
|
|
||||||
<div className="border-b pb-2 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">
|
|
||||||
<AutoSendTextSelector />
|
|
||||||
</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 last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<AutomaticPlaybackSwitch />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineTTSDropdown external={ttsExternal} />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<VoiceDropdown />
|
|
||||||
</div>
|
|
||||||
{engineTTS === 'browser' && (
|
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<CloudBrowserVoicesSwitch />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<PlaybackRate />
|
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<CacheTTSSwitch />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
</Tabs.Root>
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
</Tabs.Content>
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<LanguageSTTDropdown />
|
||||||
|
</div>
|
||||||
|
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<AutoTranscribeAudioSwitch />
|
||||||
|
</div>
|
||||||
|
{autoTranscribeAudio && (
|
||||||
|
<div className="border-b pb-2 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">
|
||||||
|
<AutoSendTextSelector />
|
||||||
|
</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 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<AutomaticPlaybackSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<VoiceDropdown />
|
||||||
|
</div>
|
||||||
|
{engineTTS === 'browser' && (
|
||||||
|
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<CloudBrowserVoicesSwitch />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<PlaybackRate />
|
||||||
|
</div>
|
||||||
|
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<CacheTTSSwitch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
className={cn(
|
className={cn(
|
||||||
'focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-md border-border-light bg-header-primary py-2 pl-3 pr-8 text-text-primary transition-all duration-100 ease-in-out hover:bg-header-hover focus:ring-ring-primary',
|
'btn-neutral focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-md border-border-light bg-header-primary py-2 pl-3 pr-8 text-text-primary transition-all duration-100 ease-in-out hover:bg-header-hover focus:ring-ring-primary',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
aria-label="Select an option"
|
aria-label="Select an option"
|
||||||
|
|
@ -88,7 +88,7 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={index}
|
key={index}
|
||||||
value={typeof item === 'string' ? item : item.value}
|
value={typeof item === 'string' ? item : item.value}
|
||||||
className="focus-visible:ring-offset ring-offset-ring-offset relative cursor-pointer select-none rounded border-border-light bg-header-primary py-2.5 pl-3 pr-3 text-sm text-text-secondary ring-ring-primary hover:bg-header-hover focus-visible:ring"
|
className="focus-visible:ring-offset ring-offset-ring-offset relative cursor-pointer select-none rounded border-border-light bg-header-primary py-2.5 pl-3 pr-3 text-sm text-text-secondary ring-ring-primary hover:bg-header-hover focus-visible:ring data-[focus]:bg-surface-hover data-[focus]:text-text-primary"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const DialogContent = React.forwardRef<
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const Switch = React.forwardRef<
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
'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',
|
'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',
|
||||||
|
'ring-ring-primary',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1178,11 +1178,19 @@ button {
|
||||||
color: rgba(64, 65, 79, var(--tw-text-opacity));
|
color: rgba(64, 65, 79, var(--tw-text-opacity));
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-neutral:hover {
|
.btn-neutral:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(236, 236, 241, var(--tw-bg-opacity));
|
background-color: rgba(236, 236, 241, var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-neutral:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--ring-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.dark .btn-neutral {
|
.dark .btn-neutral {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
|
|
@ -1191,10 +1199,15 @@ button {
|
||||||
border-color: rgba(66, 66, 66, var(--tw-border-opacity));
|
border-color: rgba(66, 66, 66, var(--tw-border-opacity));
|
||||||
color: rgba(255, 255, 240, var(--tw-text-opacity));
|
color: rgba(255, 255, 240, var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .btn-neutral:hover {
|
.dark .btn-neutral:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(66, 66, 66, var(--tw-bg-opacity));
|
background-color: rgba(66, 66, 66, var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .btn-neutral:focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--ring-primary);
|
||||||
|
}
|
||||||
.btn-small {
|
.btn-small {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue