style: update graphics (#1138)

* style: update new icon and NavLinks scale

* style: new username update

* refactor(Dropdown); style: general settings

* style(Dropdown); adjust theme

* style: dropdown and settings text

* fix(Dropdown) system theme not working

* style: topbar sticky; fix: general's menu settings transparent with light theme

* fix(SubmitButton) stop generate button

* fix: user_provided dialog for new dropdown

* fix: TS error 'display'

* fix(EditPresetDialog): for new dropdown

* style: added green send button

* converted textchat in tsx

* style(SubmitButton): tooltip

* test: fixed ThemeSelector and LangSelector

* removed transition-opacity

* fix all tests

* removed empty cn call

* chore: Update General.tsx to add Arabic option

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
This commit is contained in:
Marco Beretta 2023-11-16 14:42:03 +01:00 committed by GitHub
parent 8b28fdf240
commit 9ad47b6660
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 442 additions and 318 deletions

View file

@ -91,14 +91,9 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }: TEditP
</Label>
<Dropdown
value={endpoint || ''}
onChange={setOption('endpoint')}
onChange={(value) => setOption('endpoint')(value)}
options={availableEndpoints}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none ',
removeFocusOutlines,
)}
containerClassName="flex w-full resize-none z-[51]"
className={cn()}
/>
</div>
</div>

View file

@ -40,7 +40,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
setValue={setModel}
availableValues={models}
disabled={readonly}
className={cn(defaultTextProps, 'z-50 flex w-full resize-none', removeFocusOutlines)}
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusOutlines)}
containerClassName="flex w-full resize-none"
/>
</div>

View file

@ -42,7 +42,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
setValue={setModel}
availableValues={models}
disabled={readonly}
className={cn(defaultTextProps, 'z-50 flex w-full resize-none', removeFocusOutlines)}
className={cn(defaultTextProps, 'flex w-full resize-none', removeFocusOutlines)}
containerClassName="flex w-full resize-none"
/>
</div>

View file

@ -3,7 +3,7 @@ import type { TDialogProps } from '~/common';
import { Dialog, Dropdown } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { RevokeKeysButton } from '~/components/Nav';
import { cn, defaultTextProps, removeFocusOutlines, alternateName } from '~/utils';
import { cn, alternateName } from '~/utils';
import { useUserKey, useLocalize } from '~/hooks';
import GoogleConfig from './GoogleConfig';
import OpenAIConfig from './OpenAIConfig';
@ -75,13 +75,7 @@ const SetKeyDialog = ({
value={expiresAtLabel}
onChange={handleExpirationChange}
options={expirationOptions.map((option) => option.display)}
className={cn(
defaultTextProps,
'flex h-full w-full resize-none',
removeFocusOutlines,
)}
optionsClassName="max-h-72"
containerClassName="flex w-1/2 md:w-1/3 resize-none z-[51]"
width={185}
/>
<EndpointComponent userKey={userKey} setUserKey={setUserKey} endpoint={endpoint} />
<HelpText endpoint={endpoint} />

View file

@ -2,7 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react';
import { StopGeneratingIcon } from '~/components';
import { Settings } from 'lucide-react';
import { SetKeyDialog } from './SetKeyDialog';
import { useUserKey, useLocalize } from '~/hooks';
import { useUserKey, useLocalize, useMediaQuery } from '~/hooks';
import { SendMessageIcon } from '~/components/svg';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui/';
export default function SubmitButton({
conversation,
@ -11,6 +13,7 @@ export default function SubmitButton({
disabled,
isSubmitting,
userProvidesKey,
hasText,
}) {
const { endpoint } = conversation;
const [isDialogOpen, setDialogOpen] = useState(false);
@ -18,6 +21,16 @@ export default function SubmitButton({
const [isKeyProvided, setKeyProvided] = useState(userProvidesKey ? checkExpiry() : true);
const isKeyActive = checkExpiry();
const localize = useLocalize();
const dots = ['·', '··', '···'];
const [dotIndex, setDotIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setDotIndex((prevDotIndex) => (prevDotIndex + 1) % dots.length);
}, 500);
return () => clearInterval(interval);
}, [dots.length]);
useEffect(() => {
if (userProvidesKey) {
@ -35,22 +48,43 @@ export default function SubmitButton({
[submitMessage],
);
const [isSquareGreen, setIsSquareGreen] = useState(false);
const setKey = useCallback(() => {
setDialogOpen(true);
}, []);
if (isSubmitting) {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const iconContainerClass = `m-1 mr-0 rounded-md pb-[5px] pl-[6px] pr-[4px] pt-[5px] ${
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
} group-hover:bg-19C37D group-disabled:hover:bg-transparent dark:${
hasText ? (isSquareGreen ? 'bg-green-500' : '') : ''
} dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent`;
useEffect(() => {
setIsSquareGreen(hasText);
}, [hasText]);
if (isSubmitting && isSmallScreen) {
return (
<button
onClick={handleStopGenerating}
type="button"
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<button onClick={handleStopGenerating} type="button">
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<StopGeneratingIcon />
</div>
</button>
);
} else if (isSubmitting) {
return (
<div className="relative flex h-full">
<div
className="absolute text-2xl"
style={{ top: '50%', transform: 'translateY(-20%) translateX(-33px)' }}
>
{dots[dotIndex]}
</div>
</div>
);
} else if (!isKeyProvided) {
return (
<>
@ -73,34 +107,31 @@ export default function SubmitButton({
);
} else {
return (
<button
onClick={clickHandler}
disabled={disabled}
data-testid="submit-button"
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md pb-[9px] pl-[9.5px] pr-[7px] pt-[11px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-1 h-4 w-4 "
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22 2 15 22 11 13 2 9 22 2" />
</svg>
</div>
</button>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={clickHandler}
disabled={disabled}
data-testid="submit-button"
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className={iconContainerClass}>
{hasText ? (
<div className="bg-19C37D flex h-[24px] w-[24px] items-center justify-center rounded-full text-white">
<SendMessageIcon />
</div>
) : (
<SendMessageIcon />
)}
</div>
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={-5}>
{localize('com_nav_send_message')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
}
{
/* <div class="text-2xl"><span class="">·</span><span class="">·</span><span class="invisible">·</span></div> */
}

View file

@ -1,7 +1,8 @@
import React, { useEffect, useContext, useRef } from 'react';
import React, { useEffect, useContext, useRef, useState, useCallback } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
import SubmitButton from './SubmitButton';
import OptionsBar from './OptionsBar';
import { EndpointMenu } from './EndpointMenu';
import Footer from './Footer';
@ -9,7 +10,11 @@ import { useMessageHandler, ThemeContext } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
export default function TextChat({ isSearchView = false }) {
interface TextChatProps {
isSearchView?: boolean;
}
export default function TextChat({ isSearchView = false }: TextChatProps) {
const { ask, isSubmitting, handleStopGenerating, latestMessage, endpointsConfig } =
useMessageHandler();
const conversation = useRecoilValue(store.conversation);
@ -17,21 +22,22 @@ export default function TextChat({ isSearchView = false }) {
const [text, setText] = useRecoilState(store.text);
const { theme } = useContext(ThemeContext);
const isComposing = useRef(false);
const inputRef = useRef(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [hasText, setHasText] = useState(false);
// TODO: do we need this?
const disabled = false;
const isNotAppendable = latestMessage?.unfinished & !isSubmitting || latestMessage?.error;
const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error;
const { conversationId, jailbreak } = conversation || {};
// auto focus to input, when enter a conversation.
// auto focus to input, when entering a conversation.
useEffect(() => {
if (!conversationId) {
return;
}
// Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak
// Prevents Settings from not showing on a new conversation, also prevents showing toneStyle change without jailbreak
if (conversationId === 'new' || !jailbreak) {
setShowBingToneSetting(false);
}
@ -54,9 +60,10 @@ export default function TextChat({ isSearchView = false }) {
const submitMessage = () => {
ask({ text });
setText('');
setHasText(false);
};
const handleKeyDown = (e) => {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && isSubmitting) {
return;
}
@ -70,9 +77,9 @@ export default function TextChat({ isSearchView = false }) {
}
};
const handleKeyUp = (e) => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
setText(e.target.value);
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.keyCode === 8 && e.currentTarget.value.trim() === '') {
setText(e.currentTarget.value);
}
if (e.key === 'Enter' && e.shiftKey) {
@ -92,12 +99,24 @@ export default function TextChat({ isSearchView = false }) {
isComposing.current = false;
};
const changeHandler = (e) => {
const changeHandler = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
setText(value);
updateHasText(value);
};
const updateHasText = useCallback(
(text: string) => {
setHasText(!!text.trim() || !!latestMessage?.error);
},
[setHasText, latestMessage],
);
useEffect(() => {
updateHasText(text);
}, [text, latestMessage, updateHasText]);
const getPlaceholderText = () => {
if (isSearchView) {
return 'Click a message title to open its conversation.';
@ -153,11 +172,11 @@ export default function TextChat({ isSearchView = false }) {
<TextareaAutosize
// set test id for e2e testing
data-testid="text-input"
tabIndex="0"
tabIndex={0}
autoFocus
ref={inputRef}
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
rows="1"
rows={1}
value={disabled || isNotAppendable ? '' : text}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
@ -174,7 +193,12 @@ export default function TextChat({ isSearchView = false }) {
handleStopGenerating={handleStopGenerating}
disabled={disabled || isNotAppendable}
isSubmitting={isSubmitting}
userProvidesKey={endpointsConfig?.[conversation.endpoint]?.userProvide}
userProvidesKey={
conversation?.endpoint
? endpointsConfig?.[conversation.endpoint]?.userProvide
: undefined
}
hasText={hasText}
/>
</div>
</div>

View file

@ -89,8 +89,9 @@ const MessageHeader = ({ isSearchView = false }) => {
<>
<div
className={cn(
'flex min-h-[60px] w-full flex-wrap items-center justify-between gap-3 border-b border-black/10 bg-white text-sm text-gray-500 transition-all hover:bg-gray-50 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-800 dark:hover:bg-gray-700 dark:hover:bg-opacity-100',
isNotClickable ? '' : 'cursor-pointer ',
'flex min-h-[60px] w-full flex-wrap items-center justify-between gap-3 border-b border-black/10 bg-white text-sm text-gray-500 transition-all hover:bg-gray-50 dark:border-gray-900/50 dark:bg-gray-800 dark:hover:bg-gray-700',
isNotClickable ? '' : 'cursor-pointer',
'sticky top-0 z-10',
)}
onClick={() => (isNotClickable ? null : setSaveAsDialogShow(true))}
>

View file

@ -221,7 +221,7 @@ export default function Nav({ navVisible, setNavVisible }) {
</div>
</div>
{!navVisible && (
<div className="absolute left-2 top-2 z-10 hidden md:inline-block">
<div className="absolute left-2 top-2 z-20 hidden md:inline-block">
<TooltipTrigger asChild>
<button
type="button"

View file

@ -50,7 +50,7 @@ export default function NavLinks() {
)}
<Menu.Button
className={cn(
'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800',
'group-ui-open:bg-gray-800 rounded-sd flex w-full items-center gap-2.5 px-3 py-2 text-sm transition-colors duration-200 hover:bg-gray-800',
open ? 'bg-gray-800' : '',
)}
data-testid="nav-user"
@ -69,18 +69,24 @@ export default function NavLinks() {
/>
</div>
</div>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-white">
<div
className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left font-bold text-white"
style={{ marginTop: '-4px', marginLeft: '2px' }}
>
{user?.name || localize('com_nav_user')}
</div>
<DotsIcon />
<div style={{ marginBottom: '5px' }}>
<DotsIcon />
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100 transform"
enter="transition ease-out duration-110 transform"
enterFrom="translate-y-2 opacity-0"
enterTo="translate-y-0 opacity-100"
leave="transition ease-in duration-75 transform"
leave="transition ease-in duration-100 transform"
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
>

View file

@ -1,6 +1,6 @@
import * as Tabs from '@radix-ui/react-tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { CogIcon, DataIcon } from '~/components/svg';
import { GearIcon, DataIcon } from '~/components/svg';
import { useMediaQuery, useLocalize } from '~/hooks';
import type { TDialogProps } from '~/common';
import { General, Data } from './SettingsTabs';
@ -12,7 +12,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:w-[680px] ')}>
<DialogContent
className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:h-[373px] md:w-[680px]')}
style={{ borderRadius: '12px' }}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_settings')}
@ -21,7 +24,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<div className="px-6">
<Tabs.Root
defaultValue="general"
className="flex flex-col gap-6 md:flex-row"
className="flex flex-col gap-10 md:flex-row"
orientation="vertical"
>
<Tabs.List
@ -29,21 +32,21 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
role="tablist"
aria-orientation="vertical"
className={cn(
'-ml-[8px] flex min-w-[180px] flex-shrink-0 flex-col',
'min-w-auto -ml-[8px] flex flex-shrink-0 flex-col',
isSmallScreen ? 'flex-row rounded-lg bg-gray-100 p-1 dark:bg-gray-800/30' : '',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-gray-500 radix-state-active:bg-gray-800 radix-state-active:text-white',
'group my-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-gray-500 radix-state-active:bg-gray-800 radix-state-active:text-white',
isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '',
)}
value="general"
>
<CogIcon className="fill-gray-800" />
<GearIcon />
{localize('com_nav_setting_general')}
</Tabs.Trigger>
<Tabs.Trigger

View file

@ -20,7 +20,7 @@ export default function AutoScrollSwitch({
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_auto_scroll')}</div>
<div> {localize('com_nav_auto_scroll')} </div>
<Switch
id="autoScroll"
checked={autoScroll}

View file

@ -33,7 +33,7 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonEle
return (
<div className="flex items-center justify-between">
{showText && <div>{localize(infoTextCode)}</div>}
{showText && <div> {localize(infoTextCode)} </div>}
<DialogButton
id={id}
ref={ref}

View file

@ -14,6 +14,7 @@ import type { TDangerButtonProps } from '~/common';
import AutoScrollSwitch from './AutoScrollSwitch';
import DangerButton from './DangerButton';
import store from '~/store';
import { Dropdown } from '~/components/ui';
export const ThemeSelector = ({
theme,
@ -24,18 +25,22 @@ export const ThemeSelector = ({
}) => {
const localize = useLocalize();
const themeOptions = [
{ value: 'system', display: localize('com_nav_theme_system') },
{ value: 'dark', display: localize('com_nav_theme_dark') },
{ value: 'light', display: localize('com_nav_theme_light') },
];
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_theme')}</div>
<select
className="w-24 rounded border border-black/10 bg-transparent px-3 py-2 text-sm dark:border-white/20 dark:bg-gray-900"
onChange={(e) => onChange(e.target.value)}
<div> {localize('com_nav_theme')} </div>
<Dropdown
value={theme}
>
<option value="system">{localize('com_nav_theme_system')}</option>
<option value="dark">{localize('com_nav_theme_dark')}</option>
<option value="light">{localize('com_nav_theme_light')}</option>
</select>
onChange={onChange}
options={themeOptions}
width={150}
testId="theme-selector"
/>
</div>
);
};
@ -76,32 +81,30 @@ export const LangSelector = ({
}) => {
const localize = useLocalize();
// Create an array of options for the Dropdown
const languageOptions = [
{ value: 'auto', display: localize('com_nav_lang_auto') },
{ value: 'en-US', display: localize('com_nav_lang_english') },
{ value: 'zh-CN', display: localize('com_nav_lang_chinese') },
{ value: 'zh-TC', display: localize('com_nav_lang_traditionalchinese') },
{ value: 'ar-EG', display: localize('com_nav_lang_arabic') },
{ value: 'de-DE', display: localize('com_nav_lang_german') },
{ value: 'es-ES', display: localize('com_nav_lang_spanish') },
{ value: 'fr-FR', display: localize('com_nav_lang_french') },
{ value: 'it-IT', display: localize('com_nav_lang_italian') },
{ value: 'pl-PL', display: localize('com_nav_lang_polish') },
{ value: 'pt-BR', display: localize('com_nav_lang_brazilian_portuguese') },
{ value: 'ru-RU', display: localize('com_nav_lang_russian') },
{ value: 'ja-JP', display: localize('com_nav_lang_japanese') },
{ value: 'sv-SE', display: localize('com_nav_lang_swedish') },
{ value: 'ko-KR', display: localize('com_nav_lang_korean') },
{ value: 'vi-VN', display: localize('com_nav_lang_vietnamese') },
];
return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_language')}</div>
<select
className="w-24 rounded border border-black/10 bg-transparent px-3 py-2 text-sm dark:border-white/20 dark:bg-gray-900"
onChange={(e) => onChange(e.target.value)}
value={langcode}
>
<option value="auto">{localize('com_nav_lang_auto')}</option>
<option value="ar-EG">{localize('com_nav_lang_arabic')}</option>
<option value="en-US">{localize('com_nav_lang_english')}</option>
<option value="zh-CN">{localize('com_nav_lang_chinese')}</option>
<option value="zh-TC">{localize('com_nav_lang_traditionalchinese')}</option>
<option value="de-DE">{localize('com_nav_lang_german')}</option>
<option value="es-ES">{localize('com_nav_lang_spanish')}</option>
<option value="fr-FR">{localize('com_nav_lang_french')}</option>
<option value="it-IT">{localize('com_nav_lang_italian')}</option>
<option value="pl-PL">{localize('com_nav_lang_polish')}</option>
<option value="pt-BR">{localize('com_nav_lang_brazilian_portuguese')}</option>
<option value="ru-RU">{localize('com_nav_lang_russian')}</option>
<option value="ja-JP">{localize('com_nav_lang_japanese')}</option>
<option value="sv-SE">{localize('com_nav_lang_swedish')}</option>
<option value="ko-KR">{localize('com_nav_lang_korean')}</option>
<option value="vi-VN">{localize('com_nav_lang_vietnamese')}</option>
<option value="tr-TR">{localize('com_nav_lang_turkish')}</option>
</select>
<div> {localize('com_nav_language')} </div>
<Dropdown value={langcode} onChange={onChange} options={languageOptions} />
</div>
);
};

View file

@ -13,25 +13,36 @@ describe('LangSelector', () => {
});
it('renders correctly', () => {
const { getByText, getByDisplayValue } = render(
const { getByText } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('Language')).toBeInTheDocument();
expect(getByDisplayValue('English')).toBeInTheDocument();
expect(getByText('English')).toBeInTheDocument();
});
it('calls onChange when the select value changes', () => {
const { getByDisplayValue } = render(
it('calls onChange when the select value changes', async () => {
const { getByText, getByTestId } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
</RecoilRoot>,
);
fireEvent.change(getByDisplayValue('English'), { target: { value: 'it-IT' } });
expect(getByText('English')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledWith('it-IT');
// 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);
// Ensure that the onChange is called with the expected value after a short delay
await new Promise((resolve) => setTimeout(resolve, 0));
});
});

View file

@ -13,24 +13,37 @@ describe('ThemeSelector', () => {
});
it('renders correctly', () => {
const { getByText, getByDisplayValue } = render(
const { getByText } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
</RecoilRoot>,
);
expect(getByText('Theme')).toBeInTheDocument();
expect(getByDisplayValue('System')).toBeInTheDocument();
expect(getByText('System')).toBeInTheDocument();
});
it('calls onChange when the select value changes', () => {
const { getByDisplayValue } = render(
it('calls onChange when the select value changes', async () => {
const { getByText, getByTestId } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
</RecoilRoot>,
);
fireEvent.change(getByDisplayValue('System'), { target: { value: 'dark' } });
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 after a short delay
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockOnChange).toHaveBeenCalledWith('dark');
});

View file

@ -1,5 +1,4 @@
a {
text-decoration: underline;
color: white;
}
}

View file

@ -1,25 +0,0 @@
import { cn } from '~/utils';
export default function CogIcon({ className = '' }) {
return (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 20 20"
className={cn(
'h-4 h-5 w-4 w-5 fill-white group-radix-state-active:fill-white dark:fill-gray-500',
className,
)}
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clipRule="evenodd"
/>
</svg>
);
}

View file

@ -1,18 +1,19 @@
export default function DataIcon() {
return (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 20 20"
className="h-4 h-5 w-4 w-5 fill-gray-800 group-radix-state-active:fill-white dark:fill-gray-500"
height="1em"
width="1em"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-sm"
>
<path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z" />
<path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z" />
<path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.00633 5.5C6.02321 5.53319 6.07042 5.60366 6.19525 5.70906C6.42458 5.9027 6.81573 6.12215 7.38659 6.32603C8.5196 6.73067 10.1506 7 12 7C13.8494 7 15.4804 6.73067 16.6134 6.32603C17.1843 6.12215 17.5754 5.9027 17.8048 5.70906C17.9296 5.60366 17.9768 5.53319 17.9937 5.5C17.9768 5.46681 17.9296 5.39634 17.8048 5.29094C17.5754 5.0973 17.1843 4.87785 16.6134 4.67397C15.4804 4.26933 13.8494 4 12 4C10.1506 4 8.5196 4.26933 7.38659 4.67397C6.81573 4.87785 6.42458 5.0973 6.19525 5.29094C6.07042 5.39634 6.02321 5.46681 6.00633 5.5ZM18 7.91726C17.7726 8.02403 17.5333 8.12123 17.2861 8.20951C15.8856 8.70968 14.0166 9 12 9C9.98341 9 8.1144 8.70968 6.71392 8.20951C6.46674 8.12123 6.22738 8.02403 6 7.91726V11.9866C6.00813 12.0073 6.03931 12.0661 6.14259 12.1624C6.31976 12.3277 6.63181 12.5252 7.10609 12.7189C8.04837 13.1039 9.43027 13.3932 11.051 13.476C11.6026 13.5042 12.0269 13.9741 11.9987 14.5257C11.9705 15.0773 11.5005 15.5016 10.949 15.4734C9.17744 15.3829 7.55934 15.0646 6.34969 14.5704C6.23097 14.5219 6.11419 14.4709 6 14.4173V18.4866C6.00813 18.5073 6.03931 18.5661 6.14259 18.6624C6.31976 18.8277 6.63181 19.0252 7.10609 19.2189C8.04837 19.6039 9.43027 19.8932 11.051 19.976C11.6026 20.0042 12.0269 20.4741 11.9987 21.0257C11.9705 21.5773 11.5005 22.0016 10.949 21.9734C9.17744 21.8829 7.55934 21.5646 6.34969 21.0704C5.74801 20.8246 5.19611 20.5146 4.77833 20.1249C4.35948 19.7341 4 19.1866 4 18.5V5.5C4 4.74631 4.43048 4.16346 4.90494 3.76283C5.38405 3.35829 6.01803 3.03902 6.71392 2.79049C8.1144 2.29032 9.98341 2 12 2C14.0166 2 15.8856 2.29032 17.2861 2.79049C17.982 3.03902 18.616 3.35829 19.0951 3.76283C19.5695 4.16346 20 4.74631 20 5.5V10C20 10.5523 19.5523 11 19 11C18.4477 11 18 10.5523 18 10V7.91726ZM17.5 13C18.0523 13 18.5 13.4477 18.5 14V14.6707C18.851 14.7948 19.172 14.9823 19.4492 15.2195L20.0308 14.8837C20.5091 14.6075 21.1207 14.7714 21.3968 15.2497C21.673 15.728 21.5091 16.3396 21.0308 16.6157L20.4499 16.9511C20.4828 17.1291 20.5 17.3125 20.5 17.5C20.5 17.6873 20.4828 17.8707 20.45 18.0485L21.0308 18.3838C21.5091 18.6599 21.6729 19.2715 21.3968 19.7498C21.1206 20.2281 20.5091 20.392 20.0308 20.1158L19.4495 19.7803C19.1722 20.0176 18.8511 20.2052 18.5 20.3293V21C18.5 21.5523 18.0523 22 17.5 22C16.9477 22 16.5 21.5523 16.5 21V20.3293C16.1489 20.2052 15.8277 20.0176 15.5504 19.7802L14.969 20.1159C14.4907 20.392 13.8791 20.2282 13.603 19.7499C13.3269 19.2716 13.4907 18.66 13.969 18.3839L14.55 18.0484C14.5172 17.8706 14.5 17.6873 14.5 17.5C14.5 17.3127 14.5172 17.1294 14.55 16.9515L13.9691 16.6161C13.4908 16.34 13.3269 15.7284 13.6031 15.2501C13.8792 14.7718 14.4908 14.608 14.9691 14.8841L15.5504 15.2197C15.8278 14.9824 16.1489 14.7948 16.5 14.6707V14C16.5 13.4477 16.9477 13 17.5 13ZM16.624 17.0174C16.6274 17.0117 16.6308 17.0059 16.6342 17.0001C16.6374 16.9946 16.6405 16.989 16.6436 16.9834C16.8187 16.6937 17.1367 16.5 17.5 16.5C17.8645 16.5 18.1835 16.6951 18.3583 16.9865C18.3607 16.9909 18.3632 16.9953 18.3658 16.9997C18.3685 17.0044 18.3713 17.0091 18.3741 17.0138C18.4543 17.1577 18.5 17.3235 18.5 17.5C18.5 17.6737 18.4557 17.8371 18.3778 17.9794C18.3737 17.9861 18.3697 17.9929 18.3657 17.9998C18.3619 18.0064 18.3581 18.0131 18.3545 18.0198C18.1789 18.3077 17.8619 18.5 17.5 18.5C17.1362 18.5 16.8178 18.3058 16.6428 18.0154C16.64 18.0102 16.6371 18.005 16.6341 17.9999C16.631 17.9945 16.6278 17.9891 16.6246 17.9838C16.5452 17.8404 16.5 17.6755 16.5 17.5C16.5 17.325 16.545 17.1605 16.624 17.0174Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,19 +1,22 @@
import React from 'react';
export default function GearIcon() {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
width="18"
height="18"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md"
>
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
<path
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
></path>
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2"></circle>
</svg>
);
}

View file

@ -1,20 +1,19 @@
export default function LinkIcon() {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
width="18"
height="18"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15 5C14.4477 5 14 4.55228 14 4C14 3.44772 14.4477 3 15 3H20C20.5523 3 21 3.44772 21 4V9C21 9.55228 20.5523 10 20 10C19.4477 10 19 9.55228 19 9V6.41421L13.7071 11.7071C13.3166 12.0976 12.6834 12.0976 12.2929 11.7071C11.9024 11.3166 11.9024 10.6834 12.2929 10.2929L17.5858 5H15ZM4 7C4 5.34315 5.34315 4 7 4H10C10.5523 4 11 4.44772 11 5C11 5.55228 10.5523 6 10 6H7C6.44772 6 6 6.44772 6 7V17C6 17.5523 6.44772 18 7 18H17C17.5523 18 18 17.5523 18 17V14C18 13.4477 18.4477 13 19 13C19.5523 13 20 13.4477 20 14V17C20 18.6569 18.6569 20 17 20H7C5.34315 20 4 18.6569 4 17V7Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -3,20 +3,26 @@ import React from 'react';
export default function LogOutIcon() {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
width="18"
height="18"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
<path
d="M11 3H7C5.89543 3 5 3.89543 5 5V19C5 20.1046 5.89543 21 7 21H11"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
></path>
<path
d="M20 12H11M20 12L16 16M20 12L16 8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
);
}

View file

@ -0,0 +1,18 @@
import React from 'react';
export default function SendMessageIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="none"
className="icon-sm m-1 md:m-0"
style={{ width: '1em', height: '1em', verticalAlign: 'middle' }}
>
<path
d="M.5 1.163A1 1 0 0 1 1.97.28l12.868 6.837a1 1 0 0 1 0 1.766L1.969 15.72A1 1 0 0 1 .5 14.836V10.33a1 1 0 0 1 .816-.983L8.5 8 1.316 6.653A1 1 0 0 1 .5 5.67V1.163Z"
fill="currentColor"
></path>
</svg>
);
}

View file

@ -1,7 +1,6 @@
export { default as Plugin } from './Plugin';
export { default as GPTIcon } from './GPTIcon';
export { default as EditIcon } from './EditIcon';
export { default as CogIcon } from './CogIcon';
export { default as DataIcon } from './DataIcon';
export { default as Panel } from './Panel';
export { default as Spinner } from './Spinner';
@ -30,3 +29,4 @@ export { default as PluginMinimalIcon } from './PluginMinimalIcon';
export { default as BingAIMinimalIcon } from './BingAIMinimalIcon';
export { default as PaLMinimalIcon } from './PaLMinimalIcon';
export { default as AnthropicMinimalIcon } from './AnthropicMinimalIcon';
export { default as SendMessageIcon } from './SendMessageIcon';

View file

@ -4,7 +4,7 @@ import { VariantProps, cva } from 'class-variance-authority';
import { cn } from '../../utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800',
'inline-flex items-center justify-center text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800',
{
variants: {
variant: {

View file

@ -114,7 +114,7 @@ const DialogClose = React.forwardRef<
<DialogPrimitive.Close
ref={ref}
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent px-4 py-2 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-gray-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0',
'mt-2 inline-flex h-10 items-center justify-center rounded-lg border border-slate-200 bg-transparent px-4 py-2 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-gray-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0',
className ?? '',
)}
{...props}
@ -130,7 +130,7 @@ const DialogButton = React.forwardRef<
ref={ref}
variant="outline"
className={cn(
'mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent px-4 py-2 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-gray-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0',
'mt-2 inline-flex h-10 items-center justify-center rounded-lg border border-slate-200 bg-transparent px-4 py-2 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-gray-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0',
className ?? '',
)}
{...props}

View file

@ -1,85 +0,0 @@
import React from 'react';
import CheckMark from '../svg/CheckMark';
import { Listbox } from '@headlessui/react';
import { cn } from '~/utils/';
function Dropdown({
value,
label = '',
onChange,
options,
className,
containerClassName,
optionsClassName = '',
}) {
const currentOption =
options.find((element) => element === value || element?.value === value) ?? value;
return (
<div className={cn('flex items-center justify-center gap-2', containerClassName)}>
<div className="relative w-full">
<Listbox value={value} onChange={onChange}>
<Listbox.Button
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className || '',
)}
>
<span className="inline-flex w-full truncate">
<span className="flex h-6 items-center gap-1 truncate text-sm text-black dark:text-white">
{`${label}${currentOption?.display ?? value}`}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
<Listbox.Options
className={cn(
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%] ',
optionsClassName,
)}
>
{options.map((item, i) => (
<Listbox.Option
key={i}
value={item?.value ?? item}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
<span
className={cn(
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100',
value === (item?.value ?? item) ? 'font-semibold' : '',
)}
>
{item?.display ?? item}
</span>
{value === (item?.value ?? item) && (
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-100">
<CheckMark />
</span>
)}
</span>
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
</div>
</div>
);
}
export default Dropdown;

View file

@ -0,0 +1,121 @@
import React, { FC, useContext, useState } from 'react';
import { Listbox } from '@headlessui/react';
import { cn } from '~/utils/';
import { ThemeContext } from '~/hooks';
type OptionType = {
value: string;
display?: string;
};
interface DropdownProps {
value: string;
label?: string;
onChange: (value: string) => void;
options: (string | OptionType)[];
className?: string;
width?: number;
testId?: string;
}
const Dropdown: FC<DropdownProps> = ({
value: initialValue,
label = '',
onChange,
options,
className = '',
width,
testId = 'dropdown-menu',
}) => {
const { theme } = useContext(ThemeContext);
const [selectedValue, setSelectedValue] = useState(initialValue);
const themeStyles = {
light: 'bg-white text-gray-700 hover:bg-gray-200 border-gray-300',
dark: 'bg-[#202123] text-white hover:bg-[#323236] border-gray-600',
};
const isSystemDark =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const currentThemeStyle =
theme === 'system'
? isSystemDark
? themeStyles.dark
: themeStyles.light
: themeStyles[theme] || themeStyles.light;
return (
<div className={cn('relative', className)}>
<Listbox
value={selectedValue}
onChange={(newValue) => {
setSelectedValue(newValue);
onChange(newValue);
}}
>
<div className={cn('relative', className)}>
<Listbox.Button
data-testid={testId}
className={cn(
'relative inline-flex items-center justify-between rounded-md py-2 pl-3 pr-10',
currentThemeStyle,
'w-auto',
className,
)}
>
<span className="block truncate font-medium">
{label}
{options
.map((o) => (typeof o === 'string' ? { value: o, display: o } : o))
.find((o) => o.value === selectedValue)?.display || 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="1.5"
stroke="currentColor"
className="h-5 w-5 rotate-0 transform text-gray-400 transition-transform duration-300 ease-in-out"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
</span>
</Listbox.Button>
<Listbox.Options
className={cn(
'max-h-90 absolute z-50 mt-1 overflow-auto rounded-md shadow-lg transition-opacity focus:outline-none',
currentThemeStyle,
className,
)}
style={{ width: width ? `${width}px` : 'auto' }}
>
{options.map((item, index) => (
<Listbox.Option
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'relative cursor-pointer select-none py-1 pl-3 pr-6',
currentThemeStyle,
)}
style={{ width: width ? `${width}px` : 'auto' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
>
<span className="block truncate">
{typeof item === 'string' ? item : (item as OptionType).display}
</span>
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
);
};
export default Dropdown;

View file

@ -249,6 +249,7 @@ export default {
com_nav_confirm_clear: 'Confirmar Limpeza',
com_nav_close_sidebar: 'Fechar barra lateral',
com_nav_open_sidebar: 'Abrir barra lateral',
com_nav_send_message: 'Enviar mensagem',
com_nav_log_out: 'Sair',
com_nav_user: 'USUÁRIO',
com_nav_clear_conversation: 'Limpar conversas',
@ -259,6 +260,5 @@ export default {
com_nav_search_placeholder: 'Procurar mensagens',
com_nav_setting_general: 'Geral',
com_nav_setting_data: 'Controle de Dados',
com_nav_lang_auto: 'Auto detectar',
com_nav_lang_brazilian_portuguese: 'Português Brasileiro',
};

View file

@ -191,6 +191,7 @@ export default {
com_nav_confirm_clear: 'Bestätige Löschung aller Chats',
com_nav_close_sidebar: 'Schließe Seitenleiste',
com_nav_open_sidebar: 'Öffne Seitenleiste',
com_nav_send_message: 'Sende Nachricht',
com_nav_log_out: 'Ausloggen',
com_nav_user: 'USER',
com_nav_clear_conversation: 'Lösche Konversation',

View file

@ -252,6 +252,7 @@ export default {
com_nav_confirm_clear: 'Confirm Clear',
com_nav_close_sidebar: 'Close sidebar',
com_nav_open_sidebar: 'Open sidebar',
com_nav_send_message: 'Send message',
com_nav_log_out: 'Log out',
com_nav_user: 'USER',
com_nav_clear_conversation: 'Clear conversations',

View file

@ -253,6 +253,7 @@ export default {
com_nav_confirm_clear: 'Confirmar Limpieza',
com_nav_close_sidebar: 'Cerrar barra lateral',
com_nav_open_sidebar: 'Abrir barra lateral',
com_nav_send_message: 'Enviar mensaje',
com_nav_log_out: 'Cerrar sesión',
com_nav_user: 'USUARIO',
com_nav_clear_conversation: 'Limpiar conversaciones',

View file

@ -252,6 +252,7 @@ export default {
com_nav_confirm_clear: 'Confirmer l\'effacement',
com_nav_close_sidebar: 'Fermer la barre latérale',
com_nav_open_sidebar: 'Ouvrir la barre latérale',
com_nav_send_message: 'Envoyer un message',
com_nav_log_out: 'Se déconnecter',
com_nav_user: 'UTILISATEUR',
com_nav_clear_conversation: 'Effacer les conversations',

View file

@ -253,6 +253,7 @@ export default {
com_nav_confirm_clear: 'Conferma la cancellazione',
com_nav_close_sidebar: 'Chiudi la barra laterale',
com_nav_open_sidebar: 'Apri la barra laterale',
com_nav_send_message: 'Invia messaggio',
com_nav_log_out: 'Esci',
com_nav_user: 'UTENTE',
com_nav_clear_conversation: 'Cancella conversazioni',

View file

@ -245,6 +245,7 @@ export default {
com_nav_confirm_clear: 'Confirm Clear',
com_nav_close_sidebar: 'サイドバーを閉じる',
com_nav_open_sidebar: 'サイドバーを開く',
com_nav_send_message: 'メッセージを送信する',
com_nav_log_out: 'ログアウト',
com_nav_user: 'ユーザ',
com_nav_clear_conversation: '会話を削除する',

View file

@ -231,6 +231,7 @@ export default {
com_nav_confirm_clear: '지우기 확인',
com_nav_close_sidebar: '사이드바 닫기',
com_nav_open_sidebar: '사이드바 열기',
com_nav_send_message: '메시지 보내기',
com_nav_log_out: '로그아웃',
com_nav_user: '사용자',
com_nav_clear_conversation: '대화 지우기',

View file

@ -194,6 +194,7 @@ export default {
com_nav_confirm_clear: 'Potwierdź usunięcie',
com_nav_close_sidebar: 'Zamknij pasek boczny',
com_nav_open_sidebar: 'Otwórz pasek boczny',
com_nav_send_message: 'Wyślij wiadomość',
com_nav_log_out: 'Wyloguj',
com_nav_user: 'Użytkownik',
com_nav_clear_conversation: 'Wyczyść rozmowę',

View file

@ -206,6 +206,7 @@ export default {
com_nav_auto_scroll: 'Автоматическая прокрутка к новым сообщениям в режиме открытия',
com_nav_close_sidebar: 'Закрыть боковую панель',
com_nav_open_sidebar: 'Открыть боковую панель',
com_nav_send_message: 'Отправить сообщение',
com_nav_log_out: 'Выйти',
com_nav_user: 'ПОЛЬЗОВАТЕЛЬ',
com_nav_clear_conversation: 'Очистить разговоры',

View file

@ -246,6 +246,7 @@ export default {
com_nav_confirm_clear: 'Bekräfta rensning', // Confirm Clear
com_nav_close_sidebar: 'Stäng sidofält', // Close sidebar
com_nav_open_sidebar: 'Öppna sidofält', // Open sidebar
com_nav_send_message: 'Skicka meddelande', // Send message
com_nav_log_out: 'Logga ut', // Log out
com_nav_user: 'ANVÄNDARE', // USER
com_nav_clear_conversation: 'Rensa konversationer', // Clear conversations

View file

@ -253,6 +253,7 @@ export default {
com_nav_confirm_clear: 'Xác nhận xóa',
com_nav_close_sidebar: 'Đóng thanh bên',
com_nav_open_sidebar: 'Mở thanh bên',
com_nav_send_message: 'Gửi tin nhắn',
com_nav_log_out: 'Đăng xuất',
com_nav_user: 'NGƯỜI DÙNG',
com_nav_clear_conversation: 'Xóa cuộc trò chuyện',

View file

@ -252,6 +252,7 @@ export default {
com_nav_confirm_clear: '确认清空',
com_nav_close_sidebar: '关闭侧边栏',
com_nav_open_sidebar: '打开侧边栏',
com_nav_send_message: '发送消息',
com_nav_log_out: '注销',
com_nav_user: '默认用户',
com_nav_clear_conversation: '清空对话',

View file

@ -12,8 +12,7 @@ export default {
com_ui_capability_decline_requests: '訓練有素以拒絕不適當的請求',
com_ui_limitations: '限制',
com_ui_limitation_incorrect_info: '有時可能會產生不正確的資訊',
com_ui_limitation_harmful_biased:
'有時可能會產生有害的指示或帶有偏見的內容',
com_ui_limitation_harmful_biased: '有時可能會產生有害的指示或帶有偏見的內容',
com_ui_limitation_limited_2021: '對於 2021 年後的世界和事件的知識有限',
com_ui_input: '輸入',
com_ui_close: '關閉',
@ -49,14 +48,10 @@ export default {
com_ui_delete: '刪除',
com_ui_delete_conversation: '刪除對話?',
com_ui_delete_conversation_confirm: '這將刪除',
com_auth_error_login:
'無法使用提供的資訊登入。請檢查您的登入資訊後重試。',
com_auth_error_login_rl:
'短時間內嘗試登入的次數過多。請稍後再試。',
com_auth_error_login_ban:
'由於違反我們的服務條款,您的帳號已被暫時停用。',
com_auth_error_login_server:
'發生內部伺服器錯誤。請稍候片刻,然後重試。',
com_auth_error_login: '無法使用提供的資訊登入。請檢查您的登入資訊後重試。',
com_auth_error_login_rl: '短時間內嘗試登入的次數過多。請稍後再試。',
com_auth_error_login_ban: '由於違反我們的服務條款,您的帳號已被暫時停用。',
com_auth_error_login_server: '發生內部伺服器錯誤。請稍候片刻,然後重試。',
com_auth_no_account: '還沒有帳號?',
com_auth_sign_up: '註冊',
com_auth_sign_in: '登入',
@ -79,9 +74,8 @@ export default {
com_auth_password_not_match: '密碼不符',
com_auth_continue: '繼續',
com_auth_create_account: '建立您的帳號',
com_nav_auto_scroll:'開啟時自動捲動至最新內容',
com_auth_error_create:
'嘗試註冊您的帳號時發生錯誤。請重試。',
com_nav_auto_scroll: '開啟時自動捲動至最新內容',
com_auth_error_create: '嘗試註冊您的帳號時發生錯誤。請重試。',
com_auth_full_name: '全名',
com_auth_name_required: '名稱必填',
com_auth_name_min_length: '名稱長度必須至少有 3 個字元',
@ -97,8 +91,7 @@ export default {
com_auth_here: '這裡',
com_auth_to_reset_your_password: '重設您的密碼。',
com_auth_reset_password_link_sent: '電子郵件已傳送',
com_auth_reset_password_email_sent:
'已向您傳送電子郵件,其中包含進一步重設密碼的操作說明。',
com_auth_reset_password_email_sent: '已向您傳送電子郵件,其中包含進一步重設密碼的操作說明。',
com_auth_error_reset_password:
'重設密碼時出現問題。找不到使用提供的電子郵件地址的使用者。請重試。',
com_auth_reset_password_success: '密碼重設成功',
@ -154,8 +147,7 @@ export default {
com_endpoint_openai_pres:
'數值範圍介於 -2.0 和 2.0 之間。正值會根據該 token 是否在目前的文字中出現來進行懲罰,增加模型談及新主題的可能性。',
com_endpoint_openai_custom_name_placeholder: '為 ChatGPT 設定自定義名稱',
com_endpoint_openai_prompt_prefix_placeholder:
'在系統訊息中設定自定義提示。',
com_endpoint_openai_prompt_prefix_placeholder: '在系統訊息中設定自定義提示。',
com_endpoint_anthropic_temp:
'範圍從 0 到 1。對於分析/多選題,使用接近 0 的溫度,對於創意和生成式任務,使用接近 1 的溫度。我們建議修改這個或 Top P但不建議兩者都修改。',
com_endpoint_anthropic_topp:
@ -171,8 +163,7 @@ export default {
com_endpoint_plug_skip_completion: '跳過完成步驟',
com_endpoint_disabled_with_tools: '與工具一起停用',
com_endpoint_disabled_with_tools_placeholder: '選擇工具時停用',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
'在系統訊息中新增自定義提示。',
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder: '在系統訊息中新增自定義提示。',
com_endpoint_import: '匯入',
com_endpoint_set_custom_name: '設定自定義名稱,以便您找到此預設設定',
com_endpoint_preset: '預設設定',
@ -189,8 +180,7 @@ export default {
com_endpoint_save: '儲存',
com_endpoint_export: '匯出',
com_endpoint_save_as_preset: '另存為預設設定',
com_endpoint_presets_clear_warning:
'您確定要清除所有預設設定嗎?此操作無法復原。',
com_endpoint_presets_clear_warning: '您確定要清除所有預設設定嗎?此操作無法復原。',
com_endpoint_not_implemented: '尚未實做',
com_endpoint_no_presets: '尚無預設設定',
com_endpoint_not_available: '無可用選項',
@ -228,8 +218,7 @@ export default {
'確保點選「建立並繼續」並至少給予「Vertex AI 使用者」角色。最後,建立一個 JSON 金鑰以在此處匯入。',
com_nav_plugin_store: '外掛商店',
com_nav_plugin_search: '搜尋外掛',
com_nav_plugin_auth_error:
'嘗試驗證此外掛時發生錯誤。請重試。',
com_nav_plugin_auth_error: '嘗試驗證此外掛時發生錯誤。請重試。',
com_nav_close_menu: '關閉側邊選單',
com_nav_open_menu: '開啟側邊選單',
com_nav_export_filename: '檔名',
@ -250,11 +239,11 @@ export default {
com_nav_confirm_clear: '確認清除',
com_nav_close_sidebar: '關閉側邊選單',
com_nav_open_sidebar: '開啟側邊選單',
com_nav_send_message: '傳送訊息',
com_nav_log_out: '登出',
com_nav_user: '使用者',
com_nav_clear_conversation: '清除對話',
com_nav_clear_conversation_confirm_message:
'您確定要清除所有對話嗎?此操作無法復原。',
com_nav_clear_conversation_confirm_message: '您確定要清除所有對話嗎?此操作無法復原。',
com_nav_help_faq: '說明與常見問題',
com_nav_settings: '設定',
com_nav_search_placeholder: '搜尋訊息',

View file

@ -1592,4 +1592,4 @@ button.scroll-convo {
to {
opacity:0
}
}
}

View file

@ -30,24 +30,29 @@ test.describe('Navigation suite', () => {
const modalClearConvos = await page.getByRole('button', { name: 'Clear' }).isVisible();
expect(modalClearConvos).toBeTruthy();
const modalTheme = page.getByRole('combobox').first();
expect(modalTheme.isVisible()).toBeTruthy();
const modalTheme = page.getByTestId('theme-selector');
expect(modalTheme).toBeTruthy();
async function changeMode(theme: string) {
// change the value to 'dark' and 'light' and see if the theme changes
await modalTheme.selectOption({ label: theme });
// Ensure Element Visibility:
await page.waitForSelector('[data-testid="theme-selector"]');
await modalTheme.click();
await page.click(`[data-theme="${theme}"]`);
// Wait for the theme change
await page.waitForTimeout(1000);
// Check if the HTML element has the theme class
const html = await page.$eval(
'html',
(element, theme) => element.classList.contains(theme.toLowerCase()),
(element, selectedTheme) => element.classList.contains(selectedTheme.toLowerCase()),
theme,
);
expect(html).toBeTruthy();
}
await changeMode('Dark');
await changeMode('Light');
await changeMode('dark');
await changeMode('light');
});
});