mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
Added Settings Modal (#342)
* Improve UI with style changes and add Settings button - Improved the UI of the `Input` and `Message` components. - Added a `Settings` button to the `NavLinks` component. - Introduced a `Settings` component to handle user settings. - Refactored the `Dialog` component for consistency. * Revert not needed changes * Updated style.css to only work for select * feat: Remove Dark Mode component and add theme selection feature This commit removes the Dark Mode component from the navigation bar and replaces it with a theme selection dropdown menu in the Settings dialog. The implementation of the theme selection feature includes a function that allows the user to set the theme based on the system, light, or dark mode. * Add auto theme setting to Settings component. This commit adds a new state variable to keep track of whether the auto theme is enabled or not. It also registers an event listener to update the theme based on system preference changes. The event listener is removed when the component is unmounted. * Improve user experience by allowing customized themes - Create `selectedOption` state to track user-selected theme - Remove unused `isAutoTheme` state variable * feat(Nav): Add SVG icon to settings gear This commit adds an SVG icon to the settings gear in the Navigation component's Settings file. The new SVG icon replaces the previous GearIcon component. * refactor(ui): Update overlay background color This commit updates the background color of the overlay in the AlertDialog and Dialog components by changing the classes applied to the elements. The new color is a transition from `bg-black/50 backdrop-blur-sm` to `bg-gray-500/90 dark:bg-gray-800/90`. This change improves the readability of the dialog boxes. * Refactor ThemeContext to include system theme and fix bug in Settings The ThemeContext now includes a "system" theme and ClearConvos no longer relies on the "selected option" state to update the theme. The bug is now fixed if the system theme changes. * Refactor DialogTemplate styles and color scheme Adjusted the color scheme of the DialogTemplate component to dark mode, updated the background color to gray-900 and removed unnecessary classes. * Refactor: Change button logic to require confirmation before clearing convos This commit refactors the code by adding a confirmation dialog to prompt for a user's confirmation before clearing all conversations in the Settings.jsx file. The change ensures the user is aware of the irreversible action before initiating the clearConvos function. Additionally, the commit updates the clear chat button's class name and changes the button's onClick logic to call the confirmClearConvos function instead of directly invoking the clearConvos method. * Refactor component name to reflect functionality change. - Changed component name from ClearConvos to Settings to support potential future use cases. * Refactor conversation clearing functionality in `Settings.jsx` This commit optimizes the conversation clearing functionality in the `Settings.jsx` component by removing the `confirmClearConvos` function and directly calling the `clearConvos` function on confirmation. This change will simplify the code and improve the user experience. * Refactor Input component UI styles Simplify Input component styles by simplifying the gradient background, removing border color styles, and updating button styles. * feat: Add e2e test for Settings modal This commit adds an e2e test to verify whether the Settings modal is displayed on the landing page. It uses a headless browser to navigate to the page and interacts with it to verify if the dialog and its components are visible. * test: Add Navigation and Settings tests Add Navigation and Settings tests to verify that the navigation bar and Settings button are visible and that the Settings modal displays the expected content. The settings modal verification includes checking whether the modal is visible, if the modal title, tab list, clear conversation button and theme are present, and if the theme option can be selected to change the mode. * Quick fix * feat(navbar): Add confirmation before clearing conversations Adding confirmation modal to prevent accidentally clearing conversations. Before, once you clicked on the "Clear" button it immediately clears all conversations. With this change, if you click on "Clear" the first time, it will change the text to "Confirm Clear" and if you click it again, it will clear all conversations. * Add click functionality to the navigation bar and improve UI design The code introduced click functionality to the nav bar and improved the user interface. It also used the new theme select feature to change the theme to dark. * test: Add test for dark mode theme change Refactor the test for Navigation suite to check for the 'dark' class in the HTML element when the 'dark' theme is selected in the modal. This ensures that the dark mode theme change works correctly, and improves test coverage. * Improve navigation test clarity This commit improves code clarity and adds more detailed test assertions to the navigation suite. New assert statements are added to check whether the modal theme selection changes the theme and that the HTML element receives the 'dark' class. A new function `changeMode` was introduced to avoid code repetition. A short description was added to the commit message to adhere to best practices. * Improve navigation test clarity This commit improves code clarity and adds more detailed test assertions to the navigation suite. New assert statements are added to check whether the modal theme selection changes the theme and that the HTML element receives the 'dark' class. A new function `changeMode` was introduced to avoid code repetition. A short description was added to the commit message to adhere to best practices. * Hotfix * Removed repetation * Refactor: Change text-gray-400 to text-white/50 to make tailwind more cleaner * style: Update CSS classes to improve the conversation UI - Update Conversation component to improve UX - Changed styling for group hover effect using shades of gray - Improved color contrast of the Message component for easy readability - Replaced class names in buildTree.js with a new class name - Added a new color theme (gray-1000) in tailwind.config to replace an old background color. * Refactor EndpointItem, EndpointItems, and NewConversationMenu for better user experience - The `EndpointItem` component now accepts an `isSelected` prop instead of `onSelect` to better reflect its usage in `EndpointItems` and `NewConversationMenu`. - `EndpointItems` component now has a `selectedEndpoint` prop to highlight the selected item in the list. - `NewConversationMenu` now has a gap between the endpoint options to improve user experience. * Added error messages * refactor: Improve endpoint menu highlighting and error handling In the UI, when the user selects an endpoint, the active class is now properly set. In the error handling function, `isJson` is now a private function called by `getError`, which provides better parsing of error messages, and returns more succinct messages upon encountering specific errors. Finally, a new end-to-end test has been added to check if the active class is properly set on selecting an endpoint in the new conversation menu. * test: Add Conversation and Change Path of Auth JSON In the Landing spec, test the functionality to create conversations and check that the number of items has increased. In the Popup spec, change the path of the Auth JSON used by the context. * Fixed logo issues * Make everything not rounded * Added time --------- Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
This commit is contained in:
parent
dade7b450f
commit
7468b3011f
20 changed files with 414 additions and 90 deletions
|
@ -123,7 +123,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]" />
|
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-gray-900 group-hover:from-gray-700/70" />
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import SetTokenDialog from '../SetTokenDialog';
|
import SetTokenDialog from '../SetTokenDialog';
|
||||||
|
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import { cn } from '~/utils/index.jsx';
|
||||||
|
|
||||||
const alternateName = {
|
const alternateName = {
|
||||||
openAI: 'OpenAI',
|
openAI: 'OpenAI',
|
||||||
|
@ -15,7 +16,7 @@ const alternateName = {
|
||||||
google: 'PaLM'
|
google: 'PaLM'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ModelItem({ endpoint, value }) {
|
export default function ModelItem({ endpoint, value, isSelected }) {
|
||||||
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
|
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
|
||||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||||
|
|
||||||
|
@ -33,8 +34,11 @@ export default function ModelItem({ endpoint, value }) {
|
||||||
<>
|
<>
|
||||||
<DropdownMenuRadioItem
|
<DropdownMenuRadioItem
|
||||||
value={value}
|
value={value}
|
||||||
|
className={cn(
|
||||||
|
'group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800',
|
||||||
|
isSelected && 'dark:bg-gray-900 bg-gray-50 active'
|
||||||
|
)}
|
||||||
id={endpoint}
|
id={endpoint}
|
||||||
className="group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{alternateName[endpoint] || endpoint}
|
{alternateName[endpoint] || endpoint}
|
||||||
|
@ -42,7 +46,10 @@ export default function ModelItem({ endpoint, value }) {
|
||||||
<div className="flex w-4 flex-1" />
|
<div className="flex w-4 flex-1" />
|
||||||
{isUserProvided ? (
|
{isUserProvided ? (
|
||||||
<button
|
<button
|
||||||
className="invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200"
|
className={cn(
|
||||||
|
'invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
|
isSelected && 'visible text-gray-700 dark:text-gray-200'
|
||||||
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSetTokenDialogOpen(true);
|
setSetTokenDialogOpen(true);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import EndpointItem from './EndpointItem.jsx';
|
import EndpointItem from './EndpointItem.jsx';
|
||||||
|
|
||||||
export default function EndpointItems({ endpoints, onSelect }) {
|
export default function EndpointItems({ endpoints, onSelect, selectedEndpoint }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{endpoints.map((endpoint) => (
|
{endpoints.map((endpoint) => (
|
||||||
<EndpointItem key={endpoint} value={endpoint} onSelect={onSelect} endpoint={endpoint} />
|
<EndpointItem isSelected={selectedEndpoint === endpoint} key={endpoint} value={endpoint} onSelect={onSelect} endpoint={endpoint} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -150,11 +150,11 @@ export default function NewConversationMenu() {
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
value={endpoint}
|
value={endpoint}
|
||||||
onValueChange={onSelectEndpoint}
|
onValueChange={onSelectEndpoint}
|
||||||
className="overflow-y-auto"
|
className="overflow-y-auto gap-1 flex flex-col"
|
||||||
>
|
>
|
||||||
{showEndpoints &&
|
{showEndpoints &&
|
||||||
(availableEndpoints.length ? (
|
(availableEndpoints.length ? (
|
||||||
<EndpointItems endpoints={availableEndpoints} onSelect={onSelectEndpoint} />
|
<EndpointItems selectedEndpoint={endpoint} endpoints={availableEndpoints} onSelect={onSelectEndpoint} />
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuLabel className="dark:text-gray-300">
|
<DropdownMenuLabel className="dark:text-gray-300">
|
||||||
No endpoint available.
|
No endpoint available.
|
||||||
|
|
|
@ -11,15 +11,7 @@ import { useMessageHandler } from '~/utils/handleSubmit';
|
||||||
import { useGetConversationByIdQuery } from '~/data-provider';
|
import { useGetConversationByIdQuery } from '~/data-provider';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
import getError from '~/utils/getError';
|
||||||
function isJson(str) {
|
|
||||||
try {
|
|
||||||
JSON.parse(str);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Message({
|
export default function Message({
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -73,24 +65,6 @@ export default function Message({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getError = (text) => {
|
|
||||||
const errorMessage = text.length > 512 ? text.slice(0, 512) + '...' : text;
|
|
||||||
const match = text.match(/\{[^{}]*\}/);
|
|
||||||
var json = match ? match[0] : '';
|
|
||||||
if (isJson(json)) {
|
|
||||||
json = JSON.parse(json);
|
|
||||||
if (json.code === 'invalid_api_key') {
|
|
||||||
return 'Invalid API key. Please check your API key and try again. You can access your API key by clicking on the model logo in the top-left corner of the textbox.';
|
|
||||||
} else if (json.type === 'insufficient_quota') {
|
|
||||||
return "We're sorry, but the default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the top-left corner of the textbox.";
|
|
||||||
} else {
|
|
||||||
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
className:
|
className:
|
||||||
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
|
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
|
||||||
|
@ -103,7 +77,7 @@ export default function Message({
|
||||||
|
|
||||||
if (!isCreatedByUser)
|
if (!isCreatedByUser)
|
||||||
props.className =
|
props.className =
|
||||||
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
|
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-gray-1000';
|
||||||
|
|
||||||
if (message.bg && searchResult) {
|
if (message.bg && searchResult) {
|
||||||
props.className = message.bg.split('hover')[0];
|
props.className = message.bg.split('hover')[0];
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { forwardRef, useContext } from 'react';
|
|
||||||
import DarkModeIcon from '../svg/DarkModeIcon';
|
|
||||||
import LightModeIcon from '../svg/LightModeIcon';
|
|
||||||
import { ThemeContext } from '~/hooks/ThemeContext';
|
|
||||||
|
|
||||||
const DarkMode = forwardRef(() => {
|
|
||||||
const { theme, setTheme } = useContext(ThemeContext);
|
|
||||||
|
|
||||||
const clickHandler = () => setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
||||||
const mode = theme === 'dark' ? 'Light mode' : 'Dark mode';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
|
||||||
onClick={clickHandler}
|
|
||||||
>
|
|
||||||
{theme === 'dark' ? <LightModeIcon /> : <DarkModeIcon />}
|
|
||||||
{mode}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DarkMode;
|
|
|
@ -3,11 +3,12 @@ import { Fragment, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
|
import GearIcon from '../svg/GearIcon';
|
||||||
|
import Settings from './Settings';
|
||||||
import { Download } from 'lucide-react';
|
import { Download } from 'lucide-react';
|
||||||
import NavLink from './NavLink';
|
import NavLink from './NavLink';
|
||||||
import ExportModel from './ExportConversation/ExportModel';
|
import ExportModel from './ExportConversation/ExportModel';
|
||||||
import ClearConvos from './ClearConvos';
|
import ClearConvos from './ClearConvos';
|
||||||
import DarkMode from './DarkMode';
|
|
||||||
import Logout from './Logout';
|
import Logout from './Logout';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
@ -18,6 +19,7 @@ import store from '~/store';
|
||||||
export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
const [showExports, setShowExports] = useState(false);
|
const [showExports, setShowExports] = useState(false);
|
||||||
const [showClearConvos, setShowClearConvos] = useState(false);
|
const [showClearConvos, setShowClearConvos] = useState(false);
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
|
||||||
const conversation = useRecoilValue(store.conversation) || {};
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
|
@ -47,7 +49,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
<img
|
<img
|
||||||
className="rounded-sm"
|
className="rounded-sm"
|
||||||
src={
|
src={
|
||||||
user?.avatar || `https://avatars.dicebear.com/api/initials/${user?.name}.svg`
|
user?.avatar || `https://api.dicebear.com/6.x/initials/svg?seed=${user?.name || 'User'}&fontFamily=Verdana&fontSize=36`
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
@ -78,7 +80,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
<NavLink
|
<NavLink
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none',
|
'flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none',
|
||||||
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-gray-400'
|
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-white/50'
|
||||||
)}
|
)}
|
||||||
svg={() => <Download size={16} />}
|
svg={() => <Download size={16} />}
|
||||||
text="Export conversation"
|
text="Export conversation"
|
||||||
|
@ -86,9 +88,6 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<div className="my-1.5 h-px bg-white/20" role="none" />
|
<div className="my-1.5 h-px bg-white/20" role="none" />
|
||||||
<Menu.Item as="div">
|
|
||||||
<DarkMode />
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
<NavLink
|
<NavLink
|
||||||
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
|
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
|
||||||
|
@ -97,6 +96,14 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
clickHandler={() => setShowClearConvos(true)}
|
clickHandler={() => setShowClearConvos(true)}
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item as="div">
|
||||||
|
<NavLink
|
||||||
|
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700 rounded-none"
|
||||||
|
svg={() => <GearIcon />}
|
||||||
|
text="Settings"
|
||||||
|
clickHandler={() => setShowSettings(true)}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
<div className="my-1.5 h-px bg-white/20" role="none" />
|
<div className="my-1.5 h-px bg-white/20" role="none" />
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
<Logout />
|
<Logout />
|
||||||
|
@ -108,6 +115,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
</Menu>
|
</Menu>
|
||||||
{showExports && <ExportModel open={showExports} onOpenChange={setShowExports} />}
|
{showExports && <ExportModel open={showExports} onOpenChange={setShowExports} />}
|
||||||
{showClearConvos && <ClearConvos open={showClearConvos} onOpenChange={setShowClearConvos} />}
|
{showClearConvos && <ClearConvos open={showClearConvos} onOpenChange={setShowClearConvos} />}
|
||||||
|
{showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
170
client/src/components/Nav/Settings.jsx
Normal file
170
client/src/components/Nav/Settings.jsx
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import { Dialog } from '../ui/Dialog.tsx';
|
||||||
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
|
import { DialogContent, DialogHeader, DialogTitle } from '../ui/Dialog.tsx';
|
||||||
|
import { useEffect, useState, useContext } from 'react';
|
||||||
|
import { cn } from '~/utils/';
|
||||||
|
import { useClearConversationsMutation } from '~/data-provider';
|
||||||
|
import { ThemeContext } from '~/hooks/ThemeContext';
|
||||||
|
import store from '~/store';
|
||||||
|
import { CheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Settings({ open, onOpenChange }) {
|
||||||
|
const { theme, setTheme } = useContext(ThemeContext);
|
||||||
|
const { newConversation } = store.useConversation();
|
||||||
|
const { refreshConversations } = store.useConversations();
|
||||||
|
const clearConvosMutation = useClearConversationsMutation();
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [confirmClear, setConfirmClear] = useState(false);
|
||||||
|
|
||||||
|
const clearConvos = () => {
|
||||||
|
if (confirmClear) {
|
||||||
|
console.log('Clearing conversations...');
|
||||||
|
clearConvosMutation.mutate();
|
||||||
|
setConfirmClear(false);
|
||||||
|
} else {
|
||||||
|
setConfirmClear(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeTheme = (e) => {
|
||||||
|
setTheme(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if mobile dynamically and update
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
setIsMobile(true);
|
||||||
|
} else {
|
||||||
|
setIsMobile(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clearConvosMutation.isSuccess) {
|
||||||
|
newConversation();
|
||||||
|
refreshConversations();
|
||||||
|
}
|
||||||
|
}, [clearConvosMutation.isSuccess]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If the user clicks in the dialog when confirmClear is true, set it to false
|
||||||
|
const handleClick = (e) => {
|
||||||
|
if (confirmClear) {
|
||||||
|
if (e.target.id === 'clearConvosBtn' || e.target.id === 'clearConvosTxt') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmClear(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handleClick);
|
||||||
|
return () => window.removeEventListener('click', handleClick);
|
||||||
|
}, [confirmClear]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white')}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-gray-800 dark:text-white">Settings</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="px-6">
|
||||||
|
<Tabs.Root
|
||||||
|
defaultValue="general"
|
||||||
|
className="flex flex-col gap-6 md:flex-row"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<Tabs.List
|
||||||
|
aria-label="Settings"
|
||||||
|
role="tablist"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
className={cn(
|
||||||
|
'-ml-[8px] flex min-w-[180px] flex-shrink-0 flex-col',
|
||||||
|
isMobile && 'flex-row rounded-lg bg-gray-100 p-1 dark:bg-gray-800/30'
|
||||||
|
)}
|
||||||
|
style={{ outline: 'none' }}
|
||||||
|
>
|
||||||
|
<Tabs.Trigger
|
||||||
|
className={cn(
|
||||||
|
'radix-state-active:bg-gray-800 radix-state-active:text-white flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm',
|
||||||
|
isMobile &&
|
||||||
|
'dark:radix-state-active:text-white group flex-1 items-center justify-center text-sm dark:text-gray-500'
|
||||||
|
)}
|
||||||
|
value="general"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
strokeWidth="0"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className="group-radix-state-active:fill-white h-4 h-5 w-4 w-5 fill-white dark:fill-gray-500"
|
||||||
|
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>
|
||||||
|
General
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Content value="general" role="tabpanel" className="w-full md:min-h-[300px]">
|
||||||
|
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>Theme</div>
|
||||||
|
<select
|
||||||
|
className="w-24 rounded border border-black/10 bg-transparent text-sm dark:border-white/20 dark:bg-gray-900"
|
||||||
|
onChange={changeTheme}
|
||||||
|
value={theme}
|
||||||
|
>
|
||||||
|
<option value="system">System</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>Clear all chats</div>
|
||||||
|
<button
|
||||||
|
className="btn relative bg-red-600 text-white hover:bg-red-800"
|
||||||
|
type="button"
|
||||||
|
id="clearConvosBtn"
|
||||||
|
onClick={clearConvos}
|
||||||
|
>
|
||||||
|
{confirmClear ? (
|
||||||
|
<div
|
||||||
|
className="flex w-full items-center justify-center gap-2"
|
||||||
|
id="clearConvosTxt"
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" /> Confirm Clear
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex w-full items-center justify-center gap-2"
|
||||||
|
id="clearConvosTxt"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
19
client/src/components/svg/GearIcon.jsx
Normal file
19
client/src/components/svg/GearIcon.jsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export default function GearIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="h-4 w-4"
|
||||||
|
height="1em"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
'animate-in fade-in fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity',
|
'animate-in fade-in fixed inset-0 z-50 bg-gray-500/90 dark:bg-gray-800/90 transition-opacity',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -24,7 +24,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
'data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out fixed inset-0 z-[999] bg-black/50 backdrop-blur-sm transition-all duration-100',
|
'data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out fixed inset-0 z-[999] bg-gray-500/90 dark:bg-gray-800/90 transition-all duration-100',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white p-6 sm:max-w-lg sm:rounded-lg',
|
'animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-[999] grid w-full gap-4 rounded-b-lg bg-white pb-6 sm:rounded-lg md:w-[680px]',
|
||||||
'dark:bg-slate-900',
|
'dark:bg-slate-900',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
|
<DialogPrimitive.Close className="absolute right-4 top-[1.88rem] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
|
||||||
<X className="h-4 w-4 text-black dark:text-white" />
|
<X className="h-4 w-4 text-black dark:text-white" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
@ -59,13 +59,19 @@ const DialogContent = React.forwardRef<
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col space-y-2 border-b border-black/10 p-6 text-center dark:border-white/10 sm:text-left',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
DialogHeader.displayName = 'DialogHeader';
|
DialogHeader.displayName = 'DialogHeader';
|
||||||
|
|
||||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2', className)}
|
className={cn('flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2 px-6', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,28 +10,22 @@ import {
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
const DialogTemplate = forwardRef((props, ref) => {
|
const DialogTemplate = forwardRef((props, ref) => {
|
||||||
const {
|
const { title, description, main, buttons, leftButtons, selection, className } = props;
|
||||||
title,
|
|
||||||
description,
|
|
||||||
main,
|
|
||||||
buttons,
|
|
||||||
leftButtons,
|
|
||||||
selection,
|
|
||||||
className
|
|
||||||
} = props;
|
|
||||||
const { selectHandler, selectClasses, selectText } = selection || {};
|
const { selectHandler, selectClasses, selectText } = selection || {};
|
||||||
|
|
||||||
const defaultSelect =
|
const defaultSelect =
|
||||||
'bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900';
|
'bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900';
|
||||||
return (
|
return (
|
||||||
<DialogContent ref={ref} className={cn('shadow-2xl dark:bg-gray-800', className || '')}>
|
<DialogContent ref={ref} className={cn('shadow-2xl dark:bg-gray-900', className || '')}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
|
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
|
||||||
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
{description && (
|
||||||
{description}
|
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||||
</DialogDescription>
|
{description}
|
||||||
|
</DialogDescription>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{main ? main : null}
|
<div className="px-6">{main ? main : null}</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div>{leftButtons ? leftButtons : null}</div>
|
<div>{leftButtons ? leftButtons : null}</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
@ -26,10 +26,14 @@ export const ThemeProvider = ({ initialTheme, children }) => {
|
||||||
|
|
||||||
const rawSetTheme = (rawTheme) => {
|
const rawSetTheme = (rawTheme) => {
|
||||||
const root = window.document.documentElement;
|
const root = window.document.documentElement;
|
||||||
const isDark = rawTheme === 'dark';
|
let isDark = rawTheme === 'dark';
|
||||||
|
|
||||||
|
if (rawTheme === 'system') {
|
||||||
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
root.classList.remove(isDark ? 'light' : 'dark');
|
root.classList.remove(isDark ? 'light' : 'dark');
|
||||||
root.classList.add(rawTheme);
|
root.classList.add(isDark ? 'dark' : 'light');
|
||||||
|
|
||||||
localStorage.setItem('color-theme', rawTheme);
|
localStorage.setItem('color-theme', rawTheme);
|
||||||
};
|
};
|
||||||
|
|
|
@ -132,6 +132,24 @@
|
||||||
transition: all 1s ease-in-out;
|
transition: all 1s ease-in-out;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
select {
|
||||||
|
--tw-shadow: 0 0 transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%238e8ea0' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
|
||||||
|
background-position: right .5rem center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1.5em 1.5em;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #8e8ea0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.overflow-y-auto {
|
.overflow-y-auto {
|
||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const even =
|
const even =
|
||||||
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800 hover:bg-gray-100/25 hover:text-gray-700 dark:hover:bg-gray-900 dark:hover:text-gray-200';
|
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800 hover:bg-gray-100/25 hover:text-gray-700 dark:hover:bg-gray-900 dark:hover:text-gray-200';
|
||||||
const odd =
|
const odd =
|
||||||
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654] hover:bg-gray-100/40 hover:text-gray-700 dark:hover:bg-[#3b3d49] dark:hover:text-gray-200';
|
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-gray-1000 hover:bg-gray-100/40 hover:text-gray-700 dark:hover:bg-[#3b3d49] dark:hover:text-gray-200';
|
||||||
|
|
||||||
export default function buildTree(messages, groupAll = false) {
|
export default function buildTree(messages, groupAll = false) {
|
||||||
if (messages === null) return null;
|
if (messages === null) return null;
|
||||||
|
|
29
client/src/utils/getError.ts
Normal file
29
client/src/utils/getError.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
const isJson = (str) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getError = (text) => {
|
||||||
|
const errorMessage = text.length > 512 ? text.slice(0, 512) + '...' : text;
|
||||||
|
const match = text.match(/\{[^{}]*\}/);
|
||||||
|
let json = match ? match[0] : '';
|
||||||
|
if (isJson(json)) {
|
||||||
|
json = JSON.parse(json);
|
||||||
|
if (json.code === 'invalid_api_key') {
|
||||||
|
return 'Invalid API key. Please check your API key and try again. You can do this by clicking on the model logo in the left corner of the textbox and selecting "Set Token" for the current selected endpoint. Thank you for your understanding.';
|
||||||
|
} else if (json.type === 'insufficient_quota') {
|
||||||
|
return 'We apologize for any inconvenience caused. The default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the left corner of the textbox and selecting "Set Token" for the current selected endpoint. Thank you for your understanding.';
|
||||||
|
} else {
|
||||||
|
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getError;
|
|
@ -39,7 +39,8 @@ module.exports = {
|
||||||
'600': '#565869',
|
'600': '#565869',
|
||||||
'700': '#40414f', // Replacing .dark .dark:bg-gray-700 and .dark .dark:hover:bg-gray-700:hover
|
'700': '#40414f', // Replacing .dark .dark:bg-gray-700 and .dark .dark:hover:bg-gray-700:hover
|
||||||
'800': '#343541', // Replacing .dark .dark:bg-gray-800, .bg-gray-800, and .dark .dark:hover:bg-gray-800\/90
|
'800': '#343541', // Replacing .dark .dark:bg-gray-800, .bg-gray-800, and .dark .dark:hover:bg-gray-800\/90
|
||||||
'900': '#202123' // Replacing .dark .dark:bg-gray-900, .bg-gray-900, and .dark .dark:hover:bg-gray-900:hover
|
'900': '#202123', // Replacing .dark .dark:bg-gray-900, .bg-gray-900, and .dark .dark:hover:bg-gray-900:hover
|
||||||
|
'1000': '#444654'
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
50: "#f1f9f7",
|
50: "#f1f9f7",
|
||||||
|
|
|
@ -12,7 +12,41 @@ test.describe('Landing suite', () => {
|
||||||
test('Landing title', async () => {
|
test('Landing title', async () => {
|
||||||
const page = await myBrowser.newPage();
|
const page = await myBrowser.newPage();
|
||||||
await page.goto('http://localhost:3080/');
|
await page.goto('http://localhost:3080/');
|
||||||
const pageTitle = await page.textContent('#landing-title')
|
const pageTitle = await page.textContent('#landing-title');
|
||||||
expect(pageTitle.length).toBeGreaterThan(0);
|
expect(pageTitle.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Create Conversation', async () => {
|
||||||
|
const page = await myBrowser.newPage();
|
||||||
|
await page.goto('http://localhost:3080/');
|
||||||
|
|
||||||
|
async function getItems() {
|
||||||
|
const navDiv = await page.waitForSelector('nav > div');
|
||||||
|
if (!navDiv) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await navDiv.$$('a.group');
|
||||||
|
return items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the page to load and the SVG loader to disappear
|
||||||
|
await page.waitForSelector('nav > div');
|
||||||
|
await page.waitForSelector('nav > div > div > svg', { state: 'detached' });
|
||||||
|
|
||||||
|
let beforeAdding = (await getItems()).length;
|
||||||
|
|
||||||
|
const input = await page.locator('form').getByRole('textbox');
|
||||||
|
await input.click();
|
||||||
|
await input.fill('Hi!');
|
||||||
|
|
||||||
|
// Send the message
|
||||||
|
await page.locator('form').getByRole('button').nth(1).click();
|
||||||
|
|
||||||
|
// Wait for the message to be sent
|
||||||
|
await page.waitForTimeout(15000);
|
||||||
|
let afterAdding = (await getItems()).length;
|
||||||
|
|
||||||
|
expect(afterAdding).toBeGreaterThan(beforeAdding);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
59
e2e/specs/nav.spec.js
Normal file
59
e2e/specs/nav.spec.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Navigation suite', () => {
|
||||||
|
let myBrowser;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ browser }) => {
|
||||||
|
myBrowser = await browser.newContext({
|
||||||
|
storageState: 'e2e/auth.json',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigation bar', async () => {
|
||||||
|
const page = await myBrowser.newPage();
|
||||||
|
await page.goto('http://localhost:3080/');
|
||||||
|
|
||||||
|
await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click();
|
||||||
|
const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible();
|
||||||
|
expect(navBar).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Settings modal', async () => {
|
||||||
|
const page = await myBrowser.newPage();
|
||||||
|
await page.goto('http://localhost:3080/');
|
||||||
|
await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click();
|
||||||
|
await page.getByText('Settings').click();
|
||||||
|
|
||||||
|
const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible();
|
||||||
|
expect(modal).toBeTruthy();
|
||||||
|
|
||||||
|
const modalTitle = await page.getByRole('heading', { name: 'Settings' }).textContent();
|
||||||
|
expect(modalTitle.length).toBeGreaterThan(0);
|
||||||
|
expect(modalTitle).toEqual('Settings');
|
||||||
|
|
||||||
|
const modalTabList = await page.getByRole('tablist', { name: 'Settings' }).isVisible();
|
||||||
|
expect(modalTabList).toBeTruthy();
|
||||||
|
|
||||||
|
const generalTabPanel = await page.getByRole('tabpanel', { name: 'General' }).isVisible();
|
||||||
|
expect(generalTabPanel).toBeTruthy();
|
||||||
|
|
||||||
|
const modalClearConvos = await page.getByRole('button', { name: 'Clear' }).isVisible();
|
||||||
|
expect(modalClearConvos).toBeTruthy();
|
||||||
|
|
||||||
|
const modalTheme = await page.getByRole('combobox');
|
||||||
|
expect(modalTheme.isVisible()).toBeTruthy();
|
||||||
|
|
||||||
|
async function changeMode(theme) {
|
||||||
|
// change the value to 'dark' and 'light' and see if the theme changes
|
||||||
|
await modalTheme.selectOption({ label: theme });
|
||||||
|
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()), theme);
|
||||||
|
expect(html).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeMode('Dark');
|
||||||
|
await changeMode('Light');
|
||||||
|
});
|
||||||
|
});
|
24
e2e/specs/popup.spec.js
Normal file
24
e2e/specs/popup.spec.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Endpoints Presets suite', () => {
|
||||||
|
let myBrowser;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ browser }) => {
|
||||||
|
myBrowser = await browser.newContext({
|
||||||
|
storageState: 'e2e/auth.json',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Endpoints Suite', async () => {
|
||||||
|
const page = await myBrowser.newPage();
|
||||||
|
await page.goto('http://localhost:3080/');
|
||||||
|
await page.getByRole('button', { name: 'New Topic' }).click();
|
||||||
|
|
||||||
|
const endpointItem = await page.getByRole('menuitemradio', { name: 'OpenAI' })
|
||||||
|
await endpointItem.click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New Topic' }).click();
|
||||||
|
// Check if the active class is set on the selected endpoint
|
||||||
|
expect(await endpointItem.getAttribute('class')).toContain('active');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue