mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
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:
parent
3137f467a8
commit
be71a1947b
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 |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
client/src/components/Conversations/RenameButton.tsx
Normal file
28
client/src/components/Conversations/RenameButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
35
client/src/components/Nav/NavLink.tsx
Normal file
35
client/src/components/Nav/NavLink.tsx
Normal 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;
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue