mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
♿️ fix: Accessibility, UI consistency, dialog & avatar refactors (#9975)
* 🔧 refactor: Improve accessibility and styling in ChatGroupItem and FilterPrompts components * 🔧 fix: Add button type and keyboard accessibility to dropdown menu trigger in ChatGroupItem * 🔧 fix(757): Enhance accessibility by updating aria-labels and adding localization for prompt groups * 🔧 fix(618): Update version to 0.3.1 and enhance accessibility in InfoHoverCard component * 🔧 fix(618): Update aria-label in InfoHoverCard to use dynamic text prop for improved accessibility * 🔧 fix: Enhance accessibility by updating aria-labels and roles in Conversations components * 🔧 fix(620): Enhance accessibility by adding tabIndex to Tabs.Content components in ArtifactTabs, Settings, and Speech components * refactor: remove RevokeKeysButton component and update related components for accessibility - Deleted RevokeKeysButton component. - Updated SharedLinks and General components to use Label for accessibility. - Enhanced Personalization component with aria-labelledby and aria-describedby attributes. - Refactored ConversationModeSwitch to use ToggleSwitch for better state management. - Improved AutoSendTextSelector with local state management and accessibility attributes. - Replaced Switch components with ToggleSwitch in various Speech and TTS components for consistency. - Added aria-labelledby attributes to Dropdown components for better accessibility. - Updated translation.json to include new localization keys and improved existing ones. - Enhanced Slider component to support aria attributes for better accessibility. * 🔧 fix: Enhance user feedback for API key operations with success and error messages * 🔧 fix: Update aria-labels in Avatar component for improved localization and accessibility * 🔧 fix: Refactor handleFile and handleDrop functions for improved readability and maintainability
This commit is contained in:
parent
bcd97aad2f
commit
a5189052ec
56 changed files with 1158 additions and 857 deletions
|
|
@ -44,6 +44,7 @@ export default function ArtifactTabs({
|
||||||
value="code"
|
value="code"
|
||||||
id="artifacts-code"
|
id="artifacts-code"
|
||||||
className={cn('flex-grow overflow-auto')}
|
className={cn('flex-grow overflow-auto')}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{isMermaid ? (
|
{isMermaid ? (
|
||||||
<MermaidMarkdown content={content} isSubmitting={isSubmitting} />
|
<MermaidMarkdown content={content} isSubmitting={isSubmitting} />
|
||||||
|
|
@ -58,7 +59,7 @@ export default function ArtifactTabs({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="preview" className="flex-grow overflow-auto">
|
<Tabs.Content value="preview" className="flex-grow overflow-auto" tabIndex={-1}>
|
||||||
<ArtifactPreview
|
<ArtifactPreview
|
||||||
files={files}
|
files={files}
|
||||||
fileKey={fileKey}
|
fileKey={fileKey}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ export function BrowserVoiceDropdown() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = 'browser-voice-dropdown-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_voice_select')}</div>
|
<div id={labelId}>{localize('com_nav_voice_select')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
key={`browser-voice-dropdown-${voices.length}`}
|
key={`browser-voice-dropdown-${voices.length}`}
|
||||||
value={voice ?? ''}
|
value={voice ?? ''}
|
||||||
|
|
@ -30,6 +32,7 @@ export function BrowserVoiceDropdown() {
|
||||||
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
|
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
|
||||||
testId="BrowserVoiceDropdown"
|
testId="BrowserVoiceDropdown"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -48,9 +51,11 @@ export function ExternalVoiceDropdown() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = 'external-voice-dropdown-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_voice_select')}</div>
|
<div id={labelId}>{localize('com_nav_voice_select')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
key={`external-voice-dropdown-${voices.length}`}
|
key={`external-voice-dropdown-${voices.length}`}
|
||||||
value={voice ?? ''}
|
value={voice ?? ''}
|
||||||
|
|
@ -59,6 +64,7 @@ export function ExternalVoiceDropdown() {
|
||||||
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
|
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
|
||||||
testId="ExternalVoiceDropdown"
|
testId="ExternalVoiceDropdown"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ const LoadingSpinner = memo(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
LoadingSpinner.displayName = 'LoadingSpinner';
|
||||||
|
|
||||||
const DateLabel: FC<{ groupName: string }> = memo(({ groupName }) => {
|
const DateLabel: FC<{ groupName: string }> = memo(({ groupName }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
|
|
@ -74,6 +76,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
isSearchLoading,
|
isSearchLoading,
|
||||||
}) => {
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
const convoHeight = isSmallScreen ? 44 : 34;
|
const convoHeight = isSmallScreen ? 44 : 34;
|
||||||
|
|
||||||
|
|
@ -181,7 +184,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
{isSearchLoading ? (
|
{isSearchLoading ? (
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<Spinner className="text-text-primary" />
|
<Spinner className="text-text-primary" />
|
||||||
<span className="ml-2 text-text-primary">Loading...</span>
|
<span className="ml-2 text-text-primary">{localize('com_ui_loading')}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,9 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
'group relative flex h-12 w-full items-center rounded-lg transition-colors duration-200 md:h-9',
|
'group relative flex h-12 w-full items-center rounded-lg transition-colors duration-200 md:h-9',
|
||||||
isActiveConvo ? 'bg-surface-active-alt' : 'hover:bg-surface-active-alt',
|
isActiveConvo ? 'bg-surface-active-alt' : 'hover:bg-surface-active-alt',
|
||||||
)}
|
)}
|
||||||
role="listitem"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={renaming ? -1 : 0}
|
||||||
|
aria-label={`${title || localize('com_ui_untitled')} conversation`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (renaming) {
|
if (renaming) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -149,7 +150,8 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||||
if (renaming) {
|
if (renaming) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
handleNavigation(false);
|
handleNavigation(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,7 @@ const ConvoLink: React.FC<ConvoLinkProps> = ({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRename();
|
onRename();
|
||||||
}}
|
}}
|
||||||
role="button"
|
aria-label={title || localize('com_ui_untitled')}
|
||||||
aria-label={isSmallScreen ? undefined : title || localize('com_ui_untitled')}
|
|
||||||
>
|
>
|
||||||
{title || localize('com_ui_untitled')}
|
{title || localize('com_ui_untitled')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ function ConvoOptions({
|
||||||
<Menu.MenuButton
|
<Menu.MenuButton
|
||||||
id={`conversation-menu-${conversationId}`}
|
id={`conversation-menu-${conversationId}`}
|
||||||
aria-label={localize('com_nav_convo_menu_options')}
|
aria-label={localize('com_nav_convo_menu_options')}
|
||||||
|
aria-readonly={undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'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:opacity-50',
|
'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:opacity-50',
|
||||||
isActiveConvo === true || isPopoverActive
|
isActiveConvo === true || isPopoverActive
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { OGDialogTemplate, OGDialog, Dropdown, useToastContext } from '@librechat/client';
|
import {
|
||||||
|
OGDialog,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogTitle,
|
||||||
|
OGDialogFooter,
|
||||||
|
Dropdown,
|
||||||
|
useToastContext,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
OGDialogTrigger,
|
||||||
|
Spinner,
|
||||||
|
} from '@librechat/client';
|
||||||
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
|
import {
|
||||||
|
useRevokeAllUserKeysMutation,
|
||||||
|
useRevokeUserKeyMutation,
|
||||||
|
} from 'librechat-data-provider/react-query';
|
||||||
import type { TDialogProps } from '~/common';
|
import type { TDialogProps } from '~/common';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { RevokeKeysButton } from '~/components/Nav';
|
|
||||||
import { useUserKey, useLocalize } from '~/hooks';
|
import { useUserKey, useLocalize } from '~/hooks';
|
||||||
|
import { NotificationSeverity } from '~/common';
|
||||||
import CustomConfig from './CustomEndpoint';
|
import CustomConfig from './CustomEndpoint';
|
||||||
import GoogleConfig from './GoogleConfig';
|
import GoogleConfig from './GoogleConfig';
|
||||||
import OpenAIConfig from './OpenAIConfig';
|
import OpenAIConfig from './OpenAIConfig';
|
||||||
import OtherConfig from './OtherConfig';
|
import OtherConfig from './OtherConfig';
|
||||||
import HelpText from './HelpText';
|
import HelpText from './HelpText';
|
||||||
|
import { logger } from '~/utils';
|
||||||
|
|
||||||
const endpointComponents = {
|
const endpointComponents = {
|
||||||
[EModelEndpoint.google]: GoogleConfig,
|
[EModelEndpoint.google]: GoogleConfig,
|
||||||
|
|
@ -42,6 +59,94 @@ const EXPIRY = {
|
||||||
NEVER: { label: 'never', value: 0 },
|
NEVER: { label: 'never', value: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RevokeKeysButton = ({
|
||||||
|
endpoint,
|
||||||
|
disabled,
|
||||||
|
setDialogOpen,
|
||||||
|
}: {
|
||||||
|
endpoint: string;
|
||||||
|
disabled: boolean;
|
||||||
|
setDialogOpen: (open: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
||||||
|
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_revoke_key_success'),
|
||||||
|
status: NotificationSeverity.SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!setDialogOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_revoke_key_error'),
|
||||||
|
status: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
revokeKeyMutation.mutate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: handleSuccess,
|
||||||
|
onError: handleError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{localize('com_ui_revoke')}
|
||||||
|
</Button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogContent className="max-w-[450px]">
|
||||||
|
<OGDialogHeader>
|
||||||
|
<OGDialogTitle>{localize('com_ui_revoke_key_endpoint', { 0: endpoint })}</OGDialogTitle>
|
||||||
|
</OGDialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_revoke_key_confirm')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<OGDialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
{localize('com_ui_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
|
||||||
|
>
|
||||||
|
{isLoading ? <Spinner /> : localize('com_ui_revoke')}
|
||||||
|
</Button>
|
||||||
|
</OGDialogFooter>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SetKeyDialog = ({
|
const SetKeyDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|
@ -83,7 +188,7 @@ const SetKeyDialog = ({
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
const selectedOption = expirationOptions.find((option) => option.label === expiresAtLabel);
|
const selectedOption = expirationOptions.find((option) => option.label === expiresAtLabel);
|
||||||
let expiresAt;
|
let expiresAt: number | null;
|
||||||
|
|
||||||
if (selectedOption?.value === 0) {
|
if (selectedOption?.value === 0) {
|
||||||
expiresAt = null;
|
expiresAt = null;
|
||||||
|
|
@ -92,8 +197,20 @@ const SetKeyDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveKey = (key: string) => {
|
const saveKey = (key: string) => {
|
||||||
|
try {
|
||||||
saveUserKey(key, expiresAt);
|
saveUserKey(key, expiresAt);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_save_key_success'),
|
||||||
|
status: NotificationSeverity.SUCCESS,
|
||||||
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error saving user key:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_save_key_error'),
|
||||||
|
status: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (formSet.has(endpoint) || formSet.has(endpointType ?? '')) {
|
if (formSet.has(endpoint) || formSet.has(endpointType ?? '')) {
|
||||||
|
|
@ -148,6 +265,14 @@ const SetKeyDialog = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!userKey.trim()) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_key_required'),
|
||||||
|
status: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
saveKey(userKey);
|
saveKey(userKey);
|
||||||
setUserKey('');
|
setUserKey('');
|
||||||
};
|
};
|
||||||
|
|
@ -159,12 +284,13 @@ const SetKeyDialog = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<OGDialogTemplate
|
<OGDialogContent className="w-11/12 max-w-2xl">
|
||||||
title={`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
|
<OGDialogHeader>
|
||||||
className="w-11/12 max-w-2xl"
|
<OGDialogTitle>
|
||||||
showCancelButton={false}
|
{`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
|
||||||
main={
|
</OGDialogTitle>
|
||||||
<div className="grid w-full items-center gap-2">
|
</OGDialogHeader>
|
||||||
|
<div className="grid w-full items-center gap-2 py-4">
|
||||||
<small className="text-red-600">
|
<small className="text-red-600">
|
||||||
{expiryTime === 'never'
|
{expiryTime === 'never'
|
||||||
? localize('com_endpoint_config_key_never_expires')
|
? localize('com_endpoint_config_key_never_expires')
|
||||||
|
|
@ -195,20 +321,17 @@ const SetKeyDialog = ({
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
<HelpText endpoint={endpoint} />
|
<HelpText endpoint={endpoint} />
|
||||||
</div>
|
</div>
|
||||||
}
|
<OGDialogFooter>
|
||||||
selection={{
|
|
||||||
selectHandler: submit,
|
|
||||||
selectClasses: 'btn btn-primary',
|
|
||||||
selectText: localize('com_ui_submit'),
|
|
||||||
}}
|
|
||||||
leftButtons={
|
|
||||||
<RevokeKeysButton
|
<RevokeKeysButton
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
disabled={!(expiryTime ?? '')}
|
disabled={!(expiryTime ?? '')}
|
||||||
setDialogOpen={onOpenChange}
|
setDialogOpen={onOpenChange}
|
||||||
/>
|
/>
|
||||||
}
|
<Button variant="submit" onClick={submit}>
|
||||||
/>
|
{localize('com_ui_submit')}
|
||||||
|
</Button>
|
||||||
|
</OGDialogFooter>
|
||||||
|
</OGDialogContent>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
<line x1="18" x2="6" y1="6" y2="18"></line>
|
<line x1="18" x2="6" y1="6" y2="18"></line>
|
||||||
<line x1="6" x2="18" y1="6" y2="18"></line>
|
<line x1="6" x2="18" y1="6" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="sr-only">{localize('com_ui_close')}</span>
|
<span className="sr-only">{localize('com_ui_close_settings')}</span>
|
||||||
</button>
|
</button>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="max-h-[550px] overflow-auto px-6 md:max-h-[400px] md:min-h-[400px] md:w-[680px]">
|
<div className="max-h-[550px] overflow-auto px-6 md:max-h-[400px] md:min-h-[400px] md:w-[680px]">
|
||||||
|
|
@ -220,35 +220,35 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
))}
|
))}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<div className="overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
<div className="overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
||||||
<Tabs.Content value={SettingsTabValues.GENERAL}>
|
<Tabs.Content value={SettingsTabValues.GENERAL} tabIndex={-1}>
|
||||||
<General />
|
<General />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value={SettingsTabValues.CHAT}>
|
<Tabs.Content value={SettingsTabValues.CHAT} tabIndex={-1}>
|
||||||
<Chat />
|
<Chat />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value={SettingsTabValues.COMMANDS}>
|
<Tabs.Content value={SettingsTabValues.COMMANDS} tabIndex={-1}>
|
||||||
<Commands />
|
<Commands />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value={SettingsTabValues.SPEECH}>
|
<Tabs.Content value={SettingsTabValues.SPEECH} tabIndex={-1}>
|
||||||
<Speech />
|
<Speech />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
{hasAnyPersonalizationFeature && (
|
{hasAnyPersonalizationFeature && (
|
||||||
<Tabs.Content value={SettingsTabValues.PERSONALIZATION}>
|
<Tabs.Content value={SettingsTabValues.PERSONALIZATION} tabIndex={-1}>
|
||||||
<Personalization
|
<Personalization
|
||||||
hasMemoryOptOut={hasMemoryOptOut}
|
hasMemoryOptOut={hasMemoryOptOut}
|
||||||
hasAnyPersonalizationFeature={hasAnyPersonalizationFeature}
|
hasAnyPersonalizationFeature={hasAnyPersonalizationFeature}
|
||||||
/>
|
/>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
)}
|
)}
|
||||||
<Tabs.Content value={SettingsTabValues.DATA}>
|
<Tabs.Content value={SettingsTabValues.DATA} tabIndex={-1}>
|
||||||
<Data />
|
<Data />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
{startupConfig?.balance?.enabled && (
|
{startupConfig?.balance?.enabled && (
|
||||||
<Tabs.Content value={SettingsTabValues.BALANCE}>
|
<Tabs.Content value={SettingsTabValues.BALANCE} tabIndex={-1}>
|
||||||
<Balance />
|
<Balance />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
)}
|
)}
|
||||||
<Tabs.Content value={SettingsTabValues.ACCOUNT}>
|
<Tabs.Content value={SettingsTabValues.ACCOUNT} tabIndex={-1}>
|
||||||
<Account />
|
<Account />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
// @ts-ignore - no type definitions available
|
||||||
import AvatarEditor from 'react-avatar-editor';
|
import AvatarEditor from 'react-avatar-editor';
|
||||||
import { FileImage, RotateCw, Upload } from 'lucide-react';
|
import { FileImage, RotateCw, Upload, ZoomIn, ZoomOut, Move, X } from 'lucide-react';
|
||||||
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
Label,
|
||||||
Slider,
|
Slider,
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
|
@ -25,14 +27,20 @@ interface AvatarEditorRef {
|
||||||
getImage: () => HTMLImageElement;
|
getImage: () => HTMLImageElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
function Avatar() {
|
function Avatar() {
|
||||||
const setUser = useSetRecoilState(store.user);
|
const setUser = useSetRecoilState(store.user);
|
||||||
|
|
||||||
const [scale, setScale] = useState<number>(1);
|
const [scale, setScale] = useState<number>(1);
|
||||||
const [rotation, setRotation] = useState<number>(0);
|
const [rotation, setRotation] = useState<number>(0);
|
||||||
|
const [position, setPosition] = useState<Position>({ x: 0.5, y: 0.5 });
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
const editorRef = useRef<AvatarEditorRef | null>(null);
|
const editorRef = useRef<AvatarEditorRef | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const openButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const [image, setImage] = useState<string | File | null>(null);
|
const [image, setImage] = useState<string | File | null>(null);
|
||||||
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
@ -48,7 +56,6 @@ function Avatar() {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
showToast({ message: localize('com_ui_upload_success') });
|
showToast({ message: localize('com_ui_upload_success') });
|
||||||
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);
|
||||||
|
|
@ -61,11 +68,13 @@ function Avatar() {
|
||||||
handleFile(file);
|
handleFile(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFile = (file: File | undefined) => {
|
const handleFile = useCallback(
|
||||||
|
(file: File | undefined) => {
|
||||||
if (fileConfig.avatarSizeLimit != null && file && file.size <= fileConfig.avatarSizeLimit) {
|
if (fileConfig.avatarSizeLimit != null && file && file.size <= fileConfig.avatarSizeLimit) {
|
||||||
setImage(file);
|
setImage(file);
|
||||||
setScale(1);
|
setScale(1);
|
||||||
setRotation(0);
|
setRotation(0);
|
||||||
|
setPosition({ x: 0.5, y: 0.5 });
|
||||||
} else {
|
} else {
|
||||||
const megabytes =
|
const megabytes =
|
||||||
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
|
||||||
|
|
@ -74,16 +83,30 @@ function Avatar() {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[fileConfig.avatarSizeLimit, localize, showToast],
|
||||||
|
);
|
||||||
|
|
||||||
const handleScaleChange = (value: number[]) => {
|
const handleScaleChange = (value: number[]) => {
|
||||||
setScale(value[0]);
|
setScale(value[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
setScale((prev) => Math.min(prev + 0.2, 5));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
setScale((prev) => Math.max(prev - 0.2, 1));
|
||||||
|
};
|
||||||
|
|
||||||
const handleRotate = () => {
|
const handleRotate = () => {
|
||||||
setRotation((prev) => (prev + 90) % 360);
|
setRotation((prev) => (prev + 90) % 360);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePositionChange = (position: Position) => {
|
||||||
|
setPosition(position);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = () => {
|
const handleUpload = () => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
const canvas = editorRef.current.getImageScaledToCanvas();
|
const canvas = editorRef.current.getImageScaledToCanvas();
|
||||||
|
|
@ -98,11 +121,14 @@ function Avatar() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
const handleDrop = useCallback(
|
||||||
|
(e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const file = e.dataTransfer.files[0];
|
const file = e.dataTransfer.files[0];
|
||||||
handleFile(file);
|
handleFile(file);
|
||||||
}, []);
|
},
|
||||||
|
[handleFile],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -116,8 +142,15 @@ function Avatar() {
|
||||||
setImage(null);
|
setImage(null);
|
||||||
setScale(1);
|
setScale(1);
|
||||||
setRotation(0);
|
setRotation(0);
|
||||||
|
setPosition({ x: 0.5, y: 0.5 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setScale(1);
|
||||||
|
setRotation(0);
|
||||||
|
setPosition({ x: 0.5, y: 0.5 });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog
|
<OGDialog
|
||||||
open={isDialogOpen}
|
open={isDialogOpen}
|
||||||
|
|
@ -125,90 +158,190 @@ function Avatar() {
|
||||||
setDialogOpen(open);
|
setDialogOpen(open);
|
||||||
if (!open) {
|
if (!open) {
|
||||||
resetImage();
|
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>
|
||||||
<OGDialogTrigger ref={openButtonRef}>
|
<OGDialogTrigger asChild>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
|
<FileImage className="mr-2 flex w-[22px] items-center" />
|
||||||
<span>{localize('com_nav_change_picture')}</span>
|
<span>{localize('com_nav_change_picture')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OGDialogContent className="w-11/12 max-w-sm" style={{ borderRadius: '12px' }}>
|
<OGDialogContent showCloseButton={false} className="w-11/12 max-w-md">
|
||||||
<OGDialogHeader>
|
<OGDialogHeader>
|
||||||
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
{image != null ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
{image != null ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
||||||
</OGDialogTitle>
|
</OGDialogTitle>
|
||||||
</OGDialogHeader>
|
</OGDialogHeader>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center p-2">
|
||||||
{image != null ? (
|
{image != null ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative overflow-hidden rounded-full">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative overflow-hidden rounded-full ring-4 ring-gray-200 transition-all dark:ring-gray-700',
|
||||||
|
isDragging && 'cursor-move ring-blue-500 dark:ring-blue-400',
|
||||||
|
)}
|
||||||
|
onMouseDown={() => setIsDragging(true)}
|
||||||
|
onMouseUp={() => setIsDragging(false)}
|
||||||
|
onMouseLeave={() => setIsDragging(false)}
|
||||||
|
>
|
||||||
<AvatarEditor
|
<AvatarEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
image={image}
|
image={image}
|
||||||
width={250}
|
width={280}
|
||||||
height={250}
|
height={280}
|
||||||
border={0}
|
border={0}
|
||||||
borderRadius={125}
|
borderRadius={140}
|
||||||
color={[255, 255, 255, 0.6]}
|
color={[255, 255, 255, 0.6]}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
rotate={rotation}
|
rotate={rotation}
|
||||||
|
position={position}
|
||||||
|
onPositionChange={handlePositionChange}
|
||||||
|
className="cursor-move"
|
||||||
/>
|
/>
|
||||||
|
{!isDragging && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-opacity hover:opacity-100">
|
||||||
|
<div className="rounded-full bg-black/50 p-2">
|
||||||
|
<Move className="h-6 w-6 text-white" />
|
||||||
</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">
|
)}
|
||||||
<span className="text-sm">{localize('com_ui_zoom')}</span>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 w-full space-y-6">
|
||||||
|
{/* Zoom Controls */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="zoom-slider" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_zoom')}
|
||||||
|
</Label>
|
||||||
|
<span className="text-sm text-text-secondary">{Math.round(scale * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
disabled={scale <= 1}
|
||||||
|
aria-label={localize('com_ui_zoom_out')}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<ZoomOut className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Slider
|
<Slider
|
||||||
|
id="zoom-slider"
|
||||||
value={[scale]}
|
value={[scale]}
|
||||||
min={1}
|
min={1}
|
||||||
max={5}
|
max={5}
|
||||||
step={0.001}
|
step={0.1}
|
||||||
onValueChange={handleScaleChange}
|
onValueChange={handleScaleChange}
|
||||||
className="w-2/3 max-w-xs"
|
className="flex-1"
|
||||||
|
aria-label={localize('com_ui_zoom_level')}
|
||||||
/>
|
/>
|
||||||
</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
|
<Button
|
||||||
className={cn(
|
type="button"
|
||||||
'btn btn-primary mt-4 flex w-full hover:bg-green-600',
|
variant="outline"
|
||||||
isUploading ? 'cursor-not-allowed opacity-90' : '',
|
size="sm"
|
||||||
)}
|
onClick={handleZoomIn}
|
||||||
|
disabled={scale >= 5}
|
||||||
|
aria-label={localize('com_ui_zoom_in')}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<ZoomIn className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center space-x-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRotate}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
aria-label={localize('com_ui_rotate_90')}
|
||||||
|
>
|
||||||
|
<RotateCw className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{localize('com_ui_rotate')}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
aria-label={localize('com_ui_reset_adjustments')}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{localize('com_ui_reset')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Helper Text */}
|
||||||
|
<p className="text-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{localize('com_ui_editor_instructions')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="mt-6 flex w-full space-x-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={resetImage}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{localize('com_ui_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="submit"
|
||||||
|
type="button"
|
||||||
|
className={cn('w-full', isUploading ? 'cursor-not-allowed opacity-90' : '')}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<Spinner className="icon-sm mr-2" />
|
<Spinner className="icon-sm mr-2" />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="mr-2 h-5 w-5" />
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{localize('com_ui_upload')}
|
{localize('com_ui_upload')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="flex h-64 w-11/12 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-transparent dark:border-gray-600"
|
className="flex h-72 w-full flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-transparent transition-colors hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={openFileDialog}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openFileDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={localize('com_ui_upload_avatar_label')}
|
||||||
>
|
>
|
||||||
<FileImage className="mb-4 size-12 text-gray-400" />
|
<FileImage className="mb-4 size-16 text-gray-400" />
|
||||||
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
<p className="mb-2 text-center text-sm font-medium text-text-primary">
|
||||||
{localize('com_ui_drag_drop')}
|
{localize('com_ui_drag_drop')}
|
||||||
</p>
|
</p>
|
||||||
<Button variant="secondary" onClick={openFileDialog}>
|
<p className="mb-4 text-center text-xs text-text-secondary">
|
||||||
|
{localize('com_ui_max_file_size', {
|
||||||
|
0:
|
||||||
|
fileConfig.avatarSizeLimit != null
|
||||||
|
? formatBytes(fileConfig.avatarSizeLimit)
|
||||||
|
: '2MB',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<Button type="button" variant="secondary" onClick={openFileDialog}>
|
||||||
{localize('com_ui_select_file')}
|
{localize('com_ui_select_file')}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
|
|
@ -217,6 +350,7 @@ function Avatar() {
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".png, .jpg, .jpeg"
|
accept=".png, .jpg, .jpeg"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
|
aria-label={localize('com_ui_file_input_avatar_label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { LockIcon, Trash } from 'lucide-react';
|
import { LockIcon, Trash } from 'lucide-react';
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Label,
|
||||||
Input,
|
Input,
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
|
@ -45,11 +46,11 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
||||||
<>
|
<>
|
||||||
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{localize('com_nav_delete_account')}</span>
|
<Label id="delete-account-label">{localize('com_nav_delete_account')}</Label>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
aria-labelledby="delete-account-label"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
|
||||||
onClick={() => setDialogOpen(true)}
|
onClick={() => setDialogOpen(true)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const DisableTwoFactorToggle: React.FC<DisableTwoFactorToggleProps> = ({
|
||||||
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">
|
||||||
<Label className="font-light"> {localize('com_nav_2fa')}</Label>
|
<Label> {localize('com_nav_2fa')}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default function DisplayUsernameMessages() {
|
||||||
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">
|
||||||
<Label className="font-light">{localize('com_nav_user_name_display')}</Label>
|
<Label id="user-name-display-label">{localize('com_nav_user_name_display')}</Label>
|
||||||
<InfoHoverCard side={ESide.Bottom} text={localize('com_nav_info_user_name_display')} />
|
<InfoHoverCard side={ESide.Bottom} text={localize('com_nav_info_user_name_display')} />
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -24,6 +24,7 @@ export default function DisplayUsernameMessages() {
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
data-testid="UsernameDisplay"
|
data-testid="UsernameDisplay"
|
||||||
|
aria-labelledby="user-name-display-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,16 @@ const ChatDirection = () => {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
aria-label="Toggle chat direction"
|
aria-label={`${localize('com_nav_chat_direction')}: ${localize('com_ui_x_selected', {
|
||||||
|
0:
|
||||||
|
direction === 'LTR'
|
||||||
|
? localize('chat_direction_left_to_right')
|
||||||
|
: localize('chat_direction_right_to_left'),
|
||||||
|
})}`}
|
||||||
onClick={toggleChatDirection}
|
onClick={toggleChatDirection}
|
||||||
data-testid="chatDirection"
|
data-testid="chatDirection"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{direction.toLowerCase()}</span>
|
{direction.toLowerCase()}
|
||||||
<span id="chat-direction-status" className="sr-only">
|
|
||||||
{direction === 'LTR'
|
|
||||||
? localize('chat_direction_left_to_right')
|
|
||||||
: localize('chat_direction_right_to_left')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ export default function FontSizeSelector() {
|
||||||
{ value: 'text-xl', label: localize('com_nav_font_size_xl') },
|
{ value: 'text-xl', label: localize('com_nav_font_size_xl') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const labelId = 'font-size-selector-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div>{localize('com_nav_font_size')}</div>
|
<div id={labelId}>{localize('com_nav_font_size')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={fontSize}
|
value={fontSize}
|
||||||
options={options}
|
options={options}
|
||||||
|
|
@ -30,6 +32,7 @@ export default function FontSizeSelector() {
|
||||||
testId="font-size-selector"
|
testId="font-size-selector"
|
||||||
sizeClasses="w-[150px]"
|
sizeClasses="w-[150px]"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,14 @@ export const ForkSettings = () => {
|
||||||
<>
|
<>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div> {localize('com_ui_fork_default')} </div>
|
<div id="remember-default-fork-label"> {localize('com_ui_fork_default')} </div>
|
||||||
<Switch
|
<Switch
|
||||||
id="rememberDefaultFork"
|
id="rememberDefaultFork"
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onCheckedChange={setRemember}
|
onCheckedChange={setRemember}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
data-testid="rememberDefaultFork"
|
data-testid="rememberDefaultFork"
|
||||||
|
aria-labelledby="remember-default-fork-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -34,7 +35,7 @@ export const ForkSettings = () => {
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<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">
|
||||||
<div>{localize('com_ui_fork_change_default')}</div>
|
<div id="fork-change-default-label">{localize('com_ui_fork_change_default')}</div>
|
||||||
<InfoHoverCard
|
<InfoHoverCard
|
||||||
side={ESide.Bottom}
|
side={ESide.Bottom}
|
||||||
text={localize('com_nav_info_fork_change_default')}
|
text={localize('com_nav_info_fork_change_default')}
|
||||||
|
|
@ -47,6 +48,7 @@ export const ForkSettings = () => {
|
||||||
sizeClasses="w-[200px]"
|
sizeClasses="w-[200px]"
|
||||||
testId="fork-setting-dropdown"
|
testId="fork-setting-dropdown"
|
||||||
className="z-[50]"
|
className="z-[50]"
|
||||||
|
aria-labelledby="fork-change-default-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,7 +56,7 @@ export const ForkSettings = () => {
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<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">
|
||||||
<div>{localize('com_ui_fork_split_target_setting')}</div>
|
<div id="split-at-target-label">{localize('com_ui_fork_split_target_setting')}</div>
|
||||||
<InfoHoverCard
|
<InfoHoverCard
|
||||||
side={ESide.Bottom}
|
side={ESide.Bottom}
|
||||||
text={localize('com_nav_info_fork_split_target_setting')}
|
text={localize('com_nav_info_fork_split_target_setting')}
|
||||||
|
|
@ -66,6 +68,7 @@ export const ForkSettings = () => {
|
||||||
onCheckedChange={setSplitAtTarget}
|
onCheckedChange={setSplitAtTarget}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
data-testid="splitAtTarget"
|
data-testid="splitAtTarget"
|
||||||
|
aria-labelledby="split-at-target-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function AtCommandSwitch() {
|
|
||||||
const [atCommand, setAtCommand] = useRecoilState<boolean>(store.atCommand);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setAtCommand(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>{localize('com_nav_at_command_description')}</div>
|
|
||||||
<Switch
|
|
||||||
id="atCommand"
|
|
||||||
checked={atCommand}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="atCommand"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,33 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { InfoHoverCard, ESide } from '@librechat/client';
|
import { InfoHoverCard, ESide } from '@librechat/client';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import SlashCommandSwitch from './SlashCommandSwitch';
|
|
||||||
import { useLocalize, useHasAccess } from '~/hooks';
|
import { useLocalize, useHasAccess } from '~/hooks';
|
||||||
import PlusCommandSwitch from './PlusCommandSwitch';
|
import ToggleSwitch from '../ToggleSwitch';
|
||||||
import AtCommandSwitch from './AtCommandSwitch';
|
import store from '~/store';
|
||||||
|
|
||||||
|
const commandSwitchConfigs = [
|
||||||
|
{
|
||||||
|
stateAtom: store.atCommand,
|
||||||
|
localizationKey: 'com_nav_at_command_description' as const,
|
||||||
|
switchId: 'atCommand',
|
||||||
|
key: 'atCommand',
|
||||||
|
permissionType: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stateAtom: store.plusCommand,
|
||||||
|
localizationKey: 'com_nav_plus_command_description' as const,
|
||||||
|
switchId: 'plusCommand',
|
||||||
|
key: 'plusCommand',
|
||||||
|
permissionType: PermissionTypes.MULTI_CONVO,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stateAtom: store.slashCommand,
|
||||||
|
localizationKey: 'com_nav_slash_command_description' as const,
|
||||||
|
switchId: 'slashCommand',
|
||||||
|
key: 'slashCommand',
|
||||||
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
function Commands() {
|
function Commands() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -19,6 +42,19 @@ function Commands() {
|
||||||
permission: Permissions.USE,
|
permission: Permissions.USE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getShowSwitch = (permissionType?: PermissionTypes) => {
|
||||||
|
if (!permissionType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (permissionType === PermissionTypes.MULTI_CONVO) {
|
||||||
|
return hasAccessToMultiConvo === true;
|
||||||
|
}
|
||||||
|
if (permissionType === PermissionTypes.PROMPTS) {
|
||||||
|
return hasAccessToPrompts === true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-1">
|
<div className="space-y-4 p-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -28,19 +64,16 @@ function Commands() {
|
||||||
<InfoHoverCard side={ESide.Bottom} text={localize('com_nav_chat_commands_info')} />
|
<InfoHoverCard side={ESide.Bottom} text={localize('com_nav_chat_commands_info')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<div className="pb-3">
|
{commandSwitchConfigs.map((config) => (
|
||||||
<AtCommandSwitch />
|
<div key={config.key} className="pb-3">
|
||||||
|
<ToggleSwitch
|
||||||
|
stateAtom={config.stateAtom}
|
||||||
|
localizationKey={config.localizationKey}
|
||||||
|
switchId={config.switchId}
|
||||||
|
showSwitch={getShowSwitch(config.permissionType)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hasAccessToMultiConvo === true && (
|
))}
|
||||||
<div className="pb-3">
|
|
||||||
<PlusCommandSwitch />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasAccessToPrompts === true && (
|
|
||||||
<div className="pb-3">
|
|
||||||
<SlashCommandSwitch />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function PlusCommandSwitch() {
|
|
||||||
const [plusCommand, setPlusCommand] = useRecoilState<boolean>(store.plusCommand);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setPlusCommand(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>{localize('com_nav_plus_command_description')}</div>
|
|
||||||
<Switch
|
|
||||||
id="plusCommand"
|
|
||||||
checked={plusCommand}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="plusCommand"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function SlashCommandSwitch() {
|
|
||||||
const [slashCommand, setSlashCommand] = useRecoilState<boolean>(store.slashCommand);
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setSlashCommand(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>{localize('com_nav_slash_command_description')}</div>
|
|
||||||
<Switch
|
|
||||||
id="slashCommand"
|
|
||||||
checked={slashCommand}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
data-testid="slashCommand"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -31,12 +31,12 @@ export const ClearChats = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="font-light">{localize('com_nav_clear_all_chats')}</Label>
|
<Label id="clear-all-chats-label">{localize('com_nav_clear_all_chats')}</Label>
|
||||||
<OGDialog open={open} onOpenChange={setOpen}>
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
aria-labelledby="clear-all-chats-label"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
{localize('com_ui_delete')}
|
{localize('com_ui_delete')}
|
||||||
|
|
@ -47,7 +47,7 @@ export const ClearChats = () => {
|
||||||
title={localize('com_nav_confirm_clear')}
|
title={localize('com_nav_confirm_clear')}
|
||||||
className="max-w-[450px]"
|
className="max-w-[450px]"
|
||||||
main={
|
main={
|
||||||
<Label className="text-left text-sm font-medium">
|
<Label className="break-words">
|
||||||
{localize('com_nav_clear_conversation_confirm_message')}
|
{localize('com_nav_clear_conversation_confirm_message')}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useOnClickOutside } from '@librechat/client';
|
import { useOnClickOutside } from '@librechat/client';
|
||||||
import ImportConversations from './ImportConversations';
|
import ImportConversations from './ImportConversations';
|
||||||
import { RevokeAllKeys } from './RevokeAllKeys';
|
import { RevokeKeys } from './RevokeKeys';
|
||||||
import { DeleteCache } from './DeleteCache';
|
import { DeleteCache } from './DeleteCache';
|
||||||
import { ClearChats } from './ClearChats';
|
import { ClearChats } from './ClearChats';
|
||||||
import SharedLinks from './SharedLinks';
|
import SharedLinks from './SharedLinks';
|
||||||
|
|
@ -20,7 +20,7 @@ function Data() {
|
||||||
<SharedLinks />
|
<SharedLinks />
|
||||||
</div>
|
</div>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<RevokeAllKeys />
|
<RevokeKeys />
|
||||||
</div>
|
</div>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<DeleteCache />
|
<DeleteCache />
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,14 @@ export const DeleteCache = ({ disabled = false }: { disabled?: boolean }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="font-light">{localize('com_nav_delete_cache_storage')}</Label>
|
<Label id="delete-cache-label">{localize('com_nav_delete_cache_storage')}</Label>
|
||||||
<OGDialog open={open} onOpenChange={setOpen}>
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
disabled={disabled || isCacheEmpty}
|
disabled={disabled || isCacheEmpty}
|
||||||
|
aria-labelledby="delete-cache-label"
|
||||||
>
|
>
|
||||||
{localize('com_ui_delete')}
|
{localize('com_ui_delete')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,114 @@
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { Import } from 'lucide-react';
|
import { Import } from 'lucide-react';
|
||||||
import { Spinner, useToastContext } from '@librechat/client';
|
import { Spinner, useToastContext, Label, Button } from '@librechat/client';
|
||||||
import type { TError } from 'librechat-data-provider';
|
|
||||||
import { useUploadConversationsMutation } from '~/data-provider';
|
import { useUploadConversationsMutation } from '~/data-provider';
|
||||||
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn, logger } from '~/utils';
|
||||||
|
|
||||||
function ImportConversations() {
|
function ImportConversations() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const [, setErrors] = useState<string[]>([]);
|
|
||||||
const [allowImport, setAllowImport] = useState(true);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
|
|
||||||
|
const handleSuccess = useCallback(() => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_import_conversation_success'),
|
||||||
|
status: NotificationSeverity.SUCCESS,
|
||||||
|
});
|
||||||
|
setIsUploading(false);
|
||||||
|
}, [localize, showToast]);
|
||||||
|
|
||||||
|
const handleError = useCallback(
|
||||||
|
(error: unknown) => {
|
||||||
|
logger.error('Import error:', error);
|
||||||
|
setIsUploading(false);
|
||||||
|
|
||||||
|
const isUnsupportedType = error?.toString().includes('Unsupported import type');
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
message: localize(
|
||||||
|
isUnsupportedType
|
||||||
|
? 'com_ui_import_conversation_file_type_error'
|
||||||
|
: 'com_ui_import_conversation_error',
|
||||||
|
),
|
||||||
|
status: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[localize, showToast],
|
||||||
|
);
|
||||||
|
|
||||||
const uploadFile = useUploadConversationsMutation({
|
const uploadFile = useUploadConversationsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: handleSuccess,
|
||||||
showToast({ message: localize('com_ui_import_conversation_success') });
|
onError: handleError,
|
||||||
setAllowImport(true);
|
onMutate: () => setIsUploading(true),
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Error: ', error);
|
|
||||||
setAllowImport(true);
|
|
||||||
setError(
|
|
||||||
(error as TError).response?.data?.message ?? 'An error occurred while uploading the file.',
|
|
||||||
);
|
|
||||||
if (error?.toString().includes('Unsupported import type') === true) {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_import_conversation_file_type_error'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showToast({ message: localize('com_ui_import_conversation_error'), status: 'error' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMutate: () => {
|
|
||||||
setAllowImport(false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const startUpload = async (file: File) => {
|
const handleFileUpload = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
try {
|
||||||
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) => {
|
|
||||||
try {
|
|
||||||
await startUpload(_file);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('file handling error', error);
|
logger.error('File processing error:', error);
|
||||||
setError('An error occurred while processing the file.');
|
setIsUploading(false);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_import_conversation_upload_error'),
|
||||||
|
status: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[uploadFile, showToast, localize],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
handleFiles(file);
|
handleFileUpload(file);
|
||||||
}
|
}
|
||||||
};
|
event.target.value = '';
|
||||||
|
},
|
||||||
|
[handleFileUpload],
|
||||||
|
);
|
||||||
|
|
||||||
const handleImportClick = () => {
|
const handleImportClick = useCallback(() => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
const handleKeyDown = useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleImportClick();
|
handleImportClick();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[handleImportClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isImportDisabled = isUploading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_ui_import_conversation_info')}</div>
|
<Label id="import-conversation-label">{localize('com_ui_import_conversation_info')}</Label>
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
onClick={handleImportClick}
|
onClick={handleImportClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
disabled={!allowImport}
|
disabled={isImportDisabled}
|
||||||
aria-label={localize('com_ui_import')}
|
aria-label={localize('com_ui_import')}
|
||||||
className="btn btn-neutral relative"
|
aria-labelledby="import-conversation-label"
|
||||||
>
|
>
|
||||||
{allowImport ? (
|
{isUploading ? (
|
||||||
<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" />
|
||||||
|
) : (
|
||||||
|
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
||||||
)}
|
)}
|
||||||
<span>{localize('com_ui_import')}</span>
|
<span>{localize('com_ui_import')}</span>
|
||||||
</button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Label } from '@librechat/client';
|
|
||||||
import { RevokeKeysButton } from './RevokeKeysButton';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
export const RevokeAllKeys = () => {
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="font-light">{localize('com_ui_revoke_info')}</Label>
|
|
||||||
<RevokeKeysButton all={true} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
72
client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx
Normal file
72
client/src/components/Nav/SettingsTabs/Data/RevokeKeys.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRevokeAllUserKeysMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import {
|
||||||
|
OGDialogTemplate,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogTrigger,
|
||||||
|
Spinner,
|
||||||
|
} from '@librechat/client';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
export const RevokeKeys = ({
|
||||||
|
disabled = false,
|
||||||
|
setDialogOpen,
|
||||||
|
}: {
|
||||||
|
disabled?: boolean;
|
||||||
|
setDialogOpen?: (open: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
if (!setDialogOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
revokeKeysMutation.mutate({}, { onSuccess: handleSuccess });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = revokeKeysMutation.isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label id="revoke-info-label">{localize('com_ui_revoke_info')}</Label>
|
||||||
|
|
||||||
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-labelledby="revoke-info-label"
|
||||||
|
>
|
||||||
|
{localize('com_ui_revoke')}
|
||||||
|
</Button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_revoke_keys')}
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_revoke_keys_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: onClick,
|
||||||
|
selectClasses:
|
||||||
|
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||||
|
selectText: isLoading ? <Spinner /> : localize('com_ui_revoke'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
useRevokeAllUserKeysMutation,
|
|
||||||
useRevokeUserKeyMutation,
|
|
||||||
} from 'librechat-data-provider/react-query';
|
|
||||||
import {
|
|
||||||
OGDialogTemplate,
|
|
||||||
Button,
|
|
||||||
Label,
|
|
||||||
OGDialog,
|
|
||||||
OGDialogTrigger,
|
|
||||||
Spinner,
|
|
||||||
} from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
export const RevokeKeysButton = ({
|
|
||||||
endpoint = '',
|
|
||||||
all = false,
|
|
||||||
disabled = false,
|
|
||||||
setDialogOpen,
|
|
||||||
}: {
|
|
||||||
endpoint?: string;
|
|
||||||
all?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
setDialogOpen?: (open: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
|
||||||
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
|
||||||
|
|
||||||
const handleSuccess = () => {
|
|
||||||
if (!setDialogOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
if (all) {
|
|
||||||
revokeKeysMutation.mutate({});
|
|
||||||
} else {
|
|
||||||
revokeKeyMutation.mutate({}, { onSuccess: handleSuccess });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogTitle = all
|
|
||||||
? localize('com_ui_revoke_keys')
|
|
||||||
: localize('com_ui_revoke_key_endpoint', { 0: endpoint });
|
|
||||||
|
|
||||||
const dialogMessage = all
|
|
||||||
? localize('com_ui_revoke_keys_confirm')
|
|
||||||
: localize('com_ui_revoke_key_confirm');
|
|
||||||
|
|
||||||
const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OGDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<OGDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
|
||||||
onClick={() => setOpen(true)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{localize('com_ui_revoke')}
|
|
||||||
</Button>
|
|
||||||
</OGDialogTrigger>
|
|
||||||
<OGDialogTemplate
|
|
||||||
showCloseButton={false}
|
|
||||||
title={dialogTitle}
|
|
||||||
className="max-w-[450px]"
|
|
||||||
main={<Label className="text-left text-sm font-medium">{dialogMessage}</Label>}
|
|
||||||
selection={{
|
|
||||||
selectHandler: onClick,
|
|
||||||
selectClasses:
|
|
||||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
|
||||||
selectText: isLoading ? <Spinner /> : localize('com_ui_revoke'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</OGDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -286,11 +286,13 @@ export default function SharedLinks() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_shared_links')}</div>
|
<Label id="shared-links-label">{localize('com_nav_shared_links')}</Label>
|
||||||
|
|
||||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
||||||
<Button variant="outline">{localize('com_ui_manage')}</Button>
|
<Button aria-labelledby="shared-links-label" variant="outline">
|
||||||
|
{localize('com_ui_manage')}
|
||||||
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
|
|
||||||
<OGDialogContent
|
<OGDialogContent
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,11 @@ export const ThemeSelector = ({
|
||||||
{ value: 'light', label: localize('com_nav_theme_light') },
|
{ value: 'light', label: localize('com_nav_theme_light') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const labelId = 'theme-selector-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_theme')}</div>
|
<div id={labelId}>{localize('com_nav_theme')}</div>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={theme}
|
value={theme}
|
||||||
|
|
@ -57,6 +59,7 @@ export const ThemeSelector = ({
|
||||||
sizeClasses="w-[180px]"
|
sizeClasses="w-[180px]"
|
||||||
testId="theme-selector"
|
testId="theme-selector"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -112,9 +115,11 @@ export const LangSelector = ({
|
||||||
{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') },
|
{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const labelId = 'language-selector-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_language')}</div>
|
<div id={labelId}>{localize('com_nav_language')}</div>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={langcode}
|
value={langcode}
|
||||||
|
|
@ -122,6 +127,7 @@ export const LangSelector = ({
|
||||||
sizeClasses="[--anchor-max-height:256px]"
|
sizeClasses="[--anchor-max-height:256px]"
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,13 @@ export default function Personalization({
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div id="reference-saved-memories-label" className="flex items-center gap-2">
|
||||||
{localize('com_ui_reference_saved_memories')}
|
{localize('com_ui_reference_saved_memories')}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-text-secondary">
|
<div
|
||||||
|
id="reference-saved-memories-description"
|
||||||
|
className="mt-1 text-xs text-text-secondary"
|
||||||
|
>
|
||||||
{localize('com_ui_reference_saved_memories_description')}
|
{localize('com_ui_reference_saved_memories_description')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,7 +79,8 @@ export default function Personalization({
|
||||||
checked={referenceSavedMemories}
|
checked={referenceSavedMemories}
|
||||||
onCheckedChange={handleMemoryToggle}
|
onCheckedChange={handleMemoryToggle}
|
||||||
disabled={updateMemoryPreferencesMutation.isLoading}
|
disabled={updateMemoryPreferencesMutation.isLoading}
|
||||||
aria-label={localize('com_ui_reference_saved_memories')}
|
aria-labelledby="reference-saved-memories-label"
|
||||||
|
aria-describedby="reference-saved-memories-description"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useLocalize } from '~/hooks';
|
import ToggleSwitch from '../ToggleSwitch';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function ConversationModeSwitch({
|
export default function ConversationModeSwitch({
|
||||||
|
|
@ -8,8 +7,6 @@ export default function ConversationModeSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
|
||||||
const [conversationMode, setConversationMode] = useRecoilState<boolean>(store.conversationMode);
|
|
||||||
const speechToText = useRecoilValue(store.speechToText);
|
const speechToText = useRecoilValue(store.speechToText);
|
||||||
const textToSpeech = useRecoilValue(store.textToSpeech);
|
const textToSpeech = useRecoilValue(store.textToSpeech);
|
||||||
const [, setAutoSendText] = useRecoilState(store.autoSendText);
|
const [, setAutoSendText] = useRecoilState(store.autoSendText);
|
||||||
|
|
@ -20,27 +17,19 @@ export default function ConversationModeSwitch({
|
||||||
setAutoTranscribeAudio(value);
|
setAutoTranscribeAudio(value);
|
||||||
setAutoSendText(3);
|
setAutoSendText(3);
|
||||||
setDecibelValue(-45);
|
setDecibelValue(-45);
|
||||||
setConversationMode(value);
|
|
||||||
if (onCheckedChange) {
|
if (onCheckedChange) {
|
||||||
onCheckedChange(value);
|
onCheckedChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>
|
stateAtom={store.conversationMode}
|
||||||
<strong>{localize('com_nav_conversation_mode')}</strong>
|
localizationKey={'com_nav_conversation_mode' as const}
|
||||||
</div>
|
switchId="ConversationMode"
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Switch
|
|
||||||
id="ConversationMode"
|
|
||||||
checked={conversationMode}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4"
|
|
||||||
data-testid="ConversationMode"
|
|
||||||
disabled={!textToSpeech || !speechToText}
|
disabled={!textToSpeech || !speechToText}
|
||||||
|
strongLabel={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { Slider, InputNumber } from '@librechat/client';
|
import { Slider, InputNumber, Switch } from '@librechat/client';
|
||||||
import { cn, defaultTextProps, optionText } from '~/utils/';
|
import { cn, defaultTextProps, optionText } from '~/utils/';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -11,31 +11,93 @@ export default function AutoSendTextSelector() {
|
||||||
const speechToText = useRecoilValue(store.speechToText);
|
const speechToText = useRecoilValue(store.speechToText);
|
||||||
const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText);
|
const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText);
|
||||||
|
|
||||||
|
// Local state for enabled/disabled toggle
|
||||||
|
const [isEnabled, setIsEnabled] = useState(autoSendText !== -1);
|
||||||
|
const [delayValue, setDelayValue] = useState(autoSendText === -1 ? 3 : autoSendText);
|
||||||
|
|
||||||
|
// Sync local state when autoSendText changes externally
|
||||||
|
useEffect(() => {
|
||||||
|
setIsEnabled(autoSendText !== -1);
|
||||||
|
if (autoSendText !== -1) {
|
||||||
|
setDelayValue(autoSendText);
|
||||||
|
}
|
||||||
|
}, [autoSendText]);
|
||||||
|
|
||||||
|
const handleToggle = (enabled: boolean) => {
|
||||||
|
setIsEnabled(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
setAutoSendText(delayValue);
|
||||||
|
} else {
|
||||||
|
setAutoSendText(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSliderChange = (value: number[]) => {
|
||||||
|
const newValue = value[0];
|
||||||
|
setDelayValue(newValue);
|
||||||
|
if (isEnabled) {
|
||||||
|
setAutoSendText(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (value: number[] | null) => {
|
||||||
|
const newValue = value ? value[0] : 3;
|
||||||
|
setDelayValue(newValue);
|
||||||
|
if (isEnabled) {
|
||||||
|
setAutoSendText(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelId = 'auto-send-text-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div id={labelId}>{localize('com_nav_auto_send_text')}</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="autoSendTextToggle"
|
||||||
|
checked={isEnabled}
|
||||||
|
onCheckedChange={handleToggle}
|
||||||
|
className="ml-4"
|
||||||
|
data-testid="autoSendTextToggle"
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
disabled={!speechToText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isEnabled && (
|
||||||
|
<div className="mt-2 flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_auto_send_text')}</div>
|
<div id="auto-send-delay-label" className="text-sm text-text-secondary">
|
||||||
<div className="w-2" />
|
{localize('com_nav_setting_delay')}
|
||||||
<small className="opacity-40">({localize('com_nav_auto_send_text_disabled')})</small>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Slider
|
<Slider
|
||||||
value={[autoSendText ?? -1]}
|
value={[delayValue]}
|
||||||
onValueChange={(value) => setAutoSendText(value[0])}
|
onValueChange={handleSliderChange}
|
||||||
onDoubleClick={() => setAutoSendText(-1)}
|
onDoubleClick={() => {
|
||||||
min={-1}
|
setDelayValue(3);
|
||||||
|
if (isEnabled) {
|
||||||
|
setAutoSendText(3);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
max={60}
|
max={60}
|
||||||
step={1}
|
step={1}
|
||||||
className="ml-4 flex h-4 w-24"
|
className="ml-4 flex h-4 w-24"
|
||||||
disabled={!speechToText}
|
disabled={!speechToText || !isEnabled}
|
||||||
|
aria-labelledby="auto-send-delay-label"
|
||||||
/>
|
/>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={`${autoSendText} s`}
|
value={`${delayValue} s`}
|
||||||
disabled={!speechToText}
|
disabled={!speechToText || !isEnabled}
|
||||||
onChange={(value) => setAutoSendText(value ? value[0] : 0)}
|
onChange={handleInputChange}
|
||||||
min={-1}
|
min={0}
|
||||||
max={60}
|
max={60}
|
||||||
|
aria-labelledby="auto-send-delay-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
cn(
|
cn(
|
||||||
|
|
@ -46,5 +108,7 @@ export default function AutoSendTextSelector() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Switch } from '@librechat/client';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function AutoTranscribeAudioSwitch({
|
export default function AutoTranscribeAudioSwitch({
|
||||||
|
|
@ -8,30 +7,15 @@ export default function AutoTranscribeAudioSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
|
||||||
const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState<boolean>(
|
|
||||||
store.autoTranscribeAudio,
|
|
||||||
);
|
|
||||||
const speechToText = useRecoilValue(store.speechToText);
|
const speechToText = useRecoilValue(store.speechToText);
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setAutoTranscribeAudio(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>{localize('com_nav_auto_transcribe_audio')}</div>
|
stateAtom={store.autoTranscribeAudio}
|
||||||
<Switch
|
localizationKey={'com_nav_auto_transcribe_audio' as const}
|
||||||
id="AutoTranscribeAudio"
|
switchId="AutoTranscribeAudio"
|
||||||
checked={autoTranscribeAudio}
|
onCheckedChange={onCheckedChange}
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="AutoTranscribeAudio"
|
|
||||||
disabled={!speechToText}
|
disabled={!speechToText}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function DecibelSelector() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_db_sensitivity')}</div>
|
<div id="decibel-selector-label">{localize('com_nav_db_sensitivity')}</div>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', { 0: '-45' })})
|
({localize('com_endpoint_default_with_num', { 0: '-45' })})
|
||||||
|
|
@ -29,6 +29,7 @@ export default function DecibelSelector() {
|
||||||
step={1}
|
step={1}
|
||||||
className="ml-4 flex h-4 w-24"
|
className="ml-4 flex h-4 w-24"
|
||||||
disabled={!speechToText}
|
disabled={!speechToText}
|
||||||
|
aria-labelledby="decibel-selector-label"
|
||||||
/>
|
/>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -37,6 +38,7 @@ export default function DecibelSelector() {
|
||||||
onChange={(value) => setDecibelValue(value ? value[0] : 0)}
|
onChange={(value) => setDecibelValue(value ? value[0] : 0)}
|
||||||
min={-100}
|
min={-100}
|
||||||
max={-30}
|
max={-30}
|
||||||
|
aria-labelledby="decibel-selector-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
cn(
|
cn(
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ const EngineSTTDropdown: React.FC<EngineSTTDropdownProps> = ({ external }) => {
|
||||||
setEngineSTT(value);
|
setEngineSTT(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = 'engine-stt-dropdown-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_engine')}</div>
|
<div id={labelId}>{localize('com_nav_engine')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={engineSTT}
|
value={engineSTT}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
|
|
@ -33,6 +35,7 @@ const EngineSTTDropdown: React.FC<EngineSTTDropdownProps> = ({ external }) => {
|
||||||
sizeClasses="w-[180px]"
|
sizeClasses="w-[180px]"
|
||||||
testId="EngineSTTDropdown"
|
testId="EngineSTTDropdown"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,11 @@ export default function LanguageSTTDropdown() {
|
||||||
setLanguageSTT(value);
|
setLanguageSTT(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = 'language-stt-dropdown-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_language')}</div>
|
<div id={labelId}>{localize('com_nav_language')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={languageSTT}
|
value={languageSTT}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
|
|
@ -104,6 +106,7 @@ export default function LanguageSTTDropdown() {
|
||||||
sizeClasses="[--anchor-max-height:256px]"
|
sizeClasses="[--anchor-max-height:256px]"
|
||||||
testId="LanguageSTTDropdown"
|
testId="LanguageSTTDropdown"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function SpeechToTextSwitch({
|
export default function SpeechToTextSwitch({
|
||||||
|
|
@ -8,28 +6,13 @@ export default function SpeechToTextSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
|
||||||
const [speechToText, setSpeechToText] = useRecoilState<boolean>(store.speechToText);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setSpeechToText(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>
|
stateAtom={store.speechToText}
|
||||||
<strong>{localize('com_nav_speech_to_text')}</strong>
|
localizationKey={'com_nav_speech_to_text' as const}
|
||||||
</div>
|
switchId="SpeechToText"
|
||||||
<Switch
|
onCheckedChange={onCheckedChange}
|
||||||
id="SpeechToText"
|
strongLabel={true}
|
||||||
checked={speechToText}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="SpeechToText"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
} from './STT';
|
} from './STT';
|
||||||
import ConversationModeSwitch from './ConversationModeSwitch';
|
import ConversationModeSwitch from './ConversationModeSwitch';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn, logger } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
function Speech() {
|
function Speech() {
|
||||||
|
|
@ -186,7 +186,7 @@ function Speech() {
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs.Content value={'simple'}>
|
<Tabs.Content value={'simple'} tabIndex={-1}>
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<SpeechToTextSwitch />
|
<SpeechToTextSwitch />
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
|
|
@ -198,7 +198,7 @@ function Speech() {
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value={'advanced'}>
|
<Tabs.Content value={'advanced'} tabIndex={-1}>
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<ConversationModeSwitch />
|
<ConversationModeSwitch />
|
||||||
<div className="mt-2 h-px bg-border-medium" role="none" />
|
<div className="mt-2 h-px bg-border-medium" role="none" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function AutomaticPlaybackSwitch({
|
export default function AutomaticPlaybackSwitch({
|
||||||
|
|
@ -8,26 +6,12 @@ export default function AutomaticPlaybackSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
|
||||||
const [automaticPlayback, setAutomaticPlayback] = useRecoilState(store.automaticPlayback);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setAutomaticPlayback(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>{localize('com_nav_automatic_playback')}</div>
|
stateAtom={store.automaticPlayback}
|
||||||
<Switch
|
localizationKey={'com_nav_automatic_playback' as const}
|
||||||
id="AutomaticPlayback"
|
switchId="AutomaticPlayback"
|
||||||
checked={automaticPlayback}
|
onCheckedChange={onCheckedChange}
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="AutomaticPlayback"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Switch } from '@librechat/client';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function CacheTTSSwitch({
|
export default function CacheTTSSwitch({
|
||||||
|
|
@ -8,28 +7,15 @@ export default function CacheTTSSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const textToSpeech = useRecoilValue(store.textToSpeech);
|
||||||
const [cacheTTS, setCacheTTS] = useRecoilState<boolean>(store.cacheTTS);
|
|
||||||
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setCacheTTS(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>{localize('com_nav_enable_cache_tts')}</div>
|
stateAtom={store.cacheTTS}
|
||||||
<Switch
|
localizationKey={'com_nav_enable_cache_tts' as const}
|
||||||
id="CacheTTS"
|
switchId="CacheTTS"
|
||||||
checked={cacheTTS}
|
onCheckedChange={onCheckedChange}
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="CacheTTS"
|
|
||||||
disabled={!textToSpeech}
|
disabled={!textToSpeech}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Switch } from '@librechat/client';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function CloudBrowserVoicesSwitch({
|
export default function CloudBrowserVoicesSwitch({
|
||||||
|
|
@ -8,30 +7,15 @@ export default function CloudBrowserVoicesSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const textToSpeech = useRecoilValue(store.textToSpeech);
|
||||||
const [cloudBrowserVoices, setCloudBrowserVoices] = useRecoilState<boolean>(
|
|
||||||
store.cloudBrowserVoices,
|
|
||||||
);
|
|
||||||
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setCloudBrowserVoices(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>{localize('com_nav_enable_cloud_browser_voice')}</div>
|
stateAtom={store.cloudBrowserVoices}
|
||||||
<Switch
|
localizationKey={'com_nav_enable_cloud_browser_voice' as const}
|
||||||
id="CloudBrowserVoices"
|
switchId="CloudBrowserVoices"
|
||||||
checked={cloudBrowserVoices}
|
onCheckedChange={onCheckedChange}
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="CloudBrowserVoices"
|
|
||||||
disabled={!textToSpeech}
|
disabled={!textToSpeech}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ const EngineTTSDropdown: React.FC<EngineTTSDropdownProps> = ({ external }) => {
|
||||||
setEngineTTS(value);
|
setEngineTTS(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = 'engine-tts-dropdown-label';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_engine')}</div>
|
<div id={labelId}>{localize('com_nav_engine')}</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={engineTTS}
|
value={engineTTS}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
|
|
@ -33,6 +35,7 @@ const EngineTTSDropdown: React.FC<EngineTTSDropdownProps> = ({ external }) => {
|
||||||
sizeClasses="w-[180px]"
|
sizeClasses="w-[180px]"
|
||||||
testId="EngineTTSDropdown"
|
testId="EngineTTSDropdown"
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function DecibelSelector() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_playback_rate')}</div>
|
<div id="playback-rate-label">{localize('com_nav_playback_rate')}</div>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<small className="opacity-40">
|
<small className="opacity-40">
|
||||||
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
({localize('com_endpoint_default_with_num', { 0: '1' })})
|
||||||
|
|
@ -29,6 +29,7 @@ export default function DecibelSelector() {
|
||||||
step={0.1}
|
step={0.1}
|
||||||
className="ml-4 flex h-4 w-24"
|
className="ml-4 flex h-4 w-24"
|
||||||
disabled={!textToSpeech}
|
disabled={!textToSpeech}
|
||||||
|
aria-labelledby="playback-rate-label"
|
||||||
/>
|
/>
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
|
@ -37,6 +38,7 @@ export default function DecibelSelector() {
|
||||||
onChange={(value) => setPlaybackRate(value ? value[0] : 0)}
|
onChange={(value) => setPlaybackRate(value ? value[0] : 0)}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
max={2}
|
max={2}
|
||||||
|
aria-labelledby="playback-rate-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
cn(
|
cn(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import ToggleSwitch from '../../ToggleSwitch';
|
||||||
import { Switch } from '@librechat/client';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function TextToSpeechSwitch({
|
export default function TextToSpeechSwitch({
|
||||||
|
|
@ -8,28 +6,13 @@ export default function TextToSpeechSwitch({
|
||||||
}: {
|
}: {
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
|
||||||
const [TextToSpeech, setTextToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setTextToSpeech(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<ToggleSwitch
|
||||||
<div>
|
stateAtom={store.textToSpeech}
|
||||||
<strong>{localize('com_nav_text_to_speech')}</strong>
|
localizationKey={'com_nav_text_to_speech' as const}
|
||||||
</div>
|
switchId="TextToSpeech"
|
||||||
<Switch
|
onCheckedChange={onCheckedChange}
|
||||||
id="TextToSpeech"
|
strongLabel={true}
|
||||||
checked={TextToSpeech}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="TextToSpeech"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ interface ToggleSwitchProps {
|
||||||
hoverCardText?: LocalizeKey;
|
hoverCardText?: LocalizeKey;
|
||||||
switchId: string;
|
switchId: string;
|
||||||
onCheckedChange?: (value: boolean) => void;
|
onCheckedChange?: (value: boolean) => void;
|
||||||
|
showSwitch?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
strongLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
|
|
@ -19,6 +22,9 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
hoverCardText,
|
hoverCardText,
|
||||||
switchId,
|
switchId,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
|
showSwitch = true,
|
||||||
|
disabled = false,
|
||||||
|
strongLabel = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [switchState, setSwitchState] = useRecoilState(stateAtom);
|
const [switchState, setSwitchState] = useRecoilState(stateAtom);
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -28,10 +34,18 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
onCheckedChange?.(value);
|
onCheckedChange?.(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelId = `${switchId}-label`;
|
||||||
|
|
||||||
|
if (!showSwitch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
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">
|
||||||
<div>{localize(localizationKey)}</div>
|
<div id={labelId}>
|
||||||
|
{strongLabel ? <strong>{localize(localizationKey)}</strong> : localize(localizationKey)}
|
||||||
|
</div>
|
||||||
{hoverCardText && <InfoHoverCard side={ESide.Bottom} text={localize(hoverCardText)} />}
|
{hoverCardText && <InfoHoverCard side={ESide.Bottom} text={localize(hoverCardText)} />}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -40,6 +54,8 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
data-testid={switchId}
|
data-testid={switchId}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
export { default as General } from './General/General';
|
|
||||||
export { default as Chat } from './Chat/Chat';
|
export { default as Chat } from './Chat/Chat';
|
||||||
export { default as Data } from './Data/Data';
|
export { default as Data } from './Data/Data';
|
||||||
export { default as Commands } from './Commands/Commands';
|
|
||||||
export { RevokeKeysButton } from './Data/RevokeKeysButton';
|
|
||||||
export { default as Account } from './Account/Account';
|
|
||||||
export { default as Balance } from './Balance/Balance';
|
|
||||||
export { default as Speech } from './Speech/Speech';
|
export { default as Speech } from './Speech/Speech';
|
||||||
|
export { default as Balance } from './Balance/Balance';
|
||||||
|
export { default as General } from './General/General';
|
||||||
|
export { default as Account } from './Account/Account';
|
||||||
|
export { default as Commands } from './Commands/Commands';
|
||||||
export { default as Personalization } from './Personalization';
|
export { default as Personalization } from './Personalization';
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,12 @@ function ChatGroupItem({
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
id={`prompt-actions-${group._id}`}
|
id={`prompt-actions-${group._id}`}
|
||||||
aria-label={`${group.name} - Actions Menu`}
|
type="button"
|
||||||
aria-expanded="false"
|
aria-label={
|
||||||
aria-controls={`prompt-menu-${group._id}`}
|
localize('com_ui_sr_actions_menu', { 0: group.name }) +
|
||||||
aria-haspopup="menu"
|
' ' +
|
||||||
|
localize('com_ui_prompt')
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -86,11 +88,6 @@ function ChatGroupItem({
|
||||||
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||||
<span className="sr-only">
|
|
||||||
{localize('com_ui_sr_actions_menu', { 0: group.name }) +
|
|
||||||
' ' +
|
|
||||||
localize('com_ui_prompt')}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
|
@ -98,30 +95,29 @@ function ChatGroupItem({
|
||||||
aria-label={`Available actions for ${group.name}`}
|
aria-label={`Available actions for ${group.name}`}
|
||||||
className="z-50 w-fit rounded-xl"
|
className="z-50 w-fit rounded-xl"
|
||||||
collisionPadding={2}
|
collisionPadding={2}
|
||||||
align="end"
|
align="start"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
role="menuitem"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setPreviewDialogOpen(true);
|
setPreviewDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="w-full cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
className="w-full cursor-pointer rounded-lg text-text-primary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<TextSearch className="mr-2 h-4 w-4" aria-hidden="true" />
|
<TextSearch className="mr-2 h-4 w-4 text-text-primary" aria-hidden="true" />
|
||||||
<span>{localize('com_ui_preview')}</span>
|
<span>{localize('com_ui_preview')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
className="cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
className="cursor-pointer rounded-lg text-text-primary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEditClick(e);
|
onEditClick(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon className="mr-2 h-4 w-4" aria-hidden="true" />
|
<EditIcon className="mr-2 h-4 w-4 text-text-primary" aria-hidden="true" />
|
||||||
<span>{localize('com_ui_edit')}</span>
|
<span>{localize('com_ui_edit')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`${group.name} prompt group`}
|
aria-label={`${group.name} Prompt, ${localize('com_ui_category')}: ${group.category ?? ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2 truncate pr-2">
|
<div className="flex items-center gap-2 truncate pr-2">
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export default function FilterPrompts({ className = '' }: { className?: string }
|
||||||
value={categoryFilter || SystemCategories.ALL}
|
value={categoryFilter || SystemCategories.ALL}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
options={filterOptions}
|
options={filterOptions}
|
||||||
className="bg-transparent"
|
className="rounded-lg bg-transparent"
|
||||||
icon={<ListFilter className="h-4 w-4" />}
|
icon={<ListFilter className="h-4 w-4" />}
|
||||||
label="Filter: "
|
label="Filter: "
|
||||||
ariaLabel={localize('com_ui_filter_prompts')}
|
ariaLabel={localize('com_ui_filter_prompts')}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function List({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-grow overflow-y-auto">
|
<div className="flex-grow overflow-y-auto" aria-label={localize('com_ui_prompt_groups')}>
|
||||||
<div className="overflow-y-auto overflow-x-hidden">
|
<div className="overflow-y-auto overflow-x-hidden">
|
||||||
{isLoading && isChatRoute && (
|
{isLoading && isChatRoute && (
|
||||||
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
<Skeleton className="my-2 flex h-[84px] w-full rounded-2xl border-0 px-3 pb-4 pt-3" />
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const PreviewPrompt = ({
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<OGDialogContent className="max-h-[90vh] w-11/12 max-w-full overflow-y-auto md:max-w-[60vw]">
|
<OGDialogContent className="max-h-[90vh] w-11/12 max-w-full overflow-y-auto md:max-w-[60vw]">
|
||||||
<div className="p-2">
|
<div>
|
||||||
<PromptDetails group={group} />
|
<PromptDetails group={group} />
|
||||||
</div>
|
</div>
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"chat_direction_left_to_right": "something needs to go here. was empty",
|
"chat_direction_left_to_right": "Left to Right",
|
||||||
"chat_direction_right_to_left": "something needs to go here. was empty",
|
"chat_direction_right_to_left": "Right to Left",
|
||||||
"com_a11y_ai_composing": "The AI is still composing.",
|
"com_a11y_ai_composing": "The AI is still composing.",
|
||||||
"com_a11y_end": "The AI has finished their reply.",
|
"com_a11y_end": "The AI has finished their reply.",
|
||||||
"com_a11y_start": "The AI has started their reply.",
|
"com_a11y_start": "The AI has started their reply.",
|
||||||
|
|
@ -408,7 +408,6 @@
|
||||||
"com_nav_auto_scroll": "Auto-Scroll to latest message on chat open",
|
"com_nav_auto_scroll": "Auto-Scroll to latest message on chat open",
|
||||||
"com_nav_auto_send_prompts": "Auto-send Prompts",
|
"com_nav_auto_send_prompts": "Auto-send Prompts",
|
||||||
"com_nav_auto_send_text": "Auto send text",
|
"com_nav_auto_send_text": "Auto send text",
|
||||||
"com_nav_auto_send_text_disabled": "set -1 to disable",
|
|
||||||
"com_nav_auto_transcribe_audio": "Auto transcribe audio",
|
"com_nav_auto_transcribe_audio": "Auto transcribe audio",
|
||||||
"com_nav_automatic_playback": "Autoplay Latest Message",
|
"com_nav_automatic_playback": "Autoplay Latest Message",
|
||||||
"com_nav_balance": "Balance",
|
"com_nav_balance": "Balance",
|
||||||
|
|
@ -573,6 +572,7 @@
|
||||||
"com_nav_slash_command_description": "Toggle command \"/\" for selecting a prompt via keyboard",
|
"com_nav_slash_command_description": "Toggle command \"/\" for selecting a prompt via keyboard",
|
||||||
"com_nav_speech_to_text": "Speech to Text",
|
"com_nav_speech_to_text": "Speech to Text",
|
||||||
"com_nav_stop_generating": "Stop generating",
|
"com_nav_stop_generating": "Stop generating",
|
||||||
|
"com_nav_setting_delay": "Delay (s)",
|
||||||
"com_nav_text_to_speech": "Text to Speech",
|
"com_nav_text_to_speech": "Text to Speech",
|
||||||
"com_nav_theme": "Theme",
|
"com_nav_theme": "Theme",
|
||||||
"com_nav_theme_dark": "Dark",
|
"com_nav_theme_dark": "Dark",
|
||||||
|
|
@ -761,6 +761,7 @@
|
||||||
"com_ui_close": "Close",
|
"com_ui_close": "Close",
|
||||||
"com_ui_close_menu": "Close Menu",
|
"com_ui_close_menu": "Close Menu",
|
||||||
"com_ui_close_window": "Close Window",
|
"com_ui_close_window": "Close Window",
|
||||||
|
"com_ui_close_settings": "Close Settings",
|
||||||
"com_ui_code": "Code",
|
"com_ui_code": "Code",
|
||||||
"com_ui_collapse_chat": "Collapse Chat",
|
"com_ui_collapse_chat": "Collapse Chat",
|
||||||
"com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used",
|
"com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used",
|
||||||
|
|
@ -950,8 +951,9 @@
|
||||||
"com_ui_image_edited": "Image edited",
|
"com_ui_image_edited": "Image edited",
|
||||||
"com_ui_image_gen": "Image Gen",
|
"com_ui_image_gen": "Image Gen",
|
||||||
"com_ui_import": "Import",
|
"com_ui_import": "Import",
|
||||||
"com_ui_import_conversation_error": "There was an error importing your conversations",
|
"com_ui_import_conversation_error": "There was an error while importing your conversations",
|
||||||
"com_ui_import_conversation_file_type_error": "Unsupported import type",
|
"com_ui_import_conversation_file_type_error": "Error with file type. Please select a valid JSON file.",
|
||||||
|
"com_ui_import_conversation_upload_error": "Error uploading file. Please try again.",
|
||||||
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
||||||
"com_ui_import_conversation_success": "Conversations imported successfully",
|
"com_ui_import_conversation_success": "Conversations imported successfully",
|
||||||
"com_ui_include_shadcnui": "Include shadcn/ui components instructions",
|
"com_ui_include_shadcnui": "Include shadcn/ui components instructions",
|
||||||
|
|
@ -1077,6 +1079,7 @@
|
||||||
"com_ui_prompts_allow_create": "Allow creating Prompts",
|
"com_ui_prompts_allow_create": "Allow creating Prompts",
|
||||||
"com_ui_prompts_allow_share": "Allow sharing Prompts",
|
"com_ui_prompts_allow_share": "Allow sharing Prompts",
|
||||||
"com_ui_prompts_allow_use": "Allow using Prompts",
|
"com_ui_prompts_allow_use": "Allow using Prompts",
|
||||||
|
"com_ui_prompt_groups": "Prompt Groups List",
|
||||||
"com_ui_provider": "Provider",
|
"com_ui_provider": "Provider",
|
||||||
"com_ui_quality": "Quality",
|
"com_ui_quality": "Quality",
|
||||||
"com_ui_read_aloud": "Read aloud",
|
"com_ui_read_aloud": "Read aloud",
|
||||||
|
|
@ -1279,5 +1282,21 @@
|
||||||
"com_ui_x_selected": "{{0}} selected",
|
"com_ui_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You"
|
"com_user_message": "You",
|
||||||
|
"com_ui_rotate": "Rotate",
|
||||||
|
"com_ui_reset": "Reset",
|
||||||
|
"com_ui_zoom_in": "Zoom in",
|
||||||
|
"com_ui_zoom_out": "Zoom out",
|
||||||
|
"com_ui_zoom_level": "Zoom level",
|
||||||
|
"com_ui_rotate_90": "Rotate 90 degrees",
|
||||||
|
"com_ui_reset_adjustments": "Reset adjustments",
|
||||||
|
"com_ui_editor_instructions": "Drag the image to reposition • Use zoom slider or buttons to adjust size",
|
||||||
|
"com_ui_save_key_success": "API key saved successfully",
|
||||||
|
"com_ui_save_key_error": "Failed to save API key. Please try again.",
|
||||||
|
"com_ui_revoke_key_success": "API key revoked successfully",
|
||||||
|
"com_ui_revoke_key_error": "Failed to revoke API key. Please try again.",
|
||||||
|
"com_ui_key_required": "API key is required",
|
||||||
|
"com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})",
|
||||||
|
"com_ui_upload_avatar_label": "Upload avatar image",
|
||||||
|
"com_ui_file_input_avatar_label": "File input for avatar"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface DropdownProps {
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
renderValue?: (option: Option) => React.ReactNode;
|
renderValue?: (option: Option) => React.ReactNode;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
'aria-labelledby'?: string;
|
||||||
portal?: boolean;
|
portal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||||
iconOnly = false,
|
iconOnly = false,
|
||||||
renderValue,
|
renderValue,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
portal = true,
|
portal = true,
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
|
|
@ -77,6 +79,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||||
)}
|
)}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
{icon}
|
{icon}
|
||||||
|
|
|
||||||
|
|
@ -1,191 +1,225 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
function DropdownMenuContent({
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className = '', inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-900 dark:data-[state=open]:bg-gray-900',
|
|
||||||
inset ? 'pl-8' : '',
|
|
||||||
className,
|
className,
|
||||||
)}
|
sideOffset = 4,
|
||||||
{...props}
|
...props
|
||||||
>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
{children}
|
return (
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className = '', ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-100 bg-white p-1 text-gray-700 shadow-md animate-in slide-in-from-left-1 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className = '', sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-100 bg-white p-1 text-gray-700 shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400',
|
'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
));
|
);
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
}
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = 'default',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}
|
variant?: 'default' | 'destructive';
|
||||||
>(({ className = '', inset, ...props }, ref) => (
|
}) {
|
||||||
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-900',
|
"data-[variant=destructive]:*:[svg]:!text-destructive outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
inset ? 'pl-8' : '',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
}
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
function DropdownMenuCheckboxItem({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
children,
|
||||||
>(({ className = '', children, checked, ...props }, ref) => (
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-900',
|
"outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
));
|
);
|
||||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
}
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
function DropdownMenuRadioGroup({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
>(({ className = '', children, ...props }, ref) => (
|
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||||
<DropdownMenuPrimitive.RadioItem
|
}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className,
|
className,
|
||||||
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800',
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<CircleIcon className="size-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
));
|
);
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
}
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
function DropdownMenuLabel({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}
|
}) {
|
||||||
>(({ className = '', inset, ...props }, ref) => (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn('-mx-1 my-1 h-px bg-surface-hover', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1.5 text-sm font-semibold text-gray-900 dark:text-gray-300',
|
'outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground',
|
||||||
inset ? 'pl-8' : '',
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
'text-popover-foreground origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border border-border-medium bg-surface-secondary p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className = '', ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn('-mx-1 my-1 h-px bg-border-medium', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className = '',
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span className={cn('ml-auto text-xs tracking-widest text-gray-500', className)} {...props} />
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuSubContent,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { CircleHelpIcon } from 'lucide-react';
|
import { CircleHelpIcon } from 'lucide-react';
|
||||||
import { HoverCard, HoverCardTrigger, HoverCardPortal, HoverCardContent } from './HoverCard';
|
import { HoverCard, HoverCardTrigger, HoverCardPortal, HoverCardContent } from './HoverCard';
|
||||||
import { ESide } from '~/common';
|
import { ESide } from '~/common';
|
||||||
|
|
@ -8,15 +9,23 @@ type InfoHoverCardProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfoHoverCard = ({ side, text }: InfoHoverCardProps) => {
|
const InfoHoverCard = ({ side, text }: InfoHoverCardProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard openDelay={50}>
|
<HoverCard openDelay={50} open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<HoverCardTrigger className="cursor-help">
|
<HoverCardTrigger
|
||||||
<CircleHelpIcon className="h-5 w-5 text-text-tertiary" />{' '}
|
tabIndex={0}
|
||||||
|
className="inline-flex cursor-help items-center justify-center rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary focus-visible:ring-offset-2"
|
||||||
|
onFocus={() => setIsOpen(true)}
|
||||||
|
onBlur={() => setIsOpen(false)}
|
||||||
|
aria-label={text}
|
||||||
|
>
|
||||||
|
<CircleHelpIcon className="h-5 w-5 text-text-tertiary" aria-hidden="true" />
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardPortal>
|
<HoverCardPortal>
|
||||||
<HoverCardContent side={side} className="z-[999] w-80">
|
<HoverCardContent side={side} className="z-[999] w-80">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm text-text-secondary">{text}</p>
|
<span className="text-sm text-text-secondary">{text}</span>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCardPortal>
|
</HoverCardPortal>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const Label = React.forwardRef<
|
||||||
{...props}
|
{...props}
|
||||||
{...{
|
{...{
|
||||||
className: cn(
|
className: cn(
|
||||||
'block w-full break-all text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
|
'block w-full break-all text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
|
||||||
className,
|
className,
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,28 @@ import * as React from 'react';
|
||||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
type SliderProps = React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
}
|
'aria-describedby'?: string;
|
||||||
>(({ className, onDoubleClick, ...props }, ref) => (
|
} & (
|
||||||
|
| { 'aria-label': string; 'aria-labelledby'?: never }
|
||||||
|
| { 'aria-labelledby': string; 'aria-label'?: never }
|
||||||
|
| { 'aria-label': string; 'aria-labelledby': string }
|
||||||
|
);
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
onDoubleClick,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -29,10 +44,14 @@ const Slider = React.forwardRef<
|
||||||
{...{
|
{...{
|
||||||
className:
|
className:
|
||||||
'block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
'block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
));
|
),
|
||||||
|
);
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider };
|
export { Slider };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue