mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
👐 style: Improve a11y/theming for Settings Dialog, Dropdown Menus; fix: SearchBar focus issues (#4091)
* fix: cursor pointer not applying correct in the root component * fix: add cursor-not-allowed to disabled state in SendButton component * feat: update Dropdown to ariakit and changed LLM error's style * feat: switched to ariakit's Dropdown and style improvements * feat: archive updates * refactor: delete conversations in archive * refactor: settings * add cool settings animation * a11y: settings update * style: update settings * style: settings account settings menu; a11y(AccountSettings): switched to AriaKit * a11y: account settings update * style: update my files dialog * fix: tests * chore: remove console.log() --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
eba2c9a032
commit
2d62eca612
58 changed files with 1054 additions and 824 deletions
|
|
@ -50,7 +50,7 @@ const DeleteBookmarkButton: FC<{
|
|||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
tabIndex={tabIndex}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const EditBookmarkButton: FC<{
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<EditIcon />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useRef, useMemo } from 'react';
|
||||
import { memo, useRef, useMemo, useEffect } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import {
|
||||
supportsFiles,
|
||||
|
|
@ -44,6 +44,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
|
||||
const isSearching = useRecoilValue(store.isSearching);
|
||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
|
||||
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
||||
|
|
@ -123,6 +124,12 @@ const ChatForm = ({ index = 0 }) => {
|
|||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSearching && textAreaRef.current && !disableInputs) {
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
}, [isSearching, disableInputs]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={methods.handleSubmit((data) => submitMessage(data))}
|
||||
|
|
@ -164,9 +171,6 @@ const ChatForm = ({ index = 0 }) => {
|
|||
{endpoint && (
|
||||
<TextareaAutosize
|
||||
{...registerProps}
|
||||
// TODO: remove autofocus due to a11y issues
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
ref={(e) => {
|
||||
ref(e);
|
||||
textAreaRef.current = e;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { FileSources, FileContext } from 'librechat-data-provider';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
|
||||
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '~/components';
|
||||
import { useGetFiles } from '~/data-provider';
|
||||
import { DataTable, columns } from './Table';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function Files({ open, onOpenChange }) {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -19,20 +18,16 @@ export default function Files({ open, onOpenChange }) {
|
|||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className={cn('w-11/12 overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white')}
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent
|
||||
title={localize('com_nav_my_files')}
|
||||
className="w-11/12 overflow-x-auto bg-background text-text-primary shadow-2xl"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
{localize('com_nav_my_files')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="overflow-x-auto p-0 sm:p-6 sm:pt-4">
|
||||
<DataTable columns={columns} data={files} />
|
||||
<div className="mt-5 sm:mt-4" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_nav_my_files')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<DataTable columns={columns} data={files} />
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const SubmitButton = React.memo(
|
|||
id="send-button"
|
||||
disabled={props.disabled}
|
||||
className={cn(
|
||||
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
|
||||
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:cursor-not-allowed disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
|
||||
props.isRTL
|
||||
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
|
||||
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const ErrorMessage = ({
|
|||
<Container message={message}>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border border-red-500 bg-red-500/10 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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useDeleteConversationMutation } from '~/data-provider';
|
||||
import { OGDialog, OGDialogTrigger, Label, TooltipAnchor } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { OGDialog, Label } from '~/components';
|
||||
|
||||
type DeleteButtonProps = {
|
||||
conversationId: string;
|
||||
|
|
@ -17,19 +16,21 @@ type DeleteButtonProps = {
|
|||
setShowDeleteDialog?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export default function DeleteButton({
|
||||
export function DeleteConversationDialog({
|
||||
conversationId,
|
||||
retainView,
|
||||
title,
|
||||
showDeleteDialog,
|
||||
setShowDeleteDialog,
|
||||
}: DeleteButtonProps) {
|
||||
}: {
|
||||
conversationId: string;
|
||||
retainView: () => void;
|
||||
title: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { newConversation } = useNewConvo();
|
||||
const { conversationId: currentConvoId } = useParams();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const deleteConvoMutation = useDeleteConversationMutation({
|
||||
onSuccess: () => {
|
||||
if (currentConvoId === conversationId || currentConvoId === 'new') {
|
||||
|
|
@ -47,7 +48,7 @@ export default function DeleteButton({
|
|||
deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' });
|
||||
}, [conversationId, deleteConvoMutation, queryClient]);
|
||||
|
||||
const dialogContent = (
|
||||
return (
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_conversation')}
|
||||
|
|
@ -71,25 +72,26 @@ export default function DeleteButton({
|
|||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showDeleteDialog !== undefined && setShowDeleteDialog !== undefined) {
|
||||
return (
|
||||
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
{dialogContent}
|
||||
</OGDialog>
|
||||
);
|
||||
export default function DeleteButton({
|
||||
conversationId,
|
||||
retainView,
|
||||
title,
|
||||
showDeleteDialog,
|
||||
setShowDeleteDialog,
|
||||
}: DeleteButtonProps) {
|
||||
if (showDeleteDialog === undefined && setShowDeleteDialog === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<TooltipAnchor description={localize('com_ui_delete')}>
|
||||
<OGDialogTrigger asChild>
|
||||
<button>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
</TooltipAnchor>
|
||||
{dialogContent}
|
||||
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<DeleteConversationDialog
|
||||
conversationId={conversationId}
|
||||
retainView={retainView}
|
||||
title={title}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { default as DeleteButton } from './DeleteButton';
|
||||
export * from './DeleteButton';
|
||||
export { default as ShareButton } from './ShareButton';
|
||||
export { default as SharedLinkButton } from './SharedLinkButton';
|
||||
export { default as ConvoOptions } from './ConvoOptions';
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import { FileText } from 'lucide-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import * as Select from '@ariakit/react/select';
|
||||
import { Fragment, useState, memo } from 'react';
|
||||
import { Menu, MenuItem, MenuButton, MenuItems, Transition } from '@headlessui/react';
|
||||
import { FileText, LogOut } from 'lucide-react';
|
||||
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import { LinkIcon, GearIcon, DropdownMenuSeparator } from '~/components';
|
||||
import FilesView from '~/components/Chat/Input/Files/FilesView';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||
import { LinkIcon, GearIcon } from '~/components';
|
||||
import { UserIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Settings from './Settings';
|
||||
import NavLink from './NavLink';
|
||||
import Logout from './Logout';
|
||||
import { cn } from '~/utils/';
|
||||
import store from '~/store';
|
||||
|
||||
function AccountSettings() {
|
||||
const localize = useLocalize();
|
||||
const { user, isAuthenticated } = useAuthContext();
|
||||
const { user, isAuthenticated, logout } = useAuthContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const balanceQuery = useGetUserBalance({
|
||||
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
||||
|
|
@ -29,115 +26,105 @@ function AccountSettings() {
|
|||
const name = user?.avatar ?? user?.username ?? '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu as="div" className="group relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
aria-label={localize('com_nav_account_settings')}
|
||||
className={cn(
|
||||
'group-ui-open:bg-surface-tertiary duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-surface-secondary',
|
||||
open ? 'bg-surface-secondary' : '',
|
||||
)}
|
||||
data-testid="nav-user"
|
||||
>
|
||||
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||
<div className="relative flex">
|
||||
{name.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
) : (
|
||||
<img className="rounded-full" src={user?.avatar ?? avatarSrc} alt="avatar" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Select.SelectProvider>
|
||||
<Select.Select
|
||||
aria-label={localize('com_nav_account_settings')}
|
||||
data-testid="nav-user"
|
||||
className="duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
|
||||
>
|
||||
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||
<div className="relative flex">
|
||||
{name.length === 0 ? (
|
||||
<div
|
||||
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-text-primary"
|
||||
style={{ marginTop: '0', marginLeft: '0' }}
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{user?.name ?? user?.username ?? localize('com_nav_user')}
|
||||
<UserIcon />
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100 transform"
|
||||
enterFrom="translate-y-2 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
leave="transition ease-in duration-100 transform"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-2 opacity-0"
|
||||
>
|
||||
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-border-medium bg-header-primary p-1.5 opacity-100 shadow-lg outline-none">
|
||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
|
||||
{user?.email ?? localize('com_nav_user')}
|
||||
</div>
|
||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
||||
{startupConfig?.checkBalance === true &&
|
||||
balanceQuery.data != null &&
|
||||
!isNaN(parseFloat(balanceQuery.data)) && (
|
||||
<>
|
||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
|
||||
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
||||
</div>
|
||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
||||
</>
|
||||
)}
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<NavLink
|
||||
className={focus ? 'bg-surface-hover' : ''}
|
||||
svg={() => <FileText className="icon-md" />}
|
||||
text={localize('com_nav_my_files')}
|
||||
clickHandler={() => setShowFiles(true)}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
{startupConfig?.helpAndFaqURL !== '/' && (
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<NavLink
|
||||
className={focus ? 'bg-surface-hover' : ''}
|
||||
svg={() => <LinkIcon />}
|
||||
text={localize('com_nav_help_faq')}
|
||||
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<NavLink
|
||||
className={focus ? 'bg-surface-hover' : ''}
|
||||
svg={() => <GearIcon className="icon-md" />}
|
||||
text={localize('com_nav_settings')}
|
||||
clickHandler={() => {
|
||||
setTimeout(() => setShowSettings(true), 50);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
||||
<MenuItem>
|
||||
{({ focus }) => <Logout className={focus ? 'bg-surface-hover' : ''} />}
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
) : (
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={user?.avatar ?? avatarSrc}
|
||||
alt={`${name}'s avatar`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-text-primary"
|
||||
style={{ marginTop: '0', marginLeft: '0' }}
|
||||
>
|
||||
{user?.name ?? user?.username ?? localize('com_nav_user')}
|
||||
</div>
|
||||
</Select.Select>
|
||||
<Select.SelectPopover
|
||||
className="popover-ui w-[235px]"
|
||||
style={{
|
||||
transformOrigin: 'bottom',
|
||||
marginRight: '0px',
|
||||
translate: '0px',
|
||||
}}
|
||||
>
|
||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||
{user?.email ?? localize('com_nav_user')}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
{startupConfig?.checkBalance === true &&
|
||||
balanceQuery.data != null &&
|
||||
!isNaN(parseFloat(balanceQuery.data)) && (
|
||||
<>
|
||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<Select.SelectItem
|
||||
value=""
|
||||
onClick={() => setShowFiles(true)}
|
||||
className="select-item text-sm"
|
||||
>
|
||||
<FileText className="icon-md" aria-hidden="true" />
|
||||
{localize('com_nav_my_files')}
|
||||
</Select.SelectItem>
|
||||
{startupConfig?.helpAndFaqURL !== '/' && (
|
||||
<Select.SelectItem
|
||||
value=""
|
||||
onClick={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
|
||||
className="select-item text-sm"
|
||||
>
|
||||
<LinkIcon aria-hidden="true" />
|
||||
{localize('com_nav_help_faq')}
|
||||
</Select.SelectItem>
|
||||
)}
|
||||
<Select.SelectItem
|
||||
value=""
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="select-item text-sm"
|
||||
>
|
||||
<GearIcon className="icon-md" aria-hidden="true" />
|
||||
{localize('com_nav_settings')}
|
||||
</Select.SelectItem>
|
||||
<DropdownMenuSeparator />
|
||||
<Select.SelectItem
|
||||
aria-selected={true}
|
||||
onClick={() => logout()}
|
||||
value="logout"
|
||||
className="select-item text-sm"
|
||||
>
|
||||
<LogOut className="icon-md" />
|
||||
{localize('com_nav_log_out')}
|
||||
</Select.SelectItem>
|
||||
</Select.SelectPopover>
|
||||
{showFiles && <FilesView open={showFiles} onOpenChange={setShowFiles} />}
|
||||
{showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />}
|
||||
</>
|
||||
</Select.SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { LogOutIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const Logout = forwardRef<HTMLButtonElement, { className?: string }>((props, ref) => {
|
||||
const { logout } = useAuthContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group group flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-hover data-[focus]:bg-surface-hover',
|
||||
props.className ?? '',
|
||||
)}
|
||||
onClick={() => logout()}
|
||||
>
|
||||
<LogOutIcon />
|
||||
{localize('com_nav_log_out')}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export default Logout;
|
||||
|
|
@ -16,7 +16,7 @@ const NavLink: FC<Props> = forwardRef<HTMLButtonElement, Props>((props, ref) =>
|
|||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
} = {
|
||||
className: cn(
|
||||
'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary hover:bg-surface-hover',
|
||||
'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary',
|
||||
className,
|
||||
{
|
||||
'opacity-50 pointer-events-none': disabled,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default function NavToggle({
|
|||
description={
|
||||
navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar')
|
||||
}
|
||||
className="flex cursor-pointer items-center justify-center"
|
||||
className="flex items-center justify-center"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className="" data-state="closed">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
|||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { NewChatIcon } from '~/components/svg';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -96,15 +95,7 @@ export default function NewChat({
|
|||
</div>
|
||||
<div className="flex gap-3">
|
||||
<span className="flex items-center" data-state="closed">
|
||||
<TooltipAnchor
|
||||
side="right"
|
||||
id="nav-new-chat-btn"
|
||||
aria-label="nav-new-chat-btn"
|
||||
description={localize('com_ui_new_chat')}
|
||||
className="text-text-primary"
|
||||
>
|
||||
<NewChatIcon className="size-5" />
|
||||
</TooltipAnchor>
|
||||
<NewChatIcon className="size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
const setIsSearching = useSetRecoilState(store.isSearching);
|
||||
const localize = useLocalize();
|
||||
|
||||
const clearText = useCallback(() => {
|
||||
|
|
@ -47,6 +48,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
},
|
||||
[queryClient, clearConvoState, setSearchQuery],
|
||||
);
|
||||
|
||||
// TODO: make the debounce time configurable via yaml
|
||||
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
|
||||
|
||||
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
|
|
@ -54,6 +57,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
setShowClearIcon(value.length > 0);
|
||||
setText(value);
|
||||
debouncedSendRequest(value);
|
||||
setIsSearching(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -78,6 +82,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
aria-label={localize('com_nav_search_placeholder')}
|
||||
placeholder={localize('com_nav_search_placeholder')}
|
||||
onKeyUp={handleKeyUp}
|
||||
onFocus={() => setIsSearching(true)}
|
||||
onBlur={() => setIsSearching(true)}
|
||||
autoComplete="off"
|
||||
dir="auto"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { MessageSquare, Command } from 'lucide-react';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
|
|
@ -12,7 +12,8 @@ import { cn } from '~/utils';
|
|||
export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const localize = useLocalize();
|
||||
const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL);
|
||||
const [activeTab, setActiveTab] = useState(SettingsTabValues.GENERAL);
|
||||
const tabRefs = useRef({});
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
const tabs = [
|
||||
|
|
@ -28,12 +29,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
|
||||
break;
|
||||
|
|
@ -48,6 +47,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value as SettingsTabValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={open}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
|
||||
|
|
@ -55,7 +58,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
enter="ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
|
|
@ -70,15 +73,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 flex w-screen items-center justify-center p-4',
|
||||
isSmallScreen ? '' : '',
|
||||
)}
|
||||
>
|
||||
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
|
||||
<DialogPanel
|
||||
className={cn(
|
||||
'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]',
|
||||
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
|
||||
)}
|
||||
>
|
||||
<DialogTitle
|
||||
|
|
@ -111,18 +109,18 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</DialogTitle>
|
||||
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
|
||||
<div className="max-h-[550px] overflow-auto px-6 md:max-h-[400px] md:min-h-[400px] md:w-[680px]">
|
||||
<Tabs.Root
|
||||
value={activeTab}
|
||||
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
|
||||
onValueChange={handleTabChange}
|
||||
className="flex flex-col gap-10 md:flex-row"
|
||||
orientation="horizontal"
|
||||
orientation="vertical"
|
||||
>
|
||||
<Tabs.List
|
||||
aria-label="Settings"
|
||||
className={cn(
|
||||
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
||||
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
|
||||
'min-w-auto max-w-auto relative -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
||||
isSmallScreen ? 'flex-row rounded-xl bg-surface-secondary' : '',
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
|
|
@ -166,19 +164,20 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
<Tabs.Trigger
|
||||
key={value}
|
||||
className={cn(
|
||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
|
||||
'group relative z-10 m-1 flex items-center justify-start gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out',
|
||||
isSmallScreen
|
||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
||||
: 'bg-surface-tertiary-alt',
|
||||
? 'flex-1 justify-center text-nowrap rounded-xl p-1 px-3 text-sm text-text-secondary radix-state-active:bg-surface-hover radix-state-active:text-text-primary'
|
||||
: 'rounded-md bg-transparent text-text-primary radix-state-active:bg-surface-tertiary',
|
||||
)}
|
||||
value={value}
|
||||
ref={(el) => (tabRefs.current[value] = el)}
|
||||
>
|
||||
{icon}
|
||||
{localize(label)}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
||||
<div className="overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
|
||||
<Tabs.Content value={SettingsTabValues.GENERAL}>
|
||||
<General />
|
||||
</Tabs.Content>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import CodeArtifacts from './CodeArtifacts';
|
|||
function Beta() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<CodeArtifacts />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,26 +11,26 @@ import SaveDraft from './SaveDraft';
|
|||
function Chat() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-1 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-light pb-3 last-of-type:border-b-0">
|
||||
<FontSizeSelector />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<ChatDirection />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<SendMessageKeyEnter />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<ShowCodeSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<SaveDraft />
|
||||
</div>
|
||||
<ForkSettings />
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<ModularChat />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<LaTeXParsing />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Button } from '~/components';
|
||||
import store from '~/store';
|
||||
|
||||
const ChatDirection = () => {
|
||||
|
|
@ -16,12 +17,11 @@ const ChatDirection = () => {
|
|||
<div className="flex items-center space-x-2">
|
||||
<span id="chat-direction-label">{localize('com_nav_chat_direction')}</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label="Toggle chat direction"
|
||||
onClick={toggleChatDirection}
|
||||
data-testid="chatDirection"
|
||||
className="btn btn-neutral relative ring-ring-primary"
|
||||
aria-labelledby="chat-direction-label chat-direction-status"
|
||||
aria-pressed={direction === 'RTL'}
|
||||
>
|
||||
<span aria-hidden="true">{direction.toLowerCase()}</span>
|
||||
<span id="chat-direction-status" className="sr-only">
|
||||
|
|
@ -29,7 +29,7 @@ const ChatDirection = () => {
|
|||
? localize('chat_direction_left_to_right')
|
||||
: localize('chat_direction_right_to_left')}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const ForkSettings = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{localize('com_ui_fork_change_default')}</div>
|
||||
|
|
@ -30,12 +30,11 @@ export const ForkSettings = () => {
|
|||
onChange={setForkSetting}
|
||||
options={forkOptions}
|
||||
sizeClasses="w-[200px]"
|
||||
anchor="bottom start"
|
||||
testId="fork-setting-dropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div> {localize('com_ui_fork_default')} </div>
|
||||
<Switch
|
||||
|
|
@ -47,7 +46,7 @@ export const ForkSettings = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{localize('com_ui_fork_split_target_setting')}</div>
|
||||
|
|
|
|||
|
|
@ -28,16 +28,16 @@ function Commands() {
|
|||
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<AtCommandSwitch />
|
||||
</div>
|
||||
{hasAccessToMultiConvo === true && (
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<PlusCommandSwitch />
|
||||
</div>
|
||||
)}
|
||||
{hasAccessToPrompts === true && (
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<SlashCommandSwitch />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,45 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link as LinkIcon } from 'lucide-react';
|
||||
import { Link as LinkIcon, TrashIcon } from 'lucide-react';
|
||||
import type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider';
|
||||
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
|
||||
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
|
||||
import { Spinner, TooltipAnchor, TrashIcon } from '~/components';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TooltipAnchor,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogTrigger,
|
||||
} from '~/components';
|
||||
|
||||
function SharedLinkDeleteButton({
|
||||
shareId,
|
||||
setIsDeleting,
|
||||
}: {
|
||||
shareId: string;
|
||||
setIsDeleting: (isDeleting: boolean) => void;
|
||||
}) {
|
||||
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const localize = useLocalize();
|
||||
|
||||
const { showToast } = useToastContext();
|
||||
const mutation = useDeleteSharedLinkMutation({
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_share_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
setIsDeleting(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const confirmDelete = async (shareId: TSharedLink['shareId']) => {
|
||||
if (mutation.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -38,67 +47,78 @@ function SharedLinkDeleteButton({
|
|||
await mutation.mutateAsync({ shareId });
|
||||
setIsDeleting(false);
|
||||
};
|
||||
return (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
id="delete-shared-link"
|
||||
aria-label="Delete shared link"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</TooltipAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={sharedLink.conversationId}
|
||||
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
|
||||
>
|
||||
<td
|
||||
className={cn(
|
||||
'flex items-center py-3 text-blue-800/70 dark:text-blue-500',
|
||||
isDeleting && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
<Link to={`/share/${sharedLink.shareId}`} target="_blank" rel="noreferrer" className="flex">
|
||||
<LinkIcon className="mr-1 h-5 w-5" />
|
||||
<TableRow className={(cn(isDeleting && 'opacity-50'), 'hover:bg-transparent')}>
|
||||
<TableCell>
|
||||
<Link
|
||||
to={`/share/${sharedLink.shareId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center text-blue-500 hover:underline"
|
||||
>
|
||||
<LinkIcon className="mr-2 h-4 w-4" />
|
||||
{sharedLink.title}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<div className="flex justify-between">
|
||||
<div className={cn('flex justify-start dark:text-gray-200', isDeleting && 'opacity-50')}>
|
||||
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-end gap-3 text-gray-400',
|
||||
isDeleting && 'opacity-50',
|
||||
)}
|
||||
>
|
||||
{sharedLink.conversationId && (
|
||||
<div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
|
||||
<SharedLinkDeleteButton
|
||||
shareId={sharedLink.shareId}
|
||||
setIsDeleting={setIsDeleting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{sharedLink.conversationId && (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
aria-label="Delete shared link"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_conversation')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="dialog-confirm-delete"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{localize('com_ui_delete_confirm')} <strong>{sharedLink.title}</strong>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => confirmDelete(sharedLink.shareId),
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
export default function ShareLinkTable({ className }: { className?: string }) {
|
||||
|
||||
export default function ShareLinkTable({ className }) {
|
||||
const localize = useLocalize();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
|
@ -114,15 +134,28 @@ export default function ShareLinkTable({ className }: { className?: string }) {
|
|||
});
|
||||
|
||||
const sharedLinks = useMemo(() => data?.pages.flatMap((page) => page.sharedLinks) || [], [data]);
|
||||
const classProp: { className?: string } = {
|
||||
className: 'p-1 hover:text-black dark:hover:text-white',
|
||||
};
|
||||
if (className) {
|
||||
classProp.className = className;
|
||||
}
|
||||
|
||||
const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170;
|
||||
|
||||
const skeletons = Array.from({ length: 11 }, (_, index) => {
|
||||
const randomWidth = getRandomWidth();
|
||||
return (
|
||||
<div key={index} className="flex h-10 w-full items-center">
|
||||
<div className="flex w-[410px] items-center">
|
||||
<Skeleton className="h-4" style={{ width: `${randomWidth}px` }} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
</div>
|
||||
<div className="mr-2 flex justify-end">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner className="m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white" />;
|
||||
return <div className="text-gray-300">{skeletons}</div>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
|
|
@ -132,35 +165,34 @@ export default function ShareLinkTable({ className }: { className?: string }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
if (!sharedLinks || sharedLinks.length === 0) {
|
||||
|
||||
if (sharedLinks.length === 0) {
|
||||
return <div className="text-gray-300">{localize('com_nav_shared_links_empty')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'grid w-full gap-2',
|
||||
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
||||
'max-h-[350px]',
|
||||
'-mr-2 grid max-h-[350px] w-full flex-1 flex-col gap-2 overflow-y-auto pr-2 transition-opacity duration-500',
|
||||
className,
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<table className="table-fixed text-left">
|
||||
<thead className="sticky top-0 bg-white dark:bg-gray-700">
|
||||
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
|
||||
<th className="p-3">{localize('com_nav_shared_links_name')}</th>
|
||||
<th className="p-3">{localize('com_nav_shared_links_date_shared')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{localize('com_nav_shared_links_name')}</TableHead>
|
||||
<TableHead>{localize('com_nav_shared_links_date_shared')}</TableHead>
|
||||
<TableHead className="text-right">{localize('com_assistants_actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sharedLinks.map((sharedLink) => (
|
||||
<ShareLinkRow key={sharedLink.shareId} sharedLink={sharedLink} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{(isFetchingNextPage || showLoading) && (
|
||||
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{(isFetchingNextPage || showLoading) && <Spinner className="mx-auto my-4" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useLocalize } from '~/hooks';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { OGDialog, OGDialogTrigger } from '~/components/ui';
|
||||
import { OGDialog, OGDialogTrigger, Button } from '~/components';
|
||||
|
||||
import ArchivedChatsTable from './ArchivedChatsTable';
|
||||
|
||||
|
|
@ -12,9 +12,9 @@ export default function ArchivedChats() {
|
|||
<div>{localize('com_nav_archived_chats')}</div>
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<button className="btn btn-neutral relative ">
|
||||
<Button variant="outline" aria-label="Archived chats">
|
||||
{localize('com_nav_archived_chats_manage')}
|
||||
</button>
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_nav_archived_chats')}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,252 @@
|
|||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { MessageCircle, ArchiveRestore } from 'lucide-react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useConversationsInfiniteQuery } from '~/data-provider';
|
||||
import { ConversationListResponse } from 'librechat-data-provider';
|
||||
import { useAuthContext, useLocalize, useNavScrolling, useArchiveHandler } from '~/hooks';
|
||||
import { DeleteButton } from '~/components/Conversations/ConvoOptions';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import {
|
||||
Search,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
TrashIcon,
|
||||
MessageCircle,
|
||||
ArchiveRestore,
|
||||
ChevronsRight,
|
||||
ChevronsLeft,
|
||||
} from 'lucide-react';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import { useAuthContext, useLocalize, useArchiveHandler } from '~/hooks';
|
||||
import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions';
|
||||
import {
|
||||
TooltipAnchor,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Separator,
|
||||
Skeleton,
|
||||
Button,
|
||||
Input,
|
||||
OGDialog,
|
||||
OGDialogTrigger,
|
||||
} from '~/components';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ArchivedChatsTable() {
|
||||
const localize = useLocalize();
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery(
|
||||
{ pageNumber: '1', isArchived: true },
|
||||
{ enabled: isAuthenticated },
|
||||
const { data, isLoading, refetch } = useConversationsInfiniteQuery(
|
||||
{ pageNumber: currentPage.toString(), limit: 10, isArchived: true },
|
||||
{ enabled: isAuthenticated && isOpened },
|
||||
);
|
||||
|
||||
const { containerRef, moveToTop } = useNavScrolling<ConversationListResponse>({
|
||||
setShowLoading,
|
||||
hasNextPage: hasNextPage,
|
||||
fetchNextPage: fetchNextPage,
|
||||
isFetchingNextPage: isFetchingNextPage,
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setTotalPages(Math.ceil(Number(data.pages)));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const archiveHandler = useArchiveHandler(conversationId ?? '', false, () => {
|
||||
refetch();
|
||||
});
|
||||
|
||||
const conversations = useMemo(
|
||||
() => data?.pages.flatMap((page) => page.conversations) || [],
|
||||
[data],
|
||||
);
|
||||
const handleChatClick = useCallback((conversationId) => {
|
||||
window.open(`/c/${conversationId}`, '_blank');
|
||||
}, []);
|
||||
|
||||
const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop);
|
||||
const handlePageChange = useCallback((newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
}, []);
|
||||
|
||||
if (!data || conversations.length === 0) {
|
||||
const handleSearch = useCallback((query) => {
|
||||
setSearchQuery(query);
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170;
|
||||
|
||||
const skeletons = Array.from({ length: 11 }, (_, index) => {
|
||||
const randomWidth = getRandomWidth();
|
||||
return (
|
||||
<div key={index} className="flex h-10 w-full items-center">
|
||||
<div className="flex w-[410px] items-center">
|
||||
<Skeleton className="h-4" style={{ width: `${randomWidth}px` }} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
</div>
|
||||
<div className="mr-2 flex justify-end">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-300">{skeletons}</div>;
|
||||
}
|
||||
|
||||
if (!data || data.pages.length === 0 || data.pages[0].conversations.length === 0) {
|
||||
return <div className="text-gray-300">{localize('com_nav_archived_chats_empty')}</div>;
|
||||
}
|
||||
|
||||
const conversations = data.pages.flatMap((page) => page.conversations);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'grid w-full gap-2',
|
||||
'flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
||||
'max-h-[350px]',
|
||||
'max-h-[629px]',
|
||||
)}
|
||||
ref={containerRef}
|
||||
onMouseEnter={() => setIsOpened(true)}
|
||||
>
|
||||
<table className="table-fixed text-left">
|
||||
<thead className="sticky top-0 bg-white dark:bg-gray-700">
|
||||
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
|
||||
<th className="p-3">{localize('com_nav_archive_name')}</th>
|
||||
<th className="p-3">{localize('com_nav_archive_created_at')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conversations.map((conversation) => {
|
||||
if (!conversation.conversationId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<tr
|
||||
key={conversation.conversationId}
|
||||
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
|
||||
>
|
||||
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
|
||||
<MessageCircle className="mr-1 h-5 w-5" />
|
||||
{conversation.title}
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-start dark:text-gray-200">
|
||||
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_unarchive')}
|
||||
onClick={() => {
|
||||
setConversationId(conversation.conversationId);
|
||||
archiveHandler();
|
||||
}}
|
||||
className="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>
|
||||
<ArchiveRestore className="size-4 hover:text-gray-300" />
|
||||
</TooltipAnchor>
|
||||
<div className="size-5 hover:text-gray-300">
|
||||
<DeleteButton
|
||||
conversationId={conversation.conversationId}
|
||||
retainView={moveToTop}
|
||||
title={conversation.title ?? ''}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Search className="size-4 text-text-secondary" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={localize('com_nav_search_placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="w-full border-none"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
{conversations.length === 0 ? (
|
||||
<div className="mt-4 text-text-secondary">{localize('com_nav_no_search_results')}</div>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50%] p-4">{localize('com_nav_archive_name')}</TableHead>
|
||||
<TableHead className="w-[35%] p-1">
|
||||
{localize('com_nav_archive_created_at')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[15%] p-1 text-right">
|
||||
{localize('com_assistants_actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{conversations.map((conversation: TConversation) => (
|
||||
<TableRow key={conversation.conversationId} className="hover:bg-transparent">
|
||||
<TableCell className="flex items-center py-3 text-text-primary">
|
||||
<button
|
||||
className="flex"
|
||||
aria-label="Open conversation in a new tab"
|
||||
onClick={() => handleChatClick(conversation.conversationId)}
|
||||
>
|
||||
<MessageCircle className="mr-1 h-5 w-5" />
|
||||
<u>{conversation.title}</u>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="p-1">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-start text-text-secondary">
|
||||
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(isFetchingNextPage || showLoading) && (
|
||||
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
|
||||
</TableCell>
|
||||
<TableCell className="flex items-center justify-end gap-2 p-1">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_unarchive')}
|
||||
render={
|
||||
<Button
|
||||
aria-label="Unarchive conversation"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => {
|
||||
setConversationId(conversation.conversationId);
|
||||
archiveHandler();
|
||||
}}
|
||||
>
|
||||
<ArchiveRestore className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
aria-label="Delete archived conversation"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
</OGDialogTrigger>
|
||||
{DeleteConversationDialog({
|
||||
conversationId: conversation.conversationId ?? '',
|
||||
retainView: refetch,
|
||||
title: conversation.title ?? '',
|
||||
})}
|
||||
</OGDialog>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="flex items-center justify-end gap-6 px-2 py-4">
|
||||
<div className="text-sm font-bold text-text-primary">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Go to the previous 10 pages"
|
||||
onClick={() => handlePageChange(Math.max(currentPage - 10, 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronsLeft className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Go to the previous page"
|
||||
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Go to the next page"
|
||||
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Go to the next 10 pages"
|
||||
onClick={() => handlePageChange(Math.min(currentPage + 10, totalPages))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<ChevronsRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default function AutoScrollSwitch({
|
|||
<Switch
|
||||
id="autoScroll"
|
||||
checked={autoScroll}
|
||||
aria-label="Auto-Scroll to latest message on chat open"
|
||||
onCheckedChange={handleCheckedChange}
|
||||
className="ml-4 mt-2 ring-ring-primary"
|
||||
data-testid="autoScroll"
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ export const ThemeSelector = ({
|
|||
value={theme}
|
||||
onChange={onChange}
|
||||
options={themeOptions}
|
||||
sizeClasses="w-[220px]"
|
||||
anchor="bottom start"
|
||||
sizeClasses="w-[180px]"
|
||||
testId="theme-selector"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -112,7 +111,6 @@ export const LangSelector = ({
|
|||
value={langcode}
|
||||
onChange={onChange}
|
||||
sizeClasses="[--anchor-max-height:256px]"
|
||||
anchor="bottom start"
|
||||
options={languageOptions}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -149,26 +147,24 @@ function General() {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<ThemeSelector theme={theme} onChange={changeTheme} />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<LangSelector langcode={langcode} onChange={changeLang} />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<UserMsgMarkdownSwitch />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<AutoScrollSwitch />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<HideSidePanelSwitch />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
|
||||
<ArchivedChats />
|
||||
</div>
|
||||
{/* <div className="border-b pb-3 last-of-type:border-b-0 border-border-medium">
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export default function HideSidePanelSwitch({
|
|||
<Switch
|
||||
id="hideSidePanel"
|
||||
checked={hideSidePanel}
|
||||
aria-label="Hide right-most side panel"
|
||||
onCheckedChange={handleCheckedChange}
|
||||
className="ml-4 mt-2"
|
||||
data-testid="hideSidePanel"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'test/matchMedia.mock';
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { LangSelector } from './General';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
|
@ -18,14 +18,15 @@ describe('LangSelector', () => {
|
|||
unobserve = jest.fn();
|
||||
disconnect = jest.fn();
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByRole } = render(
|
||||
<RecoilRoot>
|
||||
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Language')).toBeInTheDocument();
|
||||
expect(getByText('English')).toBeInTheDocument();
|
||||
const dropdownButton = getByRole('combobox');
|
||||
expect(dropdownButton).toHaveTextContent('English');
|
||||
});
|
||||
|
||||
it('calls onChange when the select value changes', async () => {
|
||||
|
|
@ -34,25 +35,23 @@ describe('LangSelector', () => {
|
|||
unobserve = jest.fn();
|
||||
disconnect = jest.fn();
|
||||
};
|
||||
const { getByText, getByTestId } = render(
|
||||
const { getByRole, getByTestId } = render(
|
||||
<RecoilRoot>
|
||||
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('English')).toBeInTheDocument();
|
||||
expect(getByRole('combobox')).toHaveTextContent('English');
|
||||
|
||||
// Find the dropdown button by data-testid
|
||||
const dropdownButton = getByTestId('dropdown-menu');
|
||||
|
||||
// Open the dropdown
|
||||
fireEvent.click(dropdownButton);
|
||||
|
||||
// Find the option by text and click it
|
||||
const darkOption = getByText('Italiano');
|
||||
fireEvent.click(darkOption);
|
||||
const italianOption = getByRole('option', { name: 'Italiano' });
|
||||
fireEvent.click(italianOption);
|
||||
|
||||
// Ensure that the onChange is called with the expected value after a short delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith('it-IT');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,14 +20,15 @@ describe('ThemeSelector', () => {
|
|||
unobserve = jest.fn();
|
||||
disconnect = jest.fn();
|
||||
};
|
||||
const { getByText } = render(
|
||||
const { getByText, getByRole } = render(
|
||||
<RecoilRoot>
|
||||
<ThemeSelector theme="system" onChange={mockOnChange} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Theme')).toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
const dropdownButton = getByRole('combobox');
|
||||
expect(dropdownButton).toHaveTextContent('System');
|
||||
});
|
||||
|
||||
it('calls onChange when the select value changes', async () => {
|
||||
|
|
@ -44,17 +45,13 @@ describe('ThemeSelector', () => {
|
|||
|
||||
expect(getByText('Theme')).toBeInTheDocument();
|
||||
|
||||
// Find the dropdown button by data-testid
|
||||
const dropdownButton = getByTestId('theme-selector');
|
||||
|
||||
// Open the dropdown
|
||||
fireEvent.click(dropdownButton);
|
||||
|
||||
// Find the option by text and click it
|
||||
const darkOption = getByText('Dark');
|
||||
fireEvent.click(darkOption);
|
||||
|
||||
// Ensure that the onChange is called with the expected value
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith('dark');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ const EngineSTTDropdown: React.FC<EngineSTTDropdownProps> = ({ external }) => {
|
|||
onChange={handleSelect}
|
||||
options={endpointOptions}
|
||||
sizeClasses="w-[180px]"
|
||||
anchor="bottom start"
|
||||
testId="EngineSTTDropdown"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -146,14 +146,12 @@ function Speech() {
|
|||
value={advancedMode ? 'advanced' : 'simple'}
|
||||
>
|
||||
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
|
||||
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
|
||||
<Tabs.List className="flex justify-center bg-background">
|
||||
<Tabs.Trigger
|
||||
onClick={() => setAdvancedMode(false)}
|
||||
className={cn(
|
||||
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
||||
isSmallScreen
|
||||
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
|
||||
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
||||
'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg',
|
||||
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
|
||||
'w-full',
|
||||
)}
|
||||
value="simple"
|
||||
|
|
@ -165,10 +163,8 @@ function Speech() {
|
|||
<Tabs.Trigger
|
||||
onClick={() => setAdvancedMode(true)}
|
||||
className={cn(
|
||||
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
||||
isSmallScreen
|
||||
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
|
||||
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
|
||||
'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg',
|
||||
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
|
||||
'w-full',
|
||||
)}
|
||||
value="advanced"
|
||||
|
|
@ -181,79 +177,53 @@ function Speech() {
|
|||
</div>
|
||||
|
||||
<Tabs.Content value={'simple'}>
|
||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<SpeechToTextSwitch />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineSTTDropdown external={sttExternal} />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LanguageSTTDropdown />
|
||||
</div>
|
||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<TextToSpeechSwitch />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineTTSDropdown external={ttsExternal} />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<VoiceDropdown />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||
<SpeechToTextSwitch />
|
||||
<EngineSTTDropdown external={sttExternal} />
|
||||
<LanguageSTTDropdown />
|
||||
<div className="h-px bg-border-medium" role="none" />
|
||||
<TextToSpeechSwitch />
|
||||
<EngineTTSDropdown external={ttsExternal} />
|
||||
<VoiceDropdown />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value={'advanced'}>
|
||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ConversationModeSwitch />
|
||||
</div>
|
||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<SpeechToTextSwitch />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineSTTDropdown external={sttExternal} />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LanguageSTTDropdown />
|
||||
</div>
|
||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||
<ConversationModeSwitch />
|
||||
<div className="mt-2 h-px bg-border-medium" role="none" />
|
||||
<SpeechToTextSwitch />
|
||||
|
||||
<EngineSTTDropdown external={sttExternal} />
|
||||
|
||||
<LanguageSTTDropdown />
|
||||
<div className="pb-2">
|
||||
<AutoTranscribeAudioSwitch />
|
||||
</div>
|
||||
{autoTranscribeAudio && (
|
||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="pb-2">
|
||||
<DecibelSelector />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="pb-2">
|
||||
<AutoSendTextSelector />
|
||||
</div>
|
||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="h-px bg-border-medium" role="none" />
|
||||
<div className="pb-3">
|
||||
<TextToSpeechSwitch />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutomaticPlaybackSwitch />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<EngineTTSDropdown external={ttsExternal} />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<VoiceDropdown />
|
||||
</div>
|
||||
<AutomaticPlaybackSwitch />
|
||||
<EngineTTSDropdown external={ttsExternal} />
|
||||
<VoiceDropdown />
|
||||
{engineTTS === 'browser' && (
|
||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="pb-2">
|
||||
<CloudBrowserVoicesSwitch />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="pb-2">
|
||||
<PlaybackRate />
|
||||
</div>
|
||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
||||
<CacheTTSSwitch />
|
||||
</div>
|
||||
<CacheTTSSwitch />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export * from './ExportConversation';
|
||||
export * from './SettingsTabs/';
|
||||
export { default as ClearConvos } from './ClearConvos';
|
||||
export { default as Logout } from './Logout';
|
||||
export { default as MobileNav } from './MobileNav';
|
||||
export { default as Nav } from './Nav';
|
||||
export { default as NavLink } from './NavLink';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function FilterItem({
|
|||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={onClick}
|
||||
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary dark:focus:bg-surface-tertiary"
|
||||
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary"
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default function List({
|
|||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mx-2 w-full px-3"
|
||||
className="mx-2 w-full bg-transparent px-3"
|
||||
onClick={() => navigate('/d/prompts/new')}
|
||||
>
|
||||
+ {localize('com_ui_create_prompt')}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useLocalize, useCustomLink } from '~/hooks';
|
||||
import { buttonVariants } from '~/components/ui';
|
||||
import { Button } from '~/components/ui';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -15,13 +15,10 @@ export default function ManagePrompts({ className }: { className?: string }) {
|
|||
}, [setPromptsName, setPromptsCategory]);
|
||||
|
||||
const clickHandler = useCustomLink('/d/prompts', clickCallback);
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||
href="/d/prompts"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<Button variant="outline" className={cn(className, 'bg-transparent')} onClick={clickHandler}>
|
||||
{localize('com_ui_manage')}
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default function PromptsView() {
|
|||
<GroupSidePanel isDetailView={isDetailView} {...groupsNav}>
|
||||
<div className="mx-2 mt-1 flex flex-row items-center justify-between">
|
||||
<FilterPrompts setName={groupsNav.setName} />
|
||||
<AutoSendPrompt className="text-xs dark:text-white" />
|
||||
<AutoSendPrompt className="text-xs text-text-primary" />
|
||||
</div>
|
||||
</GroupSidePanel>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
|||
import type { NavLink, NavProps } from '~/common';
|
||||
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
||||
import { buttonVariants } from '~/components/ui/Button';
|
||||
import { TooltipAnchor, Button } from '~/components';
|
||||
import { cn, removeFocusOutlines } from '~/utils';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
||||
|
|
@ -31,29 +31,27 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
const variant = getVariant(link);
|
||||
return isCollapsed ? (
|
||||
<TooltipAnchor
|
||||
className={cn(
|
||||
buttonVariants({ variant, size: 'icon' }),
|
||||
removeFocusOutlines,
|
||||
'h-9 w-9 cursor-pointer',
|
||||
variant === 'default'
|
||||
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted bg-surface-terniary dark:hover:text-white'
|
||||
: '',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (link.onClick) {
|
||||
link.onClick(e);
|
||||
setActive('');
|
||||
return;
|
||||
}
|
||||
setActive(link.id);
|
||||
resize && resize(25);
|
||||
}}
|
||||
description={localize(link.title)}
|
||||
side="left"
|
||||
>
|
||||
<link.icon className="h-4 w-4" />
|
||||
<span className="sr-only">{link.title}</span>
|
||||
</TooltipAnchor>
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
if (link.onClick) {
|
||||
link.onClick(e);
|
||||
setActive('');
|
||||
return;
|
||||
}
|
||||
setActive(link.id);
|
||||
resize && resize(25);
|
||||
}}
|
||||
>
|
||||
<link.icon className="h-4 w-4 text-text-secondary" />
|
||||
<span className="sr-only">{link.title}</span>
|
||||
</Button>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
) : (
|
||||
<Accordion
|
||||
key={index}
|
||||
|
|
@ -65,16 +63,10 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
<AccordionItem value={link.id} className="w-full border-none">
|
||||
<AccordionPrimitive.Header asChild>
|
||||
<AccordionPrimitive.Trigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({ variant, size: 'sm' }),
|
||||
removeFocusOutlines,
|
||||
variant === 'default'
|
||||
? 'dark:bg-muted dark:hover:bg-muted dark:text-white dark:hover:text-white'
|
||||
: '',
|
||||
'hover:bg-gray-200 data-[state=open]:bg-gray-200 data-[state=open]:text-black dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-700 dark:data-[state=open]:text-white',
|
||||
'w-full justify-start rounded-md border dark:border-gray-700',
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start bg-transparent text-text-secondary data-[state=open]:bg-surface-secondary data-[state=open]:text-text-primary"
|
||||
onClick={(e) => {
|
||||
if (link.onClick) {
|
||||
link.onClick(e);
|
||||
|
|
@ -95,7 +87,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ const SidePanel = ({
|
|||
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
||||
}}
|
||||
className={cn(
|
||||
'sidenav hide-scrollbar border-l border-border-light bg-surface-primary-alt transition-opacity',
|
||||
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
|
||||
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
|
||||
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||
? 'hidden min-w-0'
|
||||
|
|
@ -264,7 +264,7 @@ const SidePanel = ({
|
|||
{interfaceConfig.modelSelect && (
|
||||
<div
|
||||
className={cn(
|
||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-surface-primary-alt',
|
||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
|
||||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,28 @@
|
|||
import * as React from 'react';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300',
|
||||
destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'bg-transparent border border-gray-200 text-gray-700 hover:bg-gray-200 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700',
|
||||
subtle:
|
||||
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
|
||||
ghost:
|
||||
'bg-transparent text-gray-900 hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-800 data-[state=open]:bg-transparent',
|
||||
link: 'bg-transparent underline-offset-4 hover:underline text-gray-600 dark:text-gray-400 hover:bg-transparent dark:hover:bg-transparent',
|
||||
success:
|
||||
'bg-green-500 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-700',
|
||||
warning:
|
||||
'bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700',
|
||||
info: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700',
|
||||
'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 py-2 px-4',
|
||||
sm: 'h-8 px-3 rounded',
|
||||
lg: 'h-12 px-6 rounded-md',
|
||||
xl: 'h-14 px-8 rounded-lg text-base',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
loading: {
|
||||
true: 'opacity-80 pointer-events-none',
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'size-10',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: ['default', 'destructive', 'success', 'warning', 'info'],
|
||||
className: 'focus-visible:ring-white focus-visible:ring-offset-2',
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
className: 'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
|
|
@ -57,62 +33,14 @@ const buttonVariants = cva(
|
|||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
loading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: string }>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
loading,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
customId,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, fullWidth, loading, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
id={customId ?? props.id ?? 'shadcn-button'}
|
||||
disabled={props.disabled || loading}
|
||||
aria-busy={loading}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="-ml-1 mr-3 h-5 w-5 animate-spin text-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{leftIcon && <span className="mr-2">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="ml-2">{rightIcon}</span>}
|
||||
</button>
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
|
||||
import type { Option } from '~/common';
|
||||
import React, { useState } from 'react';
|
||||
import * as Select from '@ariakit/react/select';
|
||||
import { cn } from '~/utils/';
|
||||
import type { Option } from '~/common';
|
||||
|
||||
interface DropdownProps {
|
||||
value: string;
|
||||
|
|
@ -16,112 +9,89 @@ interface DropdownProps {
|
|||
onChange: (value: string) => void;
|
||||
options: string[] | Option[];
|
||||
className?: string;
|
||||
anchor?: AnchorPropsWithSelection;
|
||||
sizeClasses?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
const Dropdown: FC<DropdownProps> = ({
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
value: initialValue,
|
||||
label = '',
|
||||
onChange,
|
||||
options,
|
||||
className = '',
|
||||
anchor,
|
||||
sizeClasses,
|
||||
testId = 'dropdown-menu',
|
||||
}) => {
|
||||
const [selectedValue, setSelectedValue] = useState(initialValue);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSelectedValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const selectProps = Select.useSelectStore({
|
||||
value: selectedValue,
|
||||
setValue: handleChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Listbox
|
||||
value={selectedValue}
|
||||
onChange={(newValue) => {
|
||||
setSelectedValue(newValue);
|
||||
onChange(newValue);
|
||||
}}
|
||||
<Select.Select
|
||||
store={selectProps}
|
||||
className={cn(
|
||||
'focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-lg border border-input bg-background py-2 pl-3 pr-8 text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
|
||||
className,
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className={cn('relative', className)}>
|
||||
<ListboxButton
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'btn-neutral focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-md border-border-light bg-header-primary py-2 pl-3 pr-8 text-text-primary transition-all duration-100 ease-in-out hover:bg-header-hover focus:ring-ring-primary',
|
||||
className,
|
||||
)}
|
||||
aria-label="Select an option"
|
||||
>
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
{options
|
||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className="h-4 w-5 rotate-0 transform text-text-primary transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
leave="transition ease-in duration-50"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
className={cn(
|
||||
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-border-medium bg-header-primary p-1.5 shadow-lg transition-opacity',
|
||||
sizeClasses,
|
||||
className,
|
||||
)}
|
||||
anchor={anchor}
|
||||
aria-label="List of options"
|
||||
>
|
||||
{options.map((item, index) => (
|
||||
<ListboxOption
|
||||
key={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
className="focus-visible:ring-offset ring-offset-ring-offset relative cursor-pointer select-none rounded border-border-light bg-header-primary py-2.5 pl-3 pr-3 text-sm text-text-secondary ring-ring-primary hover:bg-header-hover focus-visible:ring data-[focus]:bg-surface-hover data-[focus]:text-text-primary"
|
||||
style={{ width: '100%' }}
|
||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{typeof item === 'string' ? item : (item as Option).label}
|
||||
</span>
|
||||
{selectedValue === (typeof item === 'string' ? item : item.value) && (
|
||||
<span className="ml-auto pl-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-md block group-hover:hidden"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
{options
|
||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||
</span>
|
||||
<Select.SelectArrow />
|
||||
</div>
|
||||
</Listbox>
|
||||
</Select.Select>
|
||||
<Select.SelectPopover
|
||||
store={selectProps}
|
||||
className={cn('popover-ui', sizeClasses, className)}
|
||||
>
|
||||
{options.map((item, index) => (
|
||||
<Select.SelectItem
|
||||
key={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
className="select-item"
|
||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{typeof item === 'string' ? item : (item as Option).label}
|
||||
</span>
|
||||
{selectedValue === (typeof item === 'string' ? item : item.value) && (
|
||||
<span className="ml-auto pl-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon-md block group-hover:hidden"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Select.SelectItem>
|
||||
))}
|
||||
</Select.SelectPopover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
>(({ className = '', ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-900', className)}
|
||||
className={cn('-mx-1 my-1 h-px bg-border-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ interface DropdownProps {
|
|||
testId?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mainly used for the Speech Voice Selection Dropdown
|
||||
*/
|
||||
|
||||
const Dropdown: FC<DropdownProps> = ({
|
||||
value,
|
||||
label = '',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
|
|||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'dark:border-gray-00 flex h-10 w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50',
|
||||
'flex h-10 w-full rounded-md border border-border-light bg-transparent px-3 py-2 text-sm placeholder:text-text-tertiary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50',
|
||||
className ?? '',
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -59,10 +59,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
|||
overlayClassName={overlayClassName}
|
||||
showCloseButton={showCloseButton}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300',
|
||||
className ?? '',
|
||||
)}
|
||||
className={cn('border-none bg-background text-foreground', className ?? '')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OGDialogHeader className={cn(headerClassName ?? '')}>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -41,14 +41,14 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
|
@ -89,7 +89,7 @@ const DialogDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
105
client/src/components/ui/Pagination.tsx
Normal file
105
client/src/components/ui/Pagination.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import * as React from 'react';
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
import { ButtonProps, buttonVariants } from './Button';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = 'Pagination';
|
||||
|
||||
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
|
||||
),
|
||||
);
|
||||
PaginationContent.displayName = 'PaginationContent';
|
||||
|
||||
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
|
||||
({ className, ...props }, ref) => <li ref={ref} className={cn('', className)} {...props} />,
|
||||
);
|
||||
PaginationItem.displayName = 'PaginationItem';
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive = false,
|
||||
size = 'icon',
|
||||
children,
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children || <span className="sr-only">Page link</span>}
|
||||
</a>
|
||||
);
|
||||
PaginationLink.displayName = 'PaginationLink';
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
||||
|
||||
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = 'PaginationNext';
|
||||
|
||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
};
|
||||
|
|
@ -12,7 +12,7 @@ const Separator = React.forwardRef<
|
|||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'bg-border shrink-0',
|
||||
'shrink-0 bg-border-light',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
import { useDoubleClick } from '@zattoo/use-double-click';
|
||||
import type { clickEvent } from '@zattoo/use-double-click';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||
doubleClickHandler?: clickEvent;
|
||||
trackClassName?: string;
|
||||
}
|
||||
|
||||
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
||||
(
|
||||
{ className, trackClassName = 'bg-gray-200 dark:bg-gray-850', doubleClickHandler, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
className={cn('relative h-1 w-full grow overflow-hidden rounded-full', trackClassName)}
|
||||
>
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
onClick={
|
||||
useDoubleClick(doubleClickHandler as clickEvent) ??
|
||||
(() => {
|
||||
return;
|
||||
})
|
||||
}
|
||||
className="block h-4 w-4 cursor-pointer rounded-full border border-border-medium-alt bg-white shadow ring-ring-primary transition-colors focus-visible:ring-1 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 dark:border-none"
|
||||
/>
|
||||
</SliderPrimitive.Root>
|
||||
),
|
||||
);
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import * as React from 'react';
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-[20px] w-[32px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-black data-[state=unchecked]:bg-gray-200 dark:data-[state=checked]:bg-green-500 dark:data-[state=unchecked]:bg-gray-500',
|
||||
'ring-ring-primary',
|
||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -18,7 +16,7 @@ const Switch = React.forwardRef<
|
|||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'pointer-events-none block h-4 w-4 -translate-x-0.5 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0',
|
||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
</div>
|
||||
),
|
||||
);
|
||||
Table.displayName = 'Table';
|
||||
|
|
@ -12,7 +13,7 @@ Table.displayName = 'Table';
|
|||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = 'TableHeader';
|
||||
|
|
@ -20,7 +21,7 @@ TableHeader.displayName = 'TableHeader';
|
|||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = 'TableBody';
|
||||
|
|
@ -28,21 +29,21 @@ TableBody.displayName = 'TableBody';
|
|||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn('bg-primary text-primary-foreground font-medium', className)}
|
||||
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = 'TableFooter';
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-border-light transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -54,7 +55,7 @@ TableRow.displayName = 'TableRow';
|
|||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
|
@ -69,10 +70,10 @@ TableHead.displayName = 'TableHead';
|
|||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn('align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
@ -81,7 +82,7 @@ TableCell.displayName = 'TableCell';
|
|||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
||||
));
|
||||
TableCaption.displayName = 'TableCaption';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ const TabsList = React.forwardRef<
|
|||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md bg-gray-200 p-1 dark:bg-gray-800',
|
||||
'inline-flex items-center justify-center rounded-md bg-surface-primary',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -26,7 +25,7 @@ const TabsTrigger = React.forwardRef<
|
|||
>(({ className = '', ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn(
|
||||
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-gray-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-800 data-[state=active]:shadow-sm dark:text-gray-200 dark:data-[state=active]:bg-gray-700 dark:data-[state=active]:text-gray-200',
|
||||
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-gray-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-800 data-[state=active]:shadow-sm dark:data-[state=active]:bg-gray-700 dark:data-[state=active]:text-gray-200',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -39,11 +38,7 @@ const TabsContent = React.forwardRef<
|
|||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className = '', ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
className={cn('mt-2 rounded-md border border-gray-200 p-6 dark:border-gray-700', className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
<TabsPrimitive.Content className={cn('mt-2 rounded-md p-6', className)} {...props} ref={ref} />
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import * as Ariakit from '@ariakit/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
||||
description: string;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||
{ description, side = 'top', role, ...props },
|
||||
{ description, side = 'top', className, role, ...props },
|
||||
ref,
|
||||
) {
|
||||
const tooltip = Ariakit.useTooltipStore({ placement: side });
|
||||
|
|
@ -40,7 +42,13 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
|
|||
|
||||
return (
|
||||
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
|
||||
<Ariakit.TooltipAnchor {...props} ref={ref} role={role} onKeyDown={handleKeyDown} />
|
||||
<Ariakit.TooltipAnchor
|
||||
{...props}
|
||||
ref={ref}
|
||||
role={role}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn('cursor-pointer', className)}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{mounted && (
|
||||
<Ariakit.Tooltip
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export * from './Tag';
|
|||
export * from './Textarea';
|
||||
export * from './TextareaAutosize';
|
||||
export * from './Tooltip';
|
||||
export * from './Pagination';
|
||||
export { default as Combobox } from './Combobox';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as FileUpload } from './FileUpload';
|
||||
|
|
|
|||
|
|
@ -150,12 +150,12 @@ export const useConversationsInfiniteQuery = (
|
|||
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
|
||||
) => {
|
||||
return useInfiniteQuery<ConversationListResponse, unknown>(
|
||||
params?.isArchived ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
|
||||
params?.isArchived === true ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
|
||||
({ pageParam = '' }) =>
|
||||
dataService.listConversations({
|
||||
...params,
|
||||
pageNumber: pageParam?.toString(),
|
||||
isArchived: params?.isArchived || false,
|
||||
isArchived: params?.isArchived ?? false,
|
||||
tags: params?.tags || [],
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -777,6 +777,7 @@ export default {
|
|||
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_no_search_results: 'No search results found',
|
||||
com_nav_setting_general: 'General',
|
||||
com_nav_setting_chat: 'Chat',
|
||||
com_nav_setting_beta: 'Beta features',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ const searchQuery = atom({
|
|||
default: '',
|
||||
});
|
||||
|
||||
const isSearching = atom({
|
||||
key: 'isSearching',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export default {
|
||||
isSearchEnabled,
|
||||
searchQuery,
|
||||
isSearching,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,11 +44,37 @@ html {
|
|||
--surface-secondary: var(--gray-50);
|
||||
--surface-tertiary: var(--gray-100);
|
||||
--surface-tertiary-alt: var(--white);
|
||||
--surface-dialog: var(--white);
|
||||
--border-light: var(--gray-200);
|
||||
--border-medium-alt: var(--gray-300);
|
||||
--border-medium: var(--gray-300);
|
||||
--border-heavy: var(--gray-400);
|
||||
--border-xheavy: var(--gray-500);
|
||||
/* These are test styles */
|
||||
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
.dark {
|
||||
--text-primary: var(--gray-100);
|
||||
|
|
@ -66,11 +92,36 @@ html {
|
|||
--surface-secondary: var(--gray-800);
|
||||
--surface-tertiary: var(--gray-700);
|
||||
--surface-tertiary-alt: var(--gray-700);
|
||||
--surface-dialog: var(--gray-850);
|
||||
--border-light: var(--gray-700);
|
||||
--border-medium-alt: var(--gray-600);
|
||||
--border-medium: var(--gray-600);
|
||||
--border-heavy: var(--gray-500);
|
||||
--border-xheavy: var(--gray-400);
|
||||
/* These are test styles */
|
||||
|
||||
--background: 0 0% 7%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
.gizmo {
|
||||
--text-primary: var(--gizmo-gray-950);
|
||||
|
|
@ -2292,7 +2343,7 @@ button.scroll-convo {
|
|||
z-index: 50;
|
||||
cursor: pointer;
|
||||
border-radius: 0.275rem;
|
||||
background-color: var(--bg-gray-600);
|
||||
background-color: var(--surface-primary);
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 0.5rem;
|
||||
|
|
@ -2322,3 +2373,67 @@ button.scroll-convo {
|
|||
outline: 2px solid #fff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.popover-ui {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
max-height: min(var(--popover-available-height, 300px), 300px);
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
border-radius: 1rem;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--border-light);
|
||||
background-color: hsl(var(--background));
|
||||
padding: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
transform-origin: top;
|
||||
opacity: 0;
|
||||
transition-property: opacity, scale, translate;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
scale: 0.95;
|
||||
translate: 0 -0.5rem;
|
||||
margin-top: 4px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.popover-ui:focus-visible,
|
||||
.popover-ui[data-focus-visible] {
|
||||
outline: var(--bg-surface-hover);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.popover-ui:where(.dark, .dark *) {
|
||||
background-color: hsl(var(--background));
|
||||
color: var(--text-secondary);
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.25), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.select-item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
scroll-margin: 0.5rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.select-item[aria-disabled='true'] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.select-item[data-active-item] {
|
||||
background-color: hsl(var(--accent));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.popover-ui[data-enter] {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
translate: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,11 +77,47 @@ module.exports = {
|
|||
'surface-secondary': 'var(--surface-secondary)',
|
||||
'surface-tertiary': 'var(--surface-tertiary)',
|
||||
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
||||
'surface-dialog': 'var(--surface-dialog)',
|
||||
'border-light': 'var(--border-light)',
|
||||
'border-medium': 'var(--border-medium)',
|
||||
'border-medium-alt': 'var(--border-medium-alt)',
|
||||
'border-heavy': 'var(--border-heavy)',
|
||||
'border-xheavy': 'var(--border-xheavy)',
|
||||
/* These are test styles */
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -8087,12 +8087,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
|
||||
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
|
||||
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.1"
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
|
|
@ -8124,9 +8125,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
|
||||
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue