mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 19:00:13 +01:00
Support localization for Nav components (#688)
* init localization * Update defaul to en * Fix merge issue and import path. * Set default to en * Change jsx to tsx * Update the password max length string. * Remove languageContext as using the recoil instead. * Add localization to component endpoints pages * Revert default to en after testing. * Update LoginForm.tsx * Fix translation. * Make lint happy * Merge (#1) * Create deploy.yml * Add localization support for endpoint pages components (#667) * init localization * Update defaul to en * Fix merge issue and import path. * Set default to en * Change jsx to tsx * Update the password max length string. * Remove languageContext as using the recoil instead. * Add localization to component endpoints pages * Revert default to en after testing. * Update LoginForm.tsx * Fix translation. * Make lint happy * Add a restart to melisearch in docker-compose.yml (#684) * Oauth fixes for Cognito (#686) * Add a restart to melisearch in docker-compose.yml * Oauth fixes for Cognito * Use the username or email for full name from oath if not provided --------- Co-authored-by: Donavan <snark@hey.com> * Italian localization support for endpoint (#687) --------- Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com> Co-authored-by: Donavan Stanley <donavan.stanley@gmail.com> Co-authored-by: Donavan <snark@hey.com> Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com> * Translate Nav pages * Fix npm test --------- Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com> Co-authored-by: Donavan Stanley <donavan.stanley@gmail.com> Co-authored-by: Donavan <snark@hey.com> Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
This commit is contained in:
parent
3b865fbc59
commit
1e49b7ecb1
22 changed files with 346 additions and 130 deletions
|
|
@ -55,13 +55,15 @@ function Registration() {
|
|||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Create your account</h1>
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">
|
||||
{localize(lang, 'com_auth_create_account')}
|
||||
</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
There was an error attempting to register your account. Please try again. {errorMessage}
|
||||
{localize(lang, 'com_auth_error_create')} {errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'Edit Preset'} - ${preset?.title}`}
|
||||
title={`${title || localize(lang, 'com_endpoint_edit_preset')} - ${preset?.title}`}
|
||||
className="h-[675px] max-w-full sm:max-w-4xl "
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[475px]">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) =
|
|||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'View Options'} - ${endpointName}`}
|
||||
title={`${title || localize(lang, 'com_endpoint_view_options')} - ${endpointName}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function Settings(props) {
|
|||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
title="Agent Model (Recommended: GPT-3.5)"
|
||||
title={localize(lang, 'com_endpoint_agent_model')}
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
|
|
@ -102,7 +102,10 @@ function Settings(props) {
|
|||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_temperature')} <small className="opacity-40">({localize(lang, 'com_endpoint_default')}: 0)</small>
|
||||
{localize(lang, 'com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default')}: 0)
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { HoverCardPortal, HoverCardContent } from '~/components';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
const types = {
|
||||
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
|
||||
func: 'Enable use of Plugins as OpenAI Functions',
|
||||
skip: 'Enable skipping the completion step, which reviews the final answer and generated steps',
|
||||
max: 'The max tokens to generate. The total length of input tokens and generated tokens is limited by the model\'s context length.',
|
||||
topp: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
|
||||
freq: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
|
||||
pres: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
|
||||
temp: 'com_endpoint_openai_temp',
|
||||
func: 'com_endpoint_func_hover',
|
||||
skip: 'com_endpoint_skip_hover',
|
||||
max: 'com_endpoint_openai_max',
|
||||
topp: 'com_endpoint_openai_topp',
|
||||
freq: 'com_endpoint_openai_freq',
|
||||
pres: 'com_endpoint_openai_pres',
|
||||
};
|
||||
|
||||
function OptionHover({ type, side }) {
|
||||
// const options = {};
|
||||
// if (type === 'pres') {
|
||||
// options.sideOffset = 45;
|
||||
// }
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
|
|
@ -24,7 +24,7 @@ function OptionHover({ type, side }) {
|
|||
// {...options}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{localize(lang, types[type])}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function Settings(props) {
|
|||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
title="Completion Model (Recommended: GPT-4)"
|
||||
title={localize(lang, 'com_endpoint_completion_model')}
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
|
|
@ -71,7 +71,10 @@ function Settings(props) {
|
|||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_custom_name')}{' '}
|
||||
<small className="opacity-40">({localize(lang, 'com_endpoint_default_empty')} | {localize(lang, 'com_endpoint_disabled_with_tools')})</small>
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_empty')} |{' '}
|
||||
{localize(lang, 'com_endpoint_disabled_with_tools')})
|
||||
</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
|
|
@ -81,7 +84,7 @@ function Settings(props) {
|
|||
placeholder={
|
||||
toolsSelected
|
||||
? localize(lang, 'com_endpoint_disabled_with_tools_placeholder')
|
||||
: localize(lang, 'com_endpoint_plug_set_custom_name_for_gpt_placeholder')
|
||||
: localize(lang, 'com_endpoint_openai_custom_name_placeholder')
|
||||
}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
|
|
@ -92,7 +95,10 @@ function Settings(props) {
|
|||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_prompt_prefix')}{' '}
|
||||
<small className="opacity-40">({localize(lang, 'com_endpoint_default_empty')} | {localize(lang, 'com_endpoint_disabled_with_tools')})</small>
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_empty')} |{' '}
|
||||
{localize(lang, 'com_endpoint_disabled_with_tools')})
|
||||
</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
|
|
@ -102,7 +108,10 @@ function Settings(props) {
|
|||
placeholder={
|
||||
toolsSelected
|
||||
? localize(lang, 'com_endpoint_disabled_with_tools_placeholder')
|
||||
: localize(lang, 'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder')
|
||||
: localize(
|
||||
lang,
|
||||
'com_endpoint_plug_set_custom_instructions_for_gpt_placeholder',
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
|
|
@ -117,7 +126,10 @@ function Settings(props) {
|
|||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
Temperature <small className="opacity-40">{'(default: 0.8)'}</small>
|
||||
{localize(lang, 'com_endpoint_temperature')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_with_num', 0.8)})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
|
|
@ -154,7 +166,10 @@ function Settings(props) {
|
|||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_top_p')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 1)})</small>
|
||||
{localize(lang, 'com_endpoint_top_p')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_with_num', 1)})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
|
|
@ -192,7 +207,10 @@ function Settings(props) {
|
|||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_frequency_penalty')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 0)})</small>
|
||||
{localize(lang, 'com_endpoint_frequency_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_with_num', 0)})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
|
|
@ -230,7 +248,10 @@ function Settings(props) {
|
|||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||
{localize(lang, 'com_endpoint_presence_penalty')} <small className="opacity-40">({localize(lang, 'com_endpoint_default_with_num', 0)})</small>
|
||||
{localize(lang, 'com_endpoint_presence_penalty')}{' '}
|
||||
<small className="opacity-40">
|
||||
({localize(lang, 'com_endpoint_default_with_num', 0)})
|
||||
</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(preset?.title || 'My Preset');
|
||||
setTitle(preset?.title || localize(lang, 'com_endpoint_my_preset'));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title="Save As Preset"
|
||||
title={localize(lang, 'com_endpoint_save_as_preset')}
|
||||
main={
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import { Dialog, DialogTemplate } from '../ui/';
|
|||
import { ClearChatsButton } from './SettingsTabs/';
|
||||
import { useClearConversationsMutation } from '@librechat/data-provider';
|
||||
import store from '~/store';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
const ClearConvos = ({ open, onOpenChange }) => {
|
||||
const { newConversation } = store.useConversation();
|
||||
const { refreshConversations } = store.useConversations();
|
||||
const clearConvosMutation = useClearConversationsMutation();
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const clearConvos = useCallback(() => {
|
||||
if (confirmClear) {
|
||||
|
|
@ -30,8 +33,8 @@ const ClearConvos = ({ open, onOpenChange }) => {
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title="Clear conversations"
|
||||
description="Are you sure you want to clear all conversations? This is irreversible."
|
||||
title={localize(lang, 'com_nav_clear_conversation')}
|
||||
description={localize(lang, 'com_nav_clear_conversation_confirm_message')}
|
||||
leftButtons={
|
||||
<ClearChatsButton showText={false} confirmClear={confirmClear} onClick={clearConvos} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useScreenshot } from '~/utils/screenshotContext';
|
|||
|
||||
import store from '~/store';
|
||||
import cleanupPreset from '~/utils/cleanupPreset.js';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export default function ExportModel({ open, onOpenChange }) {
|
||||
const { captureScreenshot } = useScreenshot();
|
||||
|
|
@ -32,6 +33,8 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
const messagesTree = useRecoilValue(store.messagesTree) || [];
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const getSiblingIdx = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (messageId) =>
|
||||
|
|
@ -349,13 +352,13 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label htmlFor="filename" className="text-left text-sm font-medium">
|
||||
Filename
|
||||
{localize(lang, 'com_nav_export_filename')}
|
||||
</Label>
|
||||
<Input
|
||||
id="filename"
|
||||
value={filename}
|
||||
onChange={(e) => setFileName(filenamify(e.target.value || ''))}
|
||||
placeholder="Set the filename"
|
||||
placeholder={localize(lang, 'com_nav_export_filename_placeholder')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0',
|
||||
|
|
@ -364,7 +367,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
</div>
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label htmlFor="type" className="text-left text-sm font-medium">
|
||||
Type
|
||||
{localize(lang, 'com_nav_export_type')}
|
||||
</Label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
|
|
@ -383,7 +386,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="includeOptions" className="text-left text-sm font-medium">
|
||||
Include endpoint options
|
||||
{localize(lang, 'com_nav_export_include_endpoint_options')}
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
<Checkbox
|
||||
|
|
@ -397,14 +400,16 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
htmlFor="includeOptions"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
{exportOptionsSupport ? 'Enabled' : 'Not Supported'}
|
||||
{exportOptionsSupport
|
||||
? localize(lang, 'com_nav_enabled')
|
||||
: localize(lang, 'com_nav_not_supported')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="exportBranches" className="text-left text-sm font-medium">
|
||||
Export all message branches
|
||||
{localize(lang, 'com_nav_export_all_message_branches')}
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
<Checkbox
|
||||
|
|
@ -418,14 +423,16 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
htmlFor="exportBranches"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
{exportBranchesSupport ? 'Enabled' : 'Not Supported'}
|
||||
{exportBranchesSupport
|
||||
? localize(lang, 'com_nav_enabled')
|
||||
: localize(lang, 'com_nav_not_supported')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{type === 'json' ? (
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="recursive" className="text-left text-sm font-medium">
|
||||
Recursive or sequential?
|
||||
{localize(lang, 'com_nav_export_recursive_or_sequential')}
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
<Checkbox
|
||||
|
|
@ -438,7 +445,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
htmlFor="recursive"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
Recursive
|
||||
{localize(lang, 'com_nav_export_recursive')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -452,7 +459,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
|||
onClick={exportConversation}
|
||||
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Export
|
||||
{localize(lang, 'com_endpoint_export')}
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import { cn } from '~/utils/';
|
|||
import ExportModel from './ExportModel';
|
||||
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
const ExportConversation = forwardRef(() => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const conversation = useRecoilValue(store.conversation) || {};
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ const ExportConversation = forwardRef(() => {
|
|||
onClick={clickHandler}
|
||||
>
|
||||
<Download size={16} />
|
||||
Export conversation
|
||||
{localize(lang, 'com_nav_export_conversation')}
|
||||
</button>
|
||||
|
||||
<ExportModel open={open} onOpenChange={setOpen} />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { forwardRef } from 'react';
|
||||
import LogOutIcon from '../svg/LogOutIcon';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
const Logout = forwardRef(() => {
|
||||
const { user, logout } = useAuthContext();
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
|
|
@ -16,8 +20,8 @@ const Logout = forwardRef(() => {
|
|||
onClick={handleLogout}
|
||||
>
|
||||
<LogOutIcon />
|
||||
{user?.username || 'USER'}
|
||||
<small>Log out</small>
|
||||
{user?.username || localize(lang, 'com_nav_user')}
|
||||
<small>{localize(lang, 'com_nav_log_out')}</small>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import React from 'react';
|
|||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export default function MobileNav({ setNavVisible }) {
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
const { newConversation } = store.useConversation();
|
||||
const { title = 'New Chat' } = conversation || {};
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
|
||||
|
|
@ -15,7 +17,7 @@ export default function MobileNav({ setNavVisible }) {
|
|||
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-0 focus:ring-inset focus:ring-white dark:hover:text-white"
|
||||
onClick={() => setNavVisible((prev) => !prev)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<span className="sr-only">{localize(lang, 'com_nav_open_sidebar')}</span>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
|
|
@ -33,7 +35,9 @@ export default function MobileNav({ setNavVisible }) {
|
|||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="flex-1 text-center text-base font-normal">{title || 'New Chat'}</h1>
|
||||
<h1 className="flex-1 text-center text-base font-normal">
|
||||
{title || localize(lang, 'com_ui_new_chat')}
|
||||
</h1>
|
||||
<button type="button" className="px-3" onClick={() => newConversation()}>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ import { cn } from '~/utils/';
|
|||
|
||||
import store from '~/store';
|
||||
import { LinkIcon, DotsIcon, GearIcon, TrashIcon } from '~/components';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||
const [showExports, setShowExports] = useState(false);
|
||||
const [showClearConvos, setShowClearConvos] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const { user } = useAuthContext();
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const conversation = useRecoilValue(store.conversation) || {};
|
||||
|
||||
|
|
@ -59,7 +61,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-white">
|
||||
{user?.name || 'USER'}
|
||||
{user?.name || localize(lang, 'com_nav_user')}
|
||||
</div>
|
||||
<DotsIcon />
|
||||
</Menu.Button>
|
||||
|
|
@ -86,7 +88,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
|||
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-white/50',
|
||||
)}
|
||||
svg={() => <Download size={16} />}
|
||||
text="Export conversation"
|
||||
text={localize(lang, 'com_nav_export_conversation')}
|
||||
clickHandler={clickHandler}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
@ -95,7 +97,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
|||
<NavLink
|
||||
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
||||
svg={() => <TrashIcon />}
|
||||
text="Clear conversations"
|
||||
text={localize(lang, 'com_nav_clear_conversation')}
|
||||
clickHandler={() => setShowClearConvos(true)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
@ -103,7 +105,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
|||
<NavLink
|
||||
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
||||
svg={() => <LinkIcon />}
|
||||
text="Help & FAQ"
|
||||
text={localize(lang, 'com_nav_help_faq')}
|
||||
clickHandler={() => window.open('https://docs.librechat.ai/', '_blank')}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
@ -111,7 +113,7 @@ export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
|||
<NavLink
|
||||
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
||||
svg={() => <GearIcon />}
|
||||
text="Settings"
|
||||
text={localize(lang, 'com_nav_settings')}
|
||||
clickHandler={() => setShowSettings(true)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { forwardRef, useState, useEffect } from 'react';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
const SearchBar = forwardRef((props, ref) => {
|
||||
const { clearSearch } = props;
|
||||
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
|
||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
const { value } = e.target;
|
||||
|
|
@ -44,7 +46,7 @@ const SearchBar = forwardRef((props, ref) => {
|
|||
onKeyDown={(e) => {
|
||||
e.code === 'Space' ? e.stopPropagation() : null;
|
||||
}}
|
||||
placeholder="Search messages"
|
||||
placeholder={localize(lang, 'com_nav_search_placeholder')}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
<X
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import { CogIcon } from '~/components/svg';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '~/utils/';
|
||||
import { useClearConversationsMutation } from '@librechat/data-provider';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export default function Settings({ open, onOpenChange }) {
|
||||
const { newConversation } = store.useConversation();
|
||||
|
|
@ -13,6 +15,7 @@ export default function Settings({ open, onOpenChange }) {
|
|||
const clearConvosMutation = useClearConversationsMutation();
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
// check if mobile dynamically and update
|
||||
useEffect(() => {
|
||||
|
|
@ -56,7 +59,7 @@ export default function Settings({ open, onOpenChange }) {
|
|||
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white')}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
Settings
|
||||
{localize(lang, 'com_nav_settings')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="px-6">
|
||||
|
|
@ -84,7 +87,7 @@ export default function Settings({ open, onOpenChange }) {
|
|||
value="general"
|
||||
>
|
||||
<CogIcon />
|
||||
General
|
||||
{localize(lang, 'com_nav_setting_general')}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<General />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { ClearChatsButton } from './General';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
describe('ClearChatsButton', () => {
|
||||
let mockOnClick;
|
||||
|
|
@ -12,7 +13,9 @@ describe('ClearChatsButton', () => {
|
|||
|
||||
it('renders correctly', () => {
|
||||
const { getByText } = render(
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />,
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Clear all chats')).toBeInTheDocument();
|
||||
|
|
@ -21,7 +24,9 @@ describe('ClearChatsButton', () => {
|
|||
|
||||
it('renders confirm clear when confirmClear is true', () => {
|
||||
const { getByText } = render(
|
||||
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />,
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Confirm Clear')).toBeInTheDocument();
|
||||
|
|
@ -29,7 +34,9 @@ describe('ClearChatsButton', () => {
|
|||
|
||||
it('calls onClick when the button is clicked', () => {
|
||||
const { getByText } = render(
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />,
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Clear'));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { CheckIcon } from 'lucide-react';
|
|||
import { ThemeContext } from '~/hooks/ThemeContext';
|
||||
import React, { useState, useContext, useCallback } from 'react';
|
||||
import { useClearConversationsMutation } from '@librechat/data-provider';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export const ThemeSelector = ({
|
||||
theme,
|
||||
|
|
@ -10,20 +13,24 @@ export const ThemeSelector = ({
|
|||
}: {
|
||||
theme: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<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={(e) => onChange(e.target.value)}
|
||||
value={theme}
|
||||
>
|
||||
<option value="system">System</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize(lang, 'com_nav_theme')}</div>
|
||||
<select
|
||||
className="w-24 rounded border border-black/10 bg-transparent text-sm dark:border-white/20 dark:bg-gray-900"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={theme}
|
||||
>
|
||||
<option value="system">{localize(lang, 'com_nav_theme_system')}</option>
|
||||
<option value="dark">{localize(lang, 'com_nav_theme_dark')}</option>
|
||||
<option value="light">{localize(lang, 'com_nav_theme_light')}</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClearChatsButton = ({
|
||||
confirmClear,
|
||||
|
|
@ -33,27 +40,31 @@ export const ClearChatsButton = ({
|
|||
confirmClear: boolean;
|
||||
showText: boolean;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<div className="flex items-center justify-between">
|
||||
{showText && <div>Clear all chats</div>}
|
||||
<button
|
||||
className="btn relative bg-red-600 text-white hover:bg-red-800"
|
||||
type="button"
|
||||
id="clearConvosBtn"
|
||||
onClick={onClick}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}) => {
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{showText && <div>{localize(lang, 'com_nav_clear_all_chats')}</div>}
|
||||
<button
|
||||
className="btn relative bg-red-600 text-white hover:bg-red-800"
|
||||
type="button"
|
||||
id="clearConvosBtn"
|
||||
onClick={onClick}
|
||||
>
|
||||
{confirmClear ? (
|
||||
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
|
||||
<CheckIcon className="h-5 w-5" /> {localize(lang, 'com_nav_confirm_clear')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-center gap-2" id="clearConvosTxt">
|
||||
{localize(lang, 'com_nav_clear')}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function General() {
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { ThemeSelector } from './General';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
describe('ThemeSelector', () => {
|
||||
let mockOnChange;
|
||||
|
|
@ -12,7 +13,9 @@ describe('ThemeSelector', () => {
|
|||
|
||||
it('renders correctly', () => {
|
||||
const { getByText, getByDisplayValue } = render(
|
||||
<ThemeSelector theme="system" onChange={mockOnChange} />,
|
||||
<RecoilRoot>
|
||||
<ThemeSelector theme="system" onChange={mockOnChange} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Theme')).toBeInTheDocument();
|
||||
|
|
@ -20,7 +23,11 @@ describe('ThemeSelector', () => {
|
|||
});
|
||||
|
||||
it('calls onChange when the select value changes', () => {
|
||||
const { getByDisplayValue } = render(<ThemeSelector theme="system" onChange={mockOnChange} />);
|
||||
const { getByDisplayValue } = render(
|
||||
<RecoilRoot>
|
||||
<ThemeSelector theme="system" onChange={mockOnChange} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
fireEvent.change(getByDisplayValue('System'), { target: { value: 'dark' } });
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import { Panel, Spinner } from '~/components';
|
|||
import { cn } from '~/utils/';
|
||||
import { useAuthContext, useDebounce } from '~/hooks';
|
||||
import store from '~/store';
|
||||
import { localize } from '~/localization/Translation';
|
||||
|
||||
export default function Nav({ navVisible, setNavVisible }) {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const containerRef = useRef(null);
|
||||
const scrollPositionRef = useRef(null);
|
||||
const lang = useRecoilValue(store.lang);
|
||||
|
||||
const [conversations, setConversations] = useState([]);
|
||||
// current page
|
||||
|
|
@ -151,7 +153,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
|||
)}
|
||||
onClick={toggleNavVisible}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<span className="sr-only">{localize(lang, 'com_nav_close_sidebar')}</span>
|
||||
<Panel open={false} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -195,7 +197,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
|||
onClick={toggleNavVisible}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<span className="sr-only">{localize(lang, 'com_nav_open_sidebar')}</span>
|
||||
<Panel open={true} />
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue