mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
👐 style: Improve a11y/theming for Settings Dialog, Dropdown Menus; fix: SearchBar focus issues (#4091)
* fix: cursor pointer not applying correct in the root component * fix: add cursor-not-allowed to disabled state in SendButton component * feat: update Dropdown to ariakit and changed LLM error's style * feat: switched to ariakit's Dropdown and style improvements * feat: archive updates * refactor: delete conversations in archive * refactor: settings * add cool settings animation * a11y: settings update * style: update settings * style: settings account settings menu; a11y(AccountSettings): switched to AriaKit * a11y: account settings update * style: update my files dialog * fix: tests * chore: remove console.log() --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
eba2c9a032
commit
2d62eca612
58 changed files with 1054 additions and 824 deletions
|
|
@ -50,7 +50,7 @@ const DeleteBookmarkButton: FC<{
|
||||||
<OGDialogTrigger asChild>
|
<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>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="overflow-x-auto p-0 sm:p-6 sm:pt-4">
|
|
||||||
<DataTable columns={columns} data={files} />
|
<DataTable columns={columns} data={files} />
|
||||||
<div className="mt-5 sm:mt-4" />
|
</OGDialogContent>
|
||||||
</div>
|
</OGDialog>
|
||||||
</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,17 +26,11 @@ 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 }) => (
|
|
||||||
<>
|
|
||||||
<MenuButton
|
|
||||||
aria-label={localize('com_nav_account_settings')}
|
aria-label={localize('com_nav_account_settings')}
|
||||||
className={cn(
|
|
||||||
'group-ui-open:bg-surface-tertiary duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-surface-secondary',
|
|
||||||
open ? 'bg-surface-secondary' : '',
|
|
||||||
)}
|
|
||||||
data-testid="nav-user"
|
data-testid="nav-user"
|
||||||
|
className="duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
|
||||||
>
|
>
|
||||||
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
|
|
@ -52,11 +43,16 @@ function AccountSettings() {
|
||||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
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"
|
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<img className="rounded-full" src={user?.avatar ?? avatarSrc} alt="avatar" />
|
<img
|
||||||
|
className="rounded-full"
|
||||||
|
src={user?.avatar ?? avatarSrc}
|
||||||
|
alt={`${name}'s avatar`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,78 +62,69 @@ function AccountSettings() {
|
||||||
>
|
>
|
||||||
{user?.name ?? user?.username ?? localize('com_nav_user')}
|
{user?.name ?? user?.username ?? localize('com_nav_user')}
|
||||||
</div>
|
</div>
|
||||||
</MenuButton>
|
</Select.Select>
|
||||||
|
<Select.SelectPopover
|
||||||
<Transition
|
className="popover-ui w-[235px]"
|
||||||
as={Fragment}
|
style={{
|
||||||
enter="transition ease-out duration-100 transform"
|
transformOrigin: 'bottom',
|
||||||
enterFrom="translate-y-2 opacity-0"
|
marginRight: '0px',
|
||||||
enterTo="translate-y-0 opacity-100"
|
translate: '0px',
|
||||||
leave="transition ease-in duration-100 transform"
|
}}
|
||||||
leaveFrom="translate-y-0 opacity-100"
|
|
||||||
leaveTo="translate-y-2 opacity-0"
|
|
||||||
>
|
>
|
||||||
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-border-medium bg-header-primary p-1.5 opacity-100 shadow-lg outline-none">
|
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
|
|
||||||
{user?.email ?? localize('com_nav_user')}
|
{user?.email ?? localize('com_nav_user')}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
<DropdownMenuSeparator />
|
||||||
{startupConfig?.checkBalance === true &&
|
{startupConfig?.checkBalance === true &&
|
||||||
balanceQuery.data != null &&
|
balanceQuery.data != null &&
|
||||||
!isNaN(parseFloat(balanceQuery.data)) && (
|
!isNaN(parseFloat(balanceQuery.data)) && (
|
||||||
<>
|
<>
|
||||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
|
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||||
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem>
|
<Select.SelectItem
|
||||||
{({ focus }) => (
|
value=""
|
||||||
<NavLink
|
onClick={() => setShowFiles(true)}
|
||||||
className={focus ? 'bg-surface-hover' : ''}
|
className="select-item text-sm"
|
||||||
svg={() => <FileText className="icon-md" />}
|
>
|
||||||
text={localize('com_nav_my_files')}
|
<FileText className="icon-md" aria-hidden="true" />
|
||||||
clickHandler={() => setShowFiles(true)}
|
{localize('com_nav_my_files')}
|
||||||
/>
|
</Select.SelectItem>
|
||||||
)}
|
|
||||||
</MenuItem>
|
|
||||||
{startupConfig?.helpAndFaqURL !== '/' && (
|
{startupConfig?.helpAndFaqURL !== '/' && (
|
||||||
<MenuItem>
|
<Select.SelectItem
|
||||||
{({ focus }) => (
|
value=""
|
||||||
<NavLink
|
onClick={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
|
||||||
className={focus ? 'bg-surface-hover' : ''}
|
className="select-item text-sm"
|
||||||
svg={() => <LinkIcon />}
|
>
|
||||||
text={localize('com_nav_help_faq')}
|
<LinkIcon aria-hidden="true" />
|
||||||
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
|
{localize('com_nav_help_faq')}
|
||||||
/>
|
</Select.SelectItem>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
<Select.SelectItem
|
||||||
)}
|
value=""
|
||||||
<MenuItem>
|
onClick={() => setShowSettings(true)}
|
||||||
{({ focus }) => (
|
className="select-item text-sm"
|
||||||
<NavLink
|
>
|
||||||
className={focus ? 'bg-surface-hover' : ''}
|
<GearIcon className="icon-md" aria-hidden="true" />
|
||||||
svg={() => <GearIcon className="icon-md" />}
|
{localize('com_nav_settings')}
|
||||||
text={localize('com_nav_settings')}
|
</Select.SelectItem>
|
||||||
clickHandler={() => {
|
<DropdownMenuSeparator />
|
||||||
setTimeout(() => setShowSettings(true), 50);
|
<Select.SelectItem
|
||||||
}}
|
aria-selected={true}
|
||||||
/>
|
onClick={() => logout()}
|
||||||
)}
|
value="logout"
|
||||||
</MenuItem>
|
className="select-item text-sm"
|
||||||
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
|
>
|
||||||
<MenuItem>
|
<LogOut className="icon-md" />
|
||||||
{({ focus }) => <Logout className={focus ? 'bg-surface-hover' : ''} />}
|
{localize('com_nav_log_out')}
|
||||||
</MenuItem>
|
</Select.SelectItem>
|
||||||
</MenuItems>
|
</Select.SelectPopover>
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
{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
|
|
||||||
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" />
|
<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}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
<td
|
<LinkIcon className="mr-2 h-4 w-4" />
|
||||||
className={cn(
|
|
||||||
'flex items-center py-3 text-blue-800/70 dark:text-blue-500',
|
|
||||||
isDeleting && 'opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link to={`/share/${sharedLink.shareId}`} target="_blank" rel="noreferrer" className="flex">
|
|
||||||
<LinkIcon className="mr-1 h-5 w-5" />
|
|
||||||
{sharedLink.title}
|
{sharedLink.title}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</TableCell>
|
||||||
<td className="p-3">
|
<TableCell>
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className={cn('flex justify-start dark:text-gray-200', isDeleting && 'opacity-50')}>
|
|
||||||
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</div>
|
</TableCell>
|
||||||
<div
|
<TableCell className="text-right">
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-end gap-3 text-gray-400',
|
|
||||||
isDeleting && 'opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{sharedLink.conversationId && (
|
{sharedLink.conversationId && (
|
||||||
<div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
|
<OGDialog>
|
||||||
<SharedLinkDeleteButton
|
<OGDialogTrigger asChild>
|
||||||
shareId={sharedLink.shareId}
|
<TooltipAnchor
|
||||||
setIsDeleting={setIsDeleting}
|
description={localize('com_ui_delete')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
aria-label="Delete shared link"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
></TooltipAnchor>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_conversation')}
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<>
|
||||||
|
<div className="flex w-full flex-col items-center gap-2">
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="dialog-confirm-delete"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
{localize('com_ui_delete_confirm')} <strong>{sharedLink.title}</strong>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: () => confirmDelete(sharedLink.shareId),
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</OGDialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</TableCell>
|
||||||
</div>
|
</TableRow>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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 handlePageChange = useCallback((newPage) => {
|
||||||
|
setCurrentPage(newPage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop);
|
if (isLoading) {
|
||||||
|
return <div className="text-gray-300">{skeletons}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data || conversations.length === 0) {
|
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>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[50%] p-4">{localize('com_nav_archive_name')}</TableHead>
|
||||||
|
<TableHead className="w-[35%] p-1">
|
||||||
|
{localize('com_nav_archive_created_at')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[15%] p-1 text-right">
|
||||||
|
{localize('com_assistants_actions')}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{conversations.map((conversation: TConversation) => (
|
||||||
|
<TableRow key={conversation.conversationId} className="hover:bg-transparent">
|
||||||
|
<TableCell className="flex items-center py-3 text-text-primary">
|
||||||
|
<button
|
||||||
|
className="flex"
|
||||||
|
aria-label="Open conversation in a new tab"
|
||||||
|
onClick={() => handleChatClick(conversation.conversationId)}
|
||||||
>
|
>
|
||||||
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
|
|
||||||
<MessageCircle className="mr-1 h-5 w-5" />
|
<MessageCircle className="mr-1 h-5 w-5" />
|
||||||
{conversation.title}
|
<u>{conversation.title}</u>
|
||||||
</td>
|
</button>
|
||||||
<td className="p-1">
|
</TableCell>
|
||||||
|
<TableCell className="p-1">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="flex justify-start dark:text-gray-200">
|
<div className="flex justify-start text-text-secondary">
|
||||||
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex items-center justify-end gap-2 p-1">
|
||||||
<TooltipAnchor
|
<TooltipAnchor
|
||||||
description={localize('com_ui_unarchive')}
|
description={localize('com_ui_unarchive')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
aria-label="Unarchive conversation"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setConversationId(conversation.conversationId);
|
setConversationId(conversation.conversationId);
|
||||||
archiveHandler();
|
archiveHandler();
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer hover:text-black dark:hover:text-white"
|
|
||||||
>
|
>
|
||||||
<ArchiveRestore className="size-4 hover:text-gray-300" />
|
<ArchiveRestore className="size-4" />
|
||||||
</TooltipAnchor>
|
</Button>
|
||||||
<div className="size-5 hover:text-gray-300">
|
}
|
||||||
<DeleteButton
|
></TooltipAnchor>
|
||||||
conversationId={conversation.conversationId}
|
|
||||||
retainView={moveToTop}
|
<OGDialog>
|
||||||
title={conversation.title ?? ''}
|
<OGDialogTrigger asChild>
|
||||||
/>
|
<TooltipAnchor
|
||||||
</div>
|
description={localize('com_ui_delete')}
|
||||||
</div>
|
render={
|
||||||
</div>
|
<Button
|
||||||
</td>
|
aria-label="Delete archived conversation"
|
||||||
</tr>
|
variant="ghost"
|
||||||
);
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
></TooltipAnchor>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
{DeleteConversationDialog({
|
||||||
|
conversationId: conversation.conversationId ?? '',
|
||||||
|
retainView: refetch,
|
||||||
|
title: conversation.title ?? '',
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</OGDialog>
|
||||||
</table>
|
</TableCell>
|
||||||
{(isFetchingNextPage || showLoading) && (
|
</TableRow>
|
||||||
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
|
))}
|
||||||
|
</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,80 +177,54 @@ 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 />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<LanguageSTTDropdown />
|
<LanguageSTTDropdown />
|
||||||
</div>
|
<div className="h-px bg-border-medium" role="none" />
|
||||||
<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 />
|
<TextToSpeechSwitch />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineTTSDropdown external={ttsExternal} />
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<VoiceDropdown />
|
<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>
|
<div className="mt-2 h-px bg-border-medium" role="none" />
|
||||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<SpeechToTextSwitch />
|
<SpeechToTextSwitch />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineSTTDropdown external={sttExternal} />
|
<EngineSTTDropdown external={sttExternal} />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<LanguageSTTDropdown />
|
<LanguageSTTDropdown />
|
||||||
</div>
|
<div className="pb-2">
|
||||||
<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 />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<EngineTTSDropdown external={ttsExternal} />
|
<EngineTTSDropdown external={ttsExternal} />
|
||||||
</div>
|
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
|
||||||
<VoiceDropdown />
|
<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,14 +31,12 @@ 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(
|
description={localize(link.title)}
|
||||||
buttonVariants({ variant, size: 'icon' }),
|
side="left"
|
||||||
removeFocusOutlines,
|
render={
|
||||||
'h-9 w-9 cursor-pointer',
|
<Button
|
||||||
variant === 'default'
|
variant="ghost"
|
||||||
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted bg-surface-terniary dark:hover:text-white'
|
size="icon"
|
||||||
: '',
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (link.onClick) {
|
if (link.onClick) {
|
||||||
link.onClick(e);
|
link.onClick(e);
|
||||||
|
|
@ -48,12 +46,12 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
||||||
setActive(link.id);
|
setActive(link.id);
|
||||||
resize && resize(25);
|
resize && resize(25);
|
||||||
}}
|
}}
|
||||||
description={localize(link.title)}
|
|
||||||
side="left"
|
|
||||||
>
|
>
|
||||||
<link.icon className="h-4 w-4" />
|
<link.icon className="h-4 w-4 text-text-secondary" />
|
||||||
<span className="sr-only">{link.title}</span>
|
<span className="sr-only">{link.title}</span>
|
||||||
</TooltipAnchor>
|
</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,80 +9,60 @@ 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) => {
|
|
||||||
setSelectedValue(newValue);
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={cn('relative', className)}>
|
|
||||||
<ListboxButton
|
|
||||||
data-testid={testId}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'btn-neutral focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-md border-border-light bg-header-primary py-2 pl-3 pr-8 text-text-primary transition-all duration-100 ease-in-out hover:bg-header-hover focus:ring-ring-primary',
|
'focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-lg border border-input bg-background py-2 pl-3 pr-8 text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
aria-label="Select an option"
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{label}
|
{label}
|
||||||
{options
|
{options
|
||||||
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
|
||||||
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
|
||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<Select.SelectArrow />
|
||||||
<svg
|
</div>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</Select.Select>
|
||||||
fill="none"
|
<Select.SelectPopover
|
||||||
viewBox="0 0 24 24"
|
store={selectProps}
|
||||||
strokeWidth="2"
|
className={cn('popover-ui', sizeClasses, className)}
|
||||||
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) => (
|
{options.map((item, index) => (
|
||||||
<ListboxOption
|
<Select.SelectItem
|
||||||
key={index}
|
key={index}
|
||||||
value={typeof item === 'string' ? item : item.value}
|
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"
|
className="select-item"
|
||||||
style={{ width: '100%' }}
|
|
||||||
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
data-theme={typeof item === 'string' ? item : (item as Option).value}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
|
|
@ -116,12 +89,9 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ListboxOption>
|
</Select.SelectItem>
|
||||||
))}
|
))}
|
||||||
</ListboxOptions>
|
</Select.SelectPopover>
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</Listbox>
|
|
||||||
</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 ?? '')}>
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
||||||
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
|
||||||
(
|
|
||||||
{ className, trackClassName = 'bg-gray-200 dark:bg-gray-850', doubleClickHandler, ...props },
|
|
||||||
ref,
|
|
||||||
) => (
|
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
className={cn('relative h-1 w-full grow overflow-hidden rounded-full', trackClassName)}
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
>
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
|
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb
|
<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" />
|
||||||
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>
|
</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) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
<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