💡 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", "homepage": "https://librechat.ai",
"dependencies": { "dependencies": {
"@ariakit/react": "^0.4.8", "@ariakit/react": "^0.4.11",
"@codesandbox/sandpack-react": "^2.18.2", "@codesandbox/sandpack-react": "^2.18.2",
"@dicebear/collection": "^7.0.4", "@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4", "@dicebear/core": "^7.0.4",
@ -63,6 +63,7 @@
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"export-from-json": "^1.7.2", "export-from-json": "^1.7.2",
"filenamify": "^6.0.0", "filenamify": "^6.0.0",
"framer-motion": "^11.5.4",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"image-blob-reduce": "^4.1.0", "image-blob-reduce": "^4.1.0",
"js-cookie": "^3.0.5", "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 type { FC } from 'react';
import { Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components/ui';
import { useDeleteConversationTagMutation } from '~/data-provider'; import { useDeleteConversationTagMutation } from '~/data-provider';
import TooltipIcon from '~/components/ui/TooltipIcon'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { NotificationSeverity } from '~/common'; import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg'; import { TrashIcon } from '~/components/svg';
import { Label } from '~/components/ui';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
const DeleteBookmarkButton: FC<{ const DeleteBookmarkButton: FC<{
@ -16,6 +16,7 @@ const DeleteBookmarkButton: FC<{
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => { }> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const [open, setOpen] = useState(false);
const deleteBookmarkMutation = useDeleteConversationTagMutation({ const deleteBookmarkMutation = useDeleteConversationTagMutation({
onSuccess: () => { onSuccess: () => {
@ -35,23 +36,48 @@ const DeleteBookmarkButton: FC<{
await deleteBookmarkMutation.mutateAsync(bookmark); await deleteBookmarkMutation.mutateAsync(bookmark);
}, [bookmark, deleteBookmarkMutation]); }, [bookmark, deleteBookmarkMutation]);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
setOpen(!open);
}
};
return ( return (
<TooltipIcon <>
disabled={false} <OGDialog open={open} onOpenChange={setOpen}>
appendLabel={false} <OGDialogTrigger asChild>
title="Delete Bookmark" <TooltipAnchor
confirmMessage={ description={localize('com_ui_delete')}
<Label htmlFor="bookmark" className="text-left text-sm font-medium"> className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
{localize('com_ui_bookmark_delete_confirm')} {bookmark} tabIndex={tabIndex}
</Label> onFocus={onFocus}
} onBlur={onBlur}
confirm={confirmDelete} onClick={() => setOpen(!open)}
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover" onKeyDown={handleKeyDown}
icon={<TrashIcon className="size-4" />} >
tabIndex={tabIndex} <TrashIcon className="size-4" />
onFocus={onFocus} </TooltipAnchor>
onBlur={onBlur} </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 { FC } from 'react';
import type { TConversationTag } from 'librechat-data-provider'; import type { TConversationTag } from 'librechat-data-provider';
import BookmarkEditDialog from './BookmarkEditDialog'; import BookmarkEditDialog from './BookmarkEditDialog';
import { TooltipAnchor } from '~/components/ui';
import { EditIcon } from '~/components/svg'; import { EditIcon } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
const EditBookmarkButton: FC<{ const EditBookmarkButton: FC<{
bookmark: TConversationTag; bookmark: TConversationTag;
@ -15,6 +15,12 @@ const EditBookmarkButton: FC<{
const localize = useLocalize(); const localize = useLocalize();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter') {
setOpen(!open);
}
};
return ( return (
<> <>
<BookmarkEditDialog <BookmarkEditDialog
@ -23,25 +29,17 @@ const EditBookmarkButton: FC<{
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
/> />
<button <TooltipAnchor
type="button" description={localize('com_ui_edit')}
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
tabIndex={tabIndex} tabIndex={tabIndex}
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"
onKeyDown={handleKeyDown}
> >
<TooltipProvider delayDuration={250}> <EditIcon />
<Tooltip> </TooltipAnchor>
<TooltipTrigger asChild>
<EditIcon />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_edit')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
</> </>
); );
}; };

View file

@ -2,13 +2,15 @@ import { PlusCircle } from 'lucide-react';
import { isAssistantsEndpoint } from 'librechat-data-provider'; import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider'; import type { TConversation } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers'; import { useChatContext, useAddedChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
import { mainTextareaId } from '~/common'; import { mainTextareaId } from '~/common';
import { Button } from '~/components/ui'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
function AddMultiConvo({ className = '' }: { className?: string }) { function AddMultiConvo() {
const { conversation } = useChatContext(); const { conversation } = useChatContext();
const { setConversation: setAddedConvo } = useAddedChatContext(); const { setConversation: setAddedConvo } = useAddedChatContext();
const localize = useLocalize();
const clickHandler = () => { const clickHandler = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -33,15 +35,18 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
} }
return ( return (
<Button <TooltipAnchor
id="add-multi-conversation-button" 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} onClick={clickHandler}
variant="outline" data-testid="parameters-button"
className={cn('h-10 w-10 p-0 transition-all duration-300 ease-in-out', className)} 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} /> <PlusCircle size={16} aria-label="Plus Icon" />
</Button> </TooltipAnchor>
); );
} }

View file

@ -1,8 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { ListeningIcon, Spinner } from '~/components/svg'; import { ListeningIcon, Spinner } from '~/components/svg';
import { useLocalize, useSpeechToText } from '~/hooks'; import { useLocalize, useSpeechToText } from '~/hooks';
import { useChatFormContext } from '~/Providers'; import { useChatFormContext } from '~/Providers';
import { TooltipAnchor } from '~/components/ui';
import { globalAudioId } from '~/common'; import { globalAudioId } from '~/common';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -74,29 +74,20 @@ export default function AudioRecorder({
}; };
return ( return (
<TooltipProvider delayDuration={250}> <TooltipAnchor
<Tooltip> id="audio-recorder"
<TooltipTrigger asChild> aria-label={localize('com_ui_use_micrphone')}
<button onClick={isListening ? handleStopRecording : handleStartRecording}
id="audio-recorder" disabled={disabled}
aria-label={localize('com_ui_use_micrphone')} className={cn(
onClick={isListening ? handleStopRecording : handleStartRecording} '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',
disabled={disabled} isRTL
className={cn( ? 'bottom-1.5 left-4 md:bottom-3 md:left-12'
'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', : 'bottom-1.5 right-12 md:bottom-3 md:right-12',
isRTL )}
? 'bottom-1.5 left-4 md:bottom-3 md:left-12' description={localize('com_ui_use_micrphone')}
: 'bottom-1.5 right-12 md:bottom-3 md:right-12', >
)} {renderIcon()}
type="button" </TooltipAnchor>
>
{renderIcon()}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
{localize('com_ui_use_micrphone')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
); );
} }

View file

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

View file

@ -5,13 +5,12 @@ import { useState, useEffect, useMemo } from 'react';
import { tPresetUpdateSchema, EModelEndpoint, paramEndpoints } from 'librechat-data-provider'; import { tPresetUpdateSchema, EModelEndpoint, paramEndpoints } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider'; import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints'; import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { PluginStoreDialog, TooltipAnchor } from '~/components';
import { ModelSelect } from '~/components/Input/ModelSelect'; import { ModelSelect } from '~/components/Input/ModelSelect';
import { PluginStoreDialog } from '~/components'; import { useSetIndexOptions, useLocalize } from '~/hooks';
import OptionsPopover from './OptionsPopover'; import OptionsPopover from './OptionsPopover';
import PopoverButtons from './PopoverButtons'; import PopoverButtons from './PopoverButtons';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui';
import store from '~/store'; import store from '~/store';
export default function HeaderOptions({ export default function HeaderOptions({
@ -23,6 +22,7 @@ export default function HeaderOptions({
const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState( const [showPluginStoreDialog, setShowPluginStoreDialog] = useRecoilState(
store.showPluginStoreDialog, store.showPluginStoreDialog,
); );
const localize = useLocalize();
const { showPopover, conversation, latestMessage, setShowPopover, setShowBingToneSetting } = const { showPopover, conversation, latestMessage, setShowPopover, setShowBingToneSetting } =
useChatContext(); useChatContext();
@ -84,17 +84,18 @@ export default function HeaderOptions({
{!noSettings[endpoint] && {!noSettings[endpoint] &&
interfaceConfig?.parameters === true && interfaceConfig?.parameters === true &&
!paramEndpoints.has(endpoint) && ( !paramEndpoints.has(endpoint) && (
<Button <TooltipAnchor
aria-label="Settings/parameters"
id="parameters-button" id="parameters-button"
data-testid="parameters-button" aria-label={localize('com_ui_model_parameters')}
type="button" description={localize('com_ui_model_parameters')}
variant="outline" tabIndex={0}
role="button"
onClick={triggerAdvancedMode} 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" /> <Settings2 size={16} aria-label="Settings/Parameters Icon" />
</Button> </TooltipAnchor>
)} )}
</div> </div>
{interfaceConfig?.parameters === true && !paramEndpoints.has(endpoint) && ( {interfaceConfig?.parameters === true && !paramEndpoints.has(endpoint) && (

View file

@ -1,7 +1,7 @@
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import { useWatch } from 'react-hook-form'; import { useWatch } from 'react-hook-form';
import type { Control } 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 { SendIcon } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -17,33 +17,29 @@ const SubmitButton = React.memo(
(props: { disabled: boolean; isRTL: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => { (props: { disabled: boolean; isRTL: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
const localize = useLocalize(); const localize = useLocalize();
return ( return (
<TooltipProvider delayDuration={250}> <TooltipAnchor
<Tooltip> description={localize('com_nav_send_message')}
<TooltipTrigger asChild> render={
<button <button
ref={ref} ref={ref}
aria-label={localize('com_nav_send_message')} aria-label={localize('com_nav_send_message')}
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: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',
)} )}
data-testid="send-button" data-testid="send-button"
type="submit" type="submit"
> >
<span className="" data-state="closed"> <span className="" data-state="closed">
<SendIcon size={24} /> <SendIcon size={24} />
</span> </span>
</button> </button>
</TooltipTrigger> }
<TooltipContent side="top" sideOffset={10}> ></TooltipAnchor>
{localize('com_nav_send_message')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
); );
}, },
), ),

View file

@ -2,11 +2,11 @@ import { useMemo } from 'react';
import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider'; import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider'; import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon'; import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useSubmitMessage } from '~/hooks'; import { useLocalize, useSubmitMessage } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { BirthdayIcon } from '~/components/svg'; import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils'; import { getIconEndpoint, cn } from '~/utils';
import ConvoStarter from './ConvoStarter'; import ConvoStarter from './ConvoStarter';
@ -58,66 +58,58 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const sendConversationStarter = (text: string) => submitMessage({ text }); const sendConversationStarter = (text: string) => submitMessage({ text });
return ( return (
<TooltipProvider delayDuration={50}> <div className="relative h-full">
<Tooltip> <div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="relative h-full"> <div className="flex h-full flex-col items-center justify-center">
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div> <div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<div className="flex h-full flex-col items-center justify-center"> <ConvoIcon
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}> conversation={conversation}
<ConvoIcon assistantMap={assistantMap}
conversation={conversation} endpointsConfig={endpointsConfig}
assistantMap={assistantMap} containerClassName={containerClassName}
endpointsConfig={endpointsConfig} context="landing"
containerClassName={containerClassName} className="h-2/3 w-2/3"
context="landing" size={41}
className="h-2/3 w-2/3" />
size={41} {startupConfig?.showBirthdayIcon === true ? (
/> <TooltipAnchor
{startupConfig?.showBirthdayIcon === true ? ( className="absolute bottom-8 right-2.5"
<div> description={localize('com_ui_happy_birthday')}
<TooltipTrigger> >
<BirthdayIcon className="absolute bottom-8 right-2.5" /> <BirthdayIcon />
</TooltipTrigger> </TooltipAnchor>
<TooltipContent side="top" sideOffset={110} className=""> ) : null}
{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> </div>
</Tooltip> {assistantName ? (
</TooltipProvider> <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 { EditPresetDialog, PresetItems } from './Presets';
import { useLocalize, usePresets } from '~/hooks'; import { useLocalize, usePresets } from '~/hooks';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui'; import { TooltipAnchor } from '~/components';
const PresetsMenu: FC = () => { const PresetsMenu: FC = () => {
const localize = useLocalize(); const localize = useLocalize();
@ -25,17 +25,17 @@ const PresetsMenu: FC = () => {
return ( return (
<Root> <Root>
<Trigger asChild> <Trigger asChild>
<Button <TooltipAnchor
type="button"
variant="outline"
className="flex h-[40px] min-w-4 px-3 radix-state-open:bg-surface-hover"
id="presets-button" id="presets-button"
data-testid="presets-button"
title={localize('com_endpoint_examples')}
aria-label={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" /> <BookCopy size={16} aria-label="Preset Icon" />
</Button> </TooltipAnchor>
</Trigger> </Trigger>
<Portal> <Portal>
<div <div

View file

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

View file

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

View file

@ -1,4 +1,3 @@
export { default as ArchiveButton } from './ArchiveButton';
export { default as DeleteButton } from './DeleteButton'; export { default as DeleteButton } 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';

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 = { type BookmarkNavProps = {
tags: string[]; tags: string[];
setTags: (tags: string[]) => void; 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 localize = useLocalize();
const location = useLocation(); const location = useLocation();
@ -40,6 +41,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps)
className={cn( 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', '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' : '', open ? 'bg-surface-hover' : '',
isSmallScreen ? 'h-14 rounded-2xl' : '',
)} )}
data-testid="bookmark-menu" data-testid="bookmark-menu"
> >

View file

@ -15,11 +15,11 @@ export default function MobileNav({
const { title = 'New Chat' } = conversation || {}; const { title = 'New Chat' } = conversation || {};
return ( 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 <button
type="button" type="button"
data-testid="mobile-header-new-chat-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={() => onClick={() =>
setNavVisible((prev) => { setNavVisible((prev) => {
localStorage.setItem('navVisible', JSON.stringify(!prev)); localStorage.setItem('navVisible', JSON.stringify(!prev));
@ -49,7 +49,7 @@ export default function MobileNav({
</h1> </h1>
<button <button
type="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()} onClick={() => newConversation()}
> >
<svg <svg

View file

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

View file

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

View file

@ -2,14 +2,14 @@ import { Search } from 'lucide-react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; 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 { 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';
import type { TConversation } from 'librechat-data-provider';
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => { const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
const searchQuery = useRecoilValue(store.searchQuery); const searchQuery = useRecoilValue(store.searchQuery);
@ -80,43 +80,36 @@ export default function NewChat({
}; };
return ( return (
<TooltipProvider delayDuration={250}> <div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
<Tooltip> <div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5"> <a
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}> href="/"
<a tabIndex={0}
href="/" data-testid="nav-new-chat"
tabIndex={0} onClick={clickHandler}
data-testid="nav-new-chat" className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
onClick={clickHandler} aria-label={localize('com_ui_new_chat')}
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">
<NewChatButtonIcon conversation={conversation} /> {localize('com_ui_new_chat')}
<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> </div>
{subHeaders != null ? subHeaders : null} <div className="flex gap-3">
</div> <span className="flex items-center" data-state="closed">
</Tooltip> <TooltipAnchor
</TooltipProvider> 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} ref={ref}
className={cn( 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', '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 { 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 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 { NotificationSeverity } from '~/common'; import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { cn } from '~/utils'; import { cn } from '~/utils';
import {
Spinner,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TrashIcon,
} from '~/components';
function SharedLinkDeleteButton({ function SharedLinkDeleteButton({
shareId, shareId,
@ -36,7 +29,7 @@ function SharedLinkDeleteButton({
}, },
}); });
const handleDelete = async (e: MouseEvent<HTMLButtonElement>) => { const handleDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
if (mutation.isLoading) { if (mutation.isLoading) {
return; return;
@ -46,36 +39,14 @@ function SharedLinkDeleteButton({
setIsDeleting(false); setIsDeleting(false);
}; };
return ( return (
<TooltipProvider delayDuration={250}> <TooltipAnchor
<Tooltip> description={localize('com_ui_delete')}
<TooltipTrigger asChild> id="delete-shared-link"
<button id="delete-shared-link" aria-label="Delete shared link" onClick={handleDelete}> aria-label="Delete shared link"
<TrashIcon /> onClick={handleDelete}
</button> >
</TooltipTrigger> <TrashIcon className="size-4" />
<TooltipContent side="top" sideOffset={0}> </TooltipAnchor>
{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>
); );
} }
@ -114,15 +85,12 @@ function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
)} )}
> >
{sharedLink.conversationId && ( {sharedLink.conversationId && (
<> <div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
<SourceChatButton conversationId={sharedLink.conversationId} /> <SharedLinkDeleteButton
<div className={cn('h-4 w-4 cursor-pointer', !isDeleting && 'hover:text-gray-300')}> shareId={sharedLink.shareId}
<SharedLinkDeleteButton setIsDeleting={setIsDeleting}
shareId={sharedLink.shareId} />
setIsDeleting={setIsDeleting} </div>
/>
</div>
</>
)} )}
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { Dialog, DialogTrigger } from '~/components/ui'; import { OGDialog, OGDialogTrigger } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import ShareLinkTable from './SharedLinkTable'; import ShareLinkTable from './SharedLinkTable';
@ -11,19 +11,19 @@ export default function SharedLinks() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div>{localize('com_nav_shared_links')}</div> <div>{localize('com_nav_shared_links')}</div>
<Dialog> <OGDialog>
<DialogTrigger asChild> <OGDialogTrigger asChild>
<button className="btn btn-neutral relative "> <button className="btn btn-neutral relative ">
{localize('com_nav_shared_links_manage')} {localize('com_nav_shared_links_manage')}
</button> </button>
</DialogTrigger> </OGDialogTrigger>
<DialogTemplate <OGDialogTemplate
title={localize('com_nav_shared_links')} title={localize('com_nav_shared_links')}
className="max-w-[1000px]" className="max-w-[1000px]"
showCancelButton={false} showCancelButton={false}
main={<ShareLinkTable />} main={<ShareLinkTable />}
/> />
</Dialog> </OGDialog>
</div> </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 { MessageCircle, ArchiveRestore } from 'lucide-react';
import { useConversationsInfiniteQuery } from '~/data-provider'; import { useConversationsInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks'; import { ConversationListResponse } from 'librechat-data-provider';
import ArchiveButton from '~/components/Conversations/ConvoOptions/ArchiveButton'; import { useAuthContext, useLocalize, useNavScrolling, useArchiveHandler } from '~/hooks';
import DeleteButton from '~/components/Conversations/ConvoOptions/DeleteButton'; import { DeleteButton } from '~/components/Conversations/ConvoOptions';
import { TooltipAnchor } from '~/components/ui';
import { Spinner } from '~/components/svg'; import { Spinner } from '~/components/svg';
import { cn } from '~/utils'; 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 localize = useLocalize();
const { isAuthenticated } = useAuthContext(); const { isAuthenticated } = useAuthContext();
const [showLoading, setShowLoading] = useState(false); const [showLoading, setShowLoading] = useState(false);
const [conversationId, setConversationId] = useState<string | null>(null);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery( const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery(
{ pageNumber: '1', isArchived: true }, { pageNumber: '1', isArchived: true },
@ -30,14 +31,9 @@ export default function ArchivedChatsTable({ className }: { className?: string }
[data], [data],
); );
const classProp: { className?: string } = { const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop);
className: 'p-1 hover:text-black dark:hover:text-white',
};
if (className) {
classProp.className = className;
}
if (!conversations || conversations.length === 0) { if (!data || 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>;
} }
@ -58,48 +54,52 @@ export default function ArchivedChatsTable({ className }: { className?: string }
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{conversations.map((conversation) => ( {conversations.map((conversation) => {
<tr if (!conversation.conversationId) {
key={conversation.conversationId} return null;
className="border-b border-gray-200 text-sm font-normal dark:border-white/10" }
> return (
<td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500"> <tr
<MessageCircle className="mr-1 h-5 w-5" /> key={conversation.conversationId}
{conversation.title} className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
</td> >
<td className="p-1"> <td className="flex items-center py-3 text-blue-800/70 dark:text-blue-500">
<div className="flex justify-between"> <MessageCircle className="mr-1 h-5 w-5" />
<div className="flex justify-start dark:text-gray-200"> {conversation.title}
{new Date(conversation.createdAt).toLocaleDateString('en-US', { </td>
month: 'long', <td className="p-1">
day: 'numeric', <div className="flex justify-between">
year: 'numeric', <div className="flex justify-start dark:text-gray-200">
})} {new Date(conversation.createdAt).toLocaleDateString('en-US', {
</div> month: 'long',
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400"> day: 'numeric',
{conversation.conversationId && ( year: 'numeric',
<> })}
<ArchiveButton </div>
className="hover:text-black dark:hover:text-white" <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} conversationId={conversation.conversationId}
retainView={moveToTop} retainView={moveToTop}
shouldArchive={false} title={conversation.title ?? ''}
icon={<ArchiveRestore className="h-4 w-4 hover:text-gray-300" />}
/> />
<div className="h-5 w-5 hover:text-gray-300"> </div>
<DeleteButton </div>
conversationId={conversation.conversationId}
retainView={moveToTop}
title={conversation.title ?? ''}
/>
</div>
</>
)}
</div> </div>
</div> </td>
</td> </tr>
</tr> );
))} })}
</tbody> </tbody>
</table> </table>
{(isFetchingNextPage || showLoading) && ( {(isFetchingNextPage || showLoading) && (

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,7 @@ function SelectDropDownPop({
<button <button
data-testid="select-dropdown-button" data-testid="select-dropdown-button"
className={cn( 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', '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}`} aria-label={`Select ${title}`}

View file

@ -1,46 +1,65 @@
import * as React from 'react'; import * as Ariakit from '@ariakit/react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '~/utils'; 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< export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
React.ElementRef<typeof TooltipPrimitive.Trigger>, { description, side = 'top', role, ...props },
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger> ref,
>((props, ref) => <TooltipPrimitive.Trigger ref={ref} {...props} />); ) {
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName; 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< const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
React.ElementRef<typeof TooltipPrimitive.Arrow>, if (role === 'button' && event.key === 'Enter') {
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow> event.preventDefault();
>((props, ref) => <TooltipPrimitive.Arrow ref={ref} {...props} />); (event.target as HTMLDivElement).click();
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName; }
};
const TooltipContent = React.forwardRef< return (
React.ElementRef<typeof TooltipPrimitive.Content>, <Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> <Ariakit.TooltipAnchor {...props} ref={ref} role={role} onKeyDown={handleKeyDown} />
>(({ className = '', forceMount, children, ...props }, ref) => ( <AnimatePresence>
<TooltipPortal forceMount={forceMount}> {mounted && (
<TooltipPrimitive.Content <Ariakit.Tooltip
className={cn( gutter={4}
'shadow-xs relative z-[1000] max-w-xs rounded-lg border border-gray-900/10 bg-gray-900 p-1 transition-opacity', alwaysVisible
className, className="tooltip"
)} render={
ref={ref} <motion.div
{...props} initial={{ opacity: 0, x, y }}
style={{ userSelect: 'none' }} animate={{ opacity: 1, x: 0, y: 0 }}
> exit={{ opacity: 0, x, y }}
<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> <Ariakit.TooltipArrow />
</TooltipPrimitive.Content> {description}
</TooltipPortal> </Ariakit.Tooltip>
)); )}
TooltipContent.displayName = TooltipPrimitive.Content.displayName; </AnimatePresence>
</Ariakit.TooltipProvider>
const TooltipProvider = TooltipPrimitive.Provider; );
});
export { Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, 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 useConversation } from './useConversation';
export { default as useGenerateConvo } from './useGenerateConvo'; export { default as useGenerateConvo } from './useGenerateConvo';
export { default as useConversations } from './useConversations'; export { default as useConversations } from './useConversations';
export { default as useArchiveHandler } from './useArchiveHandler';
export { default as useDebouncedInput } from './useDebouncedInput'; export { default as useDebouncedInput } from './useDebouncedInput';
export { default as useBookmarkSuccess } from './useBookmarkSuccess'; export { default as useBookmarkSuccess } from './useBookmarkSuccess';
export { default as useNavigateToConvo } from './useNavigateToConvo'; export { default as useNavigateToConvo } from './useNavigateToConvo';

View file

@ -1,22 +1,13 @@
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; 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 { useArchiveConversationMutation } from '~/data-provider';
import useConversations from './useConversations';
import { NotificationSeverity } from '~/common'; import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import useLocalize from '../useLocalize';
import useNewConvo from '../useNewConvo';
type ArchiveButtonProps = { export default function useArchiveHandler(
children?: React.ReactNode;
conversationId: string;
retainView: () => void;
shouldArchive: boolean;
icon?: React.ReactNode;
className?: string;
};
export function useArchiveHandler(
conversationId: string, conversationId: string,
shouldArchive: boolean, shouldArchive: boolean,
retainView: () => void, 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_delete_error: 'There was an error deleting the bookmark',
com_ui_bookmarks_add_to_conversation: 'Add to current conversation', com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
com_ui_bookmarks_filter: 'Filter bookmarks...', 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_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_no_conversation_id: 'No conversation ID found',
com_ui_add_multi_conversation: 'Add multi-conversation',
com_auth_error_login: com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.', 'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl: 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_shared_link_not_found: 'Link condiviso non trovato',
com_ui_delete_conversation: 'Eliminare la chat?', com_ui_delete_conversation: 'Eliminare la chat?',
com_ui_delete_confirm: 'Questo eliminerà', com_ui_delete_confirm: 'Questo eliminerà',
com_ui_rename: 'Rinominare', com_ui_rename: 'Rinomina',
com_ui_archive: 'Arsip', com_ui_archive: 'Archivia',
com_ui_archive_error: 'Errore durante l\'archiviazione della conversazione', com_ui_archive_error: 'Errore durante l\'archiviazione della conversazione',
com_ui_unarchive: 'Disarchivia', com_ui_unarchive: 'Disarchivia',
com_ui_unarchive_error: 'Impossibile disarchiviare la conversazione', com_ui_unarchive_error: 'Impossibile disarchiviare la conversazione',
com_ui_more_options: 'Pi', com_ui_more_options: 'Più',
com_ui_delete_assistant_confirm: com_ui_delete_assistant_confirm:
'Sei sicuro di voler eliminare questo Assistente? Questa operazione non può essere annullata.', 'Sei sicuro di voler eliminare questo Assistente? Questa operazione non può essere annullata.',
com_ui_preview: 'Anteprima', com_ui_preview: 'Anteprima',
@ -1361,11 +1361,11 @@ export const comparisons = {
}, },
com_ui_rename: { com_ui_rename: {
english: 'Rename', english: 'Rename',
translated: 'Rinominare', translated: 'Rinomina',
}, },
com_ui_archive: { com_ui_archive: {
english: 'Archive', english: 'Archive',
translated: 'Arsip', translated: 'Archivia',
}, },
com_ui_archive_error: { com_ui_archive_error: {
english: 'Failed to archive conversation', 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**: - **com_ui_rename**:
- **english**: Rename - **english**: Rename
- **translated**: Rinominare - **translated**: Rinomina
- **com_ui_archive**: - **com_ui_archive**:
- **english**: Archive - **english**: Archive
- **translated**: Arsip - **translated**: Archivia
- **com_ui_archive_error**: - **com_ui_archive_error**:
- **english**: Failed to archive conversation - **english**: Failed to archive conversation

View file

@ -1963,7 +1963,7 @@ button.scroll-convo {
.prose ol > li::marker, .prose ol > li::marker,
.markdown ol > li::marker { .markdown ol > li::marker {
content: counter(list-counter) ". "; content: counter(list-counter) '. ';
color: var(--tw-prose-counters); color: var(--tw-prose-counters);
font-weight: 400; font-weight: 400;
} }
@ -1982,7 +1982,7 @@ button.scroll-convo {
.prose ol ol > li::marker, .prose ol ol > li::marker,
.markdown 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, .prose ol ol ol,
@ -1998,7 +1998,7 @@ button.scroll-convo {
.prose ol ol ol > li::marker, .prose ol ol ol > li::marker,
.markdown 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 */ /* Unordered lists */
@ -2288,6 +2288,27 @@ button.scroll-convo {
line-height: inherit; 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 { :focus {
outline: none; outline: none;
} }

53
package-lock.json generated
View file

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