mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +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>
|
<OGDialogTrigger asChild>
|
||||||
<TooltipAnchor
|
<TooltipAnchor
|
||||||
description={localize('com_ui_delete')}
|
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}
|
tabIndex={tabIndex}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const EditBookmarkButton: FC<{
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onClick={() => setOpen(!open)}
|
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}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { memo, useRef, useMemo } from 'react';
|
import { memo, useRef, useMemo, useEffect } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import {
|
import {
|
||||||
supportsFiles,
|
supportsFiles,
|
||||||
|
|
@ -44,6 +44,7 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||||
|
|
||||||
|
const isSearching = useRecoilValue(store.isSearching);
|
||||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||||
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
|
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
|
||||||
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
|
||||||
|
|
@ -123,6 +124,12 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSearching && textAreaRef.current && !disableInputs) {
|
||||||
|
textAreaRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isSearching, disableInputs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={methods.handleSubmit((data) => submitMessage(data))}
|
onSubmit={methods.handleSubmit((data) => submitMessage(data))}
|
||||||
|
|
@ -164,9 +171,6 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
{endpoint && (
|
{endpoint && (
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
{...registerProps}
|
{...registerProps}
|
||||||
// TODO: remove autofocus due to a11y issues
|
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
||||||
autoFocus
|
|
||||||
ref={(e) => {
|
ref={(e) => {
|
||||||
ref(e);
|
ref(e);
|
||||||
textAreaRef.current = e;
|
textAreaRef.current = e;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { FileSources, FileContext } from 'librechat-data-provider';
|
import { FileSources, FileContext } from 'librechat-data-provider';
|
||||||
import type { TFile } 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 { useGetFiles } from '~/data-provider';
|
||||||
import { DataTable, columns } from './Table';
|
import { DataTable, columns } from './Table';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils/';
|
|
||||||
|
|
||||||
export default function Files({ open, onOpenChange }) {
|
export default function Files({ open, onOpenChange }) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -19,20 +18,16 @@ export default function Files({ open, onOpenChange }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<OGDialogContent
|
||||||
className={cn('w-11/12 overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white')}
|
title={localize('com_nav_my_files')}
|
||||||
|
className="w-11/12 overflow-x-auto bg-background text-text-primary shadow-2xl"
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<OGDialogHeader>
|
||||||
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
|
<OGDialogTitle>{localize('com_nav_my_files')}</OGDialogTitle>
|
||||||
{localize('com_nav_my_files')}
|
</OGDialogHeader>
|
||||||
</DialogTitle>
|
<DataTable columns={columns} data={files} />
|
||||||
</DialogHeader>
|
</OGDialogContent>
|
||||||
<div className="overflow-x-auto p-0 sm:p-6 sm:pt-4">
|
</OGDialog>
|
||||||
<DataTable columns={columns} data={files} />
|
|
||||||
<div className="mt-5 sm:mt-4" />
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ const SubmitButton = React.memo(
|
||||||
id="send-button"
|
id="send-button"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
className={cn(
|
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
|
props.isRTL
|
||||||
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
|
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
|
||||||
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
|
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const ErrorMessage = ({
|
||||||
<Container message={message}>
|
<Container message={message}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import { useDeleteConversationMutation } from '~/data-provider';
|
import { useDeleteConversationMutation } from '~/data-provider';
|
||||||
import { OGDialog, OGDialogTrigger, Label, TooltipAnchor } from '~/components/ui';
|
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { TrashIcon } from '~/components/svg';
|
|
||||||
import { useLocalize, useNewConvo } from '~/hooks';
|
import { useLocalize, useNewConvo } from '~/hooks';
|
||||||
|
import { OGDialog, Label } from '~/components';
|
||||||
|
|
||||||
type DeleteButtonProps = {
|
type DeleteButtonProps = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
|
@ -17,19 +16,21 @@ type DeleteButtonProps = {
|
||||||
setShowDeleteDialog?: (value: boolean) => void;
|
setShowDeleteDialog?: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeleteButton({
|
export function DeleteConversationDialog({
|
||||||
conversationId,
|
conversationId,
|
||||||
retainView,
|
retainView,
|
||||||
title,
|
title,
|
||||||
showDeleteDialog,
|
}: {
|
||||||
setShowDeleteDialog,
|
conversationId: string;
|
||||||
}: DeleteButtonProps) {
|
retainView: () => void;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { newConversation } = useNewConvo();
|
const { newConversation } = useNewConvo();
|
||||||
const { conversationId: currentConvoId } = useParams();
|
const { conversationId: currentConvoId } = useParams();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const deleteConvoMutation = useDeleteConversationMutation({
|
const deleteConvoMutation = useDeleteConversationMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (currentConvoId === conversationId || currentConvoId === 'new') {
|
if (currentConvoId === conversationId || currentConvoId === 'new') {
|
||||||
|
|
@ -47,7 +48,7 @@ export default function DeleteButton({
|
||||||
deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' });
|
deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' });
|
||||||
}, [conversationId, deleteConvoMutation, queryClient]);
|
}, [conversationId, deleteConvoMutation, queryClient]);
|
||||||
|
|
||||||
const dialogContent = (
|
return (
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
title={localize('com_ui_delete_conversation')}
|
title={localize('com_ui_delete_conversation')}
|
||||||
|
|
@ -71,25 +72,26 @@ export default function DeleteButton({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (showDeleteDialog !== undefined && setShowDeleteDialog !== undefined) {
|
export default function DeleteButton({
|
||||||
return (
|
conversationId,
|
||||||
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
retainView,
|
||||||
{dialogContent}
|
title,
|
||||||
</OGDialog>
|
showDeleteDialog,
|
||||||
);
|
setShowDeleteDialog,
|
||||||
|
}: DeleteButtonProps) {
|
||||||
|
if (showDeleteDialog === undefined && setShowDeleteDialog === undefined) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={setOpen}>
|
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
<TooltipAnchor description={localize('com_ui_delete')}>
|
<DeleteConversationDialog
|
||||||
<OGDialogTrigger asChild>
|
conversationId={conversationId}
|
||||||
<button>
|
retainView={retainView}
|
||||||
<TrashIcon className="h-5 w-5" />
|
title={title}
|
||||||
</button>
|
/>
|
||||||
</OGDialogTrigger>
|
|
||||||
</TooltipAnchor>
|
|
||||||
{dialogContent}
|
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { default as DeleteButton } from './DeleteButton';
|
export * from './DeleteButton';
|
||||||
export { default as ShareButton } from './ShareButton';
|
export { default as ShareButton } from './ShareButton';
|
||||||
export { default as SharedLinkButton } from './SharedLinkButton';
|
export { default as SharedLinkButton } from './SharedLinkButton';
|
||||||
export { default as ConvoOptions } from './ConvoOptions';
|
export { default as ConvoOptions } from './ConvoOptions';
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
import { FileText } from 'lucide-react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
import * as Select from '@ariakit/react/select';
|
||||||
import { Fragment, useState, memo } from 'react';
|
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 { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
|
import { LinkIcon, GearIcon, DropdownMenuSeparator } from '~/components';
|
||||||
import FilesView from '~/components/Chat/Input/Files/FilesView';
|
import FilesView from '~/components/Chat/Input/Files/FilesView';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||||
import { LinkIcon, GearIcon } from '~/components';
|
|
||||||
import { UserIcon } from '~/components/svg';
|
import { UserIcon } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import NavLink from './NavLink';
|
|
||||||
import Logout from './Logout';
|
|
||||||
import { cn } from '~/utils/';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
function AccountSettings() {
|
function AccountSettings() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user, isAuthenticated } = useAuthContext();
|
const { user, isAuthenticated, logout } = useAuthContext();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const balanceQuery = useGetUserBalance({
|
const balanceQuery = useGetUserBalance({
|
||||||
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
enabled: !!isAuthenticated && startupConfig?.checkBalance,
|
||||||
|
|
@ -29,115 +26,105 @@ function AccountSettings() {
|
||||||
const name = user?.avatar ?? user?.username ?? '';
|
const name = user?.avatar ?? user?.username ?? '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Select.SelectProvider>
|
||||||
<Menu as="div" className="group relative">
|
<Select.Select
|
||||||
{({ open }) => (
|
aria-label={localize('com_nav_account_settings')}
|
||||||
<>
|
data-testid="nav-user"
|
||||||
<MenuButton
|
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"
|
||||||
aria-label={localize('com_nav_account_settings')}
|
>
|
||||||
className={cn(
|
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||||
'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',
|
<div className="relative flex">
|
||||||
open ? 'bg-surface-secondary' : '',
|
{name.length === 0 ? (
|
||||||
)}
|
|
||||||
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>
|
|
||||||
<div
|
<div
|
||||||
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-text-primary"
|
style={{
|
||||||
style={{ marginTop: '0', marginLeft: '0' }}
|
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>
|
</div>
|
||||||
</MenuButton>
|
) : (
|
||||||
|
<img
|
||||||
<Transition
|
className="rounded-full"
|
||||||
as={Fragment}
|
src={user?.avatar ?? avatarSrc}
|
||||||
enter="transition ease-out duration-100 transform"
|
alt={`${name}'s avatar`}
|
||||||
enterFrom="translate-y-2 opacity-0"
|
/>
|
||||||
enterTo="translate-y-0 opacity-100"
|
)}
|
||||||
leave="transition ease-in duration-100 transform"
|
</div>
|
||||||
leaveFrom="translate-y-0 opacity-100"
|
</div>
|
||||||
leaveTo="translate-y-2 opacity-0"
|
<div
|
||||||
>
|
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-text-primary"
|
||||||
<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">
|
style={{ marginTop: '0', marginLeft: '0' }}
|
||||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
|
>
|
||||||
{user?.email ?? localize('com_nav_user')}
|
{user?.name ?? user?.username ?? localize('com_nav_user')}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
</Select.Select>
|
||||||
{startupConfig?.checkBalance === true &&
|
<Select.SelectPopover
|
||||||
balanceQuery.data != null &&
|
className="popover-ui w-[235px]"
|
||||||
!isNaN(parseFloat(balanceQuery.data)) && (
|
style={{
|
||||||
<>
|
transformOrigin: 'bottom',
|
||||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
|
marginRight: '0px',
|
||||||
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
translate: '0px',
|
||||||
</div>
|
}}
|
||||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
>
|
||||||
</>
|
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||||
)}
|
{user?.email ?? localize('com_nav_user')}
|
||||||
<MenuItem>
|
</div>
|
||||||
{({ focus }) => (
|
<DropdownMenuSeparator />
|
||||||
<NavLink
|
{startupConfig?.checkBalance === true &&
|
||||||
className={focus ? 'bg-surface-hover' : ''}
|
balanceQuery.data != null &&
|
||||||
svg={() => <FileText className="icon-md" />}
|
!isNaN(parseFloat(balanceQuery.data)) && (
|
||||||
text={localize('com_nav_my_files')}
|
<>
|
||||||
clickHandler={() => setShowFiles(true)}
|
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||||
/>
|
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
||||||
)}
|
</div>
|
||||||
</MenuItem>
|
<DropdownMenuSeparator />
|
||||||
{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>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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} />}
|
{showFiles && <FilesView open={showFiles} onOpenChange={setShowFiles} />}
|
||||||
{showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />}
|
{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>;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
} = {
|
} = {
|
||||||
className: cn(
|
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,
|
className,
|
||||||
{
|
{
|
||||||
'opacity-50 pointer-events-none': disabled,
|
'opacity-50 pointer-events-none': disabled,
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default function NavToggle({
|
||||||
description={
|
description={
|
||||||
navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar')
|
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}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<span className="" data-state="closed">
|
<span className="" data-state="closed">
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
|
||||||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
import { useLocalize, useNewConvo } from '~/hooks';
|
import { useLocalize, useNewConvo } from '~/hooks';
|
||||||
import { TooltipAnchor } from '~/components/ui';
|
|
||||||
import { NewChatIcon } from '~/components/svg';
|
import { NewChatIcon } from '~/components/svg';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -96,15 +95,7 @@ export default function NewChat({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<span className="flex items-center" data-state="closed">
|
<span className="flex items-center" data-state="closed">
|
||||||
<TooltipAnchor
|
<NewChatIcon className="size-5" />
|
||||||
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>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
||||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
|
const setIsSearching = useSetRecoilState(store.isSearching);
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const clearText = useCallback(() => {
|
const clearText = useCallback(() => {
|
||||||
|
|
@ -47,6 +48,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
},
|
},
|
||||||
[queryClient, clearConvoState, setSearchQuery],
|
[queryClient, clearConvoState, setSearchQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: make the debounce time configurable via yaml
|
||||||
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
|
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
|
||||||
|
|
||||||
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -54,6 +57,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
setShowClearIcon(value.length > 0);
|
setShowClearIcon(value.length > 0);
|
||||||
setText(value);
|
setText(value);
|
||||||
debouncedSendRequest(value);
|
debouncedSendRequest(value);
|
||||||
|
setIsSearching(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -78,6 +82,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
aria-label={localize('com_nav_search_placeholder')}
|
aria-label={localize('com_nav_search_placeholder')}
|
||||||
placeholder={localize('com_nav_search_placeholder')}
|
placeholder={localize('com_nav_search_placeholder')}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
|
onFocus={() => setIsSearching(true)}
|
||||||
|
onBlur={() => setIsSearching(true)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
dir="auto"
|
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 * as Tabs from '@radix-ui/react-tabs';
|
||||||
import { MessageSquare, Command } from 'lucide-react';
|
import { MessageSquare, Command } from 'lucide-react';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
|
|
@ -12,7 +12,8 @@ import { cn } from '~/utils';
|
||||||
export default function Settings({ open, onOpenChange }: TDialogProps) {
|
export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||||
const localize = useLocalize();
|
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 handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
|
@ -28,12 +29,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowRight':
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
|
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'ArrowLeft':
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
|
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
|
||||||
break;
|
break;
|
||||||
|
|
@ -48,6 +47,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value as SettingsTabValues);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={open}>
|
<Transition appear show={open}>
|
||||||
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
|
<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"
|
enter="ease-out duration-200"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-100"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
|
|
@ -70,15 +73,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div
|
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
|
||||||
className={cn(
|
|
||||||
'fixed inset-0 flex w-screen items-center justify-center p-4',
|
|
||||||
isSmallScreen ? '' : '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
className={cn(
|
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
|
<DialogTitle
|
||||||
|
|
@ -111,18 +109,18 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
</DialogTitle>
|
</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
|
<Tabs.Root
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
|
onValueChange={handleTabChange}
|
||||||
className="flex flex-col gap-10 md:flex-row"
|
className="flex flex-col gap-10 md:flex-row"
|
||||||
orientation="horizontal"
|
orientation="vertical"
|
||||||
>
|
>
|
||||||
<Tabs.List
|
<Tabs.List
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
'min-w-auto max-w-auto relative -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
|
||||||
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
|
isSmallScreen ? 'flex-row rounded-xl bg-surface-secondary' : '',
|
||||||
)}
|
)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
|
@ -166,19 +164,20 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
key={value}
|
key={value}
|
||||||
className={cn(
|
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
|
isSmallScreen
|
||||||
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
|
? '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'
|
||||||
: 'bg-surface-tertiary-alt',
|
: 'rounded-md bg-transparent text-text-primary radix-state-active:bg-surface-tertiary',
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
|
ref={(el) => (tabRefs.current[value] = el)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{localize(label)}
|
{localize(label)}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tabs.List>
|
</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}>
|
<Tabs.Content value={SettingsTabValues.GENERAL}>
|
||||||
<General />
|
<General />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import CodeArtifacts from './CodeArtifacts';
|
||||||
function Beta() {
|
function Beta() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<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 />
|
<CodeArtifacts />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,26 @@ import SaveDraft from './SaveDraft';
|
||||||
function Chat() {
|
function Chat() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<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 />
|
<FontSizeSelector />
|
||||||
</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">
|
||||||
<ChatDirection />
|
<ChatDirection />
|
||||||
</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">
|
||||||
<SendMessageKeyEnter />
|
<SendMessageKeyEnter />
|
||||||
</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">
|
||||||
<ShowCodeSwitch />
|
<ShowCodeSwitch />
|
||||||
</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">
|
||||||
<SaveDraft />
|
<SaveDraft />
|
||||||
</div>
|
</div>
|
||||||
<ForkSettings />
|
<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 />
|
<ModularChat />
|
||||||
</div>
|
</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 />
|
<LaTeXParsing />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { Button } from '~/components';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const ChatDirection = () => {
|
const ChatDirection = () => {
|
||||||
|
|
@ -16,12 +17,11 @@ const ChatDirection = () => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span id="chat-direction-label">{localize('com_nav_chat_direction')}</span>
|
<span id="chat-direction-label">{localize('com_nav_chat_direction')}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
aria-label="Toggle chat direction"
|
||||||
onClick={toggleChatDirection}
|
onClick={toggleChatDirection}
|
||||||
data-testid="chatDirection"
|
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 aria-hidden="true">{direction.toLowerCase()}</span>
|
||||||
<span id="chat-direction-status" className="sr-only">
|
<span id="chat-direction-status" className="sr-only">
|
||||||
|
|
@ -29,7 +29,7 @@ const ChatDirection = () => {
|
||||||
? localize('chat_direction_left_to_right')
|
? localize('chat_direction_left_to_right')
|
||||||
: localize('chat_direction_right_to_left')}
|
: localize('chat_direction_right_to_left')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const ForkSettings = () => {
|
||||||
|
|
||||||
return (
|
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 justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{localize('com_ui_fork_change_default')}</div>
|
<div>{localize('com_ui_fork_change_default')}</div>
|
||||||
|
|
@ -30,12 +30,11 @@ export const ForkSettings = () => {
|
||||||
onChange={setForkSetting}
|
onChange={setForkSetting}
|
||||||
options={forkOptions}
|
options={forkOptions}
|
||||||
sizeClasses="w-[200px]"
|
sizeClasses="w-[200px]"
|
||||||
anchor="bottom start"
|
|
||||||
testId="fork-setting-dropdown"
|
testId="fork-setting-dropdown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 justify-between">
|
||||||
<div> {localize('com_ui_fork_default')} </div>
|
<div> {localize('com_ui_fork_default')} </div>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -47,7 +46,7 @@ export const ForkSettings = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{localize('com_ui_fork_split_target_setting')}</div>
|
<div>{localize('com_ui_fork_split_target_setting')}</div>
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,16 @@ function Commands() {
|
||||||
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
|
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<div className="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 />
|
<AtCommandSwitch />
|
||||||
</div>
|
</div>
|
||||||
{hasAccessToMultiConvo === true && (
|
{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 />
|
<PlusCommandSwitch />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasAccessToPrompts === true && (
|
{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 />
|
<SlashCommandSwitch />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,45 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
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 type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider';
|
||||||
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
|
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
|
||||||
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
|
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
|
||||||
import { Spinner, TooltipAnchor, TrashIcon } from '~/components';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TooltipAnchor,
|
||||||
|
Skeleton,
|
||||||
|
Spinner,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogTrigger,
|
||||||
|
} from '~/components';
|
||||||
|
|
||||||
function SharedLinkDeleteButton({
|
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
|
||||||
shareId,
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
setIsDeleting,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
setIsDeleting: (isDeleting: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const mutation = useDeleteSharedLinkMutation({
|
const mutation = useDeleteSharedLinkMutation({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_share_delete_error'),
|
message: localize('com_ui_share_delete_error'),
|
||||||
severity: NotificationSeverity.ERROR,
|
severity: NotificationSeverity.ERROR,
|
||||||
showIcon: true,
|
|
||||||
});
|
});
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
|
const confirmDelete = async (shareId: TSharedLink['shareId']) => {
|
||||||
e.preventDefault();
|
|
||||||
if (mutation.isLoading) {
|
if (mutation.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -38,67 +47,78 @@ function SharedLinkDeleteButton({
|
||||||
await mutation.mutateAsync({ shareId });
|
await mutation.mutateAsync({ shareId });
|
||||||
setIsDeleting(false);
|
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 (
|
return (
|
||||||
<tr
|
<TableRow className={(cn(isDeleting && 'opacity-50'), 'hover:bg-transparent')}>
|
||||||
key={sharedLink.conversationId}
|
<TableCell>
|
||||||
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
|
<Link
|
||||||
>
|
to={`/share/${sharedLink.shareId}`}
|
||||||
<td
|
target="_blank"
|
||||||
className={cn(
|
rel="noreferrer"
|
||||||
'flex items-center py-3 text-blue-800/70 dark:text-blue-500',
|
className="flex items-center text-blue-500 hover:underline"
|
||||||
isDeleting && 'opacity-50',
|
>
|
||||||
)}
|
<LinkIcon className="mr-2 h-4 w-4" />
|
||||||
>
|
|
||||||
<Link to={`/share/${sharedLink.shareId}`} target="_blank" rel="noreferrer" className="flex">
|
|
||||||
<LinkIcon className="mr-1 h-5 w-5" />
|
|
||||||
{sharedLink.title}
|
{sharedLink.title}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</TableCell>
|
||||||
<td className="p-3">
|
<TableCell>
|
||||||
<div className="flex justify-between">
|
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
||||||
<div className={cn('flex justify-start dark:text-gray-200', isDeleting && 'opacity-50')}>
|
month: 'long',
|
||||||
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
day: 'numeric',
|
||||||
month: 'long',
|
year: 'numeric',
|
||||||
day: 'numeric',
|
})}
|
||||||
year: 'numeric',
|
</TableCell>
|
||||||
})}
|
<TableCell className="text-right">
|
||||||
</div>
|
{sharedLink.conversationId && (
|
||||||
<div
|
<OGDialog>
|
||||||
className={cn(
|
<OGDialogTrigger asChild>
|
||||||
'flex items-center justify-end gap-3 text-gray-400',
|
<TooltipAnchor
|
||||||
isDeleting && 'opacity-50',
|
description={localize('com_ui_delete')}
|
||||||
)}
|
render={
|
||||||
>
|
<Button
|
||||||
{sharedLink.conversationId && (
|
aria-label="Delete shared link"
|
||||||
<div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
|
variant="ghost"
|
||||||
<SharedLinkDeleteButton
|
size="icon"
|
||||||
shareId={sharedLink.shareId}
|
className="size-8"
|
||||||
setIsDeleting={setIsDeleting}
|
>
|
||||||
/>
|
<TrashIcon className="size-4" />
|
||||||
</div>
|
</Button>
|
||||||
)}
|
}
|
||||||
</div>
|
></TooltipAnchor>
|
||||||
</div>
|
</OGDialogTrigger>
|
||||||
</td>
|
<OGDialogTemplate
|
||||||
</tr>
|
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 localize = useLocalize();
|
||||||
const { isAuthenticated } = useAuthContext();
|
const { isAuthenticated } = useAuthContext();
|
||||||
const [showLoading, setShowLoading] = useState(false);
|
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 sharedLinks = useMemo(() => data?.pages.flatMap((page) => page.sharedLinks) || [], [data]);
|
||||||
const classProp: { className?: string } = {
|
|
||||||
className: 'p-1 hover:text-black dark:hover:text-white',
|
const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170;
|
||||||
};
|
|
||||||
if (className) {
|
const skeletons = Array.from({ length: 11 }, (_, index) => {
|
||||||
classProp.className = className;
|
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) {
|
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) {
|
if (isError) {
|
||||||
|
|
@ -132,35 +165,34 @@ export default function ShareLinkTable({ className }: { className?: string }) {
|
||||||
</div>
|
</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="text-gray-300">{localize('com_nav_shared_links_empty')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid w-full gap-2',
|
'-mr-2 grid max-h-[350px] w-full flex-1 flex-col gap-2 overflow-y-auto pr-2 transition-opacity duration-500',
|
||||||
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
className,
|
||||||
'max-h-[350px]',
|
|
||||||
)}
|
)}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<table className="table-fixed text-left">
|
<Table>
|
||||||
<thead className="sticky top-0 bg-white dark:bg-gray-700">
|
<TableHeader>
|
||||||
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
|
<TableRow>
|
||||||
<th className="p-3">{localize('com_nav_shared_links_name')}</th>
|
<TableHead>{localize('com_nav_shared_links_name')}</TableHead>
|
||||||
<th className="p-3">{localize('com_nav_shared_links_date_shared')}</th>
|
<TableHead>{localize('com_nav_shared_links_date_shared')}</TableHead>
|
||||||
</tr>
|
<TableHead className="text-right">{localize('com_assistants_actions')}</TableHead>
|
||||||
</thead>
|
</TableRow>
|
||||||
<tbody>
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
{sharedLinks.map((sharedLink) => (
|
{sharedLinks.map((sharedLink) => (
|
||||||
<ShareLinkRow key={sharedLink.shareId} sharedLink={sharedLink} />
|
<ShareLinkRow key={sharedLink.shareId} sharedLink={sharedLink} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</TableBody>
|
||||||
</table>
|
</Table>
|
||||||
{(isFetchingNextPage || showLoading) && (
|
{(isFetchingNextPage || showLoading) && <Spinner className="mx-auto my-4" />}
|
||||||
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { OGDialog, OGDialogTrigger } from '~/components/ui';
|
import { OGDialog, OGDialogTrigger, Button } from '~/components';
|
||||||
|
|
||||||
import ArchivedChatsTable from './ArchivedChatsTable';
|
import ArchivedChatsTable from './ArchivedChatsTable';
|
||||||
|
|
||||||
|
|
@ -12,9 +12,9 @@ export default function ArchivedChats() {
|
||||||
<div>{localize('com_nav_archived_chats')}</div>
|
<div>{localize('com_nav_archived_chats')}</div>
|
||||||
<OGDialog>
|
<OGDialog>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<button className="btn btn-neutral relative ">
|
<Button variant="outline" aria-label="Archived chats">
|
||||||
{localize('com_nav_archived_chats_manage')}
|
{localize('com_nav_archived_chats_manage')}
|
||||||
</button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title={localize('com_nav_archived_chats')}
|
title={localize('com_nav_archived_chats')}
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,252 @@
|
||||||
import { useMemo, useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { MessageCircle, ArchiveRestore } from 'lucide-react';
|
|
||||||
import { useConversationsInfiniteQuery } from '~/data-provider';
|
import { useConversationsInfiniteQuery } from '~/data-provider';
|
||||||
import { ConversationListResponse } from 'librechat-data-provider';
|
import {
|
||||||
import { useAuthContext, useLocalize, useNavScrolling, useArchiveHandler } from '~/hooks';
|
Search,
|
||||||
import { DeleteButton } from '~/components/Conversations/ConvoOptions';
|
ChevronRight,
|
||||||
import { TooltipAnchor } from '~/components/ui';
|
ChevronLeft,
|
||||||
import { Spinner } from '~/components/svg';
|
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';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function ArchivedChatsTable() {
|
export default function ArchivedChatsTable() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { isAuthenticated } = useAuthContext();
|
const { isAuthenticated } = useAuthContext();
|
||||||
const [showLoading, setShowLoading] = useState(false);
|
|
||||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
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(
|
const { data, isLoading, refetch } = useConversationsInfiniteQuery(
|
||||||
{ pageNumber: '1', isArchived: true },
|
{ pageNumber: currentPage.toString(), limit: 10, isArchived: true },
|
||||||
{ enabled: isAuthenticated },
|
{ enabled: isAuthenticated && isOpened },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { containerRef, moveToTop } = useNavScrolling<ConversationListResponse>({
|
useEffect(() => {
|
||||||
setShowLoading,
|
if (data) {
|
||||||
hasNextPage: hasNextPage,
|
setTotalPages(Math.ceil(Number(data.pages)));
|
||||||
fetchNextPage: fetchNextPage,
|
}
|
||||||
isFetchingNextPage: isFetchingNextPage,
|
}, [data]);
|
||||||
|
|
||||||
|
const archiveHandler = useArchiveHandler(conversationId ?? '', false, () => {
|
||||||
|
refetch();
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversations = useMemo(
|
const handleChatClick = useCallback((conversationId) => {
|
||||||
() => data?.pages.flatMap((page) => page.conversations) || [],
|
window.open(`/c/${conversationId}`, '_blank');
|
||||||
[data],
|
}, []);
|
||||||
);
|
|
||||||
|
|
||||||
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>;
|
return <div className="text-gray-300">{localize('com_nav_archived_chats_empty')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conversations = data.pages.flatMap((page) => page.conversations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid w-full gap-2',
|
'grid w-full gap-2',
|
||||||
'flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
'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">
|
<div className="flex items-center">
|
||||||
<thead className="sticky top-0 bg-white dark:bg-gray-700">
|
<Search className="size-4 text-text-secondary" />
|
||||||
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
|
<Input
|
||||||
<th className="p-3">{localize('com_nav_archive_name')}</th>
|
type="text"
|
||||||
<th className="p-3">{localize('com_nav_archive_created_at')}</th>
|
placeholder={localize('com_nav_search_placeholder')}
|
||||||
</tr>
|
value={searchQuery}
|
||||||
</thead>
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
<tbody>
|
className="w-full border-none"
|
||||||
{conversations.map((conversation) => {
|
/>
|
||||||
if (!conversation.conversationId) {
|
</div>
|
||||||
return null;
|
<Separator />
|
||||||
}
|
{conversations.length === 0 ? (
|
||||||
return (
|
<div className="mt-4 text-text-secondary">{localize('com_nav_no_search_results')}</div>
|
||||||
<tr
|
) : (
|
||||||
key={conversation.conversationId}
|
<>
|
||||||
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
|
<Table>
|
||||||
>
|
<TableHeader>
|
||||||
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
|
<TableRow>
|
||||||
<MessageCircle className="mr-1 h-5 w-5" />
|
<TableHead className="w-[50%] p-4">{localize('com_nav_archive_name')}</TableHead>
|
||||||
{conversation.title}
|
<TableHead className="w-[35%] p-1">
|
||||||
</td>
|
{localize('com_nav_archive_created_at')}
|
||||||
<td className="p-1">
|
</TableHead>
|
||||||
<div className="flex justify-between">
|
<TableHead className="w-[15%] p-1 text-right">
|
||||||
<div className="flex justify-start dark:text-gray-200">
|
{localize('com_assistants_actions')}
|
||||||
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
</TableHead>
|
||||||
month: 'long',
|
</TableRow>
|
||||||
day: 'numeric',
|
</TableHeader>
|
||||||
year: 'numeric',
|
<TableBody>
|
||||||
})}
|
{conversations.map((conversation: TConversation) => (
|
||||||
</div>
|
<TableRow key={conversation.conversationId} className="hover:bg-transparent">
|
||||||
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
|
<TableCell className="flex items-center py-3 text-text-primary">
|
||||||
<TooltipAnchor
|
<button
|
||||||
description={localize('com_ui_unarchive')}
|
className="flex"
|
||||||
onClick={() => {
|
aria-label="Open conversation in a new tab"
|
||||||
setConversationId(conversation.conversationId);
|
onClick={() => handleChatClick(conversation.conversationId)}
|
||||||
archiveHandler();
|
>
|
||||||
}}
|
<MessageCircle className="mr-1 h-5 w-5" />
|
||||||
className="cursor-pointer hover:text-black dark:hover:text-white"
|
<u>{conversation.title}</u>
|
||||||
>
|
</button>
|
||||||
<ArchiveRestore className="size-4 hover:text-gray-300" />
|
</TableCell>
|
||||||
</TooltipAnchor>
|
<TableCell className="p-1">
|
||||||
<div className="size-5 hover:text-gray-300">
|
<div className="flex justify-between">
|
||||||
<DeleteButton
|
<div className="flex justify-start text-text-secondary">
|
||||||
conversationId={conversation.conversationId}
|
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
||||||
retainView={moveToTop}
|
month: 'long',
|
||||||
title={conversation.title ?? ''}
|
day: 'numeric',
|
||||||
/>
|
year: 'numeric',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</td>
|
<TableCell className="flex items-center justify-end gap-2 p-1">
|
||||||
</tr>
|
<TooltipAnchor
|
||||||
);
|
description={localize('com_ui_unarchive')}
|
||||||
})}
|
render={
|
||||||
</tbody>
|
<Button
|
||||||
</table>
|
aria-label="Unarchive conversation"
|
||||||
{(isFetchingNextPage || showLoading) && (
|
variant="ghost"
|
||||||
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export default function AutoScrollSwitch({
|
||||||
<Switch
|
<Switch
|
||||||
id="autoScroll"
|
id="autoScroll"
|
||||||
checked={autoScroll}
|
checked={autoScroll}
|
||||||
|
aria-label="Auto-Scroll to latest message on chat open"
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4 mt-2 ring-ring-primary"
|
className="ml-4 mt-2 ring-ring-primary"
|
||||||
data-testid="autoScroll"
|
data-testid="autoScroll"
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,7 @@ export const ThemeSelector = ({
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={themeOptions}
|
options={themeOptions}
|
||||||
sizeClasses="w-[220px]"
|
sizeClasses="w-[180px]"
|
||||||
anchor="bottom start"
|
|
||||||
testId="theme-selector"
|
testId="theme-selector"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -112,7 +111,6 @@ export const LangSelector = ({
|
||||||
value={langcode}
|
value={langcode}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
sizeClasses="[--anchor-max-height:256px]"
|
sizeClasses="[--anchor-max-height:256px]"
|
||||||
anchor="bottom start"
|
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,26 +147,24 @@ function General() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<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} />
|
<ThemeSelector theme={theme} onChange={changeTheme} />
|
||||||
</div>
|
</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} />
|
<LangSelector langcode={langcode} onChange={changeLang} />
|
||||||
</div>
|
</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 />
|
<UserMsgMarkdownSwitch />
|
||||||
</div>
|
</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 />
|
<AutoScrollSwitch />
|
||||||
</div>
|
</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 />
|
<HideSidePanelSwitch />
|
||||||
</div>
|
</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 />
|
<ArchivedChats />
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="border-b pb-3 last-of-type:border-b-0 border-border-medium">
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export default function HideSidePanelSwitch({
|
||||||
<Switch
|
<Switch
|
||||||
id="hideSidePanel"
|
id="hideSidePanel"
|
||||||
checked={hideSidePanel}
|
checked={hideSidePanel}
|
||||||
|
aria-label="Hide right-most side panel"
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={handleCheckedChange}
|
||||||
className="ml-4 mt-2"
|
className="ml-4 mt-2"
|
||||||
data-testid="hideSidePanel"
|
data-testid="hideSidePanel"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'test/matchMedia.mock';
|
import 'test/matchMedia.mock';
|
||||||
import React from 'react';
|
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 '@testing-library/jest-dom/extend-expect';
|
||||||
import { LangSelector } from './General';
|
import { LangSelector } from './General';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
@ -18,14 +18,15 @@ describe('LangSelector', () => {
|
||||||
unobserve = jest.fn();
|
unobserve = jest.fn();
|
||||||
disconnect = jest.fn();
|
disconnect = jest.fn();
|
||||||
};
|
};
|
||||||
const { getByText } = render(
|
const { getByText, getByRole } = render(
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
||||||
</RecoilRoot>,
|
</RecoilRoot>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText('Language')).toBeInTheDocument();
|
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 () => {
|
it('calls onChange when the select value changes', async () => {
|
||||||
|
|
@ -34,25 +35,23 @@ describe('LangSelector', () => {
|
||||||
unobserve = jest.fn();
|
unobserve = jest.fn();
|
||||||
disconnect = jest.fn();
|
disconnect = jest.fn();
|
||||||
};
|
};
|
||||||
const { getByText, getByTestId } = render(
|
const { getByRole, getByTestId } = render(
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
<LangSelector langcode="en-US" onChange={mockOnChange} />
|
||||||
</RecoilRoot>,
|
</RecoilRoot>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText('English')).toBeInTheDocument();
|
expect(getByRole('combobox')).toHaveTextContent('English');
|
||||||
|
|
||||||
// Find the dropdown button by data-testid
|
|
||||||
const dropdownButton = getByTestId('dropdown-menu');
|
const dropdownButton = getByTestId('dropdown-menu');
|
||||||
|
|
||||||
// Open the dropdown
|
|
||||||
fireEvent.click(dropdownButton);
|
fireEvent.click(dropdownButton);
|
||||||
|
|
||||||
// Find the option by text and click it
|
const italianOption = getByRole('option', { name: 'Italiano' });
|
||||||
const darkOption = getByText('Italiano');
|
fireEvent.click(italianOption);
|
||||||
fireEvent.click(darkOption);
|
|
||||||
|
|
||||||
// Ensure that the onChange is called with the expected value after a short delay
|
await waitFor(() => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
expect(mockOnChange).toHaveBeenCalledWith('it-IT');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,15 @@ describe('ThemeSelector', () => {
|
||||||
unobserve = jest.fn();
|
unobserve = jest.fn();
|
||||||
disconnect = jest.fn();
|
disconnect = jest.fn();
|
||||||
};
|
};
|
||||||
const { getByText } = render(
|
const { getByText, getByRole } = render(
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<ThemeSelector theme="system" onChange={mockOnChange} />
|
<ThemeSelector theme="system" onChange={mockOnChange} />
|
||||||
</RecoilRoot>,
|
</RecoilRoot>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText('Theme')).toBeInTheDocument();
|
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 () => {
|
it('calls onChange when the select value changes', async () => {
|
||||||
|
|
@ -44,17 +45,13 @@ describe('ThemeSelector', () => {
|
||||||
|
|
||||||
expect(getByText('Theme')).toBeInTheDocument();
|
expect(getByText('Theme')).toBeInTheDocument();
|
||||||
|
|
||||||
// Find the dropdown button by data-testid
|
|
||||||
const dropdownButton = getByTestId('theme-selector');
|
const dropdownButton = getByTestId('theme-selector');
|
||||||
|
|
||||||
// Open the dropdown
|
|
||||||
fireEvent.click(dropdownButton);
|
fireEvent.click(dropdownButton);
|
||||||
|
|
||||||
// Find the option by text and click it
|
|
||||||
const darkOption = getByText('Dark');
|
const darkOption = getByText('Dark');
|
||||||
fireEvent.click(darkOption);
|
fireEvent.click(darkOption);
|
||||||
|
|
||||||
// Ensure that the onChange is called with the expected value
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockOnChange).toHaveBeenCalledWith('dark');
|
expect(mockOnChange).toHaveBeenCalledWith('dark');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ const EngineSTTDropdown: React.FC<EngineSTTDropdownProps> = ({ external }) => {
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
options={endpointOptions}
|
options={endpointOptions}
|
||||||
sizeClasses="w-[180px]"
|
sizeClasses="w-[180px]"
|
||||||
anchor="bottom start"
|
|
||||||
testId="EngineSTTDropdown"
|
testId="EngineSTTDropdown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -146,14 +146,12 @@ function Speech() {
|
||||||
value={advancedMode ? 'advanced' : 'simple'}
|
value={advancedMode ? 'advanced' : 'simple'}
|
||||||
>
|
>
|
||||||
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
|
<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
|
<Tabs.Trigger
|
||||||
onClick={() => setAdvancedMode(false)}
|
onClick={() => setAdvancedMode(false)}
|
||||||
className={cn(
|
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',
|
'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
|
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
|
||||||
? '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',
|
|
||||||
'w-full',
|
'w-full',
|
||||||
)}
|
)}
|
||||||
value="simple"
|
value="simple"
|
||||||
|
|
@ -165,10 +163,8 @@ function Speech() {
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
onClick={() => setAdvancedMode(true)}
|
onClick={() => setAdvancedMode(true)}
|
||||||
className={cn(
|
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',
|
'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
|
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
|
||||||
? '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',
|
|
||||||
'w-full',
|
'w-full',
|
||||||
)}
|
)}
|
||||||
value="advanced"
|
value="advanced"
|
||||||
|
|
@ -181,79 +177,53 @@ function Speech() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs.Content value={'simple'}>
|
<Tabs.Content value={'simple'}>
|
||||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<SpeechToTextSwitch />
|
||||||
<SpeechToTextSwitch />
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
</div>
|
<LanguageSTTDropdown />
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<div className="h-px bg-border-medium" role="none" />
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
<TextToSpeechSwitch />
|
||||||
</div>
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<VoiceDropdown />
|
||||||
<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>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
<Tabs.Content value={'advanced'}>
|
<Tabs.Content value={'advanced'}>
|
||||||
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
|
<div className="flex flex-col gap-3 text-sm text-text-primary">
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
<ConversationModeSwitch />
|
||||||
<ConversationModeSwitch />
|
<div className="mt-2 h-px bg-border-medium" role="none" />
|
||||||
</div>
|
<SpeechToTextSwitch />
|
||||||
<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">
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
<SpeechToTextSwitch />
|
|
||||||
</div>
|
<LanguageSTTDropdown />
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-2">
|
||||||
<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">
|
|
||||||
<AutoTranscribeAudioSwitch />
|
<AutoTranscribeAudioSwitch />
|
||||||
</div>
|
</div>
|
||||||
{autoTranscribeAudio && (
|
{autoTranscribeAudio && (
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-2">
|
||||||
<DecibelSelector />
|
<DecibelSelector />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-2">
|
||||||
<AutoSendTextSelector />
|
<AutoSendTextSelector />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
<div className="h-px bg-border-medium" role="none" />
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-3">
|
||||||
<TextToSpeechSwitch />
|
<TextToSpeechSwitch />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<AutomaticPlaybackSwitch />
|
||||||
<AutomaticPlaybackSwitch />
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
</div>
|
<VoiceDropdown />
|
||||||
<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>
|
|
||||||
{engineTTS === 'browser' && (
|
{engineTTS === 'browser' && (
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-2">
|
||||||
<CloudBrowserVoicesSwitch />
|
<CloudBrowserVoicesSwitch />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="pb-2">
|
||||||
<PlaybackRate />
|
<PlaybackRate />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<CacheTTSSwitch />
|
||||||
<CacheTTSSwitch />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export * from './ExportConversation';
|
export * from './ExportConversation';
|
||||||
export * from './SettingsTabs/';
|
export * from './SettingsTabs/';
|
||||||
export { default as ClearConvos } from './ClearConvos';
|
export { default as ClearConvos } from './ClearConvos';
|
||||||
export { default as Logout } from './Logout';
|
|
||||||
export { default as MobileNav } from './MobileNav';
|
export { default as MobileNav } from './MobileNav';
|
||||||
export { default as Nav } from './Nav';
|
export { default as Nav } from './Nav';
|
||||||
export { default as NavLink } from './NavLink';
|
export { default as NavLink } from './NavLink';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function FilterItem({
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={onClick}
|
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}
|
{icon}
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default function List({
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="mx-2 w-full px-3"
|
className="mx-2 w-full bg-transparent px-3"
|
||||||
onClick={() => navigate('/d/prompts/new')}
|
onClick={() => navigate('/d/prompts/new')}
|
||||||
>
|
>
|
||||||
+ {localize('com_ui_create_prompt')}
|
+ {localize('com_ui_create_prompt')}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useLocalize, useCustomLink } from '~/hooks';
|
import { useLocalize, useCustomLink } from '~/hooks';
|
||||||
import { buttonVariants } from '~/components/ui';
|
import { Button } from '~/components/ui';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -15,13 +15,10 @@ export default function ManagePrompts({ className }: { className?: string }) {
|
||||||
}, [setPromptsName, setPromptsCategory]);
|
}, [setPromptsName, setPromptsCategory]);
|
||||||
|
|
||||||
const clickHandler = useCustomLink('/d/prompts', clickCallback);
|
const clickHandler = useCustomLink('/d/prompts', clickCallback);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<Button variant="outline" className={cn(className, 'bg-transparent')} onClick={clickHandler}>
|
||||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
|
||||||
href="/d/prompts"
|
|
||||||
onClick={clickHandler}
|
|
||||||
>
|
|
||||||
{localize('com_ui_manage')}
|
{localize('com_ui_manage')}
|
||||||
</a>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default function PromptsView() {
|
||||||
<GroupSidePanel isDetailView={isDetailView} {...groupsNav}>
|
<GroupSidePanel isDetailView={isDetailView} {...groupsNav}>
|
||||||
<div className="mx-2 mt-1 flex flex-row items-center justify-between">
|
<div className="mx-2 mt-1 flex flex-row items-center justify-between">
|
||||||
<FilterPrompts setName={groupsNav.setName} />
|
<FilterPrompts setName={groupsNav.setName} />
|
||||||
<AutoSendPrompt className="text-xs dark:text-white" />
|
<AutoSendPrompt className="text-xs text-text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</GroupSidePanel>
|
</GroupSidePanel>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
import type { NavLink, NavProps } from '~/common';
|
import type { NavLink, NavProps } from '~/common';
|
||||||
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
||||||
import { buttonVariants } from '~/components/ui/Button';
|
import { buttonVariants } from '~/components/ui/Button';
|
||||||
|
import { TooltipAnchor, Button } from '~/components';
|
||||||
import { cn, removeFocusOutlines } from '~/utils';
|
import { cn, removeFocusOutlines } from '~/utils';
|
||||||
import { TooltipAnchor } from '~/components/ui';
|
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
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);
|
const variant = getVariant(link);
|
||||||
return isCollapsed ? (
|
return isCollapsed ? (
|
||||||
<TooltipAnchor
|
<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)}
|
description={localize(link.title)}
|
||||||
side="left"
|
side="left"
|
||||||
>
|
render={
|
||||||
<link.icon className="h-4 w-4" />
|
<Button
|
||||||
<span className="sr-only">{link.title}</span>
|
variant="ghost"
|
||||||
</TooltipAnchor>
|
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
|
<Accordion
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -65,16 +63,10 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
||||||
<AccordionItem value={link.id} className="w-full border-none">
|
<AccordionItem value={link.id} className="w-full border-none">
|
||||||
<AccordionPrimitive.Header asChild>
|
<AccordionPrimitive.Header asChild>
|
||||||
<AccordionPrimitive.Trigger asChild>
|
<AccordionPrimitive.Trigger asChild>
|
||||||
<button
|
<Button
|
||||||
className={cn(
|
variant="outline"
|
||||||
buttonVariants({ variant, size: 'sm' }),
|
size="sm"
|
||||||
removeFocusOutlines,
|
className="w-full justify-start bg-transparent text-text-secondary data-[state=open]:bg-surface-secondary data-[state=open]:text-text-primary"
|
||||||
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',
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (link.onClick) {
|
if (link.onClick) {
|
||||||
link.onClick(e);
|
link.onClick(e);
|
||||||
|
|
@ -95,7 +87,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
||||||
{link.label}
|
{link.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ const SidePanel = ({
|
||||||
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
localStorage.setItem('react-resizable-panels:collapsed', 'true');
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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]',
|
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
|
||||||
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
|
||||||
? 'hidden min-w-0'
|
? 'hidden min-w-0'
|
||||||
|
|
@ -264,7 +264,7 @@ const SidePanel = ({
|
||||||
{interfaceConfig.modelSelect && (
|
{interfaceConfig.modelSelect && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,28 @@
|
||||||
import * as React from 'react';
|
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';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300',
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
|
|
||||||
outline:
|
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',
|
'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
subtle:
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost:
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
'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',
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-10 py-2 px-4',
|
default: 'h-10 px-4 py-2',
|
||||||
sm: 'h-8 px-3 rounded',
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: 'h-12 px-6 rounded-md',
|
lg: 'h-11 rounded-md px-8',
|
||||||
xl: 'h-14 px-8 rounded-lg text-base',
|
icon: 'size-10',
|
||||||
icon: 'h-10 w-10',
|
|
||||||
},
|
|
||||||
fullWidth: {
|
|
||||||
true: 'w-full',
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
true: 'opacity-80 pointer-events-none',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'default',
|
size: 'default',
|
||||||
|
|
@ -57,62 +33,14 @@ const buttonVariants = cva(
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
loading?: boolean;
|
asChild?: boolean;
|
||||||
leftIcon?: React.ReactNode;
|
|
||||||
rightIcon?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: string }>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
(
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
{
|
const Comp = asChild ? Slot : 'button';
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
fullWidth,
|
|
||||||
loading,
|
|
||||||
leftIcon,
|
|
||||||
rightIcon,
|
|
||||||
children,
|
|
||||||
customId,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
import React, { FC, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import * as Select from '@ariakit/react/select';
|
||||||
Listbox,
|
|
||||||
ListboxButton,
|
|
||||||
ListboxOption,
|
|
||||||
ListboxOptions,
|
|
||||||
Transition,
|
|
||||||
} from '@headlessui/react';
|
|
||||||
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
|
|
||||||
import type { Option } from '~/common';
|
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
import type { Option } from '~/common';
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -16,112 +9,89 @@ interface DropdownProps {
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
options: string[] | Option[];
|
options: string[] | Option[];
|
||||||
className?: string;
|
className?: string;
|
||||||
anchor?: AnchorPropsWithSelection;
|
|
||||||
sizeClasses?: string;
|
sizeClasses?: string;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown: FC<DropdownProps> = ({
|
const Dropdown: React.FC<DropdownProps> = ({
|
||||||
value: initialValue,
|
value: initialValue,
|
||||||
label = '',
|
label = '',
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
className = '',
|
className = '',
|
||||||
anchor,
|
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
testId = 'dropdown-menu',
|
testId = 'dropdown-menu',
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedValue, setSelectedValue] = useState(initialValue);
|
const [selectedValue, setSelectedValue] = useState(initialValue);
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
setSelectedValue(value);
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectProps = Select.useSelectStore({
|
||||||
|
value: selectedValue,
|
||||||
|
setValue: handleChange,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
<Listbox
|
<Select.Select
|
||||||
value={selectedValue}
|
store={selectProps}
|
||||||
onChange={(newValue) => {
|
className={cn(
|
||||||
setSelectedValue(newValue);
|
'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',
|
||||||
onChange(newValue);
|
className,
|
||||||
}}
|
)}
|
||||||
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
<div className={cn('relative', className)}>
|
<div className="flex w-full items-center justify-between">
|
||||||
<ListboxButton
|
<span className="block truncate">
|
||||||
data-testid={testId}
|
{label}
|
||||||
className={cn(
|
{options
|
||||||
'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',
|
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||||
className,
|
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||||
)}
|
</span>
|
||||||
aria-label="Select an option"
|
<Select.SelectArrow />
|
||||||
>
|
|
||||||
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className = '', ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ interface DropdownProps {
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mainly used for the Speech Voice Selection Dropdown
|
||||||
|
*/
|
||||||
|
|
||||||
const Dropdown: FC<DropdownProps> = ({
|
const Dropdown: FC<DropdownProps> = ({
|
||||||
value,
|
value,
|
||||||
label = '',
|
label = '',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
className={cn(
|
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 ?? '',
|
className ?? '',
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||||
overlayClassName={overlayClassName}
|
overlayClassName={overlayClassName}
|
||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('border-none bg-background text-foreground', className ?? '')}
|
||||||
'bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300',
|
|
||||||
className ?? '',
|
|
||||||
)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<OGDialogHeader className={cn(headerClassName ?? '')}>
|
<OGDialogHeader className={cn(headerClassName ?? '')}>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -41,14 +41,14 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{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" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|
@ -89,7 +89,7 @@ const DialogDescription = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-muted-foreground text-sm', className)}
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
{...props}
|
{...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}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-border shrink-0',
|
'shrink-0 bg-border-light',
|
||||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,22 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
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';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
const Slider = React.forwardRef<
|
||||||
doubleClickHandler?: clickEvent;
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
trackClassName?: string;
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
}
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
ref={ref}
|
||||||
(
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
{ className, trackClassName = 'bg-gray-200 dark:bg-gray-850', doubleClickHandler, ...props },
|
{...props}
|
||||||
ref,
|
>
|
||||||
) => (
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
ref={ref}
|
</SliderPrimitive.Track>
|
||||||
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
|
<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" />
|
||||||
{...props}
|
</SliderPrimitive.Root>
|
||||||
>
|
));
|
||||||
<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>
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider };
|
export { Slider };
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||||
|
import { cn } from '~/utils';
|
||||||
import { cn } from '../../utils';
|
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
const Switch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
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',
|
'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',
|
||||||
'ring-ring-primary',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -18,7 +16,7 @@ const Switch = React.forwardRef<
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
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>
|
</SwitchPrimitives.Root>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { cn } from '~/utils';
|
||||||
import { cn } from '../../utils';
|
|
||||||
|
|
||||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
({ className = '', ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Table.displayName = 'Table';
|
Table.displayName = 'Table';
|
||||||
|
|
@ -12,7 +13,7 @@ Table.displayName = 'Table';
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||||
));
|
));
|
||||||
TableHeader.displayName = 'TableHeader';
|
TableHeader.displayName = 'TableHeader';
|
||||||
|
|
@ -20,7 +21,7 @@ TableHeader.displayName = 'TableHeader';
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||||
));
|
));
|
||||||
TableBody.displayName = 'TableBody';
|
TableBody.displayName = 'TableBody';
|
||||||
|
|
@ -28,21 +29,21 @@ TableBody.displayName = 'TableBody';
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tfoot
|
<tfoot
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
TableFooter.displayName = 'TableFooter';
|
TableFooter.displayName = 'TableFooter';
|
||||||
|
|
||||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
({ className = '', ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -54,7 +55,7 @@ TableRow.displayName = 'TableRow';
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -69,10 +70,10 @@ TableHead.displayName = 'TableHead';
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
@ -81,7 +82,7 @@ TableCell.displayName = 'TableCell';
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<
|
||||||
HTMLTableCaptionElement,
|
HTMLTableCaptionElement,
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
||||||
));
|
));
|
||||||
TableCaption.displayName = 'TableCaption';
|
TableCaption.displayName = 'TableCaption';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
import { cn } from '~/utils';
|
||||||
import { cn } from '../../utils';
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root;
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
|
@ -12,7 +11,7 @@ const TabsList = React.forwardRef<
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -26,7 +25,7 @@ const TabsTrigger = React.forwardRef<
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className = '', ...props }, ref) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -39,11 +38,7 @@ const TabsContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
>(({ className = '', ...props }, ref) => (
|
>(({ className = '', ...props }, ref) => (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content className={cn('mt-2 rounded-md p-6', className)} {...props} ref={ref} />
|
||||||
className={cn('mt-2 rounded-md border border-gray-200 p-6 dark:border-gray-700', className)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { forwardRef, useMemo } from 'react';
|
import { forwardRef, useMemo } from 'react';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
||||||
description: string;
|
description: string;
|
||||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||||
{ description, side = 'top', role, ...props },
|
{ description, side = 'top', className, role, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const tooltip = Ariakit.useTooltipStore({ placement: side });
|
const tooltip = Ariakit.useTooltipStore({ placement: side });
|
||||||
|
|
@ -40,7 +42,13 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
|
<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>
|
<AnimatePresence>
|
||||||
{mounted && (
|
{mounted && (
|
||||||
<Ariakit.Tooltip
|
<Ariakit.Tooltip
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export * from './Tag';
|
||||||
export * from './Textarea';
|
export * from './Textarea';
|
||||||
export * from './TextareaAutosize';
|
export * from './TextareaAutosize';
|
||||||
export * from './Tooltip';
|
export * from './Tooltip';
|
||||||
|
export * from './Pagination';
|
||||||
export { default as Combobox } from './Combobox';
|
export { default as Combobox } from './Combobox';
|
||||||
export { default as Dropdown } from './Dropdown';
|
export { default as Dropdown } from './Dropdown';
|
||||||
export { default as FileUpload } from './FileUpload';
|
export { default as FileUpload } from './FileUpload';
|
||||||
|
|
|
||||||
|
|
@ -150,12 +150,12 @@ export const useConversationsInfiniteQuery = (
|
||||||
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
|
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
|
||||||
) => {
|
) => {
|
||||||
return useInfiniteQuery<ConversationListResponse, unknown>(
|
return useInfiniteQuery<ConversationListResponse, unknown>(
|
||||||
params?.isArchived ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
|
params?.isArchived === true ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
|
||||||
({ pageParam = '' }) =>
|
({ pageParam = '' }) =>
|
||||||
dataService.listConversations({
|
dataService.listConversations({
|
||||||
...params,
|
...params,
|
||||||
pageNumber: pageParam?.toString(),
|
pageNumber: pageParam?.toString(),
|
||||||
isArchived: params?.isArchived || false,
|
isArchived: params?.isArchived ?? false,
|
||||||
tags: params?.tags || [],
|
tags: params?.tags || [],
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -777,6 +777,7 @@ export default {
|
||||||
com_nav_slash_command_description: 'Toggle command "/" for selecting a prompt via keyboard',
|
com_nav_slash_command_description: 'Toggle command "/" for selecting a prompt via keyboard',
|
||||||
com_nav_command_settings: 'Command Settings',
|
com_nav_command_settings: 'Command Settings',
|
||||||
com_nav_command_settings_description: 'Customize which commands are available in the chat',
|
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_general: 'General',
|
||||||
com_nav_setting_chat: 'Chat',
|
com_nav_setting_chat: 'Chat',
|
||||||
com_nav_setting_beta: 'Beta features',
|
com_nav_setting_beta: 'Beta features',
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ const searchQuery = atom({
|
||||||
default: '',
|
default: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSearching = atom({
|
||||||
|
key: 'isSearching',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
isSearchEnabled,
|
isSearchEnabled,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
isSearching,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,37 @@ html {
|
||||||
--surface-secondary: var(--gray-50);
|
--surface-secondary: var(--gray-50);
|
||||||
--surface-tertiary: var(--gray-100);
|
--surface-tertiary: var(--gray-100);
|
||||||
--surface-tertiary-alt: var(--white);
|
--surface-tertiary-alt: var(--white);
|
||||||
|
--surface-dialog: var(--white);
|
||||||
--border-light: var(--gray-200);
|
--border-light: var(--gray-200);
|
||||||
--border-medium-alt: var(--gray-300);
|
--border-medium-alt: var(--gray-300);
|
||||||
--border-medium: var(--gray-300);
|
--border-medium: var(--gray-300);
|
||||||
--border-heavy: var(--gray-400);
|
--border-heavy: var(--gray-400);
|
||||||
--border-xheavy: var(--gray-500);
|
--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 {
|
.dark {
|
||||||
--text-primary: var(--gray-100);
|
--text-primary: var(--gray-100);
|
||||||
|
|
@ -66,11 +92,36 @@ html {
|
||||||
--surface-secondary: var(--gray-800);
|
--surface-secondary: var(--gray-800);
|
||||||
--surface-tertiary: var(--gray-700);
|
--surface-tertiary: var(--gray-700);
|
||||||
--surface-tertiary-alt: var(--gray-700);
|
--surface-tertiary-alt: var(--gray-700);
|
||||||
|
--surface-dialog: var(--gray-850);
|
||||||
--border-light: var(--gray-700);
|
--border-light: var(--gray-700);
|
||||||
--border-medium-alt: var(--gray-600);
|
--border-medium-alt: var(--gray-600);
|
||||||
--border-medium: var(--gray-600);
|
--border-medium: var(--gray-600);
|
||||||
--border-heavy: var(--gray-500);
|
--border-heavy: var(--gray-500);
|
||||||
--border-xheavy: var(--gray-400);
|
--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 {
|
.gizmo {
|
||||||
--text-primary: var(--gizmo-gray-950);
|
--text-primary: var(--gizmo-gray-950);
|
||||||
|
|
@ -2292,7 +2343,7 @@ button.scroll-convo {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.275rem;
|
border-radius: 0.275rem;
|
||||||
background-color: var(--bg-gray-600);
|
background-color: var(--surface-primary);
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
|
@ -2322,3 +2373,67 @@ button.scroll-convo {
|
||||||
outline: 2px solid #fff;
|
outline: 2px solid #fff;
|
||||||
outline-offset: 2px;
|
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-secondary': 'var(--surface-secondary)',
|
||||||
'surface-tertiary': 'var(--surface-tertiary)',
|
'surface-tertiary': 'var(--surface-tertiary)',
|
||||||
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
||||||
|
'surface-dialog': 'var(--surface-dialog)',
|
||||||
'border-light': 'var(--border-light)',
|
'border-light': 'var(--border-light)',
|
||||||
'border-medium': 'var(--border-medium)',
|
'border-medium': 'var(--border-medium)',
|
||||||
'border-medium-alt': 'var(--border-medium-alt)',
|
'border-medium-alt': 'var(--border-medium-alt)',
|
||||||
'border-heavy': 'var(--border-heavy)',
|
'border-heavy': 'var(--border-heavy)',
|
||||||
'border-xheavy': 'var(--border-xheavy)',
|
'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": {
|
"node_modules/@floating-ui/dom": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.11",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
|
||||||
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
|
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.6.0",
|
"@floating-ui/core": "^1.6.0",
|
||||||
"@floating-ui/utils": "^0.2.1"
|
"@floating-ui/utils": "^0.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/react": {
|
"node_modules/@floating-ui/react": {
|
||||||
|
|
@ -8124,9 +8125,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||||
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
|
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@google/generative-ai": {
|
"node_modules/@google/generative-ai": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue