💡 style: switched to Ariakit's tooltip (#3748)

* inital Tooltip implementation and test

* style(tooltip): L/R sidePanel and Nav

* style(tooltip): unarchive button; refactor: `useArchiveHandler` and `ArchiveButton`

* style(tooltip): Delete button

* refactor: remove unused className prop in DeleteButton component

* style(tooltip): finish final tooltip and fix bookmark edit and delete button

* refactor(ui): remove TooltipTest and DropDownMenu component and unused imports

* style: update mobile UI

* fix: sidePanel icon not showing

* feat(AttachFile): add tooltip

* fix(NavToggle): remove button
without this button, kb users don't have to manually press 2 times to change the focus
Also, tooltips with buttons focus don't trigger

* fix: right side panel issue with double button

* fix: merge issues

* fix: sharedLink table issue

* chore: update ariakit and framer-motion version

* a11y: kb toggle for sidebar

* feat: tooltip for some buttons
This commit is contained in:
Marco Beretta 2024-09-13 08:59:09 -04:00 committed by GitHub
parent e293ff63f9
commit 4ef5ae6f71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 747 additions and 967 deletions

View file

@ -28,7 +28,7 @@
},
"homepage": "https://librechat.ai",
"dependencies": {
"@ariakit/react": "^0.4.8",
"@ariakit/react": "^0.4.11",
"@codesandbox/sandpack-react": "^2.18.2",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
@ -63,6 +63,7 @@
"downloadjs": "^1.4.7",
"export-from-json": "^1.7.2",
"filenamify": "^6.0.0",
"framer-motion": "^11.5.4",
"html-to-image": "^1.11.11",
"image-blob-reduce": "^4.1.0",
"js-cookie": "^3.0.5",

View file

@ -1,11 +1,11 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components/ui';
import { useDeleteConversationTagMutation } from '~/data-provider';
import TooltipIcon from '~/components/ui/TooltipIcon';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg';
import { Label } from '~/components/ui';
import { useLocalize } from '~/hooks';
const DeleteBookmarkButton: FC<{
@ -16,6 +16,7 @@ const DeleteBookmarkButton: FC<{
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const [open, setOpen] = useState(false);
const deleteBookmarkMutation = useDeleteConversationTagMutation({
onSuccess: () => {
@ -35,23 +36,48 @@ const DeleteBookmarkButton: FC<{
await deleteBookmarkMutation.mutateAsync(bookmark);
}, [bookmark, deleteBookmarkMutation]);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
setOpen(!open);
}
};
return (
<TooltipIcon
disabled={false}
appendLabel={false}
title="Delete Bookmark"
confirmMessage={
<Label htmlFor="bookmark" className="text-left text-sm font-medium">
{localize('com_ui_bookmark_delete_confirm')} {bookmark}
</Label>
}
confirm={confirmDelete}
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
icon={<TrashIcon className="size-4" />}
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
/>
<>
<OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild>
<TooltipAnchor
description={localize('com_ui_delete')}
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
onClick={() => setOpen(!open)}
onKeyDown={handleKeyDown}
>
<TrashIcon className="size-4" />
</TooltipAnchor>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_bookmarks_delete')}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_ui_bookmark_delete_confirm')} {bookmark}
</Label>
}
selection={{
selectHandler: confirmDelete,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
</>
);
};

View file

@ -2,9 +2,9 @@ import { useState } from 'react';
import type { FC } from 'react';
import type { TConversationTag } from 'librechat-data-provider';
import BookmarkEditDialog from './BookmarkEditDialog';
import { TooltipAnchor } from '~/components/ui';
import { EditIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
const EditBookmarkButton: FC<{
bookmark: TConversationTag;
@ -15,6 +15,12 @@ const EditBookmarkButton: FC<{
const localize = useLocalize();
const [open, setOpen] = useState(false);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
setOpen(!open);
}
};
return (
<>
<BookmarkEditDialog
@ -23,25 +29,17 @@ const EditBookmarkButton: FC<{
open={open}
setOpen={setOpen}
/>
<button
type="button"
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
<TooltipAnchor
description={localize('com_ui_edit')}
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
onClick={() => setOpen(!open)}
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onKeyDown={handleKeyDown}
>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<EditIcon />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_edit')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
<EditIcon />
</TooltipAnchor>
</>
);
};

View file

@ -2,13 +2,15 @@ import { PlusCircle } from 'lucide-react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
import { mainTextareaId } from '~/common';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
function AddMultiConvo({ className = '' }: { className?: string }) {
function AddMultiConvo() {
const { conversation } = useChatContext();
const { setConversation: setAddedConvo } = useAddedChatContext();
const localize = useLocalize();
const clickHandler = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -33,15 +35,18 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
}
return (
<Button
<TooltipAnchor
id="add-multi-conversation-button"
aria-label="Add multi-conversation"
aria-label={localize('com_ui_add_multi_conversation')}
description={localize('com_ui_add_multi_conversation')}
tabIndex={0}
role="button"
onClick={clickHandler}
variant="outline"
className={cn('h-10 w-10 p-0 transition-all duration-300 ease-in-out', className)}
data-testid="parameters-button"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<PlusCircle size={16} />
</Button>
<PlusCircle size={16} aria-label="Plus Icon" />
</TooltipAnchor>
);
}

View file

@ -1,8 +1,8 @@
import { useEffect } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { ListeningIcon, Spinner } from '~/components/svg';
import { useLocalize, useSpeechToText } from '~/hooks';
import { useChatFormContext } from '~/Providers';
import { TooltipAnchor } from '~/components/ui';
import { globalAudioId } from '~/common';
import { cn } from '~/utils';
@ -74,29 +74,20 @@ export default function AudioRecorder({
};
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<button
id="audio-recorder"
aria-label={localize('com_ui_use_micrphone')}
onClick={isListening ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn(
'absolute flex h-[30px] w-[30px] items-center justify-center rounded-lg p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700',
isRTL
? 'bottom-1.5 left-4 md:bottom-3 md:left-12'
: 'bottom-1.5 right-12 md:bottom-3 md:right-12',
)}
type="button"
>
{renderIcon()}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
{localize('com_ui_use_micrphone')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor
id="audio-recorder"
aria-label={localize('com_ui_use_micrphone')}
onClick={isListening ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn(
'absolute flex h-[30px] w-[30px] items-center justify-center rounded-lg p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700',
isRTL
? 'bottom-1.5 left-4 md:bottom-3 md:left-12'
: 'bottom-1.5 right-12 md:bottom-3 md:right-12',
)}
description={localize('com_ui_use_micrphone')}
>
{renderIcon()}
</TooltipAnchor>
);
}

View file

@ -5,10 +5,10 @@ import {
fileConfig as defaultFileConfig,
mergeFileConfig,
} from 'librechat-data-provider';
import { FileUpload, TooltipAnchor } from '~/components/ui';
import { useFileHandling, useLocalize } from '~/hooks';
import { useGetFileConfig } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { FileUpload } from '~/components/ui';
import { useFileHandling } from '~/hooks';
import { cn } from '~/utils';
const AttachFile = ({
@ -22,6 +22,7 @@ const AttachFile = ({
isRTL: boolean;
disabled?: boolean | null;
}) => {
const localize = useLocalize();
const { handleFileChange } = useFileHandling();
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
@ -42,17 +43,18 @@ const AttachFile = ({
)}
>
<FileUpload handleFileChange={handleFileChange} className="flex">
<button
<TooltipAnchor
id="audio-recorder"
aria-label={localize('com_sidepanel_attach_files')}
disabled={!!disabled}
type="button"
className="btn relative text-black focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-opacity-50 dark:text-white"
aria-label="Attach files"
style={{ padding: 0 }}
description={localize('com_sidepanel_attach_files')}
>
<div className="flex w-full items-center justify-center gap-2">
<AttachmentIcon />
</div>
</button>
</TooltipAnchor>
</FileUpload>
</div>
);

View file

@ -5,13 +5,12 @@ import { useState, useEffect, useMemo } from 'react';
import { tPresetUpdateSchema, EModelEndpoint, paramEndpoints } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { PluginStoreDialog, TooltipAnchor } from '~/components';
import { ModelSelect } from '~/components/Input/ModelSelect';
import { PluginStoreDialog } from '~/components';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import OptionsPopover from './OptionsPopover';
import PopoverButtons from './PopoverButtons';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui';
import store from '~/store';
export default function HeaderOptions({
@ -23,6 +22,7 @@ export default function HeaderOptions({
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
store.showPluginStoreDialog,
);
const localize = useLocalize();
const { showPopover, conversation, latestMessage, setShowPopover, setShowBingToneSetting } =
useChatContext();
@ -84,17 +84,18 @@ export default function HeaderOptions({
{!noSettings[endpoint] &&
interfaceConfig?.parameters === true &&
!paramEndpoints.has(endpoint) && (
<Button
aria-label="Settings/parameters"
<TooltipAnchor
id="parameters-button"
data-testid="parameters-button"
type="button"
variant="outline"
aria-label={localize('com_ui_model_parameters')}
description={localize('com_ui_model_parameters')}
tabIndex={0}
role="button"
onClick={triggerAdvancedMode}
className="flex h-[40px] min-w-4 px-3 radix-state-open:bg-surface-hover"
data-testid="parameters-button"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<Settings2 className="w-4 text-gray-600 dark:text-white" />
</Button>
<Settings2 size={16} aria-label="Settings/Parameters Icon" />
</TooltipAnchor>
)}
</div>
{interfaceConfig?.parameters === true && !paramEndpoints.has(endpoint) && (

View file

@ -1,7 +1,7 @@
import React, { forwardRef } from 'react';
import { useWatch } from 'react-hook-form';
import type { Control } from 'react-hook-form';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { TooltipAnchor } from '~/components/ui';
import { SendIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -17,33 +17,29 @@ const SubmitButton = React.memo(
(props: { disabled: boolean; isRTL: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
const localize = useLocalize();
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<button
ref={ref}
aria-label={localize('com_nav_send_message')}
id="send-button"
disabled={props.disabled}
className={cn(
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
props.isRTL
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
)}
data-testid="send-button"
type="submit"
>
<span className="" data-state="closed">
<SendIcon size={24} />
</span>
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
{localize('com_nav_send_message')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor
description={localize('com_nav_send_message')}
render={
<button
ref={ref}
aria-label={localize('com_nav_send_message')}
id="send-button"
disabled={props.disabled}
className={cn(
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
props.isRTL
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
)}
data-testid="send-button"
type="submit"
>
<span className="" data-state="closed">
<SendIcon size={24} />
</span>
</button>
}
></TooltipAnchor>
);
},
),

View file

@ -2,11 +2,11 @@ import { useMemo } from 'react';
import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useSubmitMessage } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils';
import ConvoStarter from './ConvoStarter';
@ -58,66 +58,58 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const sendConversationStarter = (text: string) => submitMessage({ text });
return (
<TooltipProvider delayDuration={50}>
<Tooltip>
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
conversation={conversation}
assistantMap={assistantMap}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3"
size={41}
/>
{startupConfig?.showBirthdayIcon === true ? (
<div>
<TooltipTrigger>
<BirthdayIcon className="absolute bottom-8 right-2.5" />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={110} className="">
{localize('com_ui_happy_birthday')}
</TooltipContent>
</div>
) : null}
</div>
{assistantName ? (
<div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white">
{assistantName}
</div>
<div className="max-w-md text-center text-sm font-normal text-text-primary ">
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
</div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary">
<div className="text-sm text-token-text-tertiary">By Daniel Avila</div>
</div> */}
</div>
) : (
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</h2>
)}
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters.length > 0 &&
conversation_starters
.slice(0, Constants.MAX_CONVO_STARTERS)
.map((text, index) => (
<ConvoStarter
key={index}
text={text}
onClick={() => sendConversationStarter(text)}
/>
))}
</div>
</div>
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
conversation={conversation}
assistantMap={assistantMap}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3"
size={41}
/>
{startupConfig?.showBirthdayIcon === true ? (
<TooltipAnchor
className="absolute bottom-8 right-2.5"
description={localize('com_ui_happy_birthday')}
>
<BirthdayIcon />
</TooltipAnchor>
) : null}
</div>
</Tooltip>
</TooltipProvider>
{assistantName ? (
<div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white">{assistantName}</div>
<div className="max-w-md text-center text-sm font-normal text-text-primary ">
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
</div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary">
<div className="text-sm text-token-text-tertiary">By Daniel Avila</div>
</div> */}
</div>
) : (
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</h2>
)}
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters.length > 0 &&
conversation_starters
.slice(0, Constants.MAX_CONVO_STARTERS)
.map((text, index) => (
<ConvoStarter
key={index}
text={text}
onClick={() => sendConversationStarter(text)}
/>
))}
</div>
</div>
</div>
);
}

View file

@ -4,7 +4,7 @@ import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { EditPresetDialog, PresetItems } from './Presets';
import { useLocalize, usePresets } from '~/hooks';
import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui';
import { TooltipAnchor } from '~/components';
const PresetsMenu: FC = () => {
const localize = useLocalize();
@ -25,17 +25,17 @@ const PresetsMenu: FC = () => {
return (
<Root>
<Trigger asChild>
<Button
type="button"
variant="outline"
className="flex h-[40px] min-w-4 px-3 radix-state-open:bg-surface-hover"
<TooltipAnchor
id="presets-button"
data-testid="presets-button"
title={localize('com_endpoint_examples')}
aria-label={localize('com_endpoint_examples')}
description={localize('com_endpoint_examples')}
tabIndex={0}
role="button"
data-testid="presets-button"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<BookCopy className="icon-sm" id="presets-button" />
</Button>
<BookCopy size={16} aria-label="Preset Icon" />
</TooltipAnchor>
</Trigger>
<Portal>
<div

View file

@ -2,11 +2,10 @@ import { useState, useId } from 'react';
import * as Ariakit from '@ariakit/react';
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useArchiveHandler } from './ArchiveButton';
import { useLocalize, useArchiveHandler } from '~/hooks';
import { DropdownPopup } from '~/components/ui';
import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
export default function ConvoOptions({

View file

@ -4,15 +4,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import type { TMessage } from 'librechat-data-provider';
import { useDeleteConversationMutation } from '~/data-provider';
import {
OGDialog,
OGDialogTrigger,
Label,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '~/components/ui';
import { OGDialog, OGDialogTrigger, Label, TooltipAnchor } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { TrashIcon } from '~/components/svg';
import { useLocalize, useNewConvo } from '~/hooks';
@ -21,7 +13,6 @@ type DeleteButtonProps = {
conversationId: string;
retainView: () => void;
title: string;
className?: string;
showDeleteDialog?: boolean;
setShowDeleteDialog?: (value: boolean) => void;
};
@ -30,7 +21,6 @@ export default function DeleteButton({
conversationId,
retainView,
title,
className = '',
showDeleteDialog,
setShowDeleteDialog,
}: DeleteButtonProps) {
@ -92,20 +82,13 @@ export default function DeleteButton({
return (
<OGDialog open={open} onOpenChange={setOpen}>
<TooltipProvider delayDuration={250}>
<Tooltip>
<OGDialogTrigger asChild>
<TooltipTrigger asChild>
<button>
<TrashIcon className="h-5 w-5" />
</button>
</TooltipTrigger>
</OGDialogTrigger>
<TooltipContent side="top" sideOffset={0} className={className}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor description={localize('com_ui_delete')}>
<OGDialogTrigger asChild>
<button>
<TrashIcon className="h-5 w-5" />
</button>
</OGDialogTrigger>
</TooltipAnchor>
{dialogContent}
</OGDialog>
);

View file

@ -1,4 +1,3 @@
export { default as ArchiveButton } from './ArchiveButton';
export { default as DeleteButton } from './DeleteButton';
export { default as ShareButton } from './ShareButton';
export { default as SharedLinkButton } from './SharedLinkButton';

View file

@ -1,71 +0,0 @@
import { cloneElement, type FC } from 'react';
import { DotsIcon } from '~/components/svg';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useToggle } from './ToggleContext';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
type DropDownMenuProps = {
children: React.ReactNode;
icon?: React.ReactElement;
tooltip?: string;
className?: string;
};
const DropDownMenu: FC<DropDownMenuProps> = ({
children,
icon = <DotsIcon />,
tooltip = 'More',
className,
}: DropDownMenuProps) => {
const localize = useLocalize();
const { isPopoverActive, setPopoverActive } = useToggle();
return (
<Root open={isPopoverActive} onOpenChange={(open) => setPopoverActive(open)}>
<Trigger asChild>
<div
className={cn(
'pointer-cursor relative flex flex-col text-left focus:outline-none focus:ring-0 focus:ring-offset-0 sm:text-sm',
'hover:text-gray-400 radix-state-open:text-gray-400 dark:hover:text-gray-400 dark:radix-state-open:text-gray-400',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center focus:ring-0 focus:ring-offset-0',
)}
id="edit-menu-button"
data-testid="edit-menu-button"
title={localize('com_ui_more_options')}
>
<TooltipProvider delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<button type="button" className={className}>
{cloneElement(icon, {
className:
'h-[18px] w-[18px] flex-shrink-0 text-gray-500 hover:text-gray-400 dark:text-gray-300 dark:hover:text-gray-400',
})}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{tooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</Trigger>
<Portal>
<Content
side="bottom"
align="start"
className={cn(
'popover radix-side-bottom:animate-slideUpAndFade radix-side-left:animate-slideRightAndFade radix-side-right:animate-slideLeftAndFade radix-side-top:animate-slideDownAndFade overflow-hidden rounded-lg shadow-lg',
'border border-gray-200 bg-white dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'flex min-w-[200px] max-w-xs flex-wrap',
)}
>
{children}
</Content>
</Portal>
</Root>
);
};
export default DropDownMenu;

View file

@ -14,9 +14,10 @@ import store from '~/store';
type BookmarkNavProps = {
tags: string[];
setTags: (tags: string[]) => void;
isSmallScreen: boolean;
};
const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps) => {
const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: BookmarkNavProps) => {
const localize = useLocalize();
const location = useLocation();
@ -40,6 +41,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
className={cn(
'mt-text-sm flex h-10 w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors duration-200 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
isSmallScreen ? 'h-14 rounded-2xl' : '',
)}
data-testid="bookmark-menu"
>

View file

@ -15,11 +15,11 @@ export default function MobileNav({
const { title = 'New Chat' } = conversation || {};
return (
<div className="border-token-border-medium bg-token-main-surface-primary sticky top-0 z-10 flex min-h-[40px] items-center justify-center border-b bg-white pl-1 dark:bg-gray-800 dark:text-white md:hidden md:hidden">
<div className="bg-token-main-surface-primary sticky top-0 z-10 flex min-h-[40px] items-center justify-center bg-white pl-1 dark:bg-gray-800 dark:text-white md:hidden">
<button
type="button"
data-testid="mobile-header-new-chat-button"
className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white"
className="inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
onClick={() =>
setNavVisible((prev) => {
localStorage.setItem('navVisible', JSON.stringify(!prev));
@ -49,7 +49,7 @@ export default function MobileNav({
</h1>
<button
type="button"
className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white"
className="inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
onClick={() => newConversation()}
>
<svg

View file

@ -14,7 +14,6 @@ import {
useConversations,
} from '~/hooks';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { TooltipProvider, Tooltip } from '~/components/ui';
import { Conversations } from '~/components/Conversations';
import BookmarkNav from './Bookmarks/BookmarkNav';
import AccountSettings from './AccountSettings';
@ -130,111 +129,112 @@ const Nav = ({
};
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<div
data-testid="nav"
className={
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt md:max-w-[260px]'
}
style={{
width: navVisible ? navWidth : '0px',
visibility: navVisible ? 'visible' : 'hidden',
transition: 'width 0.2s, visibility 0.2s',
}}
>
<div className="h-full w-[320px] md:w-[260px]">
<div className="flex h-full min-h-0 flex-col">
<>
<div
data-testid="nav"
className={
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt md:max-w-[260px]'
}
style={{
width: navVisible ? navWidth : '0px',
visibility: navVisible ? 'visible' : 'hidden',
transition: 'width 0.2s, visibility 0.2s',
}}
>
<div className="h-full w-[320px] md:w-[260px]">
<div className="flex h-full min-h-0 flex-col">
<div
className={cn(
'flex h-full min-h-0 flex-col transition-opacity',
isToggleHovering && !isSmallScreen ? 'opacity-50' : 'opacity-100',
)}
>
<div
className={cn(
'flex h-full min-h-0 flex-col transition-opacity',
isToggleHovering && !isSmallScreen ? 'opacity-50' : 'opacity-100',
'scrollbar-trigger relative h-full w-full flex-1 items-start border-white/20',
)}
>
<div
className={cn(
'scrollbar-trigger relative h-full w-full flex-1 items-start border-white/20',
)}
<nav
id="chat-history-nav"
aria-label={localize('com_ui_chat_history')}
className="flex h-full w-full flex-col px-3 pb-3.5"
>
<nav
id="chat-history-nav"
aria-label={localize('com_ui_chat_history')}
className="flex h-full w-full flex-col px-3 pb-3.5"
<div
className={cn(
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
isHovering ? '' : 'scrollbar-transparent',
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
<div
className={cn(
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
isHovering ? '' : 'scrollbar-transparent',
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
{isSmallScreen == true ? (
<div className="pt-3.5">
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
{hasAccessToBookmarks === true && (
<BookmarkNav tags={tags} setTags={setTags} />
)}
</div>
) : (
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar
clearSearch={clearSearch}
isSmallScreen={isSmallScreen}
/>
)}
{hasAccessToBookmarks === true && (
<BookmarkNav tags={tags} setTags={setTags} />
)}
</>
}
/>
)}
<Conversations
conversations={conversations}
moveToTop={moveToTop}
{isSmallScreen == true ? (
<div className="pt-3.5">
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
{hasAccessToBookmarks === true && (
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
)}
</div>
) : (
<NewChat
toggleNav={itemToggleNav}
subHeaders={
<>
{isSearchEnabled === true && (
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
</>
}
/>
{(isFetchingNextPage || showLoading) && (
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-text-primary')} />
)}
</div>
<AccountSettings />
</nav>
</div>
)}
<Conversations
conversations={conversations}
moveToTop={moveToTop}
toggleNav={itemToggleNav}
/>
{(isFetchingNextPage || showLoading) && (
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-text-primary')} />
)}
</div>
<AccountSettings />
</nav>
</div>
</div>
</div>
</div>
<NavToggle
isHovering={isToggleHovering}
setIsHovering={setIsToggleHovering}
onToggle={toggleNavVisible}
navVisible={navVisible}
className="fixed left-0 top-1/2 z-40 hidden md:flex"
/>
<div
role="button"
tabIndex={0}
className={`nav-mask ${navVisible ? 'active' : ''}`}
onClick={toggleNavVisible}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleNavVisible();
}
}}
aria-label="Toggle navigation"
/>
</Tooltip>
</TooltipProvider>
</div>
<NavToggle
isHovering={isToggleHovering}
setIsHovering={setIsToggleHovering}
onToggle={toggleNavVisible}
navVisible={navVisible}
className="fixed left-0 top-1/2 z-40 hidden md:flex"
/>
<div
role="button"
tabIndex={0}
className={`nav-mask ${navVisible ? 'active' : ''}`}
onClick={toggleNavVisible}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleNavVisible();
}
}}
aria-label="Toggle navigation"
/>
</>
);
};

View file

@ -1,5 +1,5 @@
import { TooltipTrigger, TooltipContent } from '~/components/ui';
import { useLocalize, useLocalStorage } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { cn } from '~/utils';
export default function NavToggle({
@ -15,7 +15,6 @@ export default function NavToggle({
const transition = {
transition: 'transform 0.3s ease, opacity 0.2s ease',
};
const [newUser] = useLocalStorage('newUser', true);
const rotationDegree = 15;
const rotation = isHovering || !navVisible ? `${rotationDegree}deg` : '0deg';
@ -33,46 +32,44 @@ export default function NavToggle({
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<TooltipTrigger asChild>
<button
onClick={onToggle}
id={`toggle-${side}-nav`}
aria-label={`toggle-${side === 'left' ? 'chat-history' : 'controls'}-nav`}
>
<span className="" data-state="closed">
<div
className="flex h-[72px] w-8 items-center justify-center"
style={{ ...transition, opacity: isHovering ? 1 : 0.25 }}
>
<div className="flex h-6 w-6 flex-col items-center">
{/* Top bar */}
<div
className="h-3 w-1 rounded-full bg-black dark:bg-white"
style={{
...transition,
transform: `translateY(0.15rem) rotate(${topBarRotation}) translateZ(0px)`,
}}
/>
{/* Bottom bar */}
<div
className="h-3 w-1 rounded-full bg-black dark:bg-white"
style={{
...transition,
transform: `translateY(-0.15rem) rotate(${bottomBarRotation}) translateZ(0px)`,
}}
/>
</div>
<TooltipAnchor
side={side === 'right' ? 'left' : 'right'}
aria-label={`toggle-${side === 'left' ? 'chat-history' : 'controls'}-nav`}
id={`toggle-${side}-nav`}
onClick={onToggle}
role="button"
description={
navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar')
}
className="flex cursor-pointer items-center justify-center"
tabIndex={0}
>
<span className="" data-state="closed">
<div
className="flex h-[72px] w-8 items-center justify-center"
style={{ ...transition, opacity: isHovering ? 1 : 0.25 }}
>
<div className="flex h-6 w-6 flex-col items-center">
{/* Top bar */}
<div
className="h-3 w-1 rounded-full bg-black dark:bg-white"
style={{
...transition,
transform: `translateY(0.15rem) rotate(${topBarRotation}) translateZ(0px)`,
}}
/>
{/* Bottom bar */}
<div
className="h-3 w-1 rounded-full bg-black dark:bg-white"
style={{
...transition,
transform: `translateY(-0.15rem) rotate(${bottomBarRotation}) translateZ(0px)`,
}}
/>
</div>
<TooltipContent
forceMount={newUser ? true : undefined}
side={side === 'right' ? 'left' : 'right'}
sideOffset={4}
>
{navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar')}
</TooltipContent>
</span>
</button>
</TooltipTrigger>
</div>
</span>
</TooltipAnchor>
</div>
);
}

View file

@ -2,14 +2,14 @@ import { Search } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import type { TConversation } from 'librechat-data-provider';
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useLocalize, useNewConvo } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { NewChatIcon } from '~/components/svg';
import store from '~/store';
import type { TConversation } from 'librechat-data-provider';
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
const searchQuery = useRecoilValue(store.searchQuery);
@ -80,43 +80,36 @@ export default function NewChat({
};
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm text-text-primary">
{localize('com_ui_new_chat')}
</div>
<div className="flex gap-3">
<span className="flex items-center" data-state="closed">
<TooltipTrigger asChild>
<button
id="nav-new-chat-btn"
aria-label={localize('com_ui_new_chat')}
className="text-text-primary"
>
<NewChatIcon className="size-5" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={20}>
{localize('com_ui_new_chat')}
</TooltipContent>
</span>
</div>
</a>
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
tabIndex={0}
data-testid="nav-new-chat"
onClick={clickHandler}
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm text-text-primary">
{localize('com_ui_new_chat')}
</div>
{subHeaders != null ? subHeaders : null}
</div>
</Tooltip>
</TooltipProvider>
<div className="flex gap-3">
<span className="flex items-center" data-state="closed">
<TooltipAnchor
side="right"
id="nav-new-chat-btn"
aria-label="nav-new-chat-btn"
description={localize('com_ui_new_chat')}
className="text-text-primary"
>
<NewChatIcon className="size-5" />
</TooltipAnchor>
</span>
</div>
</a>
</div>
{subHeaders != null ? subHeaders : null}
</div>
);
}

View file

@ -61,7 +61,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
ref={ref}
className={cn(
'group relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover',
isSmallScreen === true ? 'h-16 rounded-2xl' : '',
isSmallScreen === true ? 'mb-2 h-14 rounded-2xl' : '',
)}
>
{

View file

@ -1,20 +1,13 @@
import { useMemo, useState, MouseEvent } from 'react';
import { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { MessageSquare, Link as LinkIcon } from 'lucide-react';
import { Link as LinkIcon } from 'lucide-react';
import type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider';
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import { Spinner, TooltipAnchor, TrashIcon } from '~/components';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { cn } from '~/utils';
import {
Spinner,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TrashIcon,
} from '~/components';
function SharedLinkDeleteButton({
shareId,
@ -36,7 +29,7 @@ function SharedLinkDeleteButton({
},
});
const handleDelete = async (e: MouseEvent<HTMLButtonElement>) => {
const handleDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
if (mutation.isLoading) {
return;
@ -46,36 +39,14 @@ function SharedLinkDeleteButton({
setIsDeleting(false);
};
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<button id="delete-shared-link" aria-label="Delete shared link" onClick={handleDelete}>
<TrashIcon />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function SourceChatButton({ conversationId }: { conversationId: string }) {
const localize = useLocalize();
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Link to={`/c/${conversationId}`} target="_blank" rel="noreferrer">
<MessageSquare className="h-4 w-4 hover:text-gray-300" />
</Link>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_nav_source_chat')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor
description={localize('com_ui_delete')}
id="delete-shared-link"
aria-label="Delete shared link"
onClick={handleDelete}
>
<TrashIcon className="size-4" />
</TooltipAnchor>
);
}
@ -114,15 +85,12 @@ function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
)}
>
{sharedLink.conversationId && (
<>
<SourceChatButton conversationId={sharedLink.conversationId} />
<div className={cn('h-4 w-4 cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
<SharedLinkDeleteButton
shareId={sharedLink.shareId}
setIsDeleting={setIsDeleting}
/>
</div>
</>
<div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
<SharedLinkDeleteButton
shareId={sharedLink.shareId}
setIsDeleting={setIsDeleting}
/>
</div>
)}
</div>
</div>

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks';
import { Dialog, DialogTrigger } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { OGDialog, OGDialogTrigger } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import ShareLinkTable from './SharedLinkTable';
@ -11,19 +11,19 @@ export default function SharedLinks() {
<div className="flex items-center justify-between">
<div>{localize('com_nav_shared_links')}</div>
<Dialog>
<DialogTrigger asChild>
<OGDialog>
<OGDialogTrigger asChild>
<button className="btn btn-neutral relative ">
{localize('com_nav_shared_links_manage')}
</button>
</DialogTrigger>
<DialogTemplate
</OGDialogTrigger>
<OGDialogTemplate
title={localize('com_nav_shared_links')}
className="max-w-[1000px]"
showCancelButton={false}
main={<ShareLinkTable />}
/>
</Dialog>
</OGDialog>
</div>
);
}

View file

@ -1,17 +1,18 @@
import { useMemo, useState } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { MessageCircle, ArchiveRestore } from 'lucide-react';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import ArchiveButton from '~/components/Conversations/ConvoOptions/ArchiveButton';
import DeleteButton from '~/components/Conversations/ConvoOptions/DeleteButton';
import { ConversationListResponse } from 'librechat-data-provider';
import { useAuthContext, useLocalize, useNavScrolling, useArchiveHandler } from '~/hooks';
import { DeleteButton } from '~/components/Conversations/ConvoOptions';
import { TooltipAnchor } from '~/components/ui';
import { Spinner } from '~/components/svg';
import { cn } from '~/utils';
import { ConversationListResponse } from 'librechat-data-provider';
export default function ArchivedChatsTable({ className }: { className?: string }) {
export default function ArchivedChatsTable() {
const localize = useLocalize();
const { isAuthenticated } = useAuthContext();
const [showLoading, setShowLoading] = useState(false);
const [conversationId, setConversationId] = useState<string | null>(null);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery(
{ pageNumber: '1', isArchived: true },
@ -30,14 +31,9 @@ export default function ArchivedChatsTable({ className }: { className?: string }
[data],
);
const classProp: { className?: string } = {
className: 'p-1 hover:text-black dark:hover:text-white',
};
if (className) {
classProp.className = className;
}
const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop);
if (!conversations || conversations.length === 0) {
if (!data || conversations.length === 0) {
return <div className="text-gray-300">{localize('com_nav_archived_chats_empty')}</div>;
}
@ -58,48 +54,52 @@ export default function ArchivedChatsTable({ className }: { className?: string }
</tr>
</thead>
<tbody>
{conversations.map((conversation) => (
<tr
key={conversation.conversationId}
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
>
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
<MessageCircle className="mr-1 h-5 w-5" />
{conversation.title}
</td>
<td className="p-1">
<div className="flex justify-between">
<div className="flex justify-start dark:text-gray-200">
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
{conversation.conversationId && (
<>
<ArchiveButton
className="hover:text-black dark:hover:text-white"
{conversations.map((conversation) => {
if (!conversation.conversationId) {
return null;
}
return (
<tr
key={conversation.conversationId}
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
>
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
<MessageCircle className="mr-1 h-5 w-5" />
{conversation.title}
</td>
<td className="p-1">
<div className="flex justify-between">
<div className="flex justify-start dark:text-gray-200">
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
<TooltipAnchor
description={localize('com_ui_unarchive')}
onClick={() => {
setConversationId(conversation.conversationId);
archiveHandler();
}}
className="cursor-pointer hover:text-black dark:hover:text-white"
>
<ArchiveRestore className="size-4 hover:text-gray-300" />
</TooltipAnchor>
<div className="size-5 hover:text-gray-300">
<DeleteButton
conversationId={conversation.conversationId}
retainView={moveToTop}
shouldArchive={false}
icon={<ArchiveRestore className="h-4 w-4 hover:text-gray-300" />}
title={conversation.title ?? ''}
/>
<div className="h-5 w-5 hover:text-gray-300">
<DeleteButton
conversationId={conversation.conversationId}
retainView={moveToTop}
title={conversation.title ?? ''}
/>
</div>
</>
)}
</div>
</div>
</div>
</div>
</td>
</tr>
))}
</td>
</tr>
);
})}
</tbody>
</table>
{(isFetchingNextPage || showLoading) && (

View file

@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react';
import { Plus, X } from 'lucide-react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
import { TooltipAnchor } from '~/components/ui';
import { useLocalize } from '~/hooks';
interface AssistantConversationStartersProps {
@ -106,25 +106,19 @@ const AssistantConversationStarters: React.FC<AssistantConversationStartersProps
}}
className="absolute right-1 top-1"
>
<TooltipProvider delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddStarter}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{hasReachedMax
? localize('com_assistants_max_starters_reached')
: localize('com_ui_add')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor
side="top"
description={
hasReachedMax
? localize('com_assistants_max_starters_reached')
: localize('com_ui_add')
}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddStarter}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</TooltipAnchor>
</div>
)}
</Transition>
@ -143,22 +137,14 @@ const AssistantConversationStarters: React.FC<AssistantConversationStartersProps
type="text"
maxLength={64}
/>
<TooltipProvider delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteStarter(index)}
>
<X className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipAnchor
side="top"
description={localize('com_ui_delete')}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteStarter(index)}
>
<X className="size-4" />
</TooltipAnchor>
</div>
))}
</div>

View file

@ -2,9 +2,9 @@ import { useState } from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import type { NavLink, NavProps } from '~/common';
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/Tooltip';
import { buttonVariants } from '~/components/ui/Button';
import { cn, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
@ -30,42 +30,30 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
{links.map((link, index) => {
const variant = getVariant(link);
return isCollapsed ? (
<Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild>
<button
className={cn(
buttonVariants({ variant, size: 'icon' }),
removeFocusOutlines,
'h-9 w-9',
variant === 'default'
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
: '',
)}
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
setActive('');
return;
}
setActive(link.id);
resize && resize(25);
}}
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={10}
className="flex items-center gap-4"
>
{localize(link.title)}
{link.label && (
<span className="text-muted-foreground ml-auto">{link.label}</span>
)}
</TooltipContent>
</Tooltip>
<TooltipAnchor
className={cn(
buttonVariants({ variant, size: 'icon' }),
removeFocusOutlines,
'h-9 w-9 cursor-pointer',
variant === 'default'
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted bg-surface-terniary dark:hover:text-white'
: '',
)}
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
setActive('');
return;
}
setActive(link.id);
resize && resize(25);
}}
description={localize(link.title)}
side="left"
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</TooltipAnchor>
) : (
<Accordion
key={index}

View file

@ -9,9 +9,8 @@ import {
import type { ImperativePanelHandle } from 'react-resizable-panels';
import type { TEndpointsConfig } from 'librechat-data-provider';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import NavToggle from '~/components/Nav/NavToggle';
import { useChatContext } from '~/Providers';
import Switcher from './Switcher';
@ -176,116 +175,109 @@ const SidePanel = ({
return (
<>
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-white dark:bg-gray-800"
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-white dark:bg-gray-800"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium dark:text-white" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
<TooltipProvider delayDuration={400}>
<Tooltip>
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="relative flex w-px items-center justify-center"
>
<NavToggle
navVisible={!isCollapsed}
isHovering={isHovering}
onToggle={toggleNavVisible}
setIsHovering={setIsHovering}
className={cn(
'fixed top-1/2',
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'mr-9'
: 'mr-16',
)}
translateX={false}
side="right"
/>
</div>
</Tooltip>
</TooltipProvider>
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
<ResizableHandleAlt withHandle className="bg-transparent dark:text-white" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
order={artifacts != null ? 3 : 2}
aria-label={localize('com_ui_controls')}
role="region"
collapsedSize={collapsedSize}
defaultSize={currentLayout[currentLayout.length - 1]}
collapsible={true}
minSize={minSize}
maxSize={40}
ref={panelRef}
style={{
overflowY: 'auto',
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}}
onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium dark:text-white" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="relative flex w-px items-center justify-center"
>
<NavToggle
navVisible={!isCollapsed}
isHovering={isHovering}
onToggle={toggleNavVisible}
setIsHovering={setIsHovering}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-surface-primary-alt transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) ||
fullCollapse
? 'hidden min-w-0'
: 'opacity-100',
'fixed top-1/2',
(isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'mr-9'
: 'mr-16',
)}
>
{interfaceConfig.modelSelect && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-surface-primary-alt',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>
<Switcher
isCollapsed={isCollapsed}
endpointKeyProvided={keyProvided}
endpoint={endpoint}
/>
</div>
)}
<Nav
resize={panelRef.current?.resize}
isCollapsed={isCollapsed}
defaultActive={defaultActive}
links={Links}
/>
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
translateX={false}
side="right"
/>
</div>
{(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && (
<ResizableHandleAlt withHandle className="bg-transparent dark:text-white" />
)}
<ResizablePanel
tagName="nav"
id="controls-nav"
order={artifacts != null ? 3 : 2}
aria-label={localize('com_ui_controls')}
role="region"
collapsedSize={collapsedSize}
defaultSize={currentLayout[currentLayout.length - 1]}
collapsible={true}
minSize={minSize}
maxSize={40}
ref={panelRef}
style={{
overflowY: 'auto',
transition: 'width 0.2s ease, visibility 0s linear 0.2s',
}}
onExpand={() => {
setIsCollapsed(false);
localStorage.setItem('react-resizable-panels:collapsed', 'false');
}}
onCollapse={() => {
setIsCollapsed(true);
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-surface-primary-alt transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'hidden min-w-0'
: 'opacity-100',
)}
>
{interfaceConfig.modelSelect && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-surface-primary-alt',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>
<Switcher
isCollapsed={isCollapsed}
endpointKeyProvided={keyProvided}
endpoint={endpoint}
/>
</div>
)}
<Nav
resize={panelRef.current?.resize}
isCollapsed={isCollapsed}
defaultActive={defaultActive}
links={Links}
/>
</ResizablePanel>
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}

View file

@ -3,25 +3,18 @@ import { cn } from '~/utils/';
export default function Spinner({ className = 'm-auto', size = '1em' }) {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn(className, 'animate-spin text-center')}
height={size}
width={size}
xmlns="http://www.w3.org/2000/svg"
className={cn('animate-spin', className)}
>
<line x1="12" y1="2" x2="12" y2="6" />
<line x1="12" y1="18" x2="12" y2="22" />
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76" />
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07" />
<line x1="2" y1="12" x2="6" y2="12" />
<line x1="18" y1="12" x2="22" y2="12" />
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24" />
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93" />
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
}

View file

@ -60,7 +60,7 @@ function SelectDropDownPop({
<button
data-testid="select-dropdown-button"
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-lg border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
aria-label={`Select ${title}`}

View file

@ -1,46 +1,65 @@
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '~/utils';
import * as Ariakit from '@ariakit/react';
import { AnimatePresence, motion } from 'framer-motion';
import { forwardRef, useMemo } from 'react';
const Tooltip = TooltipPrimitive.Root;
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
}
const TooltipTrigger = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
>((props, ref) => <TooltipPrimitive.Trigger ref={ref} {...props} />);
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
{ description, side = 'top', role, ...props },
ref,
) {
const tooltip = Ariakit.useTooltipStore({ placement: side });
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
const TooltipPortal = TooltipPrimitive.Portal;
const { x, y } = useMemo(() => {
const dir = placement.split('-')[0];
switch (dir) {
case 'top':
return { x: 0, y: -8 };
case 'bottom':
return { x: 0, y: 8 };
case 'left':
return { x: -8, y: 0 };
case 'right':
return { x: 8, y: 0 };
default:
return { x: 0, y: 0 };
}
}, [placement]);
const TooltipArrow = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
>((props, ref) => <TooltipPrimitive.Arrow ref={ref} {...props} />);
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (role === 'button' && event.key === 'Enter') {
event.preventDefault();
(event.target as HTMLDivElement).click();
}
};
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className = '', forceMount, children, ...props }, ref) => (
<TooltipPortal forceMount={forceMount}>
<TooltipPrimitive.Content
className={cn(
'shadow-xs relative z-[1000] max-w-xs rounded-lg border border-gray-900/10 bg-gray-900 p-1 transition-opacity',
className,
)}
ref={ref}
{...props}
style={{ userSelect: 'none' }}
>
<span className="flex items-center whitespace-pre-wrap px-2 py-1 text-center text-sm font-medium normal-case text-white">
{children}
<TooltipArrow className="TooltipArrow" />
</span>
</TooltipPrimitive.Content>
</TooltipPortal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
const TooltipProvider = TooltipPrimitive.Provider;
export { Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, TooltipProvider };
return (
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
<Ariakit.TooltipAnchor {...props} ref={ref} role={role} onKeyDown={handleKeyDown} />
<AnimatePresence>
{mounted && (
<Ariakit.Tooltip
gutter={4}
alwaysVisible
className="tooltip"
render={
<motion.div
initial={{ opacity: 0, x, y }}
animate={{ opacity: 1, x: 0, y: 0 }}
exit={{ opacity: 0, x, y }}
/>
}
>
<Ariakit.TooltipArrow />
{description}
</Ariakit.Tooltip>
)}
</AnimatePresence>
</Ariakit.TooltipProvider>
);
});

View file

@ -1,96 +0,0 @@
import { ReactElement } from 'react';
import {
OGDialog,
OGDialogTrigger,
Label,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { CrossIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
export default function TooltipIcon({
disabled,
appendLabel = false,
title,
className = '',
confirm,
confirmMessage,
icon,
tabIndex,
onFocus,
onBlur,
}: {
disabled: boolean;
title: string;
appendLabel?: boolean;
className?: string;
confirm?: () => void;
confirmMessage?: ReactElement;
icon?: ReactElement;
tabIndex?: number;
onFocus?: () => void;
onBlur?: () => void;
}) {
const localize = useLocalize();
const renderDeleteButton = () => {
if (appendLabel) {
return (
<>
{icon} {localize('com_ui_delete')}
</>
);
}
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span>{icon}</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
if (!confirmMessage) {
return (
<button
className={className}
onClick={confirm}
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
>
{disabled ? <CrossIcon /> : renderDeleteButton()}
</button>
);
}
return (
<OGDialog>
<OGDialogTrigger asChild>
<button className={className} tabIndex={tabIndex} onFocus={onFocus} onBlur={onBlur}>
{disabled ? <CrossIcon /> : renderDeleteButton()}
</button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={title}
className="max-w-[450px]"
main={<Label className="text-left text-sm font-medium">{confirmMessage}</Label>}
selection={{
selectHandler: confirm,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
);
}

View file

@ -5,6 +5,7 @@ export { default as useDefaultConvo } from './useDefaultConvo';
export { default as useConversation } from './useConversation';
export { default as useGenerateConvo } from './useGenerateConvo';
export { default as useConversations } from './useConversations';
export { default as useArchiveHandler } from './useArchiveHandler';
export { default as useDebouncedInput } from './useDebouncedInput';
export { default as useBookmarkSuccess } from './useBookmarkSuccess';
export { default as useNavigateToConvo } from './useNavigateToConvo';

View file

@ -1,22 +1,13 @@
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useConversations, useLocalize, useNewConvo } from '~/hooks';
import { useArchiveConversationMutation } from '~/data-provider';
import useConversations from './useConversations';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import useLocalize from '../useLocalize';
import useNewConvo from '../useNewConvo';
type ArchiveButtonProps = {
children?: React.ReactNode;
conversationId: string;
retainView: () => void;
shouldArchive: boolean;
icon?: React.ReactNode;
className?: string;
};
export function useArchiveHandler(
export default function useArchiveHandler(
conversationId: string,
shouldArchive: boolean,
retainView: () => void,
@ -57,31 +48,3 @@ export function useArchiveHandler(
);
};
}
export default function ArchiveButton({
conversationId,
retainView,
shouldArchive,
icon,
className = '',
}: ArchiveButtonProps) {
const localize = useLocalize();
const archiveHandler = useArchiveHandler(conversationId, shouldArchive, retainView);
return (
<button type="button" className={className} onClick={archiveHandler}>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span className="h-5 w-5">{icon}</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize(`com_ui_${shouldArchive ? 'archive' : 'unarchive'}`)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
);
}
export { useArchiveHandler as archiveHandler };

View file

@ -362,8 +362,10 @@ export default {
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
com_ui_bookmarks_filter: 'Filter bookmarks...',
com_ui_bookmarks_delete: 'Delete Bookmark',
com_ui_no_bookmarks: 'it seems like you have no bookmarks yet. Click on a chat and add a new one',
com_ui_no_conversation_id: 'No conversation ID found',
com_ui_add_multi_conversation: 'Add multi-conversation',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl:

View file

@ -207,12 +207,12 @@ export default {
com_ui_shared_link_not_found: 'Link condiviso non trovato',
com_ui_delete_conversation: 'Eliminare la chat?',
com_ui_delete_confirm: 'Questo eliminerà',
com_ui_rename: 'Rinominare',
com_ui_archive: 'Arsip',
com_ui_rename: 'Rinomina',
com_ui_archive: 'Archivia',
com_ui_archive_error: 'Errore durante l\'archiviazione della conversazione',
com_ui_unarchive: 'Disarchivia',
com_ui_unarchive_error: 'Impossibile disarchiviare la conversazione',
com_ui_more_options: 'Pi',
com_ui_more_options: 'Più',
com_ui_delete_assistant_confirm:
'Sei sicuro di voler eliminare questo Assistente? Questa operazione non può essere annullata.',
com_ui_preview: 'Anteprima',
@ -1361,11 +1361,11 @@ export const comparisons = {
},
com_ui_rename: {
english: 'Rename',
translated: 'Rinominare',
translated: 'Rinomina',
},
com_ui_archive: {
english: 'Archive',
translated: 'Arsip',
translated: 'Archivia',
},
com_ui_archive_error: {
english: 'Failed to archive conversation',

View file

@ -653,11 +653,11 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **com_ui_rename**:
- **english**: Rename
- **translated**: Rinominare
- **translated**: Rinomina
- **com_ui_archive**:
- **english**: Archive
- **translated**: Arsip
- **translated**: Archivia
- **com_ui_archive_error**:
- **english**: Failed to archive conversation

View file

@ -1963,7 +1963,7 @@ button.scroll-convo {
.prose ol > li::marker,
.markdown ol > li::marker {
content: counter(list-counter) ". ";
content: counter(list-counter) '. ';
color: var(--tw-prose-counters);
font-weight: 400;
}
@ -1982,7 +1982,7 @@ button.scroll-convo {
.prose ol ol > li::marker,
.markdown ol ol > li::marker {
content: counter(list-counter-alpha, lower-alpha) ". ";
content: counter(list-counter-alpha, lower-alpha) '. ';
}
.prose ol ol ol,
@ -1998,7 +1998,7 @@ button.scroll-convo {
.prose ol ol ol > li::marker,
.markdown ol ol ol > li::marker {
content: counter(list-counter-roman, lower-roman) ". ";
content: counter(list-counter-roman, lower-roman) '. ';
}
/* Unordered lists */
@ -2288,6 +2288,27 @@ button.scroll-convo {
line-height: inherit;
}
.tooltip {
z-index: 50;
cursor: pointer;
border-radius: 0.275rem;
background-color: var(--bg-gray-600);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
font-size: 1rem;
line-height: 1.5rem;
color: black;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.25);
}
.tooltip:where(.dark, .dark *) {
background-color: var(--surface-primary);
color: white;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35);
}
:focus {
outline: none;
}

53
package-lock.json generated
View file

@ -1223,7 +1223,7 @@
"version": "v0.7.5-rc2",
"license": "ISC",
"dependencies": {
"@ariakit/react": "^0.4.8",
"@ariakit/react": "^0.4.11",
"@codesandbox/sandpack-react": "^2.18.2",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
@ -1258,6 +1258,7 @@
"downloadjs": "^1.4.7",
"export-from-json": "^1.7.2",
"filenamify": "^6.0.0",
"framer-motion": "^11.5.4",
"html-to-image": "^1.11.11",
"image-blob-reduce": "^4.1.0",
"js-cookie": "^3.0.5",
@ -1398,16 +1399,18 @@
}
},
"node_modules/@ariakit/core": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.8.tgz",
"integrity": "sha512-HQS+9CI7pMqqVlAt5bPGenT0/e65UxXY+PKtgU7Y+0UToBDBRolO5S9+UUSDm8OmJHSnq24owEGm1Mv28l5XCQ=="
"version": "0.4.10",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.10.tgz",
"integrity": "sha512-mX3EabQbfVh5uTjsTJ3+gjj7KGdTNhIN0qZHJd5Z2iPUnKl9NBy23Lgu6PEskpVsKAZ3proirjguD7U9fKMs/A==",
"license": "MIT"
},
"node_modules/@ariakit/react": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.8.tgz",
"integrity": "sha512-Bb1vOrp0X52hxi1wE9TEHjjZ/Y08tVq2ZH+RFDwRQB3g04uVwrrhnTccHepC6rsObrDpAOV3/YlJCi4k/lSUaQ==",
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.11.tgz",
"integrity": "sha512-nLpPrmNcspqNhk4o+epsgeZfP1+Fkh4uIzNe5yrFkXolRkqHGKAxl4Hi82e0yxIBUbYbZIEwsZQQVceF1L6xrw==",
"license": "MIT",
"dependencies": {
"@ariakit/react-core": "0.4.8"
"@ariakit/react-core": "0.4.11"
},
"funding": {
"type": "opencollective",
@ -1419,11 +1422,12 @@
}
},
"node_modules/@ariakit/react-core": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.8.tgz",
"integrity": "sha512-TzsddUWQwWYhrEVWHA/Gf7KCGx8rwFohAHfuljjqidKeZi2kUmuRAImCTG9oga34FWHFf4AdXQbBKclMNt0nrQ==",
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.11.tgz",
"integrity": "sha512-i6KedWhjZkNC7tMEKO0eNjjq2HRPiHyGaBS2x2VaWwzBepoYtjyvxRXyqLJ3gaiNdlwckN1TZsRDfD+viy13IQ==",
"license": "MIT",
"dependencies": {
"@ariakit/core": "0.4.8",
"@ariakit/core": "0.4.10",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
@ -20907,6 +20911,31 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.5.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz",
"integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",