mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🙌 a11y: Accessibility Improvements (#4978)
* 🔃 fix: Safeguard against null token in SSE refresh token handling * 🔃 fix: Update import path for AnnounceOptions in LiveAnnouncer component * 🔃 a11y: Add aria-live attribute for accessibility in error messages * fix: prevent double screen reader notification for toast * 🔃 a11y: Enhance accessibility for main menus and buttons with ARIA roles and labels * refactor: better alt text for logo on login page #4095 * refactor: remove unused import for DropdownNoState in Voices component * fix: Focus management issue in the Export Options Modal #4100
This commit is contained in:
parent
763693cc1b
commit
0a5bc503b0
18 changed files with 102 additions and 26 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
// client/src/a11y/LiveAnnouncer.tsx
|
// client/src/a11y/LiveAnnouncer.tsx
|
||||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||||
import type { AnnounceOptions } from '~/Providers/AnnouncerContext';
|
import type { AnnounceOptions } from '~/common';
|
||||||
import AnnouncerContext from '~/Providers/AnnouncerContext';
|
import AnnouncerContext from '~/Providers/AnnouncerContext';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import Announcer from './Announcer';
|
import Announcer from './Announcer';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import type { Option } from '~/common';
|
import type { Option } from '~/common';
|
||||||
import DropdownNoState from '~/components/ui/DropdownNoState';
|
|
||||||
import { useLocalize, useTTSBrowser, useTTSEdge, useTTSExternal } from '~/hooks';
|
import { useLocalize, useTTSBrowser, useTTSEdge, useTTSExternal } from '~/hooks';
|
||||||
import { Dropdown } from '~/components/ui';
|
import { Dropdown } from '~/components/ui';
|
||||||
import { logger } from '~/utils';
|
import { logger } from '~/utils';
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ import Footer from './Footer';
|
||||||
const ErrorRender = ({ children }: { children: React.ReactNode }) => (
|
const ErrorRender = ({ children }: { children: React.ReactNode }) => (
|
||||||
<div className="mt-16 flex justify-center">
|
<div className="mt-16 flex justify-center">
|
||||||
<div
|
<div
|
||||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
|
||||||
role="alert"
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,8 +37,9 @@ function AuthLayout({
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
const hasStartupConfigError = startupConfigError !== null && startupConfigError !== undefined;
|
||||||
const DisplayError = () => {
|
const DisplayError = () => {
|
||||||
if (startupConfigError !== null && startupConfigError !== undefined) {
|
if (hasStartupConfigError) {
|
||||||
return <ErrorRender>{localize('com_auth_error_login_server')}</ErrorRender>;
|
return <ErrorRender>{localize('com_auth_error_login_server')}</ErrorRender>;
|
||||||
} else if (error === 'com_auth_error_invalid_reset_token') {
|
} else if (error === 'com_auth_error_invalid_reset_token') {
|
||||||
return (
|
return (
|
||||||
|
|
@ -49,7 +51,7 @@ function AuthLayout({
|
||||||
{localize('com_auth_to_try_again')}
|
{localize('com_auth_to_try_again')}
|
||||||
</ErrorRender>
|
</ErrorRender>
|
||||||
);
|
);
|
||||||
} else if (error) {
|
} else if (error != null && error) {
|
||||||
return <ErrorRender>{localize(error)}</ErrorRender>;
|
return <ErrorRender>{localize(error)}</ErrorRender>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -60,7 +62,11 @@ function AuthLayout({
|
||||||
<Banner />
|
<Banner />
|
||||||
<BlinkAnimation active={isFetching}>
|
<BlinkAnimation active={isFetching}>
|
||||||
<div className="mt-6 h-10 w-full bg-cover">
|
<div className="mt-6 h-10 w-full bg-cover">
|
||||||
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
|
<img
|
||||||
|
src="/assets/logo.svg"
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
alt={localize('com_ui_logo', startupConfig?.appTitle ?? 'LibreChat')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BlinkAnimation>
|
</BlinkAnimation>
|
||||||
<DisplayError />
|
<DisplayError />
|
||||||
|
|
@ -70,7 +76,7 @@ function AuthLayout({
|
||||||
|
|
||||||
<div className="flex flex-grow items-center justify-center">
|
<div className="flex flex-grow items-center justify-center">
|
||||||
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
|
||||||
{!startupConfigError && !isFetching && (
|
{!hasStartupConfigError && !isFetching && (
|
||||||
<h1
|
<h1
|
||||||
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
|
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
|
||||||
style={{ userSelect: 'none' }}
|
style={{ userSelect: 'none' }}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
|
export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
|
||||||
<div
|
<div
|
||||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
|
||||||
role="alert"
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useState, useId } from 'react';
|
import { useState, useId, useRef } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { Upload, Share2 } from 'lucide-react';
|
import { Upload, Share2 } from 'lucide-react';
|
||||||
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
||||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||||
|
import ExportModal from '~/components/Nav/ExportConversation/ExportModal';
|
||||||
import { DropdownPopup } from '~/components/ui';
|
import { DropdownPopup } from '~/components/ui';
|
||||||
import { ExportModal } from '../Nav';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function ExportAndShareMenu({
|
export default function ExportAndShareMenu({
|
||||||
|
|
@ -19,6 +19,7 @@ export default function ExportAndShareMenu({
|
||||||
const [showShareDialog, setShowShareDialog] = useState(false);
|
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||||
|
|
||||||
const menuId = useId();
|
const menuId = useId();
|
||||||
|
const exportButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||||
|
|
||||||
|
|
@ -68,6 +69,7 @@ export default function ExportAndShareMenu({
|
||||||
setIsOpen={setIsPopoverActive}
|
setIsOpen={setIsPopoverActive}
|
||||||
trigger={
|
trigger={
|
||||||
<Ariakit.MenuButton
|
<Ariakit.MenuButton
|
||||||
|
ref={exportButtonRef}
|
||||||
id="export-menu-button"
|
id="export-menu-button"
|
||||||
aria-label="Export options"
|
aria-label="Export options"
|
||||||
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"
|
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"
|
||||||
|
|
@ -91,7 +93,8 @@ export default function ExportAndShareMenu({
|
||||||
open={showExports}
|
open={showExports}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
aria-label="Export conversation modal"
|
triggerRef={exportButtonRef}
|
||||||
|
aria-label={localize('com_ui_export_convo_modal')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type { FC } from 'react';
|
||||||
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
|
||||||
import { mapEndpoints, getEntity } from '~/utils';
|
import { mapEndpoints, getEntity } from '~/utils';
|
||||||
import EndpointItems from './Endpoints/MenuItems';
|
import EndpointItems from './Endpoints/MenuItems';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import TitleButton from './UI/TitleButton';
|
import TitleButton from './UI/TitleButton';
|
||||||
|
|
||||||
const EndpointsMenu: FC = () => {
|
const EndpointsMenu: FC = () => {
|
||||||
|
|
@ -12,6 +13,7 @@ const EndpointsMenu: FC = () => {
|
||||||
select: mapEndpoints,
|
select: mapEndpoints,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const localize = useLocalize();
|
||||||
const agentsMap = useAgentsMapContext();
|
const agentsMap = useAgentsMapContext();
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
|
|
@ -51,6 +53,9 @@ const EndpointsMenu: FC = () => {
|
||||||
<Content
|
<Content
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
role="listbox"
|
||||||
|
id="llm-endpoint-menu"
|
||||||
|
aria-label={localize('com_ui_endpoints_available')}
|
||||||
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-border-light bg-header-primary text-text-primary shadow-lg lg:max-h-[75vh]"
|
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-border-light bg-header-primary text-text-primary shadow-lg lg:max-h-[75vh]"
|
||||||
>
|
>
|
||||||
<EndpointItems endpoints={endpoints} selected={endpoint} />
|
<EndpointItems endpoints={endpoints} selected={endpoint} />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { Trigger } from '@radix-ui/react-popover';
|
import { Trigger } from '@radix-ui/react-popover';
|
||||||
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
import type { TModelSpec, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
@ -20,6 +21,8 @@ export default function MenuButton({
|
||||||
endpointsConfig: TEndpointsConfig;
|
endpointsConfig: TEndpointsConfig;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -28,7 +31,13 @@ export default function MenuButton({
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Select ${primaryText}`}
|
aria-label={localize('com_ui_llm_menu')}
|
||||||
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls="llm-menu"
|
||||||
|
aria-activedescendant={isExpanded ? 'selected-llm' : undefined}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
{selected && selected.showIconInHeader === true && (
|
{selected && selected.showIconInHeader === true && (
|
||||||
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
|
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
role="menuitem"
|
id={selected ? 'selected-llm' : undefined}
|
||||||
|
role="option"
|
||||||
|
aria-selected={selected}
|
||||||
className="group m-1.5 flex cursor-pointer gap-2 rounded px-1 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
className="group m-1.5 flex cursor-pointer gap-2 rounded px-1 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ import MenuSeparator from '~/components/Chat/Menus/UI/MenuSeparator';
|
||||||
import ModelSpec from './ModelSpec';
|
import ModelSpec from './ModelSpec';
|
||||||
|
|
||||||
const ModelSpecs: FC<{
|
const ModelSpecs: FC<{
|
||||||
specs?: TModelSpec[];
|
specs?: Array<TModelSpec | undefined>;
|
||||||
selected?: TModelSpec;
|
selected?: TModelSpec;
|
||||||
setSelected?: (spec: TModelSpec) => void;
|
setSelected?: (spec: TModelSpec) => void;
|
||||||
endpointsConfig: TEndpointsConfig;
|
endpointsConfig: TEndpointsConfig;
|
||||||
}> = ({ specs = [], selected, setSelected = () => ({}), endpointsConfig }) => {
|
}> = ({ specs = [], selected, setSelected = () => ({}), endpointsConfig }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{specs &&
|
{specs.length &&
|
||||||
specs.map((spec, i) => {
|
specs.map((spec, i) => {
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import type { TModelSpec, TConversation, TEndpointsConfig } from 'librechat-data-provider';
|
import type { TModelSpec, TConversation, TEndpointsConfig } from 'librechat-data-provider';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
|
import { useDefaultConvo, useNewConvo, useLocalize } from '~/hooks';
|
||||||
import { getConvoSwitchLogic, getModelSpecIconURL } from '~/utils';
|
import { getConvoSwitchLogic, getModelSpecIconURL } from '~/utils';
|
||||||
import { useDefaultConvo, useNewConvo } from '~/hooks';
|
|
||||||
import MenuButton from './MenuButton';
|
import MenuButton from './MenuButton';
|
||||||
import ModelSpecs from './ModelSpecs';
|
import ModelSpecs from './ModelSpecs';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -15,6 +15,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const { newConversation } = useNewConvo();
|
const { newConversation } = useNewConvo();
|
||||||
|
|
||||||
|
const localize = useLocalize();
|
||||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
const modularChat = useRecoilValue(store.modularChat);
|
const modularChat = useRecoilValue(store.modularChat);
|
||||||
const getDefaultConversation = useDefaultConvo();
|
const getDefaultConversation = useDefaultConvo();
|
||||||
|
|
@ -111,6 +112,9 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
|
||||||
<Content
|
<Content
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
id="llm-menu"
|
||||||
|
role="listbox"
|
||||||
|
aria-label={localize('com_ui_llms_available')}
|
||||||
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
|
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
|
||||||
>
|
>
|
||||||
<ModelSpecs
|
<ModelSpecs
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ const MenuItem: FC<MenuItemProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="menuitem"
|
id={selected ? 'selected-endpoint' : undefined}
|
||||||
|
role="option"
|
||||||
|
aria-selected={selected}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
data-testid="chat-menu-item"
|
data-testid="chat-menu-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { Trigger } from '@radix-ui/react-popover';
|
import { Trigger } from '@radix-ui/react-popover';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
|
||||||
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
|
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
|
||||||
|
const localize = useLocalize();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<button
|
<button
|
||||||
className="group flex cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 text-lg font-medium transition-colors duration-200 hover:bg-surface-hover radix-state-open:bg-surface-hover"
|
className="group flex cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 text-lg font-medium transition-colors duration-200 hover:bg-surface-hover radix-state-open:bg-surface-hover"
|
||||||
aria-label={`Select ${primaryText}`}
|
aria-label={localize('com_ui_endpoint_menu')}
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
aria-controls="radix-:r6:"
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-controls="llm-endpoint-menu"
|
||||||
|
aria-activedescendant={isExpanded ? 'selected-endpoint' : undefined}
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ export const ErrorMessage = ({
|
||||||
return (
|
return (
|
||||||
<Container message={message}>
|
<Container message={message}>
|
||||||
<div
|
<div
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
|
'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
|
||||||
className,
|
className,
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ export default function ExportModal({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
conversation,
|
conversation,
|
||||||
|
triggerRef,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
conversation: TConversation | null;
|
conversation: TConversation | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
triggerRef: React.RefObject<HTMLButtonElement>;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
|
@ -31,6 +33,12 @@ export default function ExportModal({
|
||||||
{ value: 'csv', label: 'csv (.csv)' },
|
{ value: 'csv', label: 'csv (.csv)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open && triggerRef.current) {
|
||||||
|
triggerRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [open, triggerRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFileName(filenamify(String(conversation?.title ?? 'file')));
|
setFileName(filenamify(String(conversation?.title ?? 'file')));
|
||||||
setType('screenshot');
|
setType('screenshot');
|
||||||
|
|
@ -61,7 +69,7 @@ export default function ExportModal({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange} triggerRef={triggerRef}>
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title={localize('com_nav_export_conversation')}
|
title={localize('com_nav_export_conversation')}
|
||||||
className="max-w-full sm:max-w-2xl"
|
className="max-w-full sm:max-w-2xl"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,28 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
interface OGDialogProps extends DialogPrimitive.DialogProps {
|
||||||
|
triggerRef?: React.RefObject<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dialog = React.forwardRef<HTMLDivElement, OGDialogProps>(
|
||||||
|
({ children, triggerRef, onOpenChange, ...props }) => {
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open && triggerRef?.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
triggerRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
onOpenChange?.(open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Root {...props} onOpenChange={handleOpenChange}>
|
||||||
|
{children}
|
||||||
|
</DialogPrimitive.Root>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export default function Toast() {
|
||||||
className={`alert-root pointer-events-auto inline-flex flex-row gap-2 rounded-md border px-3 py-2 text-white ${
|
className={`alert-root pointer-events-auto inline-flex flex-row gap-2 rounded-md border px-3 py-2 text-white ${
|
||||||
severityClassName[toast.severity]
|
severityClassName[toast.severity]
|
||||||
}`}
|
}`}
|
||||||
role="alert"
|
|
||||||
>
|
>
|
||||||
{toast.showIcon && (
|
{toast.showIcon && (
|
||||||
<div className="mt-1 flex-shrink-0 flex-grow-0">
|
<div className="mt-1 flex-shrink-0 flex-grow-0">
|
||||||
|
|
|
||||||
|
|
@ -190,11 +190,16 @@ export default function useSSE(
|
||||||
/* token expired, refresh and retry */
|
/* token expired, refresh and retry */
|
||||||
try {
|
try {
|
||||||
const refreshResponse = await request.refreshToken();
|
const refreshResponse = await request.refreshToken();
|
||||||
|
const token = refreshResponse?.token ?? '';
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token refresh failed.');
|
||||||
|
}
|
||||||
sse.headers = {
|
sse.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${refreshResponse.token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
request.dispatchTokenUpdatedEvent(refreshResponse.token);
|
|
||||||
|
request.dispatchTokenUpdatedEvent(token);
|
||||||
sse.stream();
|
sse.stream();
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,11 @@ export default {
|
||||||
com_ui_input: 'Input',
|
com_ui_input: 'Input',
|
||||||
com_ui_close: 'Close',
|
com_ui_close: 'Close',
|
||||||
com_ui_endpoint: 'Endpoint',
|
com_ui_endpoint: 'Endpoint',
|
||||||
|
com_ui_endpoint_menu: 'LLM Endpoint Menu',
|
||||||
|
com_ui_endpoints_available: 'Available Endpoints',
|
||||||
|
com_ui_export_convo_modal: 'Export Conversation Modal',
|
||||||
|
com_ui_llms_available: 'Available LLMs',
|
||||||
|
com_ui_llm_menu: 'LLM Menu',
|
||||||
com_ui_provider: 'Provider',
|
com_ui_provider: 'Provider',
|
||||||
com_ui_model: 'Model',
|
com_ui_model: 'Model',
|
||||||
com_ui_region: 'Region',
|
com_ui_region: 'Region',
|
||||||
|
|
@ -297,6 +302,7 @@ export default {
|
||||||
com_ui_edit: 'Edit',
|
com_ui_edit: 'Edit',
|
||||||
com_ui_loading: 'Loading...',
|
com_ui_loading: 'Loading...',
|
||||||
com_ui_success: 'Success',
|
com_ui_success: 'Success',
|
||||||
|
com_ui_logo: '{0} Logo',
|
||||||
com_ui_all: 'all',
|
com_ui_all: 'all',
|
||||||
com_ui_all_proper: 'All',
|
com_ui_all_proper: 'All',
|
||||||
com_ui_clear: 'Clear',
|
com_ui_clear: 'Clear',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue