👐 style: Improve a11y/theming for Settings Dialog, Dropdown Menus; fix: SearchBar focus issues (#4091)

* fix: cursor pointer not applying correct in the root component

* fix: add cursor-not-allowed to disabled state in SendButton component

* feat: update Dropdown to ariakit and changed LLM error's style

* feat: switched to ariakit's Dropdown and style improvements

* feat: archive updates

* refactor: delete conversations in archive

* refactor: settings

* add cool settings animation

* a11y: settings update

* style: update settings

* style: settings account settings menu; a11y(AccountSettings): switched to AriaKit

* a11y: account settings update

* style: update my files dialog

* fix: tests

* chore: remove console.log()

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-09-22 04:45:50 +02:00 committed by GitHub
parent eba2c9a032
commit 2d62eca612
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1054 additions and 824 deletions

View file

@ -50,7 +50,7 @@ const DeleteBookmarkButton: FC<{
<OGDialogTrigger asChild>
<TooltipAnchor
description={localize('com_ui_delete')}
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}

View file

@ -35,7 +35,7 @@ const EditBookmarkButton: FC<{
onFocus={onFocus}
onBlur={onBlur}
onClick={() => setOpen(!open)}
className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onKeyDown={handleKeyDown}
>
<EditIcon />

View file

@ -1,4 +1,4 @@
import { memo, useRef, useMemo } from 'react';
import { memo, useRef, useMemo, useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
supportsFiles,
@ -44,6 +44,7 @@ const ChatForm = ({ index = 0 }) => {
const TextToSpeech = useRecoilValue(store.textToSpeech);
const automaticPlayback = useRecoilValue(store.automaticPlayback);
const isSearching = useRecoilValue(store.isSearching);
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index));
const [showMentionPopover, setShowMentionPopover] = useRecoilState(
@ -123,6 +124,12 @@ const ChatForm = ({ index = 0 }) => {
},
});
useEffect(() => {
if (!isSearching && textAreaRef.current && !disableInputs) {
textAreaRef.current.focus();
}
}, [isSearching, disableInputs]);
return (
<form
onSubmit={methods.handleSubmit((data) => submitMessage(data))}
@ -164,9 +171,6 @@ const ChatForm = ({ index = 0 }) => {
{endpoint && (
<TextareaAutosize
{...registerProps}
// TODO: remove autofocus due to a11y issues
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
ref={(e) => {
ref(e);
textAreaRef.current = e;

View file

@ -1,10 +1,9 @@
import { FileSources, FileContext } from 'librechat-data-provider';
import type { TFile } from 'librechat-data-provider';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '~/components';
import { useGetFiles } from '~/data-provider';
import { DataTable, columns } from './Table';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
export default function Files({ open, onOpenChange }) {
const localize = useLocalize();
@ -19,20 +18,16 @@ export default function Files({ open, onOpenChange }) {
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={cn('w-11/12 overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white')}
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogContent
title={localize('com_nav_my_files')}
className="w-11/12 overflow-x-auto bg-background text-text-primary shadow-2xl"
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_my_files')}
</DialogTitle>
</DialogHeader>
<div className="overflow-x-auto p-0 sm:p-6 sm:pt-4">
<OGDialogHeader>
<OGDialogTitle>{localize('com_nav_my_files')}</OGDialogTitle>
</OGDialogHeader>
<DataTable columns={columns} data={files} />
<div className="mt-5 sm:mt-4" />
</div>
</DialogContent>
</Dialog>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -26,7 +26,7 @@ const SubmitButton = React.memo(
id="send-button"
disabled={props.disabled}
className={cn(
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:cursor-not-allowed disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
props.isRTL
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',

View file

@ -57,7 +57,7 @@ export const ErrorMessage = ({
<Container message={message}>
<div
className={cn(
'rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
className,
)}
>

View file

@ -1,13 +1,12 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import type { TMessage } from 'librechat-data-provider';
import { useDeleteConversationMutation } from '~/data-provider';
import { OGDialog, OGDialogTrigger, Label, TooltipAnchor } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { TrashIcon } from '~/components/svg';
import { useLocalize, useNewConvo } from '~/hooks';
import { OGDialog, Label } from '~/components';
type DeleteButtonProps = {
conversationId: string;
@ -17,19 +16,21 @@ type DeleteButtonProps = {
setShowDeleteDialog?: (value: boolean) => void;
};
export default function DeleteButton({
export function DeleteConversationDialog({
conversationId,
retainView,
title,
showDeleteDialog,
setShowDeleteDialog,
}: DeleteButtonProps) {
}: {
conversationId: string;
retainView: () => void;
title: string;
}) {
const localize = useLocalize();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { newConversation } = useNewConvo();
const { conversationId: currentConvoId } = useParams();
const [open, setOpen] = useState(false);
const deleteConvoMutation = useDeleteConversationMutation({
onSuccess: () => {
if (currentConvoId === conversationId || currentConvoId === 'new') {
@ -47,7 +48,7 @@ export default function DeleteButton({
deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' });
}, [conversationId, deleteConvoMutation, queryClient]);
const dialogContent = (
return (
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
@ -71,25 +72,26 @@ export default function DeleteButton({
}}
/>
);
}
if (showDeleteDialog !== undefined && setShowDeleteDialog !== undefined) {
return (
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
{dialogContent}
</OGDialog>
);
export default function DeleteButton({
conversationId,
retainView,
title,
showDeleteDialog,
setShowDeleteDialog,
}: DeleteButtonProps) {
if (showDeleteDialog === undefined && setShowDeleteDialog === undefined) {
return null;
}
return (
<OGDialog open={open} onOpenChange={setOpen}>
<TooltipAnchor description={localize('com_ui_delete')}>
<OGDialogTrigger asChild>
<button>
<TrashIcon className="h-5 w-5" />
</button>
</OGDialogTrigger>
</TooltipAnchor>
{dialogContent}
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DeleteConversationDialog
conversationId={conversationId}
retainView={retainView}
title={title}
/>
</OGDialog>
);
}

View file

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

View file

@ -1,23 +1,20 @@
import { FileText } from 'lucide-react';
import { useRecoilState } from 'recoil';
import * as Select from '@ariakit/react/select';
import { Fragment, useState, memo } from 'react';
import { Menu, MenuItem, MenuButton, MenuItems, Transition } from '@headlessui/react';
import { FileText, LogOut } from 'lucide-react';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { LinkIcon, GearIcon, DropdownMenuSeparator } from '~/components';
import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';
import { LinkIcon, GearIcon } from '~/components';
import { UserIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import Settings from './Settings';
import NavLink from './NavLink';
import Logout from './Logout';
import { cn } from '~/utils/';
import store from '~/store';
function AccountSettings() {
const localize = useLocalize();
const { user, isAuthenticated } = useAuthContext();
const { user, isAuthenticated, logout } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
const balanceQuery = useGetUserBalance({
enabled: !!isAuthenticated && startupConfig?.checkBalance,
@ -29,17 +26,11 @@ function AccountSettings() {
const name = user?.avatar ?? user?.username ?? '';
return (
<>
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
<Select.SelectProvider>
<Select.Select
aria-label={localize('com_nav_account_settings')}
className={cn(
'group-ui-open:bg-surface-tertiary duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-surface-secondary',
open ? 'bg-surface-secondary' : '',
)}
data-testid="nav-user"
className="duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
>
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex">
@ -52,11 +43,16 @@ function AccountSettings() {
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
}}
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
aria-hidden="true"
>
<UserIcon />
</div>
) : (
<img className="rounded-full" src={user?.avatar ?? avatarSrc} alt="avatar" />
<img
className="rounded-full"
src={user?.avatar ?? avatarSrc}
alt={`${name}'s avatar`}
/>
)}
</div>
</div>
@ -66,78 +62,69 @@ function AccountSettings() {
>
{user?.name ?? user?.username ?? localize('com_nav_user')}
</div>
</MenuButton>
<Transition
as={Fragment}
enter="transition ease-out duration-100 transform"
enterFrom="translate-y-2 opacity-0"
enterTo="translate-y-0 opacity-100"
leave="transition ease-in duration-100 transform"
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
</Select.Select>
<Select.SelectPopover
className="popover-ui w-[235px]"
style={{
transformOrigin: 'bottom',
marginRight: '0px',
translate: '0px',
}}
>
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-border-medium bg-header-primary p-1.5 opacity-100 shadow-lg outline-none">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
{user?.email ?? localize('com_nav_user')}
</div>
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
<DropdownMenuSeparator />
{startupConfig?.checkBalance === true &&
balanceQuery.data != null &&
!isNaN(parseFloat(balanceQuery.data)) && (
<>
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
</div>
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
<DropdownMenuSeparator />
</>
)}
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)}
/>
)}
</MenuItem>
<Select.SelectItem
value=""
onClick={() => setShowFiles(true)}
className="select-item text-sm"
>
<FileText className="icon-md" aria-hidden="true" />
{localize('com_nav_my_files')}
</Select.SelectItem>
{startupConfig?.helpAndFaqURL !== '/' && (
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
/>
<Select.SelectItem
value=""
onClick={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
className="select-item text-sm"
>
<LinkIcon aria-hidden="true" />
{localize('com_nav_help_faq')}
</Select.SelectItem>
)}
</MenuItem>
)}
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')}
clickHandler={() => {
setTimeout(() => setShowSettings(true), 50);
}}
/>
)}
</MenuItem>
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
<MenuItem>
{({ focus }) => <Logout className={focus ? 'bg-surface-hover' : ''} />}
</MenuItem>
</MenuItems>
</Transition>
</>
)}
</Menu>
<Select.SelectItem
value=""
onClick={() => setShowSettings(true)}
className="select-item text-sm"
>
<GearIcon className="icon-md" aria-hidden="true" />
{localize('com_nav_settings')}
</Select.SelectItem>
<DropdownMenuSeparator />
<Select.SelectItem
aria-selected={true}
onClick={() => logout()}
value="logout"
className="select-item text-sm"
>
<LogOut className="icon-md" />
{localize('com_nav_log_out')}
</Select.SelectItem>
</Select.SelectPopover>
{showFiles && <FilesView open={showFiles} onOpenChange={setShowFiles} />}
{showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />}
</>
</Select.SelectProvider>
);
}

View file

@ -1,26 +0,0 @@
import { forwardRef } from 'react';
import { useAuthContext } from '~/hooks/AuthContext';
import { LogOutIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const Logout = forwardRef<HTMLButtonElement, { className?: string }>((props, ref) => {
const { logout } = useAuthContext();
const localize = useLocalize();
return (
<button
ref={ref}
className={cn(
'group group flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-hover data-[focus]:bg-surface-hover',
props.className ?? '',
)}
onClick={() => logout()}
>
<LogOutIcon />
{localize('com_nav_log_out')}
</button>
);
});
export default Logout;

View file

@ -16,7 +16,7 @@ const NavLink: FC<Props> = forwardRef<HTMLButtonElement, Props>((props, ref) =>
onClick?: React.MouseEventHandler<HTMLButtonElement>;
} = {
className: cn(
'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary hover:bg-surface-hover',
'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary',
className,
{
'opacity-50 pointer-events-none': disabled,

View file

@ -41,7 +41,7 @@ export default function NavToggle({
description={
navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar')
}
className="flex cursor-pointer items-center justify-center"
className="flex items-center justify-center"
tabIndex={0}
>
<span className="" data-state="closed">

View file

@ -7,7 +7,6 @@ import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useLocalize, useNewConvo } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { NewChatIcon } from '~/components/svg';
import store from '~/store';
@ -96,15 +95,7 @@ export default function NewChat({
</div>
<div className="flex gap-3">
<span className="flex items-center" data-state="closed">
<TooltipAnchor
side="right"
id="nav-new-chat-btn"
aria-label="nav-new-chat-btn"
description={localize('com_ui_new_chat')}
className="text-text-primary"
>
<NewChatIcon className="size-5" />
</TooltipAnchor>
</span>
</div>
</a>

View file

@ -20,6 +20,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
const setSearchQuery = useSetRecoilState(store.searchQuery);
const [showClearIcon, setShowClearIcon] = useState(false);
const [text, setText] = useState('');
const setIsSearching = useSetRecoilState(store.isSearching);
const localize = useLocalize();
const clearText = useCallback(() => {
@ -47,6 +48,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
},
[queryClient, clearConvoState, setSearchQuery],
);
// TODO: make the debounce time configurable via yaml
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
@ -54,6 +57,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
setShowClearIcon(value.length > 0);
setText(value);
debouncedSendRequest(value);
setIsSearching(true);
};
return (
@ -78,6 +82,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
aria-label={localize('com_nav_search_placeholder')}
placeholder={localize('com_nav_search_placeholder')}
onKeyUp={handleKeyUp}
onFocus={() => setIsSearching(true)}
onBlur={() => setIsSearching(true)}
autoComplete="off"
dir="auto"
/>

View file

@ -1,4 +1,4 @@
import * as React from 'react';
import { useState, useRef } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { MessageSquare, Command } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
@ -12,7 +12,8 @@ import { cn } from '~/utils';
export default function Settings({ open, onOpenChange }: TDialogProps) {
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const localize = useLocalize();
const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL);
const [activeTab, setActiveTab] = useState(SettingsTabValues.GENERAL);
const tabRefs = useRef({});
const handleKeyDown = (event: React.KeyboardEvent) => {
const tabs = [
@ -28,12 +29,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
break;
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
break;
@ -48,6 +47,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
}
};
const handleTabChange = (value: string) => {
setActiveTab(value as SettingsTabValues);
};
return (
<Transition appear show={open}>
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
@ -55,7 +58,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
@ -70,15 +73,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className={cn(
'fixed inset-0 flex w-screen items-center justify-center p-4',
isSmallScreen ? '' : '',
)}
>
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
<DialogPanel
className={cn(
'overflow-hidden rounded-xl rounded-b-lg bg-surface-tertiary-alt pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
)}
>
<DialogTitle
@ -111,18 +109,18 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<span className="sr-only">Close</span>
</button>
</DialogTitle>
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<div className="max-h-[550px] overflow-auto px-6 md:max-h-[400px] md:min-h-[400px] md:w-[680px]">
<Tabs.Root
value={activeTab}
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
onValueChange={handleTabChange}
className="flex flex-col gap-10 md:flex-row"
orientation="horizontal"
orientation="vertical"
>
<Tabs.List
aria-label="Settings"
className={cn(
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
'min-w-auto max-w-auto relative -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-xl bg-surface-secondary' : '',
)}
onKeyDown={handleKeyDown}
>
@ -166,19 +164,20 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Tabs.Trigger
key={value}
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
'group relative z-10 m-1 flex items-center justify-start gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
? 'flex-1 justify-center text-nowrap rounded-xl p-1 px-3 text-sm text-text-secondary radix-state-active:bg-surface-hover radix-state-active:text-text-primary'
: 'rounded-md bg-transparent text-text-primary radix-state-active:bg-surface-tertiary',
)}
value={value}
ref={(el) => (tabRefs.current[value] = el)}
>
{icon}
{localize(label)}
</Tabs.Trigger>
))}
</Tabs.List>
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
<div className="overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
<Tabs.Content value={SettingsTabValues.GENERAL}>
<General />
</Tabs.Content>

View file

@ -4,7 +4,7 @@ import CodeArtifacts from './CodeArtifacts';
function Beta() {
return (
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<CodeArtifacts />
</div>
</div>

View file

@ -11,26 +11,26 @@ import SaveDraft from './SaveDraft';
function Chat() {
return (
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<FontSizeSelector />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<ChatDirection />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<SendMessageKeyEnter />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<ShowCodeSwitch />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<SaveDraft />
</div>
<ForkSettings />
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<ModularChat />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<LaTeXParsing />
</div>
</div>

View file

@ -1,6 +1,7 @@
import React from 'react';
import { useRecoilState } from 'recoil';
import { useLocalize } from '~/hooks';
import { Button } from '~/components';
import store from '~/store';
const ChatDirection = () => {
@ -16,12 +17,11 @@ const ChatDirection = () => {
<div className="flex items-center space-x-2">
<span id="chat-direction-label">{localize('com_nav_chat_direction')}</span>
</div>
<button
<Button
variant="outline"
aria-label="Toggle chat direction"
onClick={toggleChatDirection}
data-testid="chatDirection"
className="btn btn-neutral relative ring-ring-primary"
aria-labelledby="chat-direction-label chat-direction-status"
aria-pressed={direction === 'RTL'}
>
<span aria-hidden="true">{direction.toLowerCase()}</span>
<span id="chat-direction-status" className="sr-only">
@ -29,7 +29,7 @@ const ChatDirection = () => {
? localize('chat_direction_left_to_right')
: localize('chat_direction_right_to_left')}
</span>
</button>
</Button>
</div>
);
};

View file

@ -19,7 +19,7 @@ export const ForkSettings = () => {
return (
<>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_ui_fork_change_default')}</div>
@ -30,12 +30,11 @@ export const ForkSettings = () => {
onChange={setForkSetting}
options={forkOptions}
sizeClasses="w-[200px]"
anchor="bottom start"
testId="fork-setting-dropdown"
/>
</div>
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<div className="flex items-center justify-between">
<div> {localize('com_ui_fork_default')} </div>
<Switch
@ -47,7 +46,7 @@ export const ForkSettings = () => {
/>
</div>
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_ui_fork_split_target_setting')}</div>

View file

@ -28,16 +28,16 @@ function Commands() {
<HoverCardSettings side="bottom" text="com_nav_chat_commands_info" />
</div>
<div className="flex flex-col gap-3 text-sm text-text-primary">
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<AtCommandSwitch />
</div>
{hasAccessToMultiConvo === true && (
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<PlusCommandSwitch />
</div>
)}
{hasAccessToPrompts === true && (
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<SlashCommandSwitch />
</div>
)}

View file

@ -1,36 +1,45 @@
import { useMemo, useState } from 'react';
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Link as LinkIcon } from 'lucide-react';
import { Link as LinkIcon, TrashIcon } from 'lucide-react';
import type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider';
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import { Spinner, TooltipAnchor, TrashIcon } from '~/components';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { cn } from '~/utils';
import {
Button,
Label,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
TooltipAnchor,
Skeleton,
Spinner,
OGDialog,
OGDialogTrigger,
} from '~/components';
function SharedLinkDeleteButton({
shareId,
setIsDeleting,
}: {
shareId: string;
setIsDeleting: (isDeleting: boolean) => void;
}) {
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
const [isDeleting, setIsDeleting] = useState(false);
const localize = useLocalize();
const { showToast } = useToastContext();
const mutation = useDeleteSharedLinkMutation({
onError: () => {
showToast({
message: localize('com_ui_share_delete_error'),
severity: NotificationSeverity.ERROR,
showIcon: true,
});
setIsDeleting(false);
},
});
const handleDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
const confirmDelete = async (shareId: TSharedLink['shareId']) => {
if (mutation.isLoading) {
return;
}
@ -38,67 +47,78 @@ function SharedLinkDeleteButton({
await mutation.mutateAsync({ shareId });
setIsDeleting(false);
};
return (
<TooltipAnchor
description={localize('com_ui_delete')}
id="delete-shared-link"
aria-label="Delete shared link"
onClick={handleDelete}
>
<TrashIcon className="size-4" />
</TooltipAnchor>
);
}
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
const [isDeleting, setIsDeleting] = useState(false);
return (
<tr
key={sharedLink.conversationId}
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
<TableRow className={(cn(isDeleting && 'opacity-50'), 'hover:bg-transparent')}>
<TableCell>
<Link
to={`/share/${sharedLink.shareId}`}
target="_blank"
rel="noreferrer"
className="flex items-center text-blue-500 hover:underline"
>
<td
className={cn(
'flex items-center py-3 text-blue-800/70 dark:text-blue-500',
isDeleting && 'opacity-50',
)}
>
<Link to={`/share/${sharedLink.shareId}`} target="_blank" rel="noreferrer" className="flex">
<LinkIcon className="mr-1 h-5 w-5" />
<LinkIcon className="mr-2 h-4 w-4" />
{sharedLink.title}
</Link>
</td>
<td className="p-3">
<div className="flex justify-between">
<div className={cn('flex justify-start dark:text-gray-200', isDeleting && 'opacity-50')}>
</TableCell>
<TableCell>
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
<div
className={cn(
'flex items-center justify-end gap-3 text-gray-400',
isDeleting && 'opacity-50',
)}
>
</TableCell>
<TableCell className="text-right">
{sharedLink.conversationId && (
<div className={cn('cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
<SharedLinkDeleteButton
shareId={sharedLink.shareId}
setIsDeleting={setIsDeleting}
<OGDialog>
<OGDialogTrigger asChild>
<TooltipAnchor
description={localize('com_ui_delete')}
render={
<Button
aria-label="Delete shared link"
variant="ghost"
size="icon"
className="size-8"
>
<TrashIcon className="size-4" />
</Button>
}
></TooltipAnchor>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label
htmlFor="dialog-confirm-delete"
className="text-left text-sm font-medium"
>
{localize('com_ui_delete_confirm')} <strong>{sharedLink.title}</strong>
</Label>
</div>
</div>
</>
}
selection={{
selectHandler: () => confirmDelete(sharedLink.shareId),
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</div>
</OGDialog>
)}
</div>
</div>
</td>
</tr>
</TableCell>
</TableRow>
);
}
export default function ShareLinkTable({ className }: { className?: string }) {
export default function ShareLinkTable({ className }) {
const localize = useLocalize();
const { isAuthenticated } = useAuthContext();
const [showLoading, setShowLoading] = useState(false);
@ -114,15 +134,28 @@ export default function ShareLinkTable({ className }: { className?: string }) {
});
const sharedLinks = useMemo(() => data?.pages.flatMap((page) => page.sharedLinks) || [], [data]);
const classProp: { className?: string } = {
className: 'p-1 hover:text-black dark:hover:text-white',
};
if (className) {
classProp.className = className;
}
const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170;
const skeletons = Array.from({ length: 11 }, (_, index) => {
const randomWidth = getRandomWidth();
return (
<div key={index} className="flex h-10 w-full items-center">
<div className="flex w-[410px] items-center">
<Skeleton className="h-4" style={{ width: `${randomWidth}px` }} />
</div>
<div className="flex flex-grow justify-center">
<Skeleton className="h-4 w-28" />
</div>
<div className="mr-2 flex justify-end">
<Skeleton className="h-4 w-12" />
</div>
</div>
);
});
if (isLoading) {
return <Spinner className="m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white" />;
return <div className="text-gray-300">{skeletons}</div>;
}
if (isError) {
@ -132,35 +165,34 @@ export default function ShareLinkTable({ className }: { className?: string }) {
</div>
);
}
if (!sharedLinks || sharedLinks.length === 0) {
if (sharedLinks.length === 0) {
return <div className="text-gray-300">{localize('com_nav_shared_links_empty')}</div>;
}
return (
<div
className={cn(
'grid w-full gap-2',
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
'max-h-[350px]',
'-mr-2 grid max-h-[350px] w-full flex-1 flex-col gap-2 overflow-y-auto pr-2 transition-opacity duration-500',
className,
)}
ref={containerRef}
>
<table className="table-fixed text-left">
<thead className="sticky top-0 bg-white dark:bg-gray-700">
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
<th className="p-3">{localize('com_nav_shared_links_name')}</th>
<th className="p-3">{localize('com_nav_shared_links_date_shared')}</th>
</tr>
</thead>
<tbody>
<Table>
<TableHeader>
<TableRow>
<TableHead>{localize('com_nav_shared_links_name')}</TableHead>
<TableHead>{localize('com_nav_shared_links_date_shared')}</TableHead>
<TableHead className="text-right">{localize('com_assistants_actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sharedLinks.map((sharedLink) => (
<ShareLinkRow key={sharedLink.shareId} sharedLink={sharedLink} />
))}
</tbody>
</table>
{(isFetchingNextPage || showLoading) && (
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
)}
</TableBody>
</Table>
{(isFetchingNextPage || showLoading) && <Spinner className="mx-auto my-4" />}
</div>
);
}

View file

@ -1,6 +1,6 @@
import { useLocalize } from '~/hooks';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, OGDialogTrigger } from '~/components/ui';
import { OGDialog, OGDialogTrigger, Button } from '~/components';
import ArchivedChatsTable from './ArchivedChatsTable';
@ -12,9 +12,9 @@ export default function ArchivedChats() {
<div>{localize('com_nav_archived_chats')}</div>
<OGDialog>
<OGDialogTrigger asChild>
<button className="btn btn-neutral relative ">
<Button variant="outline" aria-label="Archived chats">
{localize('com_nav_archived_chats_manage')}
</button>
</Button>
</OGDialogTrigger>
<OGDialogTemplate
title={localize('com_nav_archived_chats')}

View file

@ -1,109 +1,252 @@
import { useMemo, useState, useCallback } from 'react';
import { MessageCircle, ArchiveRestore } from 'lucide-react';
import { useState, useCallback, useEffect } from 'react';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { ConversationListResponse } from 'librechat-data-provider';
import { useAuthContext, useLocalize, useNavScrolling, useArchiveHandler } from '~/hooks';
import { DeleteButton } from '~/components/Conversations/ConvoOptions';
import { TooltipAnchor } from '~/components/ui';
import { Spinner } from '~/components/svg';
import {
Search,
ChevronRight,
ChevronLeft,
TrashIcon,
MessageCircle,
ArchiveRestore,
ChevronsRight,
ChevronsLeft,
} from 'lucide-react';
import type { TConversation } from 'librechat-data-provider';
import { useAuthContext, useLocalize, useArchiveHandler } from '~/hooks';
import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions';
import {
TooltipAnchor,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Separator,
Skeleton,
Button,
Input,
OGDialog,
OGDialogTrigger,
} from '~/components';
import { cn } from '~/utils';
export default function ArchivedChatsTable() {
const localize = useLocalize();
const { isAuthenticated } = useAuthContext();
const [showLoading, setShowLoading] = useState(false);
const [conversationId, setConversationId] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [totalPages, setTotalPages] = useState(1);
const [isOpened, setIsOpened] = useState(false);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery(
{ pageNumber: '1', isArchived: true },
{ enabled: isAuthenticated },
const { data, isLoading, refetch } = useConversationsInfiniteQuery(
{ pageNumber: currentPage.toString(), limit: 10, isArchived: true },
{ enabled: isAuthenticated && isOpened },
);
const { containerRef, moveToTop } = useNavScrolling<ConversationListResponse>({
setShowLoading,
hasNextPage: hasNextPage,
fetchNextPage: fetchNextPage,
isFetchingNextPage: isFetchingNextPage,
useEffect(() => {
if (data) {
setTotalPages(Math.ceil(Number(data.pages)));
}
}, [data]);
const archiveHandler = useArchiveHandler(conversationId ?? '', false, () => {
refetch();
});
const conversations = useMemo(
() => data?.pages.flatMap((page) => page.conversations) || [],
[data],
const handleChatClick = useCallback((conversationId) => {
window.open(`/c/${conversationId}`, '_blank');
}, []);
const handlePageChange = useCallback((newPage) => {
setCurrentPage(newPage);
}, []);
const handleSearch = useCallback((query) => {
setSearchQuery(query);
setCurrentPage(1);
}, []);
const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170;
const skeletons = Array.from({ length: 11 }, (_, index) => {
const randomWidth = getRandomWidth();
return (
<div key={index} className="flex h-10 w-full items-center">
<div className="flex w-[410px] items-center">
<Skeleton className="h-4" style={{ width: `${randomWidth}px` }} />
</div>
<div className="flex flex-grow justify-center">
<Skeleton className="h-4 w-28" />
</div>
<div className="mr-2 flex justify-end">
<Skeleton className="h-4 w-12" />
</div>
</div>
);
});
const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop);
if (isLoading) {
return <div className="text-gray-300">{skeletons}</div>;
}
if (!data || conversations.length === 0) {
if (!data || data.pages.length === 0 || data.pages[0].conversations.length === 0) {
return <div className="text-gray-300">{localize('com_nav_archived_chats_empty')}</div>;
}
const conversations = data.pages.flatMap((page) => page.conversations);
return (
<div
className={cn(
'grid w-full gap-2',
'flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
'max-h-[350px]',
'max-h-[629px]',
)}
ref={containerRef}
onMouseEnter={() => setIsOpened(true)}
>
<table className="table-fixed text-left">
<thead className="sticky top-0 bg-white dark:bg-gray-700">
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
<th className="p-3">{localize('com_nav_archive_name')}</th>
<th className="p-3">{localize('com_nav_archive_created_at')}</th>
</tr>
</thead>
<tbody>
{conversations.map((conversation) => {
if (!conversation.conversationId) {
return null;
}
return (
<tr
key={conversation.conversationId}
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
<div className="flex items-center">
<Search className="size-4 text-text-secondary" />
<Input
type="text"
placeholder={localize('com_nav_search_placeholder')}
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="w-full border-none"
/>
</div>
<Separator />
{conversations.length === 0 ? (
<div className="mt-4 text-text-secondary">{localize('com_nav_no_search_results')}</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50%] p-4">{localize('com_nav_archive_name')}</TableHead>
<TableHead className="w-[35%] p-1">
{localize('com_nav_archive_created_at')}
</TableHead>
<TableHead className="w-[15%] p-1 text-right">
{localize('com_assistants_actions')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{conversations.map((conversation: TConversation) => (
<TableRow key={conversation.conversationId} className="hover:bg-transparent">
<TableCell className="flex items-center py-3 text-text-primary">
<button
className="flex"
aria-label="Open conversation in a new tab"
onClick={() => handleChatClick(conversation.conversationId)}
>
<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">
<u>{conversation.title}</u>
</button>
</TableCell>
<TableCell className="p-1">
<div className="flex justify-between">
<div className="flex justify-start dark:text-gray-200">
<div className="flex justify-start text-text-secondary">
{new Date(conversation.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className="ml-auto mr-4 flex items-center justify-end gap-1 text-gray-400">
</div>
</TableCell>
<TableCell className="flex items-center justify-end gap-2 p-1">
<TooltipAnchor
description={localize('com_ui_unarchive')}
render={
<Button
aria-label="Unarchive conversation"
variant="ghost"
size="icon"
className="size-8"
onClick={() => {
setConversationId(conversation.conversationId);
archiveHandler();
}}
className="cursor-pointer hover:text-black dark:hover:text-white"
>
<ArchiveRestore className="size-4 hover:text-gray-300" />
</TooltipAnchor>
<div className="size-5 hover:text-gray-300">
<DeleteButton
conversationId={conversation.conversationId}
retainView={moveToTop}
title={conversation.title ?? ''}
/>
</div>
</div>
</div>
</td>
</tr>
);
<ArchiveRestore className="size-4" />
</Button>
}
></TooltipAnchor>
<OGDialog>
<OGDialogTrigger asChild>
<TooltipAnchor
description={localize('com_ui_delete')}
render={
<Button
aria-label="Delete archived conversation"
variant="ghost"
size="icon"
className="size-8"
>
<TrashIcon className="size-4" />
</Button>
}
></TooltipAnchor>
</OGDialogTrigger>
{DeleteConversationDialog({
conversationId: conversation.conversationId ?? '',
retainView: refetch,
title: conversation.title ?? '',
})}
</tbody>
</table>
{(isFetchingNextPage || showLoading) && (
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
</OGDialog>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex items-center justify-end gap-6 px-2 py-4">
<div className="text-sm font-bold text-text-primary">
Page {currentPage} of {totalPages}
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="icon"
aria-label="Go to the previous 10 pages"
onClick={() => handlePageChange(Math.max(currentPage - 10, 1))}
disabled={currentPage === 1}
>
<ChevronsLeft className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
aria-label="Go to the previous page"
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
aria-label="Go to the next page"
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))}
disabled={currentPage === totalPages}
>
<ChevronRight className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
aria-label="Go to the next 10 pages"
onClick={() => handlePageChange(Math.min(currentPage + 10, totalPages))}
disabled={currentPage === totalPages}
>
<ChevronsRight className="size-4" />
</Button>
</div>
</div>
</>
)}
</div>
);

View file

@ -24,6 +24,7 @@ export default function AutoScrollSwitch({
<Switch
id="autoScroll"
checked={autoScroll}
aria-label="Auto-Scroll to latest message on chat open"
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2 ring-ring-primary"
data-testid="autoScroll"

View file

@ -34,8 +34,7 @@ export const ThemeSelector = ({
value={theme}
onChange={onChange}
options={themeOptions}
sizeClasses="w-[220px]"
anchor="bottom start"
sizeClasses="w-[180px]"
testId="theme-selector"
/>
</div>
@ -112,7 +111,6 @@ export const LangSelector = ({
value={langcode}
onChange={onChange}
sizeClasses="[--anchor-max-height:256px]"
anchor="bottom start"
options={languageOptions}
/>
</div>
@ -149,26 +147,24 @@ function General() {
return (
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<ThemeSelector theme={theme} onChange={changeTheme} />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<LangSelector langcode={langcode} onChange={changeLang} />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<UserMsgMarkdownSwitch />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<AutoScrollSwitch />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<HideSidePanelSwitch />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<div className="border-b border-border-light pb-3 last-of-type:border-b-0">
<ArchivedChats />
</div>
{/* <div className="border-b pb-3 last-of-type:border-b-0 border-border-medium">
</div> */}
</div>
);
}

View file

@ -25,6 +25,7 @@ export default function HideSidePanelSwitch({
<Switch
id="hideSidePanel"
checked={hideSidePanel}
aria-label="Hide right-most side panel"
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2"
data-testid="hideSidePanel"

View file

@ -1,6 +1,6 @@
import 'test/matchMedia.mock';
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { LangSelector } from './General';
import { RecoilRoot } from 'recoil';
@ -18,14 +18,15 @@ describe('LangSelector', () => {
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
const { getByText, getByRole } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('Language')).toBeInTheDocument();
expect(getByText('English')).toBeInTheDocument();
const dropdownButton = getByRole('combobox');
expect(dropdownButton).toHaveTextContent('English');
});
it('calls onChange when the select value changes', async () => {
@ -34,25 +35,23 @@ describe('LangSelector', () => {
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText, getByTestId } = render(
const { getByRole, getByTestId } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('English')).toBeInTheDocument();
expect(getByRole('combobox')).toHaveTextContent('English');
// Find the dropdown button by data-testid
const dropdownButton = getByTestId('dropdown-menu');
// Open the dropdown
fireEvent.click(dropdownButton);
// Find the option by text and click it
const darkOption = getByText('Italiano');
fireEvent.click(darkOption);
const italianOption = getByRole('option', { name: 'Italiano' });
fireEvent.click(italianOption);
// Ensure that the onChange is called with the expected value after a short delay
await new Promise((resolve) => setTimeout(resolve, 0));
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('it-IT');
});
});
});

View file

@ -20,14 +20,15 @@ describe('ThemeSelector', () => {
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
const { getByText, getByRole } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('Theme')).toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
const dropdownButton = getByRole('combobox');
expect(dropdownButton).toHaveTextContent('System');
});
it('calls onChange when the select value changes', async () => {
@ -44,17 +45,13 @@ describe('ThemeSelector', () => {
expect(getByText('Theme')).toBeInTheDocument();
// Find the dropdown button by data-testid
const dropdownButton = getByTestId('theme-selector');
// Open the dropdown
fireEvent.click(dropdownButton);
// Find the option by text and click it
const darkOption = getByText('Dark');
fireEvent.click(darkOption);
// Ensure that the onChange is called with the expected value
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('dark');
});

View file

@ -31,7 +31,6 @@ const EngineSTTDropdown: React.FC<EngineSTTDropdownProps> = ({ external }) => {
onChange={handleSelect}
options={endpointOptions}
sizeClasses="w-[180px]"
anchor="bottom start"
testId="EngineSTTDropdown"
/>
</div>

View file

@ -146,14 +146,12 @@ function Speech() {
value={advancedMode ? 'advanced' : 'simple'}
>
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
<Tabs.List className="flex justify-center bg-background">
<Tabs.Trigger
onClick={() => setAdvancedMode(false)}
className={cn(
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg',
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
'w-full',
)}
value="simple"
@ -165,10 +163,8 @@ function Speech() {
<Tabs.Trigger
onClick={() => setAdvancedMode(true)}
className={cn(
'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700',
'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg',
isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl',
'w-full',
)}
value="advanced"
@ -181,80 +177,54 @@ function Speech() {
</div>
<Tabs.Content value={'simple'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<div className="flex flex-col gap-3 text-sm text-text-primary">
<SpeechToTextSwitch />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown external={sttExternal} />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<LanguageSTTDropdown />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<div className="h-px bg-border-medium" role="none" />
<TextToSpeechSwitch />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineTTSDropdown external={ttsExternal} />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
</div>
</Tabs.Content>
<Tabs.Content value={'advanced'}>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="flex flex-col gap-3 text-sm text-text-primary">
<ConversationModeSwitch />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<div className="mt-2 h-px bg-border-medium" role="none" />
<SpeechToTextSwitch />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineSTTDropdown external={sttExternal} />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<LanguageSTTDropdown />
</div>
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<div className="pb-2">
<AutoTranscribeAudioSwitch />
</div>
{autoTranscribeAudio && (
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<div className="pb-2">
<DecibelSelector />
</div>
)}
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="pb-2">
<AutoSendTextSelector />
</div>
<div className="h-px bg-black/20 bg-white/20" role="none" />
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<div className="h-px bg-border-medium" role="none" />
<div className="pb-3">
<TextToSpeechSwitch />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<AutomaticPlaybackSwitch />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<EngineTTSDropdown external={ttsExternal} />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<VoiceDropdown />
</div>
{engineTTS === 'browser' && (
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<div className="pb-2">
<CloudBrowserVoicesSwitch />
</div>
)}
<div className="border-b pb-2 last-of-type:border-b-0 dark:border-gray-700">
<div className="pb-2">
<PlaybackRate />
</div>
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
<CacheTTSSwitch />
</div>
</div>
</Tabs.Content>
</Tabs.Root>
);

View file

@ -1,7 +1,6 @@
export * from './ExportConversation';
export * from './SettingsTabs/';
export { default as ClearConvos } from './ClearConvos';
export { default as Logout } from './Logout';
export { default as MobileNav } from './MobileNav';
export { default as Nav } from './Nav';
export { default as NavLink } from './NavLink';

View file

@ -31,7 +31,7 @@ export function FilterItem({
return (
<DropdownMenuItem
onClick={onClick}
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary dark:focus:bg-surface-tertiary"
className="relative cursor-pointer gap-2 text-text-secondary hover:bg-surface-tertiary focus:bg-surface-tertiary"
>
{icon}
<span>{label}</span>

View file

@ -31,7 +31,7 @@ export default function List({
<div className="flex w-full justify-end">
<Button
variant="outline"
className="mx-2 w-full px-3"
className="mx-2 w-full bg-transparent px-3"
onClick={() => navigate('/d/prompts/new')}
>
+ {localize('com_ui_create_prompt')}

View file

@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useLocalize, useCustomLink } from '~/hooks';
import { buttonVariants } from '~/components/ui';
import { Button } from '~/components/ui';
import { cn } from '~/utils';
import store from '~/store';
@ -15,13 +15,10 @@ export default function ManagePrompts({ className }: { className?: string }) {
}, [setPromptsName, setPromptsCategory]);
const clickHandler = useCustomLink('/d/prompts', clickCallback);
return (
<a
className={cn(buttonVariants({ variant: 'outline' }), className)}
href="/d/prompts"
onClick={clickHandler}
>
<Button variant="outline" className={cn(className, 'bg-transparent')} onClick={clickHandler}>
{localize('com_ui_manage')}
</a>
</Button>
);
}

View file

@ -41,7 +41,7 @@ export default function PromptsView() {
<GroupSidePanel isDetailView={isDetailView} {...groupsNav}>
<div className="mx-2 mt-1 flex flex-row items-center justify-between">
<FilterPrompts setName={groupsNav.setName} />
<AutoSendPrompt className="text-xs dark:text-white" />
<AutoSendPrompt className="text-xs text-text-primary" />
</div>
</GroupSidePanel>
<div

View file

@ -3,8 +3,8 @@ import * as AccordionPrimitive from '@radix-ui/react-accordion';
import type { NavLink, NavProps } from '~/common';
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
import { buttonVariants } from '~/components/ui/Button';
import { TooltipAnchor, Button } from '~/components';
import { cn, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
@ -31,14 +31,12 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
const variant = getVariant(link);
return isCollapsed ? (
<TooltipAnchor
className={cn(
buttonVariants({ variant, size: 'icon' }),
removeFocusOutlines,
'h-9 w-9 cursor-pointer',
variant === 'default'
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted bg-surface-terniary dark:hover:text-white'
: '',
)}
description={localize(link.title)}
side="left"
render={
<Button
variant="ghost"
size="icon"
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
@ -48,12 +46,12 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
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 text-text-secondary" />
<span className="sr-only">{link.title}</span>
</TooltipAnchor>
</Button>
}
></TooltipAnchor>
) : (
<Accordion
key={index}
@ -65,16 +63,10 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
<AccordionItem value={link.id} className="w-full border-none">
<AccordionPrimitive.Header asChild>
<AccordionPrimitive.Trigger asChild>
<button
className={cn(
buttonVariants({ variant, size: 'sm' }),
removeFocusOutlines,
variant === 'default'
? 'dark:bg-muted dark:hover:bg-muted dark:text-white dark:hover:text-white'
: '',
'hover:bg-gray-200 data-[state=open]:bg-gray-200 data-[state=open]:text-black dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-700 dark:data-[state=open]:text-white',
'w-full justify-start rounded-md border dark:border-gray-700',
)}
<Button
variant="outline"
size="sm"
className="w-full justify-start bg-transparent text-text-secondary data-[state=open]:bg-surface-secondary data-[state=open]:text-text-primary"
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
@ -95,7 +87,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
{link.label}
</span>
)}
</button>
</Button>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>

View file

@ -254,7 +254,7 @@ const SidePanel = ({
localStorage.setItem('react-resizable-panels:collapsed', 'true');
}}
className={cn(
'sidenav hide-scrollbar border-l border-border-light bg-surface-primary-alt transition-opacity',
'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity',
isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]',
(isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse
? 'hidden min-w-0'
@ -264,7 +264,7 @@ const SidePanel = ({
{interfaceConfig.modelSelect && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-surface-primary-alt',
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
isCollapsed ? 'h-[52px]' : 'px-2',
)}
>

View file

@ -1,52 +1,28 @@
import * as React from 'react';
import { VariantProps, cva } from 'class-variance-authority';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/utils';
const buttonVariants = cva(
'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default:
'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300',
destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700',
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'bg-transparent border border-gray-200 text-gray-700 hover:bg-gray-200 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700',
subtle:
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
ghost:
'bg-transparent text-gray-900 hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-800 data-[state=open]:bg-transparent',
link: 'bg-transparent underline-offset-4 hover:underline text-gray-600 dark:text-gray-400 hover:bg-transparent dark:hover:bg-transparent',
success:
'bg-green-500 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-700',
warning:
'bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700',
info: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700',
'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-8 px-3 rounded',
lg: 'h-12 px-6 rounded-md',
xl: 'h-14 px-8 rounded-lg text-base',
icon: 'h-10 w-10',
},
fullWidth: {
true: 'w-full',
},
loading: {
true: 'opacity-80 pointer-events-none',
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'size-10',
},
},
compoundVariants: [
{
variant: ['default', 'destructive', 'success', 'warning', 'info'],
className: 'focus-visible:ring-white focus-visible:ring-offset-2',
},
{
variant: 'outline',
className: 'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500',
},
],
defaultVariants: {
variant: 'default',
size: 'default',
@ -57,62 +33,14 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & { customId?: string }>(
(
{
className,
variant,
size,
fullWidth,
loading,
leftIcon,
rightIcon,
children,
customId,
...props
},
ref,
) => {
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<button
className={cn(buttonVariants({ variant, size, fullWidth, loading, className }))}
ref={ref}
{...props}
id={customId ?? props.id ?? 'shadcn-button'}
disabled={props.disabled || loading}
aria-busy={loading}
>
{loading && (
<svg
className="-ml-1 mr-3 h-5 w-5 animate-spin text-current"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)}
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2">{rightIcon}</span>}
</button>
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
},
);

View file

@ -1,14 +1,7 @@
import React, { FC, useState } from 'react';
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
Transition,
} from '@headlessui/react';
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
import type { Option } from '~/common';
import React, { useState } from 'react';
import * as Select from '@ariakit/react/select';
import { cn } from '~/utils/';
import type { Option } from '~/common';
interface DropdownProps {
value: string;
@ -16,80 +9,60 @@ interface DropdownProps {
onChange: (value: string) => void;
options: string[] | Option[];
className?: string;
anchor?: AnchorPropsWithSelection;
sizeClasses?: string;
testId?: string;
}
const Dropdown: FC<DropdownProps> = ({
const Dropdown: React.FC<DropdownProps> = ({
value: initialValue,
label = '',
onChange,
options,
className = '',
anchor,
sizeClasses,
testId = 'dropdown-menu',
}) => {
const [selectedValue, setSelectedValue] = useState(initialValue);
const handleChange = (value: string) => {
setSelectedValue(value);
onChange(value);
};
const selectProps = Select.useSelectStore({
value: selectedValue,
setValue: handleChange,
});
return (
<div className={cn('relative', className)}>
<Listbox
value={selectedValue}
onChange={(newValue) => {
setSelectedValue(newValue);
onChange(newValue);
}}
>
<div className={cn('relative', className)}>
<ListboxButton
data-testid={testId}
<Select.Select
store={selectProps}
className={cn(
'btn-neutral focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-md border-border-light bg-header-primary py-2 pl-3 pr-8 text-text-primary transition-all duration-100 ease-in-out hover:bg-header-hover focus:ring-ring-primary',
'focus:ring-offset-ring-offset relative inline-flex w-auto items-center justify-between rounded-lg border border-input bg-background py-2 pl-3 pr-8 text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
className,
)}
aria-label="Select an option"
data-testid={testId}
>
<div className="flex w-full items-center justify-between">
<span className="block truncate">
{label}
{options
.map((o) => (typeof o === 'string' ? { value: o, label: o } : o))
.find((o) => o.value === selectedValue)?.label ?? selectedValue}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="h-4 w-5 rotate-0 transform text-text-primary transition-transform duration-300 ease-in-out"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-border-medium bg-header-primary p-1.5 shadow-lg transition-opacity',
sizeClasses,
className,
)}
anchor={anchor}
aria-label="List of options"
<Select.SelectArrow />
</div>
</Select.Select>
<Select.SelectPopover
store={selectProps}
className={cn('popover-ui', sizeClasses, className)}
>
{options.map((item, index) => (
<ListboxOption
<Select.SelectItem
key={index}
value={typeof item === 'string' ? item : item.value}
className="focus-visible:ring-offset ring-offset-ring-offset relative cursor-pointer select-none rounded border-border-light bg-header-primary py-2.5 pl-3 pr-3 text-sm text-text-secondary ring-ring-primary hover:bg-header-hover focus-visible:ring data-[focus]:bg-surface-hover data-[focus]:text-text-primary"
style={{ width: '100%' }}
className="select-item"
data-theme={typeof item === 'string' ? item : (item as Option).value}
>
<div className="flex w-full items-center justify-between">
@ -116,12 +89,9 @@ const Dropdown: FC<DropdownProps> = ({
</span>
)}
</div>
</ListboxOption>
</Select.SelectItem>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
</Select.SelectPopover>
</div>
);
};

View file

@ -156,7 +156,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className = '', ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-900', className)}
className={cn('-mx-1 my-1 h-px bg-border-medium', className)}
{...props}
/>
));

View file

@ -21,6 +21,10 @@ interface DropdownProps {
testId?: string;
}
/*
* Mainly used for the Speech Voice Selection Dropdown
*/
const Dropdown: FC<DropdownProps> = ({
value,
label = '',

View file

@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
return (
<input
className={cn(
'dark:border-gray-00 flex h-10 w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50',
'flex h-10 w-full rounded-md border border-border-light bg-transparent px-3 py-2 text-sm placeholder:text-text-tertiary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50',
className ?? '',
)}
ref={ref}

View file

@ -59,10 +59,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
overlayClassName={overlayClassName}
showCloseButton={showCloseButton}
ref={ref}
className={cn(
'bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300',
className ?? '',
)}
className={cn('border-none bg-background text-foreground', className ?? '')}
onClick={(e) => e.stopPropagation()}
>
<OGDialogHeader className={cn(headerClassName ?? '')}>

View file

@ -41,14 +41,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@ -89,7 +89,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));

View file

@ -0,0 +1,105 @@
import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { ButtonProps, buttonVariants } from './Button';
import { cn } from '~/utils';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
),
);
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
({ className, ...props }, ref) => <li ref={ref} className={cn('', className)} {...props} />,
);
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive = false,
size = 'icon',
children,
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className,
)}
{...props}
>
{children || <span className="sr-only">Page link</span>}
</a>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View file

@ -12,7 +12,7 @@ const Separator = React.forwardRef<
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0',
'shrink-0 bg-border-light',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className,
)}

View file

@ -1,41 +1,22 @@
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { useDoubleClick } from '@zattoo/use-double-click';
import type { clickEvent } from '@zattoo/use-double-click';
import { cn } from '~/utils';
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
doubleClickHandler?: clickEvent;
trackClassName?: string;
}
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
(
{ className, trackClassName = 'bg-gray-200 dark:bg-gray-850', doubleClickHandler, ...props },
ref,
) => (
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track
className={cn('relative h-1 w-full grow overflow-hidden rounded-full', trackClassName)}
>
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
onClick={
useDoubleClick(doubleClickHandler as clickEvent) ??
(() => {
return;
})
}
className="block h-4 w-4 cursor-pointer rounded-full border border-border-medium-alt bg-white shadow ring-ring-primary transition-colors focus-visible:ring-1 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 dark:border-none"
/>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
),
);
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View file

@ -1,16 +1,14 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '../../utils';
import { cn } from '~/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'focus-visible:ring-ring focus-visible:ring-offset-background peer inline-flex h-[20px] w-[32px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-black data-[state=unchecked]:bg-gray-200 dark:data-[state=checked]:bg-green-500 dark:data-[state=unchecked]:bg-gray-500',
'ring-ring-primary',
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className,
)}
{...props}
@ -18,7 +16,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-4 w-4 -translate-x-0.5 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0',
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitives.Root>

View file

@ -1,10 +1,11 @@
import * as React from 'react';
import { cn } from '../../utils';
import { cn } from '~/utils';
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className = '', ...props }, ref) => (
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
),
);
Table.displayName = 'Table';
@ -12,7 +13,7 @@ Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
@ -20,7 +21,7 @@ TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
));
TableBody.displayName = 'TableBody';
@ -28,21 +29,21 @@ TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('bg-primary text-primary-foreground font-medium', className)}
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className = '', ...props }, ref) => (
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-border-light transition-colors',
className,
)}
{...props}
@ -54,7 +55,7 @@ TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
@ -69,10 +70,10 @@ TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('align-middle [&:has([role=checkbox])]:pr-0', className)}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
@ -81,7 +82,7 @@ TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className = '', ...props }, ref) => (
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';

View file

@ -1,7 +1,6 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../../utils';
import { cn } from '~/utils';
const Tabs = TabsPrimitive.Root;
@ -12,7 +11,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md bg-gray-200 p-1 dark:bg-gray-800',
'inline-flex items-center justify-center rounded-md bg-surface-primary',
className,
)}
{...props}
@ -26,7 +25,7 @@ const TabsTrigger = React.forwardRef<
>(({ className = '', ...props }, ref) => (
<TabsPrimitive.Trigger
className={cn(
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-gray-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-800 data-[state=active]:shadow-sm dark:text-gray-200 dark:data-[state=active]:bg-gray-700 dark:data-[state=active]:text-gray-200',
'inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-gray-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-800 data-[state=active]:shadow-sm dark:data-[state=active]:bg-gray-700 dark:data-[state=active]:text-gray-200',
className,
)}
{...props}
@ -39,11 +38,7 @@ const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className = '', ...props }, ref) => (
<TabsPrimitive.Content
className={cn('mt-2 rounded-md border border-gray-200 p-6 dark:border-gray-700', className)}
{...props}
ref={ref}
/>
<TabsPrimitive.Content className={cn('mt-2 rounded-md p-6', className)} {...props} ref={ref} />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;

View file

@ -1,14 +1,16 @@
import * as Ariakit from '@ariakit/react';
import { AnimatePresence, motion } from 'framer-motion';
import { forwardRef, useMemo } from 'react';
import { cn } from '~/utils';
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
className?: string;
}
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
{ description, side = 'top', role, ...props },
{ description, side = 'top', className, role, ...props },
ref,
) {
const tooltip = Ariakit.useTooltipStore({ placement: side });
@ -40,7 +42,13 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
return (
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
<Ariakit.TooltipAnchor {...props} ref={ref} role={role} onKeyDown={handleKeyDown} />
<Ariakit.TooltipAnchor
{...props}
ref={ref}
role={role}
onKeyDown={handleKeyDown}
className={cn('cursor-pointer', className)}
/>
<AnimatePresence>
{mounted && (
<Ariakit.Tooltip

View file

@ -23,6 +23,7 @@ export * from './Tag';
export * from './Textarea';
export * from './TextareaAutosize';
export * from './Tooltip';
export * from './Pagination';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as FileUpload } from './FileUpload';

View file

@ -150,12 +150,12 @@ export const useConversationsInfiniteQuery = (
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
) => {
return useInfiniteQuery<ConversationListResponse, unknown>(
params?.isArchived ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
params?.isArchived === true ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
({ pageParam = '' }) =>
dataService.listConversations({
...params,
pageNumber: pageParam?.toString(),
isArchived: params?.isArchived || false,
isArchived: params?.isArchived ?? false,
tags: params?.tags || [],
}),
{

View file

@ -777,6 +777,7 @@ export default {
com_nav_slash_command_description: 'Toggle command "/" for selecting a prompt via keyboard',
com_nav_command_settings: 'Command Settings',
com_nav_command_settings_description: 'Customize which commands are available in the chat',
com_nav_no_search_results: 'No search results found',
com_nav_setting_general: 'General',
com_nav_setting_chat: 'Chat',
com_nav_setting_beta: 'Beta features',

View file

@ -10,7 +10,13 @@ const searchQuery = atom({
default: '',
});
const isSearching = atom({
key: 'isSearching',
default: false,
});
export default {
isSearchEnabled,
searchQuery,
isSearching,
};

View file

@ -44,11 +44,37 @@ html {
--surface-secondary: var(--gray-50);
--surface-tertiary: var(--gray-100);
--surface-tertiary-alt: var(--white);
--surface-dialog: var(--white);
--border-light: var(--gray-200);
--border-medium-alt: var(--gray-300);
--border-medium: var(--gray-300);
--border-heavy: var(--gray-400);
--border-xheavy: var(--gray-500);
/* These are test styles */
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--text-primary: var(--gray-100);
@ -66,11 +92,36 @@ html {
--surface-secondary: var(--gray-800);
--surface-tertiary: var(--gray-700);
--surface-tertiary-alt: var(--gray-700);
--surface-dialog: var(--gray-850);
--border-light: var(--gray-700);
--border-medium-alt: var(--gray-600);
--border-medium: var(--gray-600);
--border-heavy: var(--gray-500);
--border-xheavy: var(--gray-400);
/* These are test styles */
--background: 0 0% 7%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
.gizmo {
--text-primary: var(--gizmo-gray-950);
@ -2292,7 +2343,7 @@ button.scroll-convo {
z-index: 50;
cursor: pointer;
border-radius: 0.275rem;
background-color: var(--bg-gray-600);
background-color: var(--surface-primary);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
@ -2322,3 +2373,67 @@ button.scroll-convo {
outline: 2px solid #fff;
outline-offset: 2px;
}
.popover-ui {
z-index: 1000;
display: flex;
max-height: min(var(--popover-available-height, 300px), 300px);
flex-direction: column;
overflow: auto;
overscroll-behavior: contain;
border-radius: 1rem;
border-width: 1px;
border-style: solid;
border-color: var(--border-light);
background-color: hsl(var(--background));
padding: 0.5rem;
color: var(--text-primary);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
transform-origin: top;
opacity: 0;
transition-property: opacity, scale, translate;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
scale: 0.95;
translate: 0 -0.5rem;
margin-top: 4px;
margin-right: -2px;
}
.popover-ui:focus-visible,
.popover-ui[data-focus-visible] {
outline: var(--bg-surface-hover);
outline-offset: -1px;
}
.popover-ui:where(.dark, .dark *) {
background-color: hsl(var(--background));
color: var(--text-secondary);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.25), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.select-item {
display: flex;
cursor: pointer;
scroll-margin: 0.5rem;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
padding: 0.5rem;
outline: none !important;
}
.select-item[aria-disabled='true'] {
opacity: 0.5;
}
.select-item[data-active-item] {
background-color: hsl(var(--accent));
color: var(--text-primary);
}
.popover-ui[data-enter] {
opacity: 1;
scale: 1;
translate: 0;
}

View file

@ -77,11 +77,47 @@ module.exports = {
'surface-secondary': 'var(--surface-secondary)',
'surface-tertiary': 'var(--surface-tertiary)',
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
'surface-dialog': 'var(--surface-dialog)',
'border-light': 'var(--border-light)',
'border-medium': 'var(--border-medium)',
'border-medium-alt': 'var(--border-medium-alt)',
'border-heavy': 'var(--border-heavy)',
'border-xheavy': 'var(--border-xheavy)',
/* These are test styles */
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},

15
package-lock.json generated
View file

@ -8087,12 +8087,13 @@
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.1"
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react": {
@ -8124,9 +8125,9 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {