mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🎛️ refactor: Add keyboard command toggles & only trigger for the 1st character (#3566)
* refactor: Improve handling of key up events in useHandleKeyUp hook to only trigger for first char and fix linter issues * refactor: Update Beta component styling with theme twcss variables * refactor: theming for Settings, add toggle enable/disable keyboard commands
This commit is contained in:
parent
270c6d2350
commit
01a88991ab
12 changed files with 224 additions and 64 deletions
|
|
@ -1,17 +1,10 @@
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare, Command } from 'lucide-react';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
import type { TDialogProps } from '~/common';
|
import type { TDialogProps } from '~/common';
|
||||||
import {
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogPanel,
|
|
||||||
DialogTitle,
|
|
||||||
Transition,
|
|
||||||
TransitionChild,
|
|
||||||
} from '@headlessui/react';
|
|
||||||
import { GearIcon, DataIcon, SpeechIcon, UserIcon, ExperimentIcon } from '~/components/svg';
|
import { GearIcon, DataIcon, SpeechIcon, UserIcon, ExperimentIcon } from '~/components/svg';
|
||||||
import { General, Chat, Speech, Beta, Data, Account } from './SettingsTabs';
|
import { General, Chat, Speech, Beta, Commands, Data, Account } from './SettingsTabs';
|
||||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
|
@ -30,7 +23,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black/50 dark:bg-black/80" aria-hidden="true" />
|
<div className="fixed inset-0 bg-black opacity-50 dark:opacity-80" aria-hidden="true" />
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
|
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
|
|
@ -49,19 +42,19 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
>
|
>
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
className={cn(
|
className={cn(
|
||||||
'overflow-hidden rounded-xl rounded-b-lg bg-white pb-6 shadow-2xl backdrop-blur-2xl animate-in dark:bg-gray-700 sm:rounded-lg md:min-h-[373px] md:w-[680px]',
|
'overflow-hidden rounded-xl rounded-b-lg bg-surface-tertiary-alt pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
className="mb-3 flex items-center justify-between border-b border-black/10 p-6 pb-5 text-left dark:border-white/10"
|
className="mb-3 flex items-center justify-between border-b border-border-medium p-6 pb-5 text-left"
|
||||||
as="div"
|
as="div"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
|
<h2 className="text-lg font-medium leading-6 text-text-primary">
|
||||||
{localize('com_nav_settings')}
|
{localize('com_nav_settings')}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 dark:data-[state=open]:bg-gray-800"
|
className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-surface-primary dark:focus:ring-offset-surface-primary"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -74,7 +67,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="h-5 w-5 text-black dark:text-white"
|
className="h-5 w-5 text-text-primary"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
@ -94,17 +87,17 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
||||||
isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-800' : '',
|
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
|
||||||
)}
|
)}
|
||||||
style={{ outline: 'none' }}
|
style={{ outline: 'none' }}
|
||||||
>
|
>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.GENERAL}
|
value={SettingsTabValues.GENERAL}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -113,12 +106,12 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
{localize('com_nav_setting_general')}
|
{localize('com_nav_setting_general')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.CHAT}
|
value={SettingsTabValues.CHAT}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -127,12 +120,12 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
{localize('com_nav_setting_chat')}
|
{localize('com_nav_setting_chat')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.BETA}
|
value={SettingsTabValues.BETA}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -141,12 +134,26 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
{localize('com_nav_setting_beta')}
|
{localize('com_nav_setting_beta')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
)}
|
||||||
|
value={SettingsTabValues.COMMANDS}
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
<Command className="icon-sm" />
|
||||||
|
{localize('com_nav_commands')}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
|
isSmallScreen
|
||||||
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
|
: 'bg-surface-tertiary-alt',
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.SPEECH}
|
value={SettingsTabValues.SPEECH}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -155,12 +162,12 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
{localize('com_nav_setting_speech')}
|
{localize('com_nav_setting_speech')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.DATA}
|
value={SettingsTabValues.DATA}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -169,12 +176,12 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
{localize('com_nav_setting_data')}
|
{localize('com_nav_setting_data')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-primary',
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||||
: 'bg-white radix-state-active:bg-gray-200',
|
: 'bg-surface-tertiary-alt',
|
||||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
|
||||||
)}
|
)}
|
||||||
value={SettingsTabValues.ACCOUNT}
|
value={SettingsTabValues.ACCOUNT}
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
@ -187,6 +194,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
<General />
|
<General />
|
||||||
<Chat />
|
<Chat />
|
||||||
<Beta />
|
<Beta />
|
||||||
|
<Commands />
|
||||||
<Speech />
|
<Speech />
|
||||||
<Data />
|
<Data />
|
||||||
<Account />
|
<Account />
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ function Beta() {
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
className="w-full md:min-h-[271px]"
|
className="w-full md:min-h-[271px]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
<ModularChat />
|
<ModularChat />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
<LaTeXParsing />
|
<LaTeXParsing />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Switch } from '~/components/ui';
|
||||||
|
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 mt-2"
|
||||||
|
data-testid="atCommand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
client/src/components/Nav/SettingsTabs/Commands/Commands.tsx
Normal file
30
client/src/components/Nav/SettingsTabs/Commands/Commands.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
|
import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
|
import SlashCommandSwitch from './SlashCommandSwitch';
|
||||||
|
import PlusCommandSwitch from './PlusCommandSwitch';
|
||||||
|
import AtCommandSwitch from './AtCommandSwitch';
|
||||||
|
|
||||||
|
function Commands() {
|
||||||
|
return (
|
||||||
|
<Tabs.Content
|
||||||
|
value={SettingsTabValues.COMMANDS}
|
||||||
|
role="tabpanel"
|
||||||
|
className="w-full md:min-h-[271px]"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<AtCommandSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<PlusCommandSwitch />
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
<SlashCommandSwitch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Commands);
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Switch } from '~/components/ui';
|
||||||
|
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 mt-2"
|
||||||
|
data-testid="plusCommand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Switch } from '~/components/ui';
|
||||||
|
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}
|
||||||
|
className="ml-4 mt-2"
|
||||||
|
data-testid="slashCommand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ export { default as Chat } from './Chat/Chat';
|
||||||
export { ClearChatsButton } from './General/General';
|
export { ClearChatsButton } from './General/General';
|
||||||
export { default as Data } from './Data/Data';
|
export { default as Data } from './Data/Data';
|
||||||
export { default as Beta } from './Beta/Beta';
|
export { default as Beta } from './Beta/Beta';
|
||||||
|
export { default as Commands } from './Commands/Commands';
|
||||||
export { RevokeKeysButton } from './Data/RevokeKeysButton';
|
export { RevokeKeysButton } from './Data/RevokeKeysButton';
|
||||||
export { default as Account } from './Account/Account';
|
export { default as Account } from './Account/Account';
|
||||||
export { default as Speech } from './Speech/Speech';
|
export { default as Speech } from './Speech/Speech';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useSetRecoilState } from 'recoil';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
||||||
|
|
@ -20,20 +20,16 @@ const shouldTriggerCommand = (
|
||||||
commandChar: string,
|
commandChar: string,
|
||||||
) => {
|
) => {
|
||||||
const text = textAreaRef.current?.value;
|
const text = textAreaRef.current?.value;
|
||||||
if (!(text && text[text.length - 1] === commandChar)) {
|
if (typeof text !== 'string' || text.length === 0 || text[0] !== commandChar) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPos = textAreaRef.current?.selectionStart;
|
const startPos = textAreaRef.current?.selectionStart;
|
||||||
if (!startPos) {
|
if (typeof startPos !== 'number') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAtStart = startPos === 1;
|
return startPos === 1;
|
||||||
const isPrecededBySpace = textAreaRef.current?.value.charAt(startPos - 2) === ' ';
|
|
||||||
|
|
||||||
const shouldTrigger = isAtStart || isPrecededBySpace;
|
|
||||||
return shouldTrigger;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,26 +51,32 @@ const useHandleKeyUp = ({
|
||||||
permission: Permissions.USE,
|
permission: Permissions.USE,
|
||||||
});
|
});
|
||||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||||
|
|
||||||
|
// Get the current state of command toggles
|
||||||
|
const atCommandEnabled = useRecoilValue(store.atCommand);
|
||||||
|
const plusCommandEnabled = useRecoilValue(store.plusCommand);
|
||||||
|
const slashCommandEnabled = useRecoilValue(store.slashCommand);
|
||||||
|
|
||||||
const handleAtCommand = useCallback(() => {
|
const handleAtCommand = useCallback(() => {
|
||||||
if (shouldTriggerCommand(textAreaRef, '@')) {
|
if (atCommandEnabled && shouldTriggerCommand(textAreaRef, '@')) {
|
||||||
setShowMentionPopover(true);
|
setShowMentionPopover(true);
|
||||||
}
|
}
|
||||||
}, [textAreaRef, setShowMentionPopover]);
|
}, [textAreaRef, setShowMentionPopover, atCommandEnabled]);
|
||||||
|
|
||||||
const handlePlusCommand = useCallback(() => {
|
const handlePlusCommand = useCallback(() => {
|
||||||
if (shouldTriggerCommand(textAreaRef, '+')) {
|
if (plusCommandEnabled && shouldTriggerCommand(textAreaRef, '+')) {
|
||||||
setShowPlusPopover(true);
|
setShowPlusPopover(true);
|
||||||
}
|
}
|
||||||
}, [textAreaRef, setShowPlusPopover]);
|
}, [textAreaRef, setShowPlusPopover, plusCommandEnabled]);
|
||||||
|
|
||||||
const handlePromptsCommand = useCallback(() => {
|
const handlePromptsCommand = useCallback(() => {
|
||||||
if (!hasAccess) {
|
if (!hasAccess || !slashCommandEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (shouldTriggerCommand(textAreaRef, '/')) {
|
if (shouldTriggerCommand(textAreaRef, '/')) {
|
||||||
setShowPromptsPopover(true);
|
setShowPromptsPopover(true);
|
||||||
}
|
}
|
||||||
}, [textAreaRef, hasAccess, setShowPromptsPopover]);
|
}, [textAreaRef, hasAccess, setShowPromptsPopover, slashCommandEnabled]);
|
||||||
|
|
||||||
const commandHandlers = useMemo(
|
const commandHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -91,18 +93,18 @@ const useHandleKeyUp = ({
|
||||||
const handleKeyUp = useCallback(
|
const handleKeyUp = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const text = textAreaRef.current?.value;
|
const text = textAreaRef.current?.value;
|
||||||
if (!text) {
|
if (typeof text !== 'string' || text.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invalidKeys[event.key]) {
|
if (invalidKeys[event.key as keyof typeof invalidKeys]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastChar = text[text.length - 1];
|
const firstChar = text[0];
|
||||||
const handler = commandHandlers[lastChar];
|
const handler = commandHandlers[firstChar as keyof typeof commandHandlers];
|
||||||
|
|
||||||
if (handler) {
|
if (typeof handler === 'function') {
|
||||||
handler();
|
handler();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -682,6 +682,18 @@ export default {
|
||||||
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
|
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
|
||||||
com_nav_info_bookmarks_rebuild:
|
com_nav_info_bookmarks_rebuild:
|
||||||
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
|
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
|
||||||
|
// Command Settings Tab
|
||||||
|
com_nav_commands: 'Commands',
|
||||||
|
com_nav_commands_tab: 'Command Settings',
|
||||||
|
com_nav_at_command: '@-Command',
|
||||||
|
com_nav_at_command_description:
|
||||||
|
'Toggle command "@" for switching endpoints, models, presets, etc.',
|
||||||
|
com_nav_plus_command: '+-Command',
|
||||||
|
com_nav_plus_command_description: 'Toggle command "+" for adding a multi-response setting',
|
||||||
|
com_nav_slash_command: '/-Command',
|
||||||
|
com_nav_slash_command_description: 'Toggle command "/" for selecting a prompt via keyboard',
|
||||||
|
com_nav_command_settings: 'Command Settings',
|
||||||
|
com_nav_command_settings_description: 'Customize which commands are available in the chat',
|
||||||
com_nav_setting_general: 'General',
|
com_nav_setting_general: 'General',
|
||||||
com_nav_setting_chat: 'Chat',
|
com_nav_setting_chat: 'Chat',
|
||||||
com_nav_setting_beta: 'Beta features',
|
com_nav_setting_beta: 'Beta features',
|
||||||
|
|
|
||||||
|
|
@ -291,4 +291,24 @@
|
||||||
|
|
||||||
.scrollbar-gutter-stable {
|
.scrollbar-gutter-stable {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles for Chrome scrollbar */
|
||||||
|
.chrome-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 12px; /* Increase the width of the scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2); /* Color of the scroll thumb */
|
||||||
|
border-radius: 6px; /* Rounded corners on the scroll thumb */
|
||||||
|
border: 2px solid transparent; /* Creates padding around scroll thumb */
|
||||||
|
background-clip: padding-box; /* Prevents background color from leaking outside the border */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3); /* Darker color when hovering */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chrome-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent; /* Color of the tracking area */
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ const localStorageAtoms = {
|
||||||
modularChat: atomWithLocalStorage('modularChat', true),
|
modularChat: atomWithLocalStorage('modularChat', true),
|
||||||
LaTeXParsing: atomWithLocalStorage('LaTeXParsing', true),
|
LaTeXParsing: atomWithLocalStorage('LaTeXParsing', true),
|
||||||
|
|
||||||
|
// Commands settings
|
||||||
|
atCommand: atomWithLocalStorage('atCommand', true),
|
||||||
|
plusCommand: atomWithLocalStorage('plusCommand', true),
|
||||||
|
slashCommand: atomWithLocalStorage('slashCommand', true),
|
||||||
|
|
||||||
// Speech settings
|
// Speech settings
|
||||||
conversationMode: atomWithLocalStorage('conversationMode', false),
|
conversationMode: atomWithLocalStorage('conversationMode', false),
|
||||||
advancedMode: atomWithLocalStorage('advancedMode', false),
|
advancedMode: atomWithLocalStorage('advancedMode', false),
|
||||||
|
|
|
||||||
|
|
@ -895,6 +895,10 @@ export enum SettingsTabValues {
|
||||||
* Tab for Account Settings
|
* Tab for Account Settings
|
||||||
*/
|
*/
|
||||||
ACCOUNT = 'account',
|
ACCOUNT = 'account',
|
||||||
|
/**
|
||||||
|
* Chat input commands
|
||||||
|
*/
|
||||||
|
COMMANDS = 'commands',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum STTProviders {
|
export enum STTProviders {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue