mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 07:46:47 +00:00
feat(shortcuts): add panel/submit/bookmark/continue/read-aloud shortcuts
- Wire stop, regenerate, continue, and read-aloud handlers to existing
buttons via data-testid, fixing handlers that previously queried
selectors with no matching DOM nodes.
- Add data-testid='nav-panel-${id}' to expanded sidebar nav buttons so
the panel-opener shortcuts can target them.
- Add new shortcut definitions and handlers: submitMessage,
bookmarkConversation, continueResponse, readAloudLastResponse, and
the open* panel openers (assistants, agents, prompts, memories,
parameters, files, bookmarks, MCP).
- Drop the toggleRightSidebar shortcut — there is no right sidebar to
toggle in this codebase.
- Refresh the KeyboardShortcutsDialog layout and ShortcutRecorder for
the new groups, tighten ShortcutKeyCombo styling, and surface the
shortcuts hint chips in the account menu.
This commit is contained in:
parent
c127d8e26f
commit
2884fed30d
11 changed files with 417 additions and 209 deletions
|
|
@ -7,7 +7,6 @@ import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
|||
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||
import { useSetIndexOptions, useLocalize } from '~/hooks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { useShortcutAriaKey, useShortcutHint } from '~/hooks/useKeyboardShortcuts';
|
||||
import OptionsPopover from './OptionsPopover';
|
||||
import PopoverButtons from './PopoverButtons';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
|
@ -25,11 +24,6 @@ export default function HeaderOptions({
|
|||
const { showPopover, conversation, setShowPopover } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
const { endpoint } = conversation ?? {};
|
||||
const toggleRightSidebarHint = useShortcutHint(
|
||||
'toggleRightSidebar',
|
||||
localize('com_ui_model_parameters'),
|
||||
);
|
||||
const toggleRightSidebarAriaKey = useShortcutAriaKey('toggleRightSidebar');
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
|
|
@ -57,12 +51,11 @@ export default function HeaderOptions({
|
|||
<TooltipAnchor
|
||||
id="parameters-button"
|
||||
aria-label={localize('com_ui_model_parameters')}
|
||||
description={toggleRightSidebarHint}
|
||||
description={localize('com_ui_model_parameters')}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={triggerAdvancedMode}
|
||||
data-testid="parameters-button"
|
||||
aria-keyshortcuts={toggleRightSidebarAriaKey}
|
||||
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
>
|
||||
<Settings2 size={16} aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export default memo(function StopButton({
|
|||
render={
|
||||
<button
|
||||
type="button"
|
||||
data-testid="stop-generation-button"
|
||||
className={cn(
|
||||
'rounded-full bg-text-primary p-1.5 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type HoverButtonProps = {
|
|||
isLast?: boolean;
|
||||
className?: string;
|
||||
buttonStyle?: string;
|
||||
dataTestId?: string;
|
||||
};
|
||||
|
||||
const extractMessageContent = (message: TMessage): string => {
|
||||
|
|
@ -80,6 +81,7 @@ const HoverButton = memo(
|
|||
isDisabled = false,
|
||||
isLast = false,
|
||||
className = '',
|
||||
dataTestId,
|
||||
}: HoverButtonProps) => {
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
|
||||
|
|
@ -95,6 +97,7 @@ const HoverButton = memo(
|
|||
return (
|
||||
<button
|
||||
id={id}
|
||||
data-testid={dataTestId}
|
||||
className={buttonStyle}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
|
|
@ -169,6 +172,7 @@ const HoverButtons = ({
|
|||
title={localize('com_ui_regenerate')}
|
||||
icon={<RegenerateIcon size="19" />}
|
||||
isLast={isLast}
|
||||
dataTestId={isLast ? 'regenerate-generation-button' : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -200,6 +204,7 @@ const HoverButtons = ({
|
|||
icon={props.icon}
|
||||
isActive={props.isActive}
|
||||
isLast={isLast}
|
||||
dataTestId={isLast ? 'read-aloud-button' : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -255,6 +260,7 @@ const HoverButtons = ({
|
|||
title={localize('com_ui_regenerate')}
|
||||
icon={<RegenerateIcon size="19" />}
|
||||
isLast={isLast}
|
||||
dataTestId={isLast ? 'regenerate-generation-button' : undefined}
|
||||
className="active"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -266,6 +272,7 @@ const HoverButtons = ({
|
|||
title={localize('com_ui_continue')}
|
||||
icon={<ContinueIcon className="w-19 h-19 -rotate-180" />}
|
||||
isLast={isLast}
|
||||
dataTestId={isLast ? 'continue-generation-button' : undefined}
|
||||
className="active"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { GearIcon, DropdownMenuSeparator, Avatar } from '@librechat/client';
|
|||
import { MyFilesModal } from '~/components/Chat/Input/Files/MyFilesModal';
|
||||
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useShortcutDisplay } from '~/hooks/useKeyboardShortcuts';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Settings from './Settings';
|
||||
import store from '~/store';
|
||||
|
|
@ -25,13 +24,11 @@ function HelpSubmenu({
|
|||
termsOfServiceURL,
|
||||
privacyPolicyURL,
|
||||
onShowShortcuts,
|
||||
showShortcutsDisplay,
|
||||
}: {
|
||||
helpAndFaqURL?: string;
|
||||
termsOfServiceURL?: string;
|
||||
privacyPolicyURL?: string;
|
||||
onShowShortcuts: () => void;
|
||||
showShortcutsDisplay: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const hasHelpFaq = !!helpAndFaqURL && helpAndFaqURL !== '/';
|
||||
|
|
@ -53,7 +50,7 @@ function HelpSubmenu({
|
|||
</Menu.MenuItem>
|
||||
<Menu.Menu
|
||||
portal
|
||||
gutter={4}
|
||||
gutter={12}
|
||||
className="account-settings-popover popover-ui popover-from-left z-[126] w-[244px] rounded-lg"
|
||||
>
|
||||
{hasHelpFaq && (
|
||||
|
|
@ -67,8 +64,7 @@ function HelpSubmenu({
|
|||
)}
|
||||
<Menu.MenuItem onClick={onShowShortcuts} className="select-item text-sm">
|
||||
<Keyboard className="icon-md" aria-hidden="true" />
|
||||
<span className="flex-1">{localize('com_shortcut_keyboard_shortcuts')}</span>
|
||||
<span className="text-xs text-text-secondary">{showShortcutsDisplay}</span>
|
||||
{localize('com_shortcut_keyboard_shortcuts')}
|
||||
</Menu.MenuItem>
|
||||
{showLegalDivider && (hasTos || hasPrivacy) && <DropdownMenuSeparator />}
|
||||
{hasTos && (
|
||||
|
|
@ -105,8 +101,6 @@ function AccountSettings({ collapsed = false }: { collapsed?: boolean }) {
|
|||
const [showFiles, setShowFiles] = useState(false);
|
||||
const setShowShortcutsDialog = useSetRecoilState(store.showShortcutsDialog);
|
||||
const accountSettingsButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const showShortcutsDisplay = useShortcutDisplay('showShortcuts');
|
||||
const openSettingsDisplay = useShortcutDisplay('openSettings');
|
||||
|
||||
return (
|
||||
<Menu.MenuProvider>
|
||||
|
|
@ -158,6 +152,12 @@ function AccountSettings({ collapsed = false }: { collapsed?: boolean }) {
|
|||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<HelpSubmenu
|
||||
helpAndFaqURL={startupConfig?.helpAndFaqURL}
|
||||
termsOfServiceURL={startupConfig?.interface?.termsOfService?.externalUrl}
|
||||
privacyPolicyURL={startupConfig?.interface?.privacyPolicy?.externalUrl}
|
||||
onShowShortcuts={() => setShowShortcutsDialog(true)}
|
||||
/>
|
||||
<Menu.MenuItem onClick={() => setShowFiles(true)} className="select-item text-sm">
|
||||
<FileText className="icon-md" aria-hidden="true" />
|
||||
{localize('com_nav_my_files')}
|
||||
|
|
@ -168,16 +168,8 @@ function AccountSettings({ collapsed = false }: { collapsed?: boolean }) {
|
|||
data-testid="nav-settings"
|
||||
>
|
||||
<GearIcon className="icon-md" aria-hidden="true" />
|
||||
<span className="flex-1">{localize('com_nav_settings')}</span>
|
||||
<span className="text-xs text-text-secondary">{openSettingsDisplay}</span>
|
||||
{localize('com_nav_settings')}
|
||||
</Menu.MenuItem>
|
||||
<HelpSubmenu
|
||||
helpAndFaqURL={startupConfig?.helpAndFaqURL}
|
||||
termsOfServiceURL={startupConfig?.interface?.termsOfService?.externalUrl}
|
||||
privacyPolicyURL={startupConfig?.interface?.privacyPolicy?.externalUrl}
|
||||
onShowShortcuts={() => setShowShortcutsDialog(true)}
|
||||
showShortcutsDisplay={showShortcutsDisplay}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<Menu.MenuItem onClick={() => logout()} className="select-item text-sm">
|
||||
<LogOut className="icon-md" aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Pencil, RotateCcw, X } from 'lucide-react';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { OGDialog, OGDialogContent, OGDialogTitle, OGDialogClose } from '@librechat/client';
|
||||
import type { ShortcutActionId, ShortcutBindingInfo } from '~/hooks/useKeyboardShortcuts';
|
||||
import type { TranslationKeys } from '~/hooks/useLocalize';
|
||||
import type { ShortcutBinding } from '~/utils/shortcuts';
|
||||
import { isMac, useShortcutBindings, useShortcutDisplay } from '~/hooks/useKeyboardShortcuts';
|
||||
import { isMac, useShortcutBindings } from '~/hooks/useKeyboardShortcuts';
|
||||
import { RecorderInfo, RecorderPill, useShortcutRecorder } from './ShortcutRecorder';
|
||||
import { bindingDisplayKeys } from '~/utils/shortcuts';
|
||||
import ShortcutKeyCombo from './ShortcutKeyCombo';
|
||||
|
|
@ -15,6 +15,8 @@ import store from '~/store';
|
|||
|
||||
type GroupedBindings = Record<string, ShortcutBindingInfo[]>;
|
||||
|
||||
const PANELS_GROUP = 'com_shortcut_group_panels';
|
||||
|
||||
function EditingRow({
|
||||
info,
|
||||
label,
|
||||
|
|
@ -59,9 +61,9 @@ function EditingRow({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="truncate text-[13.5px] font-medium text-text-primary">{label}</span>
|
||||
<span className="truncate text-[13px] text-text-primary">{label}</span>
|
||||
<RecorderPill
|
||||
state={recorder}
|
||||
ariaLabel={localize('com_shortcut_edit_aria', { 0: label })}
|
||||
|
|
@ -100,17 +102,12 @@ function ShortcutRow({
|
|||
const localize = useLocalize();
|
||||
const label = localize(info.labelKey as TranslationKeys);
|
||||
const displayKeys = useMemo(() => bindingDisplayKeys(info.binding, isMac), [info.binding]);
|
||||
const editAriaLabel = localize('com_shortcut_edit_aria', { 0: label });
|
||||
const isUnset = displayKeys.length === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative rounded-xl px-3 py-2.5 transition-all duration-200',
|
||||
isEditing
|
||||
? 'bg-surface-tertiary/60 dark:bg-surface-secondary-alt/60 ring-1 ring-blue-500/30'
|
||||
: 'hover:bg-surface-tertiary/50 dark:hover:bg-surface-secondary-alt/40',
|
||||
)}
|
||||
>
|
||||
{isEditing ? (
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="px-2 py-2">
|
||||
<EditingRow
|
||||
info={info}
|
||||
label={label}
|
||||
|
|
@ -119,50 +116,53 @@ function ShortcutRow({
|
|||
setBinding={setBinding}
|
||||
onStopEdit={onStopEdit}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="truncate text-[13.5px] font-medium text-text-primary">{label}</span>
|
||||
{info.isCustom && (
|
||||
<span
|
||||
className="inline-flex h-1.5 w-1.5 shrink-0 rounded-full bg-blue-500 ring-2 ring-blue-500/20"
|
||||
aria-hidden="true"
|
||||
title={localize('com_shortcut_edit_aria', { 0: label })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{displayKeys.length > 0 ? (
|
||||
<ShortcutKeyCombo keys={displayKeys} />
|
||||
) : (
|
||||
<span className="text-[12px] font-medium italic text-text-secondary">
|
||||
{localize('com_shortcut_not_set')}
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-1 flex items-center gap-0.5 opacity-0 transition-all duration-200 focus-within:opacity-100 group-hover:opacity-100">
|
||||
{info.isCustom && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => resetBinding(info.id)}
|
||||
aria-label={localize('com_shortcut_reset_aria', { 0: label })}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-active-alt hover:text-text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStartEdit(info.id)}
|
||||
aria-label={localize('com_shortcut_edit_aria', { 0: label })}
|
||||
data-testid={`edit-shortcut-${info.id}`}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary hover:bg-surface-active-alt hover:text-text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group flex items-center justify-between gap-3 px-2 py-2">
|
||||
<span
|
||||
className={cn(
|
||||
'truncate text-[13px]',
|
||||
isUnset ? 'text-text-secondary' : 'text-text-primary',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{info.isCustom && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => resetBinding(info.id)}
|
||||
className="text-[11.5px] text-text-secondary opacity-0 transition-opacity hover:text-text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100"
|
||||
>
|
||||
{localize('com_shortcut_reset')}
|
||||
</button>
|
||||
)}
|
||||
{isUnset ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStartEdit(info.id)}
|
||||
aria-label={editAriaLabel}
|
||||
data-testid={`edit-shortcut-${info.id}`}
|
||||
className="inline-flex h-[22px] items-center gap-1 rounded-md border border-dashed border-border-medium bg-transparent px-2 text-[11px] font-medium text-text-secondary transition-colors hover:border-border-heavy hover:bg-surface-tertiary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:bg-surface-secondary-alt"
|
||||
>
|
||||
<Plus className="h-3 w-3" aria-hidden="true" />
|
||||
{localize('com_shortcut_set')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStartEdit(info.id)}
|
||||
aria-label={editAriaLabel}
|
||||
data-testid={`edit-shortcut-${info.id}`}
|
||||
className="rounded-md px-1 py-0.5 transition-colors hover:bg-surface-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:bg-surface-secondary-alt"
|
||||
>
|
||||
<ShortcutKeyCombo keys={displayKeys} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -190,11 +190,60 @@ function ShortcutGroup({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<section className="mb-5 last:mb-0">
|
||||
<h3 className="mb-1.5 px-3 text-[10.5px] font-semibold uppercase tracking-[0.12em] text-text-secondary">
|
||||
<section className="mb-6 last:mb-0">
|
||||
<h3 className="mb-2 px-2 text-[12px] font-medium text-text-secondary">
|
||||
{localize(groupKey as TranslationKeys)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex flex-col">
|
||||
{bindings.map((info) => (
|
||||
<ShortcutRow
|
||||
key={info.id}
|
||||
info={info}
|
||||
isEditing={editingId === info.id}
|
||||
onStartEdit={onStartEdit}
|
||||
onStopEdit={onStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelsSection({
|
||||
bindings,
|
||||
editingId,
|
||||
onStartEdit,
|
||||
onStopEdit,
|
||||
bindingMap,
|
||||
getActionLabel,
|
||||
setBinding,
|
||||
resetBinding,
|
||||
}: {
|
||||
bindings: ShortcutBindingInfo[];
|
||||
editingId: ShortcutActionId | null;
|
||||
onStartEdit: (id: ShortcutActionId) => void;
|
||||
onStopEdit: () => void;
|
||||
bindingMap: Map<string, ShortcutActionId>;
|
||||
getActionLabel: (id: string) => string;
|
||||
setBinding: (id: ShortcutActionId, binding: ShortcutBinding | null) => void;
|
||||
resetBinding: (id: ShortcutActionId) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<section className="border-t border-border-light px-5 pb-2 pt-4">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-3 px-2">
|
||||
<h3 className="text-[12px] font-medium text-text-secondary">
|
||||
{localize('com_shortcut_group_panels')}
|
||||
</h3>
|
||||
<p className="text-text-secondary/80 text-[11.5px]">
|
||||
{localize('com_shortcut_group_panels_hint')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-10 md:grid-cols-2">
|
||||
{bindings.map((info) => (
|
||||
<ShortcutRow
|
||||
key={info.id}
|
||||
|
|
@ -216,7 +265,6 @@ function ShortcutGroup({
|
|||
function KeyboardShortcutsDialog() {
|
||||
const localize = useLocalize();
|
||||
const { bindings, bindingMap, setBinding, resetBinding, resetAll } = useShortcutBindings();
|
||||
const showShortcutsDisplay = useShortcutDisplay('showShortcuts');
|
||||
const [open, setOpen] = useRecoilState(store.showShortcutsDialog);
|
||||
const [editingId, setEditingId] = useState<ShortcutActionId | null>(null);
|
||||
|
||||
|
|
@ -235,13 +283,14 @@ function KeyboardShortcutsDialog() {
|
|||
const groupEntries = useMemo(() => Object.entries(grouped), [grouped]);
|
||||
|
||||
const leftColumn = useMemo(
|
||||
() => groupEntries.filter(([key]) => key !== 'com_shortcut_group_chat'),
|
||||
() => groupEntries.filter(([key]) => key !== 'com_shortcut_group_chat' && key !== PANELS_GROUP),
|
||||
[groupEntries],
|
||||
);
|
||||
const rightColumn = useMemo(
|
||||
() => groupEntries.filter(([key]) => key === 'com_shortcut_group_chat'),
|
||||
[groupEntries],
|
||||
);
|
||||
const panelEntries = useMemo(() => grouped[PANELS_GROUP] ?? [], [grouped]);
|
||||
|
||||
const labelMap = useMemo<Map<string, string>>(() => {
|
||||
const map = new Map<string, string>();
|
||||
|
|
@ -274,80 +323,78 @@ function KeyboardShortcutsDialog() {
|
|||
>
|
||||
<OGDialogContent
|
||||
showCloseButton={false}
|
||||
className="flex max-h-[85vh] w-11/12 max-w-4xl flex-col overflow-hidden p-0"
|
||||
className="flex max-h-[85vh] w-11/12 max-w-3xl flex-col overflow-hidden p-0"
|
||||
>
|
||||
<header className="flex shrink-0 items-start justify-between gap-4 px-7 pb-4 pt-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<OGDialogTitle className="text-[18px] font-semibold tracking-tight text-text-primary">
|
||||
{localize('com_shortcut_keyboard_shortcuts')}
|
||||
</OGDialogTitle>
|
||||
<p className="text-[13px] text-text-secondary">
|
||||
{localize('com_shortcut_dialog_subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<OGDialogClose className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-active-alt hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<header className="flex shrink-0 items-center justify-between gap-4 px-7 pt-6">
|
||||
<OGDialogTitle className="text-[16px] font-semibold text-text-primary">
|
||||
{localize('com_shortcut_keyboard_shortcuts')}
|
||||
</OGDialogTitle>
|
||||
<OGDialogClose className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-surface-tertiary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:bg-surface-secondary-alt">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">{localize('com_ui_close')}</span>
|
||||
</OGDialogClose>
|
||||
</header>
|
||||
|
||||
<div className="grid flex-1 grid-cols-1 gap-x-8 overflow-y-auto px-4 pb-2 md:grid-cols-2">
|
||||
<div>
|
||||
{leftColumn.map(([groupKey, items]) => (
|
||||
<ShortcutGroup
|
||||
key={groupKey}
|
||||
groupKey={groupKey}
|
||||
bindings={items}
|
||||
editingId={editingId}
|
||||
onStartEdit={handleStartEdit}
|
||||
onStopEdit={handleStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{rightColumn.map(([groupKey, items]) => (
|
||||
<ShortcutGroup
|
||||
key={groupKey}
|
||||
groupKey={groupKey}
|
||||
bindings={items}
|
||||
editingId={editingId}
|
||||
onStartEdit={handleStartEdit}
|
||||
onStopEdit={handleStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
))}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 gap-x-10 px-5 pb-2 pt-5 md:grid-cols-2">
|
||||
<div>
|
||||
{leftColumn.map(([groupKey, items]) => (
|
||||
<ShortcutGroup
|
||||
key={groupKey}
|
||||
groupKey={groupKey}
|
||||
bindings={items}
|
||||
editingId={editingId}
|
||||
onStartEdit={handleStartEdit}
|
||||
onStopEdit={handleStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{rightColumn.map(([groupKey, items]) => (
|
||||
<ShortcutGroup
|
||||
key={groupKey}
|
||||
groupKey={groupKey}
|
||||
bindings={items}
|
||||
editingId={editingId}
|
||||
onStartEdit={handleStartEdit}
|
||||
onStopEdit={handleStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{panelEntries.length > 0 && (
|
||||
<PanelsSection
|
||||
bindings={panelEntries}
|
||||
editingId={editingId}
|
||||
onStartEdit={handleStartEdit}
|
||||
onStopEdit={handleStopEdit}
|
||||
bindingMap={bindingMap}
|
||||
getActionLabel={getActionLabel}
|
||||
setBinding={setBinding}
|
||||
resetBinding={resetBinding}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="bg-surface-primary-alt/50 flex shrink-0 items-center justify-between border-t border-border-light px-7 py-3.5">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasAnyCustom}
|
||||
onClick={resetAll}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12.5px] font-medium transition-all',
|
||||
hasAnyCustom
|
||||
? 'text-text-secondary hover:bg-surface-active-alt hover:text-text-primary'
|
||||
: 'cursor-not-allowed text-text-secondary opacity-40',
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
{localize('com_shortcut_reset_all')}
|
||||
</button>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-[12.5px] font-medium text-text-secondary">
|
||||
{localize('com_shortcut_show_shortcuts')}
|
||||
</span>
|
||||
<ShortcutKeyCombo display={showShortcutsDisplay} />
|
||||
</div>
|
||||
</footer>
|
||||
{hasAnyCustom && (
|
||||
<footer className="flex shrink-0 justify-end border-t border-border-light px-7 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetAll}
|
||||
className="text-[12px] text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{localize('com_shortcut_reset_all')}
|
||||
</button>
|
||||
</footer>
|
||||
)}
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default function NavToggle({
|
|||
}
|
||||
|
||||
const ariaDescription = localize(actionKey, { 0: sidebarLabel });
|
||||
const shortcutId = side === 'left' ? 'toggleSidebar' : 'toggleRightSidebar';
|
||||
const shortcutId = side === 'left' ? 'toggleSidebar' : undefined;
|
||||
const tooltipDescription = useShortcutHint(shortcutId, ariaDescription);
|
||||
const ariaKey = useShortcutAriaKey(shortcutId);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ function ShortcutKbd({ children, className = '' }: { children: ReactNode; classN
|
|||
return (
|
||||
<kbd
|
||||
className={cn(
|
||||
'inline-flex h-[26px] min-w-[26px] items-center justify-center rounded-[7px] border border-border-medium bg-surface-primary-alt px-1.5 font-sans text-[11.5px] font-semibold leading-none text-text-primary shadow-[0_1px_0_0_rgba(0,0,0,0.06),inset_0_-1px_0_0_rgba(0,0,0,0.04)] dark:shadow-[0_1px_0_0_rgba(255,255,255,0.04),inset_0_-1px_0_0_rgba(0,0,0,0.4)]',
|
||||
'inline-flex h-[22px] min-w-[22px] items-center justify-center rounded-md border border-border-light bg-surface-primary-alt px-1.5 font-sans text-[11px] font-medium leading-none text-text-primary',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ArrowRight, X } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { ShortcutBinding } from '~/utils/shortcuts';
|
||||
import {
|
||||
|
|
@ -171,6 +171,12 @@ export function RecorderPill({
|
|||
const localize = useLocalize();
|
||||
const { containerRef, previewKeys, hasConflict, showInvalid, showHint, onKeyDown, onKeyUp } =
|
||||
state;
|
||||
let stateBorder = 'border-border-medium';
|
||||
if (hasConflict) {
|
||||
stateBorder = 'border-amber-500/60';
|
||||
} else if (showInvalid) {
|
||||
stateBorder = 'animate-shortcut-shake border-red-500/60';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
|
@ -182,24 +188,13 @@ export function RecorderPill({
|
|||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
className={cn(
|
||||
'group relative flex h-[30px] items-center gap-1.5 rounded-lg px-2.5 outline-none transition-all duration-200',
|
||||
'ring-2 ring-offset-2 ring-offset-surface-primary',
|
||||
hasConflict
|
||||
? 'bg-amber-500/10 ring-amber-500/50'
|
||||
: showInvalid
|
||||
? 'animate-shortcut-shake bg-red-500/10 ring-red-500/50'
|
||||
: 'bg-blue-500/10 ring-blue-500/50',
|
||||
'flex h-[30px] items-center gap-1.5 rounded-md border bg-surface-primary px-2 outline-none transition-colors',
|
||||
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-surface-primary-alt',
|
||||
stateBorder,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'inline-block h-1.5 w-1.5 shrink-0 animate-pulse rounded-full',
|
||||
hasConflict ? 'bg-amber-500' : showInvalid ? 'bg-red-500' : 'bg-blue-500',
|
||||
)}
|
||||
/>
|
||||
{showHint ? (
|
||||
<span className="text-[11.5px] font-medium text-text-secondary">
|
||||
<span className="text-[11.5px] text-text-secondary">
|
||||
{localize('com_shortcut_recorder_placeholder')}
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -227,29 +222,26 @@ export function RecorderInfo({
|
|||
return (
|
||||
<div
|
||||
id={`${ownerId}-recorder-hint`}
|
||||
className="flex w-full items-center justify-between gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2"
|
||||
className="flex flex-wrap items-center justify-end gap-x-2 gap-y-1 pl-1 text-[11.5px]"
|
||||
>
|
||||
<span className="text-[12.5px] text-text-primary">
|
||||
<span className="font-medium text-text-secondary">
|
||||
{localize('com_shortcut_recorder_conflict_prefix')}
|
||||
</span>{' '}
|
||||
<span className="font-semibold">{conflict.conflictLabel}</span>
|
||||
<span className="text-text-secondary">
|
||||
{localize('com_shortcut_recorder_conflict_prefix')}{' '}
|
||||
<span className="font-medium text-text-primary">{conflict.conflictLabel}</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTryAgain}
|
||||
className="inline-flex items-center rounded-md px-2.5 py-1 text-[12px] font-medium text-text-secondary transition-colors hover:bg-surface-active-alt hover:text-text-primary"
|
||||
className="whitespace-nowrap rounded-md px-1.5 py-0.5 text-text-secondary transition-colors hover:text-text-primary"
|
||||
>
|
||||
{localize('com_shortcut_recorder_try_again')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSaveReplacing(conflict.binding, conflict.conflictId)}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-amber-500 px-3 py-1 text-[12px] font-semibold text-white shadow-sm transition-colors hover:bg-amber-600"
|
||||
className="whitespace-nowrap rounded-md bg-surface-tertiary px-2 py-0.5 font-medium text-text-primary transition-colors hover:bg-surface-active-alt"
|
||||
>
|
||||
{localize('com_shortcut_recorder_replace')}
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -257,21 +249,22 @@ export function RecorderInfo({
|
|||
}
|
||||
|
||||
return (
|
||||
<div id={`${ownerId}-recorder-hint`} className="flex items-center justify-end gap-2">
|
||||
{showInvalid ? (
|
||||
<span className="text-[11.5px] font-medium text-red-600 dark:text-red-400">
|
||||
{localize('com_shortcut_recorder_needs_modifier')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[11.5px] font-medium text-text-secondary">
|
||||
{localize('com_shortcut_recorder_hint')}
|
||||
</span>
|
||||
)}
|
||||
<div id={`${ownerId}-recorder-hint`} className="flex items-center justify-end gap-2 pl-1">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[11.5px]',
|
||||
showInvalid ? 'text-red-600 dark:text-red-400' : 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{showInvalid
|
||||
? localize('com_shortcut_recorder_needs_modifier')
|
||||
: localize('com_shortcut_recorder_hint')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
aria-label={localize('com_ui_cancel')}
|
||||
className="ml-1 inline-flex h-5 w-5 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-surface-active-alt hover:text-text-primary"
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded text-text-secondary transition-colors hover:bg-surface-active-alt hover:text-text-primary"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ const NavIconButton = memo(function NavIconButton({
|
|||
variant="ghost"
|
||||
aria-label={localize(link.title)}
|
||||
aria-pressed={isActive}
|
||||
data-testid={`nav-panel-${link.id}`}
|
||||
className={cn(
|
||||
'h-9 w-9 rounded-lg',
|
||||
isActive ? 'bg-surface-active-alt text-text-primary' : 'text-text-secondary',
|
||||
|
|
|
|||
|
|
@ -81,14 +81,6 @@ export const shortcutDefinitions = {
|
|||
ariaMac: 'Meta+Shift+S',
|
||||
ariaOther: 'Control+Shift+S',
|
||||
},
|
||||
toggleRightSidebar: {
|
||||
labelKey: 'com_shortcut_toggle_right_sidebar',
|
||||
groupKey: 'com_shortcut_group_navigation',
|
||||
displayMac: '⌘ ⇧ R',
|
||||
displayOther: 'Ctrl+Shift+R',
|
||||
ariaMac: 'Meta+Shift+R',
|
||||
ariaOther: 'Control+Shift+R',
|
||||
},
|
||||
openModelSelector: {
|
||||
labelKey: 'com_shortcut_open_model_selector',
|
||||
groupKey: 'com_shortcut_group_navigation',
|
||||
|
|
@ -185,6 +177,102 @@ export const shortcutDefinitions = {
|
|||
ariaMac: 'Meta+Shift+Backspace',
|
||||
ariaOther: 'Control+Shift+Backspace',
|
||||
},
|
||||
submitMessage: {
|
||||
labelKey: 'com_shortcut_submit_message',
|
||||
groupKey: 'com_shortcut_group_general',
|
||||
displayMac: '⌘ ↵',
|
||||
displayOther: 'Ctrl+Enter',
|
||||
ariaMac: 'Meta+Enter',
|
||||
ariaOther: 'Control+Enter',
|
||||
},
|
||||
bookmarkConversation: {
|
||||
labelKey: 'com_shortcut_bookmark_conversation',
|
||||
groupKey: 'com_shortcut_group_navigation',
|
||||
displayMac: '⌘ ⇧ B',
|
||||
displayOther: 'Ctrl+Shift+B',
|
||||
ariaMac: 'Meta+Shift+B',
|
||||
ariaOther: 'Control+Shift+B',
|
||||
},
|
||||
continueResponse: {
|
||||
labelKey: 'com_shortcut_continue_response',
|
||||
groupKey: 'com_shortcut_group_chat',
|
||||
displayMac: '⌘ ⇧ C',
|
||||
displayOther: 'Ctrl+Shift+C',
|
||||
ariaMac: 'Meta+Shift+C',
|
||||
ariaOther: 'Control+Shift+C',
|
||||
},
|
||||
readAloudLastResponse: {
|
||||
labelKey: 'com_shortcut_read_aloud',
|
||||
groupKey: 'com_shortcut_group_chat',
|
||||
displayMac: '⌘ ⇧ V',
|
||||
displayOther: 'Ctrl+Shift+V',
|
||||
ariaMac: 'Meta+Shift+V',
|
||||
ariaOther: 'Control+Shift+V',
|
||||
},
|
||||
openAssistants: {
|
||||
labelKey: 'com_shortcut_open_assistants',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openAgents: {
|
||||
labelKey: 'com_shortcut_open_agents',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openPrompts: {
|
||||
labelKey: 'com_shortcut_open_prompts',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openMemories: {
|
||||
labelKey: 'com_shortcut_open_memories',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openParameters: {
|
||||
labelKey: 'com_shortcut_open_parameters',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openFiles: {
|
||||
labelKey: 'com_shortcut_open_files',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openBookmarks: {
|
||||
labelKey: 'com_shortcut_open_bookmarks',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
openMCP: {
|
||||
labelKey: 'com_shortcut_open_mcp',
|
||||
groupKey: 'com_shortcut_group_panels',
|
||||
displayMac: '',
|
||||
displayOther: '',
|
||||
ariaMac: '',
|
||||
ariaOther: '',
|
||||
},
|
||||
} as const satisfies Record<string, ShortcutDefinition>;
|
||||
|
||||
export type ShortcutActionId = keyof typeof shortcutDefinitions;
|
||||
|
|
@ -196,6 +284,15 @@ export type ShortcutAction = ShortcutDefinition & {
|
|||
const shortcutActionIds = Object.keys(shortcutDefinitions) as ShortcutActionId[];
|
||||
|
||||
function getMainScrollContainer(): Element | null {
|
||||
const end = document.getElementById('messages-end');
|
||||
let node: HTMLElement | null = end?.parentElement ?? null;
|
||||
while (node) {
|
||||
const overflowY = getComputedStyle(node).overflowY;
|
||||
if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
|
||||
return node;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return document.querySelector('main[role="main"]');
|
||||
}
|
||||
|
||||
|
|
@ -304,11 +401,6 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
setSidebarExpanded((prev) => !prev);
|
||||
}, [setSidebarExpanded]);
|
||||
|
||||
const handleToggleRightSidebar = useCallback(() => {
|
||||
const btn = document.querySelector<HTMLButtonElement>('[data-testid="parameters-button"]');
|
||||
btn?.click();
|
||||
}, []);
|
||||
|
||||
const handleOpenModelSelector = useCallback(() => {
|
||||
const btn = document.querySelector<HTMLButtonElement>('[data-testid="model-selector-button"]');
|
||||
btn?.click();
|
||||
|
|
@ -469,6 +561,46 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
navigate,
|
||||
]);
|
||||
|
||||
const handleSubmitMessage = useCallback(() => {
|
||||
const btn = document.querySelector<HTMLButtonElement>('[data-testid="send-button"]');
|
||||
if (btn && !btn.disabled) {
|
||||
btn.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleContinueResponse = useCallback(() => {
|
||||
const btn = document.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="continue-generation-button"]',
|
||||
);
|
||||
btn?.click();
|
||||
}, []);
|
||||
|
||||
const handleReadAloudLastResponse = useCallback(() => {
|
||||
const btn = document.querySelector<HTMLButtonElement>('[data-testid="read-aloud-button"]');
|
||||
btn?.click();
|
||||
}, []);
|
||||
|
||||
const handleBookmarkConversation = useCallback(() => {
|
||||
const btn =
|
||||
document.querySelector<HTMLButtonElement>('[data-testid="bookmark-menu"]') ??
|
||||
document.getElementById('bookmark-menu-button');
|
||||
(btn as HTMLButtonElement | null)?.click();
|
||||
}, []);
|
||||
|
||||
const handleOpenPanel = useCallback((panelId: string) => {
|
||||
const btn = document.querySelector<HTMLButtonElement>(`[data-testid="nav-panel-${panelId}"]`);
|
||||
btn?.click();
|
||||
}, []);
|
||||
|
||||
const handleOpenAssistants = useCallback(() => handleOpenPanel('assistants'), [handleOpenPanel]);
|
||||
const handleOpenAgents = useCallback(() => handleOpenPanel('agents'), [handleOpenPanel]);
|
||||
const handleOpenPrompts = useCallback(() => handleOpenPanel('prompts'), [handleOpenPanel]);
|
||||
const handleOpenMemories = useCallback(() => handleOpenPanel('memories'), [handleOpenPanel]);
|
||||
const handleOpenParameters = useCallback(() => handleOpenPanel('parameters'), [handleOpenPanel]);
|
||||
const handleOpenFiles = useCallback(() => handleOpenPanel('files'), [handleOpenPanel]);
|
||||
const handleOpenBookmarks = useCallback(() => handleOpenPanel('bookmarks'), [handleOpenPanel]);
|
||||
const handleOpenMCP = useCallback(() => handleOpenPanel('mcp-builder'), [handleOpenPanel]);
|
||||
|
||||
const handlers = useMemo<Record<ShortcutActionId, () => void>>(
|
||||
() => ({
|
||||
showShortcuts: handleShowShortcuts,
|
||||
|
|
@ -477,7 +609,6 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
copyLastResponse: handleCopyLastResponse,
|
||||
uploadFile: handleUploadFile,
|
||||
toggleSidebar: handleToggleSidebar,
|
||||
toggleRightSidebar: handleToggleRightSidebar,
|
||||
openModelSelector: handleOpenModelSelector,
|
||||
focusSearch: handleFocusSearch,
|
||||
openSettings: handleOpenSettings,
|
||||
|
|
@ -490,6 +621,18 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
toggleTemporaryChat: handleToggleTemporaryChat,
|
||||
archiveConversation: handleArchiveConversation,
|
||||
deleteConversation: handleDeleteConversation,
|
||||
submitMessage: handleSubmitMessage,
|
||||
bookmarkConversation: handleBookmarkConversation,
|
||||
continueResponse: handleContinueResponse,
|
||||
readAloudLastResponse: handleReadAloudLastResponse,
|
||||
openAssistants: handleOpenAssistants,
|
||||
openAgents: handleOpenAgents,
|
||||
openPrompts: handleOpenPrompts,
|
||||
openMemories: handleOpenMemories,
|
||||
openParameters: handleOpenParameters,
|
||||
openFiles: handleOpenFiles,
|
||||
openBookmarks: handleOpenBookmarks,
|
||||
openMCP: handleOpenMCP,
|
||||
}),
|
||||
[
|
||||
handleShowShortcuts,
|
||||
|
|
@ -498,7 +641,6 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
handleCopyLastResponse,
|
||||
handleUploadFile,
|
||||
handleToggleSidebar,
|
||||
handleToggleRightSidebar,
|
||||
handleOpenModelSelector,
|
||||
handleFocusSearch,
|
||||
handleOpenSettings,
|
||||
|
|
@ -511,6 +653,18 @@ export function useShortcutActions(): ShortcutAction[] {
|
|||
handleToggleTemporaryChat,
|
||||
handleArchiveConversation,
|
||||
handleDeleteConversation,
|
||||
handleSubmitMessage,
|
||||
handleBookmarkConversation,
|
||||
handleContinueResponse,
|
||||
handleReadAloudLastResponse,
|
||||
handleOpenAssistants,
|
||||
handleOpenAgents,
|
||||
handleOpenPrompts,
|
||||
handleOpenMemories,
|
||||
handleOpenParameters,
|
||||
handleOpenFiles,
|
||||
handleOpenBookmarks,
|
||||
handleOpenMCP,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -674,7 +828,12 @@ export default function useKeyboardShortcuts() {
|
|||
const isEditing =
|
||||
tagName === 'INPUT' || tagName === 'TEXTAREA' || target?.isContentEditable === true;
|
||||
|
||||
const allowedWhileEditing: ShortcutActionId[] = ['focusChat', 'focusSearch', 'showShortcuts'];
|
||||
const allowedWhileEditing: ShortcutActionId[] = [
|
||||
'focusChat',
|
||||
'focusSearch',
|
||||
'showShortcuts',
|
||||
'submitMessage',
|
||||
];
|
||||
if (isEditing && !allowedWhileEditing.includes(matchedId)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -611,6 +611,8 @@
|
|||
"com_nav_user_name_display": "Display username in messages",
|
||||
"com_nav_voice_select": "Voice",
|
||||
"com_shortcut_archive_conversation": "Archive conversation",
|
||||
"com_shortcut_bookmark_conversation": "Bookmark conversation",
|
||||
"com_shortcut_continue_response": "Continue response",
|
||||
"com_shortcut_copy_last_code": "Copy last code block",
|
||||
"com_shortcut_copy_last_response": "Copy last response",
|
||||
"com_shortcut_delete_conversation": "Delete conversation",
|
||||
|
|
@ -622,9 +624,20 @@
|
|||
"com_shortcut_group_chat": "Chat",
|
||||
"com_shortcut_group_general": "General",
|
||||
"com_shortcut_group_navigation": "Navigation",
|
||||
"com_shortcut_group_panels": "Open panels",
|
||||
"com_shortcut_group_panels_hint": "Click any row to assign",
|
||||
"com_shortcut_keyboard_shortcuts": "Keyboard Shortcuts",
|
||||
"com_shortcut_not_set": "Not set",
|
||||
"com_shortcut_open_agents": "Open agents",
|
||||
"com_shortcut_open_assistants": "Open assistants",
|
||||
"com_shortcut_open_bookmarks": "Open bookmarks",
|
||||
"com_shortcut_open_files": "Open files",
|
||||
"com_shortcut_open_mcp": "Open MCP settings",
|
||||
"com_shortcut_open_memories": "Open memories",
|
||||
"com_shortcut_open_model_selector": "Open model selector",
|
||||
"com_shortcut_open_parameters": "Open parameters",
|
||||
"com_shortcut_open_prompts": "Open prompts",
|
||||
"com_shortcut_read_aloud": "Read last response aloud",
|
||||
"com_shortcut_recorder_conflict": "Already used by \"{{0}}\".",
|
||||
"com_shortcut_recorder_conflict_prefix": "Conflicts with",
|
||||
"com_shortcut_recorder_hint": "Press a combination — Esc to cancel",
|
||||
|
|
@ -633,12 +646,14 @@
|
|||
"com_shortcut_recorder_replace": "Replace",
|
||||
"com_shortcut_recorder_try_again": "Try again",
|
||||
"com_shortcut_regenerate_response": "Regenerate response",
|
||||
"com_shortcut_reset": "Reset",
|
||||
"com_shortcut_reset_all": "Reset all to defaults",
|
||||
"com_shortcut_reset_aria": "Reset shortcut for {{0}} to default",
|
||||
"com_shortcut_scroll_to_bottom": "Scroll to bottom",
|
||||
"com_shortcut_scroll_to_top": "Scroll to top",
|
||||
"com_shortcut_set": "Set shortcut",
|
||||
"com_shortcut_show_shortcuts": "Show keyboard shortcuts",
|
||||
"com_shortcut_toggle_right_sidebar": "Toggle right sidebar",
|
||||
"com_shortcut_submit_message": "Submit message",
|
||||
"com_shortcut_toggle_sidebar": "Toggle sidebar",
|
||||
"com_shortcut_upload_file": "Upload file",
|
||||
"com_show_examples": "Show Examples",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue