style: adjust icon scale, favicon, azure icon; chore: convert files to TSX; ci: unit tests for generation buttons (#987)

* some jsx to tsx and added 3 new test

* test(stop)

* new librechat and azure icon, small fix

* fix(tsc error)

* fix(tsc error) Endpoint Item
This commit is contained in:
Marco Beretta 2023-10-03 16:28:19 +02:00 committed by GitHub
parent 3137f467a8
commit be71a1947b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 187 additions and 56 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 709 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -22,7 +22,7 @@ export default function DeleteButton({ conversationId, renaming, retainView, tit
useEffect(() => { useEffect(() => {
if (deleteConvoMutation.isSuccess) { if (deleteConvoMutation.isSuccess) {
if (currentConversation?.conversationId == conversationId) { if ((currentConversation as { conversationId?: string }).conversationId == conversationId) {
newConversation(); newConversation();
} }

View file

@ -1,16 +0,0 @@
import React from 'react';
import RenameIcon from '../svg/RenameIcon';
import CheckMark from '../svg/CheckMark';
export default function RenameButton({ renaming, renameHandler, onRename, twcss }) {
const handler = renaming ? onRename : renameHandler;
const classProp = { className: 'p-1 hover:text-white' };
if (twcss) {
classProp.className = twcss;
}
return (
<button {...classProp} onClick={handler}>
{renaming ? <CheckMark /> : <RenameIcon />}
</button>
);
}

View file

@ -0,0 +1,28 @@
import React, { ReactElement } from 'react';
import RenameIcon from '../svg/RenameIcon';
import CheckMark from '../svg/CheckMark';
interface RenameButtonProps {
renaming: boolean;
renameHandler: () => void;
onRename: () => void;
twcss?: string;
}
export default function RenameButton({
renaming,
renameHandler,
onRename,
twcss,
}: RenameButtonProps): ReactElement {
const handler = renaming ? onRename : renameHandler;
const classProp: { className?: string } = { className: 'p-1 hover:text-white' };
if (twcss) {
classProp.className = twcss;
}
return (
<button {...classProp} onClick={handler}>
{renaming ? <CheckMark /> : <RenameIcon />}
</button>
);
}

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Plugin, GPTIcon, AnthropicIcon } from '~/components/svg'; import { Plugin, GPTIcon, AnthropicIcon, AzureMinimalIcon } from '~/components/svg';
import { useAuthContext } from '~/hooks'; import { useAuthContext } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
import { IconProps } from '~/common'; import { IconProps } from '~/common';
@ -34,12 +34,12 @@ const Icon: React.FC<IconProps> = (props) => {
} else { } else {
const endpointIcons = { const endpointIcons = {
azureOpenAI: { azureOpenAI: {
icon: <GPTIcon size={size * 0.7} />, icon: <AzureMinimalIcon size={size * 0.55} />,
bg: 'linear-gradient(0.375turn, #61bde2, #4389d0)', bg: 'linear-gradient(0.375turn, #61bde2, #4389d0)',
name: 'ChatGPT', name: 'ChatGPT',
}, },
openAI: { openAI: {
icon: <GPTIcon size={size * 0.7} />, icon: <GPTIcon size={size * 0.55} />,
bg: bg:
typeof model === 'string' && model.toLowerCase().includes('gpt-4') typeof model === 'string' && model.toLowerCase().includes('gpt-4')
? '#AB68FF' ? '#AB68FF'
@ -52,7 +52,7 @@ const Icon: React.FC<IconProps> = (props) => {
name: 'Plugins', name: 'Plugins',
}, },
google: { icon: <img src="/assets/google-palm.svg" alt="Palm Icon" />, name: 'PaLM2' }, google: { icon: <img src="/assets/google-palm.svg" alt="Palm Icon" />, name: 'PaLM2' },
anthropic: { icon: <AnthropicIcon size={size * 0.7} />, bg: '#d09a74', name: 'Claude' }, anthropic: { icon: <AnthropicIcon size={size * 0.55} />, bg: '#d09a74', name: 'Claude' },
bingAI: { bingAI: {
icon: jailbreak ? ( icon: jailbreak ? (
<img src="/assets/bingai-jb.png" alt="Bing Icon" /> <img src="/assets/bingai-jb.png" alt="Bing Icon" />
@ -62,7 +62,7 @@ const Icon: React.FC<IconProps> = (props) => {
name: jailbreak ? 'Sydney' : 'BingAI', name: jailbreak ? 'Sydney' : 'BingAI',
}, },
chatGPTBrowser: { chatGPTBrowser: {
icon: <GPTIcon size={size * 0.7} />, icon: <GPTIcon size={size * 0.55} />,
bg: bg:
typeof model === 'string' && model.toLowerCase().includes('gpt-4') typeof model === 'string' && model.toLowerCase().includes('gpt-4')
? '#AB68FF' ? '#AB68FF'
@ -73,7 +73,7 @@ const Icon: React.FC<IconProps> = (props) => {
default: { icon: <GPTIcon size={size * 0.7} />, bg: 'grey', name: 'UNKNOWN' }, default: { icon: <GPTIcon size={size * 0.7} />, bg: 'grey', name: 'UNKNOWN' },
}; };
const { icon, bg, name } = endpointIcons[endpoint ?? ''] ?? endpointIcons.default; const { icon, bg, name } = endpointIcons[endpoint ?? 'default'];
return ( return (
<div <div

View file

@ -27,6 +27,7 @@ export default function ModelItem({
error: false, error: false,
className: 'mr-2', className: 'mr-2',
message: false, message: false,
isCreatedByUser: false,
}); });
const userProvidesKey = endpointsConfig?.[endpoint]?.userProvide; const userProvidesKey = endpointsConfig?.[endpoint]?.userProvide;

View file

@ -1,6 +1,12 @@
import EndpointItem from './EndpointItem'; import EndpointItem from './EndpointItem';
export default function EndpointItems({ endpoints, onSelect, selectedEndpoint }) { interface EndpointItemsProps {
endpoints: string[];
onSelect: (endpoint: string) => void;
selectedEndpoint: string;
}
export default function EndpointItems({ endpoints, selectedEndpoint }: EndpointItemsProps) {
return ( return (
<> <>
{endpoints.map((endpoint) => ( {endpoints.map((endpoint) => (
@ -8,7 +14,6 @@ export default function EndpointItems({ endpoints, onSelect, selectedEndpoint })
isSelected={selectedEndpoint === endpoint} isSelected={selectedEndpoint === endpoint}
key={endpoint} key={endpoint}
value={endpoint} value={endpoint}
onSelect={onSelect}
endpoint={endpoint} endpoint={endpoint}
/> />
))} ))}

View file

@ -17,6 +17,7 @@ export default function PresetItem({
model: preset?.model, model: preset?.model,
error: false, error: false,
className: 'mr-2', className: 'mr-2',
isCreatedByUser: false,
}); });
const getPresetTitle = () => { const getPresetTitle = () => {

View file

@ -0,0 +1,30 @@
import { render, fireEvent } from '@testing-library/react';
import Button from '../Button';
describe('Button', () => {
it('renders with the correct type and children', () => {
const { getByTestId, getByText } = render(
<Button
type="regenerate"
onClick={() => {
('');
}}
>
Regenerate
</Button>,
);
expect(getByTestId('regenerate-generation-button')).toBeInTheDocument();
expect(getByText('Regenerate')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(
<Button type="continue" onClick={handleClick}>
Continue
</Button>,
);
fireEvent.click(getByText('Continue'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,22 @@
import { render, fireEvent } from '@testing-library/react';
import Continue from '../Continue';
describe('Continue', () => {
it('should render the Continue button', () => {
const { getByText } = render(
<Continue
onClick={() => {
('');
}}
/>,
);
expect(getByText('Continue')).toBeInTheDocument();
});
it('should call onClick when the button is clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(<Continue onClick={handleClick} />);
fireEvent.click(getByText('Continue'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,22 @@
import { render, fireEvent } from '@testing-library/react';
import Regenerate from '../Regenerate';
describe('Regenerate', () => {
it('should render the Regenerate button', () => {
const { getByText } = render(
<Regenerate
onClick={() => {
('');
}}
/>,
);
expect(getByText('Regenerate')).toBeInTheDocument();
});
it('should call onClick when the button is clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(<Regenerate onClick={handleClick} />);
fireEvent.click(getByText('Regenerate'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,22 @@
import { render, fireEvent } from '@testing-library/react';
import Stop from '../Stop';
describe('Stop', () => {
it('should render the Stop button', () => {
const { getByText } = render(
<Stop
onClick={() => {
('');
}}
/>,
);
expect(getByText('Stop')).toBeInTheDocument();
});
it('should call onClick when the button is clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(<Stop onClick={handleClick} />);
fireEvent.click(getByText('Stop'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -94,6 +94,7 @@ export default function Message({
...conversation, ...conversation,
...message, ...message,
model: message?.model ?? conversation?.model, model: message?.model ?? conversation?.model,
size: 38,
}); });
if (message?.bg && searchResult) { if (message?.bg && searchResult) {
@ -135,7 +136,7 @@ export default function Message({
<> <>
<div {...props} onWheel={handleScroll} onTouchMove={handleScroll}> <div {...props} onWheel={handleScroll} onTouchMove={handleScroll}>
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm"> <div className="relative flex h-[40px] w-[40px] flex-col items-end text-right text-xs md:text-sm">
{typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? ( {typeof icon === 'string' && /[^\\x00-\\x7F]+/.test(icon as string) ? (
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span> <span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
) : ( ) : (

View file

@ -12,6 +12,7 @@ const ClearConvos = ({ open, onOpenChange }) => {
const [confirmClear, setConfirmClear] = useState(false); const [confirmClear, setConfirmClear] = useState(false);
const localize = useLocalize(); const localize = useLocalize();
// Clear all conversations
const clearConvos = useCallback(() => { const clearConvos = useCallback(() => {
if (confirmClear) { if (confirmClear) {
console.log('Clearing conversations...'); console.log('Clearing conversations...');
@ -22,6 +23,7 @@ const ClearConvos = ({ open, onOpenChange }) => {
} }
}, [confirmClear, clearConvosMutation]); }, [confirmClear, clearConvosMutation]);
// Refresh conversations after clearing
useEffect(() => { useEffect(() => {
if (clearConvosMutation.isSuccess) { if (clearConvosMutation.isSuccess) {
refreshConversations(); refreshConversations();

View file

@ -1,25 +0,0 @@
import { forwardRef } from 'react';
import { cn } from '~/utils/';
const NavLink = forwardRef((props, ref) => {
const { svg, text, clickHandler, className = '' } = props;
const defaultProps = {};
defaultProps.className = cn(
'flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10',
className,
);
if (clickHandler) {
defaultProps.onClick = clickHandler;
}
return (
<a {...defaultProps} ref={ref}>
{svg()}
{text}
</a>
);
});
export default NavLink;

View file

@ -0,0 +1,35 @@
import { FC, forwardRef } from 'react';
import { cn } from '~/utils/';
interface Props {
svg: () => JSX.Element;
text: string;
clickHandler?: () => void;
className?: string;
}
const NavLink: FC<Props> = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const { svg, text, clickHandler, className = '' } = props;
const defaultProps: {
className: string;
onClick?: () => void;
} = {
className: cn(
'flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10',
className,
),
};
if (clickHandler) {
defaultProps.onClick = clickHandler;
}
return (
<a {...defaultProps} ref={ref}>
{svg()}
{text}
</a>
);
});
export default NavLink;

View file

@ -1,7 +1,10 @@
/* eslint-disable indent */ /* eslint-disable indent */
import React from 'react'; import { cn } from '~/utils/';
export default function AzureMinimalIcon({ size = 25, className = 'h-4 w-4' }) {
const height = size;
const width = size;
export default function AzureMinimalIcon() {
return ( return (
<svg <svg
stroke="currentColor" stroke="currentColor"
@ -10,9 +13,9 @@ export default function AzureMinimalIcon() {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="h-4 w-4" className={cn(className, '')}
height="1em" width={width}
width="1em" height={height}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="m8.0458 0.81981a1.1197 1.1197 0 0 0-1.0608 0.76184l-6.7912 20.123a1.1178 1.1178 0 0 0 1.0592 1.4751h5.4647a1.1197 1.1197 0 0 0 1.0608-0.7615l1.3528-4.0084-2.3684-2.2107a0.51536 0.51536 0 0 1 0.35193-0.8923h3.0639l1.8213-5.3966-2.8111-8.3294a1.1181 1.1181 0 0 0-1.0595-0.76049h-0.0836z" /> <path d="m8.0458 0.81981a1.1197 1.1197 0 0 0-1.0608 0.76184l-6.7912 20.123a1.1178 1.1178 0 0 0 1.0592 1.4751h5.4647a1.1197 1.1197 0 0 0 1.0608-0.7615l1.3528-4.0084-2.3684-2.2107a0.51536 0.51536 0 0 1 0.35193-0.8923h3.0639l1.8213-5.3966-2.8111-8.3294a1.1181 1.1181 0 0 0-1.0595-0.76049h-0.0836z" />