🪟 fix+feat: General UI Enhancements (#2619)

* feat: Minor design changes to mimic OpenAI's latest login page

* fix: Optimize ThemeSelector for mobile

* fix: Use a svg for the logo for transperency in dark mode

* feat: Update styles for Registration

* feat: Update error colors for login & registration

* fix: remove medium font

* wip: Dropdown menu

* feat: Update dropdown to match ChatGPT

* feat: Improve rounding and padding

* feat: Add UI Updates to RequestPasswordReset, PasswordRest and increase width for theme dropdown

* fix: Modify the My Files modal's width to not touch the screen

* feat: fix scrolling for dropdown, and make border width lighter

* feat: Match popup menu design to OpenAI (p1/2)

* fix+feat: fix dark mode, add user email, add lighter borders

* fix: Add border color on focus of chat input.

* feat: Move Export Conversation to a seperate button (testing)

* fix: Properly center Login, Registration, Reset Password Flow

* fix: Border colors on dark mode for settings modal

* feat: Improve wording for settings menu

* fix: Optimize settings modal for mobile and fix height for modal

* feat: Optimize for desktop

* fix: make TooltipTrigger asChild of button, improve settings mobile responsiveness

* feat: Handle dropdowns properly
TODO: Make height dynamic, fix dark mode colors

* fix: input styles
fix: make endpoint icon smaller

* feat: Update UI to Match ChatGPT Style

- Updated the dropdown styles to match the aesthetic of ChatGPT.
- Decreased spacing within the conversation area for cleanliness.
- Replaced the current archive icon with the ChatGPT's icon.

* fix: fix colors for EditMenuButton & ArchiveButton for dark mode and light mode

* fix: ui fixes

* fix: Fix Conversation UI Bugs

* fix: transparency of HoverToggle to make buttons not visible

* fix: dark mode HoverToggle & compress menu item spacing

* fix: responsiveness of export icon

* fix: first mentionitem is set to always be highlighted

* fix: improve hover state to text instead of bg

* feat: Update icons to ChatGPT Style

* fix: dark mode hover for PanelFileCell

* fix: change navlinks z-index to 100

* fix: hover states for DataTable

* feat: Move ExportButton to seperate component

* chore: remove unused imports
This commit is contained in:
Anirudh 2024-05-10 03:16:16 +05:30 committed by GitHub
parent d73ea8e1f2
commit 8f20fb28e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 716 additions and 469 deletions

2
.gitignore vendored
View file

@ -69,6 +69,8 @@ src/style - official.css
/playwright/.cache/ /playwright/.cache/
.DS_Store .DS_Store
*.code-workspace *.code-workspace
.idx
monospace.json
.idea .idea
*.iml *.iml
*.pem *.pem

View file

@ -0,0 +1,32 @@
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient22708">
<stop stop-color="#21facf" offset="0"/>
<stop stop-color="#0970ef" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient6949" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientTransform="translate(-5.754,-56.594)" gradientUnits="userSpaceOnUse">
<stop stop-color="#72004e" offset="0"/>
<stop stop-color="#0015b1" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient22718" x1="56.735" x2="155.2" y1="246.96" y2="58.575" gradientUnits="userSpaceOnUse">
<stop stop-color="#4f00da" offset="0"/>
<stop stop-color="#e5311b" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient23463" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
<linearGradient id="linearGradient903" x1="54.478" x2="192.1" y1="247.56" y2="9.8095" gradientTransform="matrix(.87923 0 0 .87923 -9.551 48.787)" gradientUnits="userSpaceOnUse">
<stop stop-color="#dc180d" offset="0"/>
<stop stop-color="#f96e20" offset=".5"/>
<stop stop-color="#f4ce41" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient918" x1="39.468" x2="154.99" y1="204.22" y2="124.47" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
</defs>
<g transform="matrix(2.473 0 0 2.473 -4.8978 -4.8812)">
<path transform="translate(-5.5496,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" display="none" fill="url(#linearGradient903)"/>
<path transform="translate(-5.5498,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" fill="url(#linearGradient918)"/>
<g transform="translate(0 2.0218e-5)">
<path transform="translate(-5.7543,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient22718)"/>
<path transform="translate(-5.754,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient23463)"/>
<path d="m105.5 24.43c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" fill="url(#linearGradient6949)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -96,7 +96,7 @@ function Login() {
const privacyPolicyRender = privacyPolicy?.externalUrl && ( const privacyPolicyRender = privacyPolicy?.externalUrl && (
<a <a
className="text-xs font-medium text-green-500" className="text-sm text-green-500"
href={privacyPolicy.externalUrl} href={privacyPolicy.externalUrl}
target={privacyPolicy.openNewTab ? '_blank' : undefined} target={privacyPolicy.openNewTab ? '_blank' : undefined}
rel="noreferrer" rel="noreferrer"
@ -107,7 +107,7 @@ function Login() {
const termsOfServiceRender = termsOfService?.externalUrl && ( const termsOfServiceRender = termsOfService?.externalUrl && (
<a <a
className="text-xs font-medium text-green-500" className="text-sm text-green-500"
href={termsOfService.externalUrl} href={termsOfService.externalUrl}
target={termsOfService.openNewTab ? '_blank' : undefined} target={termsOfService.openNewTab ? '_blank' : undefined}
rel="noreferrer" rel="noreferrer"
@ -117,57 +117,62 @@ function Login() {
); );
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<div className="absolute bottom-0 left-0 m-4"> <div className="mt-12 h-24 w-full bg-cover">
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
</div>
<div className="absolute bottom-0 left-0 md:m-4">
<ThemeSelector /> <ThemeSelector />
</div> </div>
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg"> <div className="flex flex-grow items-center justify-center">
<h1 <div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white" <h1
style={{ userSelect: 'none' }} className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
> style={{ userSelect: 'none' }}
{localize('com_auth_welcome_back')}
</h1>
{error && (
<div
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
role="alert"
> >
{localize(getLoginError(error))} {localize('com_auth_welcome_back')}
</div> </h1>
)} {error && (
{startupConfig.emailLoginEnabled && <LoginForm onSubmit={login} />} <div
{startupConfig.registrationEnabled && ( className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white"> role="alert"
{' '} >
{localize('com_auth_no_account')}{' '} {localize(getLoginError(error))}
<a href="/register" className="p-1 font-medium text-green-500">
{localize('com_auth_sign_up')}
</a>
</p>
)}
{startupConfig.socialLoginEnabled && (
<>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
Or
</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div> </div>
</> )}
)} {startupConfig.emailLoginEnabled && <LoginForm onSubmit={login} />}
{startupConfig.registrationEnabled && (
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{' '}
{localize('com_auth_no_account')}{' '}
<a href="/register" className="p-1 text-green-500">
{localize('com_auth_sign_up')}
</a>
</p>
)}
{startupConfig.socialLoginEnabled && (
<>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t border-gray-300 uppercase dark:border-gray-600">
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
Or
</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div>
</>
)}
</div>
</div> </div>
<div className="flex justify-center gap-4 align-middle"> <div className="align-end m-4 flex justify-center gap-2">
{privacyPolicyRender} {privacyPolicyRender}
{privacyPolicyRender && termsOfServiceRender && ( {privacyPolicyRender && termsOfServiceRender && (
<div className="border-r-[1px] border-gray-300" /> <div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
)} )}
{termsOfServiceRender} {termsOfServiceRender}
</div> </div>

View file

@ -18,7 +18,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
const renderError = (fieldName: string) => { const renderError = (fieldName: string) => {
const errorMessage = errors[fieldName]?.message; const errorMessage = errors[fieldName]?.message;
return errorMessage ? ( return errorMessage ? (
<span role="alert" className="mt-1 text-sm text-black dark:text-white"> <span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{String(errorMessage)} {String(errorMessage)}
</span> </span>
) : null; ) : null;
@ -44,12 +44,12 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
pattern: { value: /\S+@\S+\.\S+/, message: localize('com_auth_email_pattern') }, pattern: { value: /\S+@\S+\.\S+/, message: localize('com_auth_email_pattern') },
})} })}
aria-invalid={!!errors.email} aria-invalid={!!errors.email}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500" className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="email" htmlFor="email"
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200" className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
> >
{localize('com_auth_email_address')} {localize('com_auth_email_address')}
</label> </label>
@ -69,19 +69,19 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit }) => {
maxLength: { value: 128, message: localize('com_auth_password_max_length') }, maxLength: { value: 128, message: localize('com_auth_password_max_length') },
})} })}
aria-invalid={!!errors.password} aria-invalid={!!errors.password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500" className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="password" htmlFor="password"
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200" className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
> >
{localize('com_auth_password')} {localize('com_auth_password')}
</label> </label>
</div> </div>
{renderError('password')} {renderError('password')}
</div> </div>
<a href="/forgot-password" className="text-sm font-medium text-green-500"> <a href="/forgot-password" className="text-sm text-green-500">
{localize('com_auth_password_forgot')} {localize('com_auth_password_forgot')}
</a> </a>
<div className="mt-6"> <div className="mt-6">

View file

@ -64,19 +64,19 @@ const Registration: React.FC = () => {
validation, validation,
)} )}
aria-invalid={!!errors[id]} aria-invalid={!!errors[id]}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500" className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" " placeholder=" "
data-testid={id} data-testid={id}
></input> ></input>
<label <label
htmlFor={id} htmlFor={id}
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200" className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
> >
{localize(label)} {localize(label)}
</label> </label>
</div> </div>
{errors[id] && ( {errors[id] && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white"> <span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{String(errors[id]?.message) ?? ''} {String(errors[id]?.message) ?? ''}
</span> </span>
)} )}
@ -147,117 +147,154 @@ const Registration: React.FC = () => {
), ),
}; };
const privacyPolicy = startupConfig.interface?.privacyPolicy;
const termsOfService = startupConfig.interface?.termsOfService;
const privacyPolicyRender = privacyPolicy?.externalUrl && (
<a
className="text-sm text-green-500"
href={privacyPolicy.externalUrl}
target={privacyPolicy.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_privacy_policy')}
</a>
);
const termsOfServiceRender = termsOfService?.externalUrl && (
<a
className="text-sm text-green-500"
href={termsOfService.externalUrl}
target={termsOfService.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_terms_of_service')}
</a>
);
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<div className="absolute bottom-0 left-0 m-4"> <div className="mt-12 h-24 w-full bg-cover">
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
</div>
<div className="absolute bottom-0 left-0 md:m-4">
<ThemeSelector /> <ThemeSelector />
</div> </div>
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg"> <div className="flex flex-grow items-center justify-center">
<h1 <div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white" <h1
style={{ userSelect: 'none' }} className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
> style={{ userSelect: 'none' }}
{localize('com_auth_create_account')}
</h1>
{error && (
<div
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
role="alert"
data-testid="registration-error"
> >
{localize('com_auth_error_create')} {errorMessage} {localize('com_auth_create_account')}
</div> </h1>
)} {error && (
<form <div
className="mt-6" className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
aria-label="Registration form" role="alert"
method="POST" data-testid="registration-error"
onSubmit={handleSubmit(onRegisterUserFormSubmit)}
>
{renderInput('name', 'com_auth_full_name', 'text', {
required: localize('com_auth_name_required'),
minLength: {
value: 3,
message: localize('com_auth_name_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_name_max_length'),
},
})}
{renderInput('username', 'com_auth_username', 'text', {
minLength: {
value: 2,
message: localize('com_auth_username_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_username_max_length'),
},
})}
{renderInput('email', 'com_auth_email', 'email', {
required: localize('com_auth_email_required'),
minLength: {
value: 1,
message: localize('com_auth_email_min_length'),
},
maxLength: {
value: 120,
message: localize('com_auth_email_max_length'),
},
pattern: {
value: /\S+@\S+\.\S+/,
message: localize('com_auth_email_pattern'),
},
})}
{renderInput('password', 'com_auth_password', 'password', {
required: localize('com_auth_password_required'),
minLength: {
value: 8,
message: localize('com_auth_password_min_length'),
},
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
},
})}
{renderInput('confirm_password', 'com_auth_password_confirm', 'password', {
validate: (value) => value === password || localize('com_auth_password_not_match'),
})}
<div className="mt-6">
<button
disabled={Object.keys(errors).length > 0}
type="submit"
aria-label="Submit registration"
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
> >
{localize('com_auth_continue')} {localize('com_auth_error_create')} {errorMessage}
</button>
</div>
</form>
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{localize('com_auth_already_have_account')}{' '}
<a href="/login" aria-label="Login" className="p-1 font-medium text-green-500">
{localize('com_auth_login')}
</a>
</p>
{startupConfig.socialLoginEnabled && (
<>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
Or
</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div> </div>
</> )}
<form
className="mt-6"
aria-label="Registration form"
method="POST"
onSubmit={handleSubmit(onRegisterUserFormSubmit)}
>
{renderInput('name', 'com_auth_full_name', 'text', {
required: localize('com_auth_name_required'),
minLength: {
value: 3,
message: localize('com_auth_name_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_name_max_length'),
},
})}
{renderInput('username', 'com_auth_username', 'text', {
minLength: {
value: 2,
message: localize('com_auth_username_min_length'),
},
maxLength: {
value: 80,
message: localize('com_auth_username_max_length'),
},
})}
{renderInput('email', 'com_auth_email', 'email', {
required: localize('com_auth_email_required'),
minLength: {
value: 1,
message: localize('com_auth_email_min_length'),
},
maxLength: {
value: 120,
message: localize('com_auth_email_max_length'),
},
pattern: {
value: /\S+@\S+\.\S+/,
message: localize('com_auth_email_pattern'),
},
})}
{renderInput('password', 'com_auth_password', 'password', {
required: localize('com_auth_password_required'),
minLength: {
value: 8,
message: localize('com_auth_password_min_length'),
},
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
},
})}
{renderInput('confirm_password', 'com_auth_password_confirm', 'password', {
validate: (value) => value === password || localize('com_auth_password_not_match'),
})}
<div className="mt-6">
<button
disabled={Object.keys(errors).length > 0}
type="submit"
aria-label="Submit registration"
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500"
>
{localize('com_auth_continue')}
</button>
</div>
</form>
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{localize('com_auth_already_have_account')}{' '}
<a href="/login" aria-label="Login" className="p-1 text-green-500">
{localize('com_auth_login')}
</a>
</p>
{startupConfig.socialLoginEnabled && (
<>
{startupConfig.emailLoginEnabled && (
<>
<div className="relative mt-6 flex w-full items-center justify-center border border-t border-gray-300 uppercase dark:border-gray-600">
<div className="absolute bg-white px-3 text-xs text-black dark:bg-gray-900 dark:text-white">
Or
</div>
</div>
<div className="mt-8" />
</>
)}
<div className="mt-2">
{socialLogins.map((provider) => providerComponents[provider] || null)}
</div>
</>
)}
</div>
</div>
<div className="align-end m-4 flex justify-center gap-2">
{privacyPolicyRender}
{privacyPolicyRender && termsOfServiceRender && (
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
)} )}
{termsOfServiceRender}
</div> </div>
</div> </div>
); );

View file

@ -49,7 +49,7 @@ function RequestPasswordReset() {
setBodyText( setBodyText(
<span> <span>
{localize('com_auth_click')}{' '} {localize('com_auth_click')}{' '}
<a className="font-medium text-green-500 hover:underline" href={resetLink}> <a className="text-green-500 hover:underline" href={resetLink}>
{localize('com_auth_here')} {localize('com_auth_here')}
</a>{' '} </a>{' '}
{localize('com_auth_to_reset_your_password')} {localize('com_auth_to_reset_your_password')}
@ -66,7 +66,7 @@ function RequestPasswordReset() {
if (bodyText) { if (bodyText) {
return ( return (
<div <div
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-gray-900 dark:text-white" className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-green-900 dark:text-white"
role="alert" role="alert"
> >
{bodyText} {bodyText}
@ -103,18 +103,18 @@ function RequestPasswordReset() {
}, },
})} })}
aria-invalid={!!errors.email} aria-invalid={!!errors.email}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500" className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" " placeholder=" "
></input> ></input>
<label <label
htmlFor="email" htmlFor="email"
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200" className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
> >
{localize('com_auth_email_address')} {localize('com_auth_email_address')}
</label> </label>
</div> </div>
{errors.email && ( {errors.email && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white"> <span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{/* @ts-ignore not sure why */} {/* @ts-ignore not sure why */}
{errors.email.message} {errors.email.message}
</span> </span>
@ -129,7 +129,7 @@ function RequestPasswordReset() {
{localize('com_auth_continue')} {localize('com_auth_continue')}
</button> </button>
<div className="mt-4 flex justify-center"> <div className="mt-4 flex justify-center">
<a href="/login" className="text-sm font-medium text-green-500"> <a href="/login" className="text-sm text-green-500">
{localize('com_auth_back_to_login')} {localize('com_auth_back_to_login')}
</a> </a>
</div> </div>
@ -139,24 +139,61 @@ function RequestPasswordReset() {
} }
}; };
const privacyPolicy = config.data?.interface?.privacyPolicy;
const termsOfService = config.data?.interface?.termsOfService;
const privacyPolicyRender = privacyPolicy?.externalUrl && (
<a
className="text-sm text-green-500"
href={privacyPolicy.externalUrl}
target={privacyPolicy.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_privacy_policy')}
</a>
);
const termsOfServiceRender = termsOfService?.externalUrl && (
<a
className="text-sm text-green-500"
href={termsOfService.externalUrl}
target={termsOfService.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_terms_of_service')}
</a>
);
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<div className="absolute bottom-0 left-0 m-4"> <div className="mt-12 h-24 w-full bg-cover">
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
</div>
<div className="absolute bottom-0 left-0 md:m-4">
<ThemeSelector /> <ThemeSelector />
</div> </div>
<div className="mt-5 w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg"> <div className="flex flex-grow items-center justify-center">
<h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"> <div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
{headerText} <h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white">
</h1> {headerText}
{requestError && ( </h1>
<div {requestError && (
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200" <div
role="alert" className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
> role="alert"
{localize('com_auth_error_reset_password')} >
</div> {localize('com_auth_error_reset_password')}
</div>
)}
{renderFormContent()}
</div>
</div>
<div className="align-end m-4 flex justify-center gap-2">
{privacyPolicyRender}
{privacyPolicyRender && termsOfServiceRender && (
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
)} )}
{renderFormContent()} {termsOfServiceRender}
</div> </div>
</div> </div>
); );

View file

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useResetPasswordMutation } from 'librechat-data-provider/react-query'; import { useGetStartupConfig, useResetPasswordMutation } from 'librechat-data-provider/react-query';
import type { TResetPassword } from 'librechat-data-provider'; import type { TResetPassword } from 'librechat-data-provider';
import { ThemeSelector } from '~/components/ui'; import { ThemeSelector } from '~/components/ui';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
@ -15,6 +15,7 @@ function ResetPassword() {
formState: { errors }, formState: { errors },
} = useForm<TResetPassword>(); } = useForm<TResetPassword>();
const resetPassword = useResetPasswordMutation(); const resetPassword = useResetPasswordMutation();
const config = useGetStartupConfig();
const [resetError, setResetError] = useState<boolean>(false); const [resetError, setResetError] = useState<boolean>(false);
const [params] = useSearchParams(); const [params] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
@ -28,6 +29,31 @@ function ResetPassword() {
}); });
}; };
const privacyPolicy = config.data?.interface?.privacyPolicy;
const termsOfService = config.data?.interface?.termsOfService;
const privacyPolicyRender = privacyPolicy?.externalUrl && (
<a
className="text-sm text-green-500"
href={privacyPolicy.externalUrl}
target={privacyPolicy.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_privacy_policy')}
</a>
);
const termsOfServiceRender = termsOfService?.externalUrl && (
<a
className="text-sm text-green-500"
href={termsOfService.externalUrl}
target={termsOfService.openNewTab ? '_blank' : undefined}
rel="noreferrer"
>
{localize('com_ui_terms_of_service')}
</a>
);
if (resetPassword.isSuccess) { if (resetPassword.isSuccess) {
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0">
@ -56,134 +82,146 @@ function ResetPassword() {
); );
} else { } else {
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<div className="absolute bottom-0 left-0 m-4"> <div className="mt-12 h-24 w-full bg-cover">
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
</div>
<div className="absolute bottom-0 left-0 md:m-4">
<ThemeSelector /> <ThemeSelector />
</div> </div>
<div className="mt-6 w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg"> <div className="flex flex-grow items-center justify-center">
<h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"> <div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
{localize('com_auth_reset_password')} <h1 className="mb-4 text-center text-3xl font-semibold text-black dark:text-white">
</h1> {localize('com_auth_reset_password')}
{resetError && ( </h1>
<div {resetError && (
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200" <div
role="alert" className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
> role="alert"
{localize('com_auth_error_invalid_reset_token')}{' '}
<a className="font-semibold text-green-600 hover:underline" href="/forgot-password">
{localize('com_auth_click_here')}
</a>{' '}
{localize('com_auth_to_try_again')}
</div>
)}
<form
className="mt-6"
aria-label="Password reset form"
method="POST"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-2">
<div className="relative">
<input
type="hidden"
id="token"
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
value={params.get('token')}
{...register('token', { required: 'Unable to process: No valid reset token' })}
/>
<input
type="hidden"
id="userId"
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
value={params.get('userId')}
{...register('userId', { required: 'Unable to process: No valid user id' })}
/>
<input
type="password"
id="password"
autoComplete="current-password"
aria-label={localize('com_auth_password')}
{...register('password', {
required: localize('com_auth_password_required'),
minLength: {
value: 8,
message: localize('com_auth_password_min_length'),
},
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
},
})}
aria-invalid={!!errors.password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500"
placeholder=" "
></input>
<label
htmlFor="password"
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200"
>
{localize('com_auth_password')}
</label>
</div>
{errors.password && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white">
{/* @ts-ignore not sure why */}
{errors.password.message}
</span>
)}
</div>
<div className="mb-2">
<div className="relative">
<input
type="password"
id="confirm_password"
aria-label={localize('com_auth_password_confirm')}
{...register('confirm_password', {
validate: (value) =>
value === password || localize('com_auth_password_not_match'),
})}
aria-invalid={!!errors.confirm_password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-black/10 bg-white px-2.5 pb-2.5 pt-5 text-sm text-gray-800 focus:border-green-500 focus:outline-none dark:border-white/20 dark:bg-gray-900 dark:text-white dark:focus:border-green-500"
placeholder=" "
></input>
<label
htmlFor="confirm_password"
className="pointer-events-none absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-100 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500 dark:text-gray-200"
>
{localize('com_auth_password_confirm')}
</label>
</div>
{errors.confirm_password && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white">
{/* @ts-ignore not sure why */}
{errors.confirm_password.message}
</span>
)}
{errors.token && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white">
{/* @ts-ignore not sure why */}
{errors.token.message}
</span>
)}
{errors.userId && (
<span role="alert" className="mt-1 text-sm text-black dark:text-white">
{/* @ts-ignore not sure why */}
{errors.userId.message}
</span>
)}
</div>
<div className="mt-6">
<button
disabled={!!errors.password || !!errors.confirm_password}
type="submit"
aria-label={localize('com_auth_submit_registration')}
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none"
> >
{localize('com_auth_continue')} {localize('com_auth_error_invalid_reset_token')}{' '}
</button> <a className="font-semibold text-green-600 hover:underline" href="/forgot-password">
</div> {localize('com_auth_click_here')}
</form> </a>{' '}
{localize('com_auth_to_try_again')}
</div>
)}
<form
className="mt-6"
aria-label="Password reset form"
method="POST"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-2">
<div className="relative">
<input
type="hidden"
id="token"
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
value={params.get('token')}
{...register('token', { required: 'Unable to process: No valid reset token' })}
/>
<input
type="hidden"
id="userId"
// @ts-ignore - Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'
value={params.get('userId')}
{...register('userId', { required: 'Unable to process: No valid user id' })}
/>
<input
type="password"
id="password"
autoComplete="current-password"
aria-label={localize('com_auth_password')}
{...register('password', {
required: localize('com_auth_password_required'),
minLength: {
value: 8,
message: localize('com_auth_password_min_length'),
},
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
},
})}
aria-invalid={!!errors.password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" "
></input>
<label
htmlFor="password"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
>
{localize('com_auth_password')}
</label>
</div>
{errors.password && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{/* @ts-ignore not sure why */}
{errors.password.message}
</span>
)}
</div>
<div className="mb-2">
<div className="relative">
<input
type="password"
id="confirm_password"
aria-label={localize('com_auth_password_confirm')}
{...register('confirm_password', {
validate: (value) =>
value === password || localize('com_auth_password_not_match'),
})}
aria-invalid={!!errors.confirm_password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" "
></input>
<label
htmlFor="confirm_password"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 peer-focus:dark:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
>
{localize('com_auth_password_confirm')}
</label>
</div>
{errors.confirm_password && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{/* @ts-ignore not sure why */}
{errors.confirm_password.message}
</span>
)}
{errors.token && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{/* @ts-ignore not sure why */}
{errors.token.message}
</span>
)}
{errors.userId && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
{/* @ts-ignore not sure why */}
{errors.userId.message}
</span>
)}
</div>
<div className="mt-6">
<button
disabled={!!errors.password || !!errors.confirm_password}
type="submit"
aria-label={localize('com_auth_submit_registration')}
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none"
>
{localize('com_auth_continue')}
</button>
</div>
</form>
</div>
</div>
<div className="align-end m-4 flex justify-center gap-2">
{privacyPolicyRender}
{privacyPolicyRender && termsOfServiceRender && (
<div className="border-r-[1px] border-gray-300 dark:border-gray-600" />
)}
{termsOfServiceRender}
</div> </div>
</div> </div>
); );

View file

@ -15,6 +15,7 @@ const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) =>
const handleMouseLeave = () => { const handleMouseLeave = () => {
setIsHovered(false); setIsHovered(false);
if (isPressed) {setIsPressed(false);}
}; };
const handleMouseDown = () => { const handleMouseDown = () => {
@ -28,7 +29,7 @@ const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) =>
const getButtonStyles = () => { const getButtonStyles = () => {
// Define Tailwind CSS classes based on state // Define Tailwind CSS classes based on state
const baseStyles = 'border border-solid border-gray-300 dark:border-gray-800 transition-colors'; const baseStyles = 'border border-solid border-gray-300 dark:border-gray-600 transition-colors';
const pressedStyles = 'bg-blue-200 border-blue-200 dark:bg-blue-900 dark:border-blue-600'; const pressedStyles = 'bg-blue-200 border-blue-200 dark:bg-blue-900 dark:border-blue-600';
const hoverStyles = 'bg-gray-100 dark:bg-gray-700'; const hoverStyles = 'bg-gray-100 dark:bg-gray-700';

View file

@ -0,0 +1,71 @@
import React from 'react';
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import type { TConversation } from 'librechat-data-provider';
import { Download } from 'lucide-react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { ExportModal } from '../Nav';
import { useRecoilValue } from 'recoil';
import store from '~/store';
function ExportButton() {
const localize = useLocalize();
const location = useLocation();
const [showExports, setShowExports] = useState(false);
const activeConvo = useRecoilValue(store.conversationByIndex(0));
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
let conversation: TConversation | null | undefined;
if (location.state?.from?.pathname.includes('/chat')) {
conversation = globalConvo;
} else {
conversation = activeConvo;
}
const clickHandler = () => {
if (exportable) {
setShowExports(true);
}
};
const exportable =
conversation &&
conversation.conversationId &&
conversation.conversationId !== 'new' &&
conversation.conversationId !== 'search';
return (
<>
{exportable && (
<div className="flex gap-1 gap-2 pr-1">
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<button
className="btn btn-neutral btn-small relative flex h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg"
onClick={clickHandler}
>
<div className="flex w-full items-center justify-center gap-2">
<Download size={16} />
</div>
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={5}>
{localize('com_nav_export_conversation')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{showExports && (
<ExportModal open={showExports} onOpenChange={setShowExports} conversation={conversation} />
)}
</>
);
}
export default ExportButton;

View file

@ -5,6 +5,7 @@ import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ContextType } from '~/common'; import type { ContextType } from '~/common';
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus'; import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
import HeaderOptions from './Input/HeaderOptions'; import HeaderOptions from './Input/HeaderOptions';
import ExportButton from './ExportButton';
const defaultInterface = getConfigDefaults().interface; const defaultInterface = getConfigDefaults().interface;
@ -19,12 +20,15 @@ export default function Header() {
return ( return (
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold dark:bg-gray-800 dark:text-white"> <div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold dark:bg-gray-800 dark:text-white">
<div className="hide-scrollbar flex items-center gap-2 overflow-x-auto"> <div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
{!navVisible && <HeaderNewChat />} <div className="flex items-center gap-2">
{interfaceConfig.endpointsMenu && <EndpointsMenu />} {!navVisible && <HeaderNewChat />}
{modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />} {interfaceConfig.endpointsMenu && <EndpointsMenu />}
{<HeaderOptions interfaceConfig={interfaceConfig} />} {modelSpecs?.length > 0 && <ModelSpecsMenu modelSpecs={modelSpecs} />}
{interfaceConfig.presets && <PresetsMenu />} {<HeaderOptions interfaceConfig={interfaceConfig} />}
{interfaceConfig.presets && <PresetsMenu />}
</div>
<ExportButton />
</div> </div>
{/* Empty div for spacing */} {/* Empty div for spacing */}
<div /> <div />

View file

@ -100,7 +100,7 @@ const ChatForm = ({ index = 0 }) => {
{showMentionPopover && ( {showMentionPopover && (
<Mention setShowMentionPopover={setShowMentionPopover} textAreaRef={textAreaRef} /> <Mention setShowMentionPopover={setShowMentionPopover} textAreaRef={textAreaRef} />
)} )}
<div className="[&:has(textarea:focus)]:border-token-border-xheavy border-token-border-medium bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500"> <div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
<FileRow <FileRow
files={files} files={files}
setFiles={setFiles} setFiles={setFiles}

View file

@ -27,7 +27,9 @@ export default function Files({ open, onOpenChange }) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white')}> <DialogContent
className={cn('w-11/12 overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white')}
>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200"> <DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_my_files')} {localize('com_nav_my_files')}

View file

@ -220,7 +220,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
)} )}
</div> </div>
<Button <Button
className="dark:border-gray-500" className="dark:border-gray-500 dark:hover:bg-gray-600"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
@ -229,7 +229,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{localize('com_ui_prev')} {localize('com_ui_prev')}
</Button> </Button>
<Button <Button
className="dark:border-gray-500" className="dark:border-gray-500 dark:hover:bg-gray-600"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => table.nextPage()} onClick={() => table.nextPage()}

View file

@ -22,7 +22,6 @@ export default function MentionItem({
<div <div
className={cn( className={cn(
'hover:bg-token-main-surface-secondary text-token-text-primary bg-token-main-surface-secondary group flex h-10 items-center gap-2 rounded-lg px-2 text-sm font-medium dark:hover:bg-gray-600', 'hover:bg-token-main-surface-secondary text-token-text-primary bg-token-main-surface-secondary group flex h-10 items-center gap-2 rounded-lg px-2 text-sm font-medium dark:hover:bg-gray-600',
index === 0 ? 'dark:bg-gray-600' : '',
isActive ? 'dark:bg-gray-600' : '', isActive ? 'dark:bg-gray-600' : '',
)} )}
> >

View file

@ -66,7 +66,7 @@ export default function OptionsPopover({
{presetsDisabled ? null : ( {presetsDisabled ? null : (
<Button <Button
type="button" type="button"
className="h-auto w-[150px] justify-start rounded-md border-2 border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500" className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500"
onClick={saveAsPreset} onClick={saveAsPreset}
> >
<Save className="mr-1 w-[14px]" /> <Save className="mr-1 w-[14px]" />

View file

@ -44,12 +44,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
<div className="relative h-full"> <div className="relative h-full">
<div className="absolute left-0 right-0">{Header && Header}</div> <div className="absolute left-0 right-0">{Header && Header}</div>
<div className="flex h-full flex-col items-center justify-center"> <div className="flex h-full flex-col items-center justify-center">
<div <div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
className={cn(
'relative h-[72px] w-[72px]',
assistantName && avatar ? 'mb-0' : 'mb-3',
)}
>
<ConvoIcon <ConvoIcon
conversation={conversation} conversation={conversation}
assistantMap={assistantMap} assistantMap={assistantMap}

View file

@ -118,7 +118,7 @@ const MenuItem: FC<MenuItemProps> = ({
'invisible flex gap-x-1 group-hover:visible', 'invisible flex gap-x-1 group-hover:visible',
selected ? 'visible' : '', selected ? 'visible' : '',
expiryTime expiryTime
? 'w-full rounded-lg p-2 hover:bg-gray-200 dark:hover:bg-gray-600' ? 'w-full rounded-lg p-2 hover:text-gray-400 dark:hover:text-gray-400'
: '', : '',
)} )}
onClick={(e) => { onClick={(e) => {

View file

@ -34,6 +34,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const [titleInput, setTitleInput] = useState(title); const [titleInput, setTitleInput] = useState(title);
const [renaming, setRenaming] = useState(false); const [renaming, setRenaming] = useState(false);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const clickHandler = async (event: React.MouseEvent<HTMLAnchorElement>) => { const clickHandler = async (event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.button === 0 && event.ctrlKey) { if (event.button === 0 && event.ctrlKey) {
@ -117,13 +118,17 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
(isLatestConvo && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new'); (isLatestConvo && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new');
return ( return (
<div className="hover:bg-token-sidebar-surface-secondary group relative rounded-lg active:opacity-90"> <div
className={cn(
'hover:bg-token-sidebar-surface-secondary group relative rounded-lg active:opacity-90',
)}
>
{renaming ? ( {renaming ? (
<div className="absolute bottom-0 left-0 right-0 top-0 z-50 flex w-full items-center rounded-lg bg-gray-200 dark:bg-gray-700"> <div className="absolute inset-0 z-50 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
className="w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none" className="w-full rounded border border-blue-500 bg-transparent p-0.5 text-sm leading-tight outline-none"
value={titleInput} value={titleInput}
onChange={(e) => setTitleInput(e.target.value)} onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename} onBlur={onRename}
@ -131,14 +136,18 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
/> />
</div> </div>
) : ( ) : (
<HoverToggle isActiveConvo={isActiveConvo}> <HoverToggle
isActiveConvo={isActiveConvo}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
>
<EditMenuButton> <EditMenuButton>
<RenameButton <RenameButton
renaming={renaming} renaming={renaming}
onRename={onRename} onRename={onRename}
renameHandler={renameHandler} renameHandler={renameHandler}
appendLabel={true} appendLabel={true}
className="group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600" className="mb-[3.5px]"
/> />
<DeleteButton <DeleteButton
conversationId={conversationId} conversationId={conversationId}
@ -146,7 +155,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
renaming={renaming} renaming={renaming}
title={title} title={title}
appendLabel={true} appendLabel={true}
className="group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600" className="mt-[3.5px]"
/> />
</EditMenuButton> </EditMenuButton>
<ArchiveButton <ArchiveButton
@ -162,7 +171,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
data-testid="convo-item" data-testid="convo-item"
onClick={clickHandler} onClick={clickHandler}
className={cn( className={cn(
isActiveConvo isActiveConvo || isPopoverActive
? 'group relative mt-2 flex cursor-pointer items-center gap-2 break-all rounded-lg rounded-lg bg-gray-200 px-2 py-2 active:opacity-50 dark:bg-gray-700' ? 'group relative mt-2 flex cursor-pointer items-center gap-2 break-all rounded-lg rounded-lg bg-gray-200 px-2 py-2 active:opacity-50 dark:bg-gray-700'
: 'group relative mt-2 flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg rounded-lg px-2 py-2 hover:bg-gray-200 active:opacity-50 dark:hover:bg-gray-700', : 'group relative mt-2 flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg rounded-lg px-2 py-2 hover:bg-gray-200 active:opacity-50 dark:hover:bg-gray-700',
!isActiveConvo && !renaming ? 'peer-hover:bg-gray-200 dark:peer-hover:bg-gray-800' : '', !isActiveConvo && !renaming ? 'peer-hover:bg-gray-200 dark:peer-hover:bg-gray-800' : '',

View file

@ -16,6 +16,7 @@ import {
import DialogTemplate from '~/components/ui/DialogTemplate'; import DialogTemplate from '~/components/ui/DialogTemplate';
import { TrashIcon, CrossIcon } from '~/components/svg'; import { TrashIcon, CrossIcon } from '~/components/svg';
import { useLocalize, useNewConvo } from '~/hooks'; import { useLocalize, useNewConvo } from '~/hooks';
import { cn } from '~/utils';
export default function DeleteButton({ export default function DeleteButton({
conversationId, conversationId,
@ -72,7 +73,14 @@ export default function DeleteButton({
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className={className}>{renaming ? <CrossIcon /> : renderDeleteButton()}</button> <button
className={cn(
'group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600',
className,
)}
>
{renaming ? <CrossIcon /> : renderDeleteButton()}
</button>
</DialogTrigger> </DialogTrigger>
<DialogTemplate <DialogTemplate
showCloseButton={false} showCloseButton={false}

View file

@ -46,7 +46,7 @@ const EditMenuButton: FC<EditMenuButtonProps> = ({ children }: EditMenuButtonPro
align="start" align="start"
className={cn( className={cn(
'popover radix-side-bottom:animate-slideUpAndFade radix-side-left:animate-slideRightAndFade radix-side-right:animate-slideLeftAndFade radix-side-top:animate-slideDownAndFade overflow-hidden rounded-lg shadow-lg', 'popover radix-side-bottom:animate-slideUpAndFade radix-side-left:animate-slideRightAndFade radix-side-right:animate-slideLeftAndFade radix-side-top:animate-slideDownAndFade overflow-hidden rounded-lg shadow-lg',
'border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-700 dark:text-white', 'border border-gray-200 bg-white dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'flex min-w-[200px] max-w-xs flex-wrap', 'flex min-w-[200px] max-w-xs flex-wrap',
)} )}
> >

View file

@ -5,11 +5,14 @@ import { cn } from '~/utils';
const HoverToggle = ({ const HoverToggle = ({
children, children,
isActiveConvo, isActiveConvo,
isPopoverActive,
setIsPopoverActive,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
isActiveConvo: boolean; isActiveConvo: boolean;
isPopoverActive: boolean;
setIsPopoverActive: (isActive: boolean) => void;
}) => { }) => {
const [isPopoverActive, setIsPopoverActive] = useState(false);
const setPopoverActive = (value: boolean) => setIsPopoverActive(value); const setPopoverActive = (value: boolean) => setIsPopoverActive(value);
return ( return (
<ToggleContext.Provider value={{ setPopoverActive }}> <ToggleContext.Provider value={{ setPopoverActive }}>

View file

@ -1,6 +1,7 @@
import type { MouseEvent, ReactElement } from 'react'; import type { MouseEvent, ReactElement } from 'react';
import { EditIcon, CheckMark } from '~/components/svg'; import { EditIcon, CheckMark } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface RenameButtonProps { interface RenameButtonProps {
renaming: boolean; renaming: boolean;
@ -21,7 +22,13 @@ export default function RenameButton({
const handler = renaming ? onRename : renameHandler; const handler = renaming ? onRename : renameHandler;
return ( return (
<button className={className} onClick={handler}> <button
className={cn(
'group m-1.5 flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm hover:bg-gray-200 focus-visible:bg-gray-200 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600',
className,
)}
onClick={handler}
>
{renaming ? ( {renaming ? (
<CheckMark /> <CheckMark />
) : appendLabel ? ( ) : appendLabel ? (

View file

@ -1,7 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query'; import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import type { TEditPresetProps } from '~/common'; import type { TEditPresetProps } from '~/common';
import { cn, defaultTextPropsLabel, removeFocusOutlines, cleanupPreset } from '~/utils/'; import {
cn,
defaultTextPropsLabel,
removeFocusOutlines,
cleanupPreset,
defaultTextProps,
} from '~/utils/';
import DialogTemplate from '~/components/ui/DialogTemplate'; import DialogTemplate from '~/components/ui/DialogTemplate';
import { Dialog, Input, Label } from '~/components/ui/'; import { Dialog, Input, Label } from '~/components/ui/';
import { NotificationSeverity } from '~/common'; import { NotificationSeverity } from '~/common';
@ -59,12 +65,12 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
{localize('com_endpoint_preset_name')} {localize('com_endpoint_preset_name')}
</Label> </Label>
<Input <Input
id="chatGptLabel" id="chatGpt"
value={title || ''} value={title || ''}
onChange={(e) => setTitle(e.target.value || '')} onChange={(e) => setTitle(e.target.value || '')}
placeholder="Set a custom name for this preset" placeholder="Set a custom name for this preset"
className={cn( className={cn(
defaultTextPropsLabel, defaultTextProps,
'flex h-10 max-h-10 w-full resize-none border-gray-100 px-3 py-2 dark:border-gray-600', 'flex h-10 max-h-10 w-full resize-none border-gray-100 px-3 py-2 dark:border-gray-600',
removeFocusOutlines, removeFocusOutlines,
)} )}

View file

@ -9,7 +9,7 @@ const Logout = forwardRef(() => {
return ( return (
<button <button
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-black transition-colors duration-200 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" className="group group flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm transition-colors duration-200 hover:bg-gray-500/10 focus:ring-0 dark:text-white dark:hover:bg-gray-600"
onClick={() => logout()} onClick={() => logout()}
> >
<LogOutIcon /> <LogOutIcon />

View file

@ -6,17 +6,21 @@ interface Props {
text: string; text: string;
clickHandler?: () => void; clickHandler?: () => void;
className?: string; className?: string;
disabled?: boolean;
} }
const NavLink: FC<Props> = forwardRef<HTMLAnchorElement, Props>((props, ref) => { const NavLink: FC<Props> = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const { svg, text, clickHandler, className = '' } = props; const { svg, text, clickHandler, disabled, className = '' } = props;
const defaultProps: { const defaultProps: {
className: string; className: string;
onClick?: () => void; onClick?: () => void;
} = { } = {
className: cn( className: cn(
'flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10', 'flex gap-2 rounded p-2.5 text-sm cursor-pointer focus:ring-0 group items-center transition-colors duration-200 hover:bg-gray-500/10 dark:text-white dark:hover:bg-gray-600',
className, className,
{
'opacity-50 pointer-events-none': disabled,
},
), ),
}; };

View file

@ -1,6 +1,6 @@
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Fragment, useState, memo } from 'react'; import { Fragment, useState, memo } from 'react';
import { Download, FileText } from 'lucide-react'; import { FileText } from 'lucide-react';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { useRecoilValue, useRecoilState } from 'recoil'; import { useRecoilValue, useRecoilState } from 'recoil';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query'; import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
@ -8,7 +8,6 @@ import type { TConversation } from 'librechat-data-provider';
import FilesView from '~/components/Chat/Input/Files/FilesView'; import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext'; import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar'; import useAvatar from '~/hooks/Messages/useAvatar';
import { ExportModal } from './ExportConversation';
import { LinkIcon, GearIcon } from '~/components'; import { LinkIcon, GearIcon } from '~/components';
import { UserIcon } from '~/components/svg'; import { UserIcon } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
@ -26,7 +25,6 @@ function NavLinks() {
const balanceQuery = useGetUserBalance({ const balanceQuery = useGetUserBalance({
enabled: !!isAuthenticated && startupConfig?.checkBalance, enabled: !!isAuthenticated && startupConfig?.checkBalance,
}); });
const [showExports, setShowExports] = useState(false);
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showFiles, setShowFiles] = useRecoilState(store.showFiles); const [showFiles, setShowFiles] = useRecoilState(store.showFiles);
@ -42,34 +40,15 @@ function NavLinks() {
conversation = activeConvo; conversation = activeConvo;
} }
const exportable =
conversation &&
conversation.conversationId &&
conversation.conversationId !== 'new' &&
conversation.conversationId !== 'search';
const clickHandler = () => {
if (exportable) {
setShowExports(true);
}
};
return ( return (
<> <>
<Menu as="div" className="group relative"> <Menu as="div" className="group relative">
{({ open }) => ( {({ open }) => (
<> <>
{startupConfig?.checkBalance &&
balanceQuery.data &&
!isNaN(parseFloat(balanceQuery.data)) && (
<div className="m-1 ml-3 whitespace-nowrap text-left text-sm text-black dark:text-gray-200">
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
</div>
)}
<Menu.Button <Menu.Button
className={cn( className={cn(
'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm mb-1 flex h-11 w-full items-center gap-2 rounded-lg px-3 py-3 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700', 'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800',
open ? 'bg-gray-100 dark:bg-gray-700' : '', open ? 'bg-gray-100 dark:bg-gray-800' : '',
)} )}
data-testid="nav-user" data-testid="nav-user"
> >
@ -93,7 +72,7 @@ function NavLinks() {
</div> </div>
</div> </div>
<div <div
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-black dark:text-white" className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-black dark:text-gray-100"
style={{ marginTop: '0', marginLeft: '0' }} style={{ marginTop: '0', marginLeft: '0' }}
> >
{user?.name || user?.username || localize('com_nav_user')} {user?.name || user?.username || localize('com_nav_user')}
@ -109,24 +88,23 @@ function NavLinks() {
leaveFrom="translate-y-0 opacity-100" leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0" leaveTo="translate-y-2 opacity-0"
> >
<Menu.Items className="absolute bottom-full left-0 z-20 mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg bg-white py-1.5 opacity-100 outline-none dark:bg-gray-800"> <Menu.Items className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
{user?.email || localize('com_nav_user')}
</div>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
{startupConfig?.checkBalance &&
balanceQuery.data &&
!isNaN(parseFloat(balanceQuery.data)) && (
<>
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
</div>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
</>
)}
<Menu.Item as="div"> <Menu.Item as="div">
<NavLink <NavLink
className={cn(
'flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-black transition-colors duration-200 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700',
exportable
? 'cursor-pointer text-black dark:text-white'
: 'cursor-not-allowed text-black/50 dark:text-white/50',
)}
svg={() => <Download size={16} />}
text={localize('com_nav_export_conversation')}
clickHandler={clickHandler}
/>
</Menu.Item>
<div className="my-1 h-px bg-black/20 dark:bg-white/20" role="none" />
<Menu.Item as="div">
<NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-black transition-colors duration-200 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700"
svg={() => <FileText className="icon-md" />} svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')} text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)} clickHandler={() => setShowFiles(true)}
@ -135,7 +113,6 @@ function NavLinks() {
{startupConfig?.helpAndFaqURL !== '/' && ( {startupConfig?.helpAndFaqURL !== '/' && (
<Menu.Item as="div"> <Menu.Item as="div">
<NavLink <NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-black transition-colors duration-200 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700"
svg={() => <LinkIcon />} svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')} text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')} clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
@ -144,13 +121,12 @@ function NavLinks() {
)} )}
<Menu.Item as="div"> <Menu.Item as="div">
<NavLink <NavLink
className="flex w-full cursor-pointer items-center gap-3 rounded-none px-3 py-3 text-sm text-black transition-colors duration-200 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700"
svg={() => <GearIcon className="icon-md" />} svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')} text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)} clickHandler={() => setShowSettings(true)}
/> />
</Menu.Item> </Menu.Item>
<div className="my-1 h-px bg-black/20 bg-white/20" role="none" /> <div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
<Menu.Item as="div"> <Menu.Item as="div">
<Logout /> <Logout />
</Menu.Item> </Menu.Item>
@ -159,9 +135,6 @@ function NavLinks() {
</> </>
)} )}
</Menu> </Menu>
{showExports && (
<ExportModal open={showExports} onOpenChange={setShowExports} conversation={conversation} />
)}
{showFiles && <FilesView open={showFiles} onOpenChange={setShowFiles} />} {showFiles && <FilesView open={showFiles} onOpenChange={setShowFiles} />}
{showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />} {showSettings && <Settings open={showSettings} onOpenChange={setShowSettings} />}
</> </>

View file

@ -16,8 +16,8 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
className={cn( className={cn(
'shadow-2xl md:min-h-[373px] md:w-[680px]', 'overflow-hidden shadow-2xl md:min-h-[373px] md:w-[680px]',
isSmallScreen ? 'top-20 -translate-y-0' : '', isSmallScreen ? 'top-5 -translate-y-0' : '',
)} )}
> >
<DialogHeader> <DialogHeader>
@ -25,19 +25,19 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
{localize('com_nav_settings')} {localize('com_nav_settings')}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="px-6"> <div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root <Tabs.Root
defaultValue={SettingsTabValues.GENERAL} defaultValue={SettingsTabValues.GENERAL}
className="flex flex-col gap-10 md:flex-row" className="flex flex-col gap-10 md:flex-row"
orientation="vertical" orientation="horizontal"
> >
<Tabs.List <Tabs.List
aria-label="Settings" aria-label="Settings"
role="tablist" role="tablist"
aria-orientation="vertical" aria-orientation="horizontal"
className={cn( className={cn(
'min-w-auto -ml-[8px] flex flex-shrink-0 flex-col', 'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-wrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-700' : '', isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-800' : '',
)} )}
style={{ outline: 'none' }} style={{ outline: 'none' }}
> >
@ -112,11 +112,13 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
{localize('com_nav_setting_account')} {localize('com_nav_setting_account')}
</Tabs.Trigger> </Tabs.Trigger>
</Tabs.List> </Tabs.List>
<General /> <div className="h-screen max-h-[373px] overflow-auto sm:w-full sm:max-w-none">
<Messages /> <General />
<Beta /> <Messages />
<Data /> <Beta />
<Account /> <Data />
<Account />
</div>
</Tabs.Root> </Tabs.Root>
</div> </div>
</DialogContent> </DialogContent>

View file

@ -22,10 +22,10 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
<Tabs.Content <Tabs.Content
value={SettingsTabValues.ACCOUNT} value={SettingsTabValues.ACCOUNT}
role="tabpanel" role="tabpanel"
className="w-full md:min-h-[300px]" className="w-full md:min-h-[271px]"
> >
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50"> <div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<Avatar /> <Avatar />
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -39,7 +39,7 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
/> />
</div> </div>
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"></div> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600"></div>
</Tabs.Content> </Tabs.Content>
); );
} }

View file

@ -9,13 +9,13 @@ function Beta() {
<Tabs.Content <Tabs.Content
value={SettingsTabValues.BETA} value={SettingsTabValues.BETA}
role="tabpanel" role="tabpanel"
className="w-full md:min-h-[300px]" className="w-full md:min-h-[271px]"
> >
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50"> <div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ModularChat /> <ModularChat />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<LaTeXParsing /> <LaTeXParsing />
</div> </div>
</div> </div>

View file

@ -100,18 +100,18 @@ function Data() {
<Tabs.Content <Tabs.Content
value={SettingsTabValues.DATA} value={SettingsTabValues.DATA}
role="tabpanel" role="tabpanel"
className="w-full md:min-h-[300px]" className="w-full md:min-h-[271px]"
ref={dataTabRef} ref={dataTabRef}
> >
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50"> <div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ImportConversations /> <ImportConversations />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<RevokeKeysButton all={true} /> <RevokeKeysButton all={true} />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ClearChatsButton <ClearChatsButton
confirmClear={confirmClearConvos} confirmClear={confirmClearConvos}
onClick={clearConvos} onClick={clearConvos}

View file

@ -33,7 +33,9 @@ export const ThemeSelector = ({
value={theme} value={theme}
onChange={onChange} onChange={onChange}
options={themeOptions} options={themeOptions}
width={150} width={220}
position={'left'}
maxHeight="200px"
testId="theme-selector" testId="theme-selector"
/> />
</div> </div>
@ -103,7 +105,13 @@ export const LangSelector = ({
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> {localize('com_nav_language')} </div> <div> {localize('com_nav_language')} </div>
<Dropdown value={langcode} onChange={onChange} options={languageOptions} /> <Dropdown
value={langcode}
onChange={onChange}
position={'left'}
maxHeight="271px"
options={languageOptions}
/>
</div> </div>
); );
}; };
@ -142,26 +150,26 @@ function General() {
<Tabs.Content <Tabs.Content
value={SettingsTabValues.GENERAL} value={SettingsTabValues.GENERAL}
role="tabpanel" role="tabpanel"
className="w-full md:min-h-[300px]" className="w-full md:min-h-[271px]"
ref={contentRef} ref={contentRef}
> >
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50"> <div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ThemeSelector theme={theme} onChange={changeTheme} /> <ThemeSelector theme={theme} onChange={changeTheme} />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<LangSelector langcode={selectedLang} onChange={changeLang} /> <LangSelector langcode={selectedLang} onChange={changeLang} />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<AutoScrollSwitch /> <AutoScrollSwitch />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<HideSidePanelSwitch /> <HideSidePanelSwitch />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ArchivedChats /> <ArchivedChats />
</div> </div>
{/* <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> {/* <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
</div> */} </div> */}
</div> </div>
</Tabs.Content> </Tabs.Content>

View file

@ -18,7 +18,7 @@ export const ForkSettings = () => {
return ( return (
<> <>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> {localize('com_ui_fork_change_default')} </div> <div> {localize('com_ui_fork_change_default')} </div>
<Dropdown <Dropdown
@ -26,11 +26,13 @@ export const ForkSettings = () => {
onChange={setForkSetting} onChange={setForkSetting}
options={forkOptions} options={forkOptions}
width={200} width={200}
position={'left'}
maxHeight="199px"
testId="fork-setting-dropdown" testId="fork-setting-dropdown"
/> />
</div> </div>
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> {localize('com_ui_fork_default')} </div> <div> {localize('com_ui_fork_default')} </div>
<Switch <Switch
@ -42,7 +44,7 @@ export const ForkSettings = () => {
/> />
</div> </div>
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> {localize('com_ui_fork_split_target_setting')} </div> <div> {localize('com_ui_fork_split_target_setting')} </div>
<Switch <Switch

View file

@ -7,16 +7,12 @@ import { ForkSettings } from './ForkSettings';
function Messages() { function Messages() {
return ( return (
<Tabs.Content <Tabs.Content value={SettingsTabValues.MESSAGES} role="tabpanel" className="md: w-full">
value={SettingsTabValues.MESSAGES}
role="tabpanel"
className="w-full md:min-h-[300px]"
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50"> <div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<SendMessageKeyEnter /> <SendMessageKeyEnter />
</div> </div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700"> <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ShowCodeSwitch /> <ShowCodeSwitch />
</div> </div>
<ForkSettings /> <ForkSettings />

View file

@ -92,7 +92,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
return ( return (
<div <div
onClick={handleFileClick} onClick={handleFileClick}
className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-900" className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700"
> >
{fileType && <FilePreview fileType={fileType} />} {fileType && <FilePreview fileType={fileType} />}
<span className="self-center truncate">{file.filename}</span> <span className="self-center truncate">{file.filename}</span>

View file

@ -3,21 +3,18 @@ import React from 'react';
export default function Clipboard() { export default function Clipboard() {
return ( return (
<svg <svg
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" xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
className="icon-md-heavy"
> >
<path <path
fillRule="evenodd"
clipRule="evenodd"
d="M12 4C10.8954 4 10 4.89543 10 6H14C14 4.89543 13.1046 4 12 4ZM8.53513 4C9.22675 2.8044 10.5194 2 12 2C13.4806 2 14.7733 2.8044 15.4649 4H17C18.6569 4 20 5.34315 20 7V19C20 20.6569 18.6569 22 17 22H7C5.34315 22 4 20.6569 4 19V7C4 5.34315 5.34315 4 7 4H8.53513ZM8 6H7C6.44772 6 6 6.44772 6 7V19C6 19.5523 6.44772 20 7 20H17C17.5523 20 18 19.5523 18 19V7C18 6.44772 17.5523 6 17 6H16C16 7.10457 15.1046 8 14 8H10C8.89543 8 8 7.10457 8 6Z"
fill="currentColor" fill="currentColor"
fillRule="evenodd"
d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z"
clipRule="evenodd"
></path> ></path>
</svg> </svg>
); );

View file

@ -6,7 +6,7 @@ export default function EditIcon() {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="h-4 w-4" className="icon-md"
height="1em" height="1em"
width="1em" width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -3,21 +3,16 @@ import { cn } from '~/utils';
export default function RegenerateIcon({ className = '' }: { className?: string }) { export default function RegenerateIcon({ className = '' }: { className?: string }) {
return ( return (
<svg <svg
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className={cn('h-4 w-4', className)}
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
className={cn('icon-md-heavy', className)}
> >
<path <path
fillRule="evenodd"
clipRule="evenodd"
d="M4.5 2.5C5.05228 2.5 5.5 2.94772 5.5 3.5V5.07196C7.19872 3.47759 9.48483 2.5 12 2.5C17.2467 2.5 21.5 6.75329 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C7.1307 21.5 3.11828 17.8375 2.565 13.1164C2.50071 12.5679 2.89327 12.0711 3.4418 12.0068C3.99033 11.9425 4.48712 12.3351 4.5514 12.8836C4.98798 16.6089 8.15708 19.5 12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C9.7796 4.5 7.7836 5.46469 6.40954 7H9C9.55228 7 10 7.44772 10 8C10 8.55228 9.55228 9 9 9H4.5C3.96064 9 3.52101 8.57299 3.50073 8.03859C3.49983 8.01771 3.49958 7.99677 3.5 7.9758V3.5C3.5 2.94772 3.94771 2.5 4.5 2.5Z"
fill="currentColor" fill="currentColor"
d="M3.07 10.876C3.623 6.436 7.41 3 12 3a9.15 9.15 0 0 1 6.012 2.254V4a1 1 0 1 1 2 0v4a1 1 0 0 1-1 1H15a1 1 0 1 1 0-2h1.957A7.15 7.15 0 0 0 12 5a7 7 0 0 0-6.946 6.124 1 1 0 1 1-1.984-.248m16.992 1.132a1 1 0 0 1 .868 1.116C20.377 17.564 16.59 21 12 21a9.15 9.15 0 0 1-6-2.244V20a1 1 0 1 1-2 0v-4a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H7.043A7.15 7.15 0 0 0 12 19a7 7 0 0 0 6.946-6.124 1 1 0 0 1 1.116-.868"
></path> ></path>
</svg> </svg>
); );

View file

@ -6,7 +6,7 @@ export default function TrashIcon() {
viewBox="0 0 24 24" viewBox="0 0 24 24"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="h-4 w-4" className="icon-md"
height="1em" height="1em"
width="1em" width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -7,13 +7,17 @@ type OptionType = {
display?: string; display?: string;
}; };
type DropdownPosition = 'left' | 'right';
interface DropdownProps { interface DropdownProps {
value: string; value: string;
label?: string; label?: string;
onChange: (value: string) => void; onChange: (value: string) => void;
options: (string | OptionType)[]; options: (string | OptionType)[];
className?: string; className?: string;
position?: DropdownPosition;
width?: number; width?: number;
maxHeight?: string;
testId?: string; testId?: string;
} }
@ -23,11 +27,18 @@ const Dropdown: FC<DropdownProps> = ({
onChange, onChange,
options, options,
className = '', className = '',
position = 'right',
width, width,
maxHeight = 'auto',
testId = 'dropdown-menu', testId = 'dropdown-menu',
}) => { }) => {
const [selectedValue, setSelectedValue] = useState(initialValue); const [selectedValue, setSelectedValue] = useState(initialValue);
const positionClasses = {
right: 'origin-bottom-left left-0',
left: 'origin-bottom-right right-0',
};
return ( return (
<div className={cn('relative', className)}> <div className={cn('relative', className)}>
<Listbox <Listbox
@ -41,7 +52,7 @@ const Dropdown: FC<DropdownProps> = ({
<Listbox.Button <Listbox.Button
data-testid={testId} data-testid={testId}
className={cn( className={cn(
'relative inline-flex items-center justify-between rounded-md border-gray-300 bg-white py-2 pl-3 pr-8 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 ', 'relative inline-flex items-center justify-between rounded-md border-gray-300 bg-white py-2 pl-3 pr-8 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
'w-auto', 'w-auto',
className, className,
)} )}
@ -67,19 +78,19 @@ const Dropdown: FC<DropdownProps> = ({
</Listbox.Button> </Listbox.Button>
<Listbox.Options <Listbox.Options
className={cn( className={cn(
'absolute z-50 mt-1 max-h-[40vh] overflow-auto rounded-md border-gray-300 bg-white text-gray-700 shadow-lg transition-opacity hover:bg-gray-50 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600', `absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
className, className,
)} )}
style={{ width: width ? `${width}px` : 'auto' }} style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
> >
{options.map((item, index) => ( {options.map((item, index) => (
<Listbox.Option <Listbox.Option
key={index} key={index}
value={typeof item === 'string' ? item : item.value} value={typeof item === 'string' ? item : item.value}
className={cn( className={cn(
'relative cursor-pointer select-none border-gray-300 bg-white py-1 pl-3 pr-6 text-gray-700 hover:bg-gray-50 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600', 'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-6 text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
)} )}
style={{ width: width ? `${width}px` : 'auto' }} style={{ width: '100%' }}
data-theme={typeof item === 'string' ? item : (item as OptionType).value} data-theme={typeof item === 'string' ? item : (item as OptionType).value}
> >
<span className="block truncate"> <span className="block truncate">

View file

@ -28,7 +28,7 @@ const ThemeSelector = () => {
); );
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0"> <div className="flex flex-col items-center justify-center bg-white pt-6 dark:bg-gray-900 sm:pt-0">
<div className="absolute bottom-0 left-0 m-4"> <div className="absolute bottom-0 left-0 m-4">
<Theme theme={theme} onChange={changeTheme} /> <Theme theme={theme} onChange={changeTheme} />
</div> </div>

View file

@ -144,7 +144,7 @@ export default {
com_ui_fork_success: 'Successfully forked conversation', com_ui_fork_success: 'Successfully forked conversation',
com_ui_fork_processing: 'Forking conversation...', com_ui_fork_processing: 'Forking conversation...',
com_ui_fork_error: 'There was an error forking the conversation', com_ui_fork_error: 'There was an error forking the conversation',
com_ui_fork_change_default: 'Change default fork option', com_ui_fork_change_default: 'Default fork option',
com_ui_fork_default: 'Use default fork option', com_ui_fork_default: 'Use default fork option',
com_ui_fork_remember: 'Remember', com_ui_fork_remember: 'Remember',
com_ui_fork_split_target_setting: 'Start fork from target message by default', com_ui_fork_split_target_setting: 'Start fork from target message by default',
@ -431,8 +431,8 @@ export default {
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.', 'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_welcome_assistant: 'Please Select an Assistant', com_nav_welcome_assistant: 'Please Select an Assistant',
com_nav_welcome_message: 'How can I help you today?', com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-scroll to Newest on Open', com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open',
com_nav_hide_panel: 'Hide Right-most Side Panel', com_nav_hide_panel: 'Hide right-most side panel',
com_nav_modular_chat: 'Enable switching Endpoints mid-conversation', com_nav_modular_chat: 'Enable switching Endpoints mid-conversation',
com_nav_latex_parsing: 'Parsing LaTeX in messages (may affect performance)', com_nav_latex_parsing: 'Parsing LaTeX in messages (may affect performance)',
com_nav_profile_picture: 'Profile Picture', com_nav_profile_picture: 'Profile Picture',

View file

@ -1725,10 +1725,13 @@ html {
height:1rem; height:1rem;
width:1rem width:1rem
} }
.icon-md { .icon-md, .icon-md-heavy {
stroke-width:1.5; stroke-width:1.5;
height:1.25rem; height:1.125rem;
width:1.25rem width:1.125rem
}
.icon-md-heavy {
stroke-width: 2.5;
} }
.icon-lg { .icon-lg {
stroke-width:1.5; stroke-width:1.5;

View file

@ -1,11 +1,11 @@
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { clsx } from 'clsx'; import { type ClassValue, clsx } from 'clsx';
/** /**
* Merges the tailwind clases (using twMerge). Conditionally removes false values * Merges the tailwind clases (using twMerge). Conditionally removes false values
* @param inputs The tailwind classes to merge * @param inputs The tailwind classes to merge
* @returns className string to apply to an element or HOC * @returns className string to apply to an element or HOC
*/ */
export default function cn(...inputs: Array<string | boolean>) { export default function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }