Compare commits

..

69 Commits

Author SHA1 Message Date
Danny Avila
302b28fc9b v0.7.4-rc1 (#3099)
* fix(openIdStrategy): return user object on new user creation

*  v0.7.4-rc1
2024-06-17 12:47:28 -04:00
Marco Beretta
dad25bd297 📝 docs: update README (#3093) 2024-06-17 07:51:13 -04:00
Marco Beretta
a338decf90 ✉️ fix: email address encoding in verification link (#3085)
Related to #3084

Implements URL encoding for email addresses in verification links and decodes them upon verification.

- **Encode email addresses** in `sendVerificationEmail` and `resendVerificationEmail` functions using `encodeURIComponent` to ensure special characters like `+` are correctly handled in the verification link.
- **Decode email addresses** in the `verifyEmail` function using `decodeURIComponent` to accurately retrieve and validate the email address from the verification link against the database.


---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/danny-avila/LibreChat/issues/3084?shareId=9c32df30-4156-4082-a3eb-fff54eaba5b3).
2024-06-16 16:05:53 -04:00
Danny Avila
2cf5228021 🕑 fix: Add Suspense to Connection Error Messages (#3074) 2024-06-15 16:16:06 -04:00
Danny Avila
0294cfc881 v0.7.3 (#3067)
* refactor: revert BaseClient to use node-fetch instead of undici

* chore: bump version

* chore: update npm dependencies to latest versions

* chore: fix custom footer
2024-06-15 12:17:10 -04:00
Yuichi Oneda
8d8b17e7ed 🚀 feat: Add Option to Disable Shared Links (#2986)
* feat: add option to disable shared links

* chore: update languages

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-15 11:12:03 -04:00
Danny Avila
04502e9525 👤 fix: Create User with timestamps (#3070)
* 👤 fix: Create User with timestamps

* chore: fix lint script to ignore venv

* chore: linting
2024-06-15 10:36:49 -04:00
btribonde
bcaa7d5d29 🛤️ feat: Proxy Support for OpenID Login (#3051)
https://github.com/danny-avila/LibreChat/issues/3041
2024-06-15 09:41:34 -04:00
Marco Beretta
c288b458b6 📝 docs: update README's features (#3011)
* Update README.md

* Update README.md

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-15 09:36:05 -04:00
Neelesh Kumar
447bbcb8ca 📝 chore: Update .env.example with Custom Endpoint API Key examples (#2970)
Add env variables for cohere and databricks
2024-06-15 09:31:39 -04:00
Marco Beretta
68bf7ac7c0 🪲 fix(useTextarea): enterToSend bugs (#2988)
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-15 09:30:19 -04:00
Denis Palnitsky
97d12d03d1 📊 feat: Google tag manager integration (#2469)
* Google tag manager integration

* change location of react-gtm-module package

* refactor: move react-gtm-module usage from Chat/Footer to useAppStartup hook

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-15 08:09:18 -04:00
Danny Avila
4416f69a9b 🚀 refactor: Use Undici Instead of Node-Fetch, prevent Event Close, add Index (#3052)
* feat: Add index to conversationId field in messageSchema

* refactor: prevent immediate event close on error

* refactor: use undici instead of node-fetch in non-Bun environment
2024-06-13 13:38:15 -04:00
Yuichi Oneda
29e71e98ad ✍️ feat: Automatic Save and Restore for Chat (#2942)
* feat: added "Save draft locally" to Message settings

* feat: add hook to save chat input as draft every second

* fix: use filepath if the file does not have a preview prop

* fix: not to delete temporary files when navigating to a new chat

* chore: translations

* chore: import order

* chore: import order

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-13 09:52:30 -04:00
Jakub Mieszczak
e9bbf39618 📝 feat: Markdown support for Custom Footer links (#2960)
* feat: Add markdown support for custom footer links

* fix: Update Footer component with revised ReactMarkdown props

* fix: Adjusted footer links and pipe styles for consistent appearance

* refactor: remove unused footer.tsx file
2024-06-13 09:51:28 -04:00
Danny Avila
08b8ae120e 🤖 fix(Assistants): Ensure Required Actions always have Outputs (#3031) 2024-06-10 22:01:52 -04:00
Danny Avila
803fd63121 📋 fix(handlePaste): Remove Custom rich-text handling (#3025)
* fix: rich text edge case

* fix(useTextarea): removing custom handling fixes RTF slow loading, default behavior with form prefers plain-text
2024-06-10 14:36:51 -04:00
Yuichi Oneda
ef76cc195e 🎨 style(Textarea): Message Edit Textarea Styling (#3009) 2024-06-10 13:01:07 -04:00
Danny Avila
2e559137ae ℹ️ refactor: Remove use of Agenda for Conversation Imports (#3024)
* chore: remove agenda and npm audit fix

* refactor: import conversations without agenda

* chore: update package-lock.json and data-provider version to 0.6.7

* fix: import conversations

* chore: client npm audit fix
2024-06-10 13:00:34 -04:00
Danny Avila
92232afaca 📧 fix: Cancel Signup if Email Issuance Fails (#3010)
* fix: user.id assignment in jwtStrategy.js

* refactor(sendEmail): pass params as object, await email sending to propogate errors and restrict registration flow

* fix(Conversations): handle missing updatedAt field

* refactor: use `processDeleteRequest` when deleting user account for user file deletion

* refactor: delete orphaned files when deleting user account

* fix: remove unnecessary 404 status code in server/index.js
2024-06-08 06:51:29 -04:00
Danny Avila
084cf266a2 🗃️ fix: revise RAG prompt (#3006) 2024-06-07 20:51:43 -04:00
Danny Avila
baf0848021 📧 fix: LDAP login after User verification changes (#3003) 2024-06-07 17:43:36 -04:00
Danny Avila
1da92111aa 🚀 refactor: Remove Local Login Redundancies (#3002) 2024-06-07 16:45:31 -04:00
Danny Avila
35f8053f45 📧 fix: Ensure User Verification for Instances without Email Service (#2998) 2024-06-07 15:43:43 -04:00
Marco Beretta
ee673d682e 📧 feat: email verification (#2344)
* feat: verification email

* chore: email verification invalid; localize: update

* fix: redirect to login when signup: fix: save emailVerified correctly

* docs: update ALLOW_UNVERIFIED_EMAIL_LOGIN; fix: don't accept login only when ALLOW_UNVERIFIED_EMAIL_LOGIN = true

* fix: user needs to be authenticated

* style: update

* fix: registration success message and redirect logic

* refactor: use `isEnabled` in ALLOW_UNVERIFIED_EMAIL_LOGIN

* refactor: move checkEmailConfig to server/utils

* refactor: use req as param for verifyEmail function

* chore: jsdoc

* chore: remove console log

* refactor: rename `createNewUser` to `createSocialUser`

* refactor: update typing and add expiresAt field to userSchema

* refactor: begin use of user methods over direct model access for User

* refactor: initial email verification rewrite

* chore: typing

* refactor: registration flow rewrite

* chore: remove help center text

* refactor: update getUser to getUserById and add findUser methods. general fixes from recent changes

* refactor: Update updateUser method to remove expiresAt field and use $set and $unset operations, createUser now returns Id only

* refactor: Update openidStrategy to use optional chaining for avatar check, move saveBuffer init to buffer condition

* refactor: logout on deleteUser mutatation

* refactor: Update openidStrategy login success message format

* refactor: Add emailVerified field to Discord and Facebook profile details

* refactor: move limiters to separate middleware dir

* refactor: Add limiters for email verification and password reset

* refactor: Remove getUserController and update routes and controllers accordingly

* refactor: Update getUserById method to exclude password and version fields

* refactor: move verification to user route, add resend verification option

* refactor: Improve email verification process and resend option

* refactor: remove more direct model access of User and remove unused code

* refactor: replace user authentication methods and token generation

* fix: add user.id to jwt user

* refactor: Update AuthContext to include setError function, add resend link to Login Form, make registration redirect shorter

* fix(updateUserPluginsService): ensure userPlugins variable is defined

* refactor: Delete all shared links for a specific user

* fix: remove use of direct User.save() in handleExistingUser

* fix(importLibreChatConvo): handle missing createdAt field in messages

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-07 15:06:47 -04:00
Marco Beretta
b7fef6958b 🔒refactor: social login and remove direct user model access in strategies (#2946)
* refactor: checking `ALLOW_SOCIAL_REGISTRATION` with `isEnabled`

* feat: Add findUserByEmail function to UserService

This commit adds a new function, , to the  module. This function retrieves a user document from the database based on the provided email. It returns the user document if found, otherwise it returns null. If there is a problem during user retrieval, an error is thrown.

* refactor: add socialLogin to remove repetitive code
2024-06-06 13:23:11 -04:00
Marco Beretta
5452d4c20c 🔒 feat: password reset disable option; fix: account email error message (#2327)
* feat: password reset  disable option; fix: account email leak

* fix(LoginSpec): typo

* test: fixed LoginForm test

* fix: disable password reset when undefined

* refactor: use a helper function

* fix: tests

* feat: Remove unused error message in password reset process

* chore: Update password reset email message

* refactor: only allow password reset if explicitly allowed

* feat: Add password reset email service configuration check

The code changes in `checks.js` add a new function `checkPasswordReset()` that checks if the email service is configured when password reset is enabled. If the email service is not configured, a warning message is logged. This change ensures secure password reset functionality by prompting the user to configure the email service.

Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>

* chore: remove import order rules

* refactor: simplify password reset logic and align against Observable Response Discrepancy

* chore: make password reset warning more prominent

* chore(AuthService): better logging for password resets, refactor requestPasswordReset to use req object, fix sendEmail error when email config is not present

* refactor: fix styling of password reset email message

* chore: add missing type for passwordResetEnabled, TStartupConfig

* fix(LoginForm): prevent login form flickering

* fix(ci): Update login form to use mocked startupConfig for rendering correctly

* refactor: Improve password reset UI, applies DRY

* chore: Add logging to password reset validation middleware

* chore(CONTRIBUTING): Update import order conventions

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
2024-06-06 11:39:36 -04:00
Marco Beretta
a7f5b57272 🚫👤feat: delete user from UI (#1526)
* initial commit

* fix: UserController bugs; fix: lint errors

* fix: delete files

* language support

* style(DeleteAccount): update to the latest style

* style: fix after merge main

* chore: Add canDeleteAccount middleware for user deletion endpoint

* chore: renamed to ALLOW_ACCOUNT_DELETION

* fix(canDeleteAccount): use uppercase admin role

* chore: imports order

* chore: Enable account deletion by default if omitted/commented out

* chore: Add logging for user account deletion

* chore: Bump data-provider package version to 0.6.6

* chore: Import Transaction model in UserController

* chore: Update CONFIG_VERSION to 1.1.4

* chore: Update user account deletion logging

* chore: Refactor user account deletion logic

---------

Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-05 19:35:12 -04:00
Marco Beretta
f69b317171 🔧 fix(useMentions): handle empty assistant lists (#2966)
The useMentions hook in the client/src/hooks/Input/useMentions.ts file has been updated to handle cases where the assistant lists for the endpoints 'assistants' and 'azureAssistants' are empty. This change ensures that the hook does not throw an error when attempting to access assistantListMap[EModelEndpoint.assistants] or assistantListMap[EModelEndpoint.azureAssistants]. Instead, it defaults to an empty array for these cases.
2024-06-05 14:56:15 -04:00
Yuichi Oneda
4469ba72fc 🧹 chore(.eslintrc.js): Update Import Order of The React Types (#2964) 2024-06-05 14:55:42 -04:00
Yuichi Oneda
0e3e45e77d 🔧 chore: Add import/order Eslint Rule (#2928)
* chore: add import order eslint rule

* refactor: apply 'import/order' rule
2024-06-04 08:56:26 -04:00
Marco Beretta
9f0c1914a5 🎂 fix: birthday icon (#2950)
* fix: tooltip and birthday icon

* chore: update conditional render

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-06-04 08:55:41 -04:00
Ventz Petkov
37ae484fbc 🚅 docs: Updated Example for LiteLLM ports and Volume mount (#2941)
* Added necessary "ports" section for it to work by default
* Added (commented out) example GCP Vertex volume mount for auth config and for ENV variable.
2024-06-01 08:51:18 -04:00
Danny Avila
8939d8af37 🦙 feat: Add LLama 3 System Context Length (#2938) 2024-05-31 12:16:08 -04:00
Danny Avila
f9a0166352 🔄 refactor(EditPresetDialog): Update Model on Endpoint Change (#2936)
* refactor(EditPresetDialog): dynamically update current editable preset model on endpoint change

* feat: Add null check for models in EditPresetDialog

* chore(AlertDialogPortal): typing

* fix(EditPresetDialog): prevent Unknown endpoint edge case for custom endpoints
2024-05-31 11:43:14 -04:00
Danny Avila
248dfb8b5b 🐛 fix: Resolve Preset Button Disappearing in Mobile View (#2935)
* refactor: Update import paths for ExportAndShareMenu component and add localization

* fix: mobile view for export/share button
2024-05-31 08:46:09 -04:00
Danny Avila
b8e35002f4 🗣️ fix: Set Audio Run ID at Top of Autoplayback Request (#2926) 2024-05-30 22:01:40 -04:00
Danny Avila
8318f26d66 🔉 feat: TTS/STT rate limiters (#2925)
* fix: remove double initialization of speech routes

* refactor(useMessageHelpers): more consistent latestMessage updates based on unique textKey and early returns when setting

* feat: TTS/STT rate limiters

* chore: remove console log

* fix: make modular chat true by default
2024-05-30 18:39:21 -04:00
Danny Avila
08d6bea359 🛠️ refactor: Improve Logging and Error Handling in ToolService and useSSE (#2922)
* refactor(ToolService): streamline logging and tool error handling, also ensure generated outputs always have `output` field

* refactor(useSSE): error message for server connection issue

* refactor: add back capture group of sensitive information.js

* hotfix: cohere chinese character unicode issue, return aggregated reply
2024-05-30 12:58:43 -04:00
Arthur Barrett
a6058c5669 🔧 chore: Update OpenIDStrategy Logging (#2911) 2024-05-30 10:48:03 -04:00
Danny Avila
e0402b71f0 🎨 style: Focus Outlines (#2913)
* style: add back focus ring removal as the outer/inner containers maintain focus stylings

* style: add focus outline to default buttonVariants class
2024-05-29 23:07:52 -04:00
Yuichi Oneda
a618266905 🔒 feature(auth): LDAP Authentication (#2859)
* 🔧 chore: npm install passport-ldapauth

*  feat(auth): add ldap authentication support

* chore: merge conflict fix

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-05-29 17:46:20 -04:00
Danny Avila
d5a7806e32 ⬆️ chore: bump @langchain/google-vertexai to v0.0.17 (#2905) 2024-05-29 10:47:05 -04:00
Danny Avila
e2cb2905e7 🖱️ feat: Minor Accessibility Changes (#2903)
* feat: focus rings for dialog buttons

* chore: temp: soft removal of removeFocusOutlines

* feat: allow tabbing of endpoint menu
2024-05-29 10:14:30 -04:00
Danny Avila
3f600f0d3f ⬇️ fix: JSON LibreChat Imports (#2897)
* chore: remove unused code

* refactor: Update NewChatButtonIcon component to use JSX syntax

The NewChatButtonIcon component in the Nav folder has been updated to use JSX syntax instead of calling the Icon function directly. This change improves code readability and maintainability.

* remove use memo

* refactor: allow passing `select` to messages db query

* fix: initial fix for non-recursive messages

* ci: first pass, importers test rewrite

* fix(groupConversationsByDate): handle edge case of conversation.updatedAt being null

* fix: correctly handle non-recursive uploads

* feat: imports non-recursive conversations with branches correctly

* feat: support retaining original options on import

* refactor: Allow `messageTree` field for Import of non-recursive conversations
2024-05-29 09:15:05 -04:00
Yuichi Oneda
c9e7d4ac18 🚑 fix(export): Export Issue with New Chat (#2777)
* 🚑 fix: re-fetch messages when exporting

* Revert "🚑 fix: re-fetch messages when exporting"

This reverts commit 693b86e955.

* 🚑 fix: use the same logic to get export data as useChatHelper

* refactor(useExportConversation): use query cache to build messages tree on request

* chore: organize imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-05-28 18:10:33 -04:00
Danny Avila
40685f6eb4 🚀 refactor: Enhance Custom Endpoints, Message Logic, and Payload Handling (#2895)
* chore: use node-fetch for OpenAIClient fetch key for non-crashing usage of AbortController in Bun runtime

* chore: variable order

* fix(useSSE): prevent finalHandler call in abortConversation to update messages/conversation after user navigated away

* chore: params order

* refactor: organize intermediate message logic and ensure correct variables are passed

* fix: Add stt and tts routes before upload limiters, prevent bans

* fix(abortRun): temp fix to delete unfinished messages to avoid message thread parent relationship issues

* refactor: Update AnthropicClient to use node-fetch for fetch key and add proxy support

* fix(gptPlugins): ensure parentMessageId/messageId relationship is maintained

* feat(BaseClient): custom fetch function to analyze/edit payloads just before sending (also prevents abortController crash on Bun runtime)

* feat: `directEndpoint` and `titleMessageRole` custom endpoint options

* chore: Bump version to 0.6.6 in data-provider package.json
2024-05-28 14:52:12 -04:00
Yuichi Oneda
0ee060d730 🚑 fix: Prevent Infinite Re-Rendering of the Password Reset UI (#2887)
* 🔧 fix: prevent unnecessary re-rendering of components using useLocalize hook

The useLocalize hook now uses useCallback to create a memoized version of the localize function. This will prevent unnecessary recalculations when the language value changes.

* 🚑 fix: not reset the bodyText if it has a value set.
2024-05-28 14:07:08 -04:00
Danny Avila
5dc5d875ba 🤖 feat: Private Assistants (#2881)
* feat: add configuration for user private assistants

* filter private assistant message requests

* add test for privateAssistants

* add privateAssistants configuration to tests

* fix: destructuring error when assistants config is not added

* chore: revert chat controller changes

* chore: add payload type, add metadata types

* feat: validateAssistant

* refactor(fetchAssistants): allow for flexibility

* feat: validateAuthor

* refactor: return all assistants to ADMIN role

* feat: add assistant doc on assistant creation

* refactor(listAssistants): use `listAllAssistants` to exhaustively fetch all assistants

* chore: add suggestion to tts error

* refactor(validateAuthor): attempt database check first

* refactor: author validation when patching/deleting assistant

---------

Co-authored-by: Leon Juenemann <leon.juenemann@maibornwolff.de>
2024-05-28 08:27:45 -04:00
Yuichi Oneda
9f2538fcd9 ♻️ refactor: Login and Registration component Improvement (#2716)
* ♻️ refactor: Login form improvement

* display error message when API is down
* add loading animation to Login form while fetching data
* optimize startupConfig to fetch data only on initial render to prevent unnecessary API calls

* 🚑 fix: clear authentication error messages on successful login

* ♻️ refactor: componentize duplicate codes on registration and login screens

* chore: update types

* refactor: layout rendering order

* refactor: startup title fix

* refactor: reset/request-reset-password under new AuthLayout

* ci: fix Login.spec.ts

* ci: fix registration.spec.tsx

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2024-05-28 08:25:07 -04:00
Danny Avila
2b7a973a33 😎 fix: Azure Bug, short default max_tokens response using gpt-4-vision-preview (#2885) 2024-05-27 22:56:26 -04:00
Danny Avila
c704a23749 ⚛️ fix(atomWithLocalStorage): Handle Parsing Error (#2883) 2024-05-27 21:23:46 -04:00
Danny Avila
eb5733083e 🔈 fix(tts): update min value for playback rate (#2880)
* 🔈 fix: update min value for playback rate in TTS component

* fix: prevent playbackRate from being set if less than or equal to 0
2024-05-27 12:51:45 -04:00
Danny Avila
b80f38e49e 🔢 style: ol Counter Fix (#2867) 2024-05-26 21:13:12 -04:00
Yuichi Oneda
4369e75ca7 🚑 fix: resolve missing data in infinite queries (#2852)
* An issue where the InfiniteQuery was missing data
* Post add/delete operations, inconsistencies between client-side data structures and the database could lead to data being missed or duplicated.
* To address this, implemented normalization of client data following add/delete operations.
* performed refetching of data in the last page when necessary to ensure consistency.
2024-05-24 12:38:38 -04:00
Danny Avila
35ba4ba1a4 🔊 fix(tts): NotAllowedError (mobile/safari), Unsupported MediaSource type (firefox), Hide Audio Element (#2854)
* fix: hide audio element on mobile

* chore: add tts docs link

* fix: select voice option on first render

* fix: NotAllowedError, prevent async playback for mobile triggers, consolidate MessageAudio code, user user-triggered unmutes

* fix: Firefox/unsupported type for MediaSource hack

* refactor(STT): make icon red when recording. consolidate logic to AudioRecorder component

* fix: revert Redis changes to use separate client for sessions
2024-05-24 12:18:11 -04:00
Arthur Barrett
dcd2e3e62d 🔧 fix(redis): Resolve Redis Standalone vs Cluster mode discrepancy for social logins (#2848) 2024-05-24 09:54:33 -04:00
Danny Avila
514a502b9c ⏯️ fix(tts): Resolve Voice Selection and Manual Playback Issues (#2845)
* fix: voice setting for autoplayback TTS

* fix(useTextToSpeechExternal): resolve stateful playback issues and consolidate state logic

* refactor: initialize tts voice and provider schema once per request

* fix(tts): edge case, longer text inputs. TODO: use continuous stream for longer text inputs

* fix(tts): pause global audio on conversation change

* refactor: keyvMongo ban cache to allow db updates for unbanning, to prevent server restart

* chore: eslint fix

* refactor: make ban cache exclusively keyvMongo
2024-05-23 16:27:36 -04:00
Danny Avila
8e66683577 🗣️ fix(tts): Add Text Parser for Message Content Parts (#2840)
* fix: manual TTS trigger for message content parts

* ci(streamAudio): processChunks test
2024-05-22 23:27:37 -04:00
suzuki.sh
dc1778b11f 🐋 chore: add restart: always to meilisearch service in docker-compose.yml (#2788) 2024-05-22 23:20:35 -04:00
nidasfly
795bb9c568 🌏 refactor: Improve Title Prompt for Multilingual Titles (#2832) 2024-05-22 23:18:59 -04:00
gjz
a937650df6 🔧 fix: Recognize azureAssistants Endpoint Config (#2824)
* fix bug: azureAssistants endpoint config

* refactor(assistants): make default value last

* refactor(AppService): make assistantsConfigSetup potentially undefined value last arg

* chore: Update JSDocs params assistants.js

* Update assistants.js

---------

Co-authored-by: 彭修照 <pengxiuzhao.uh@haier.com>
Co-authored-by: Danny Avila <danacordially@gmail.com>
2024-05-22 23:17:52 -04:00
bsu3338
6cf1c85363 🐞 fix: add LocalAI to GET /voices (#2837) 2024-05-22 22:42:27 -04:00
Danny Avila
b3e03b75d0 🔉 feat: Speech-to-text / Text-to-speech (initial support) (#2836)
* Update TextChat.jsx

* Update SubmitButton.jsx

* Update TextChat.jsx

* Update SubmitButton.jsx

* Create ListeningIcon.tsx

* Update index.ts

* Update SubmitButton.jsx

* Update TextChat.jsx

* Update ListeningIcon.tsx

* Update ListeningIcon.tsx

* Create SpeechRecognition.tsx

* Update TextChat.jsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SubmitButton.jsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Create SpeechSynthesis.tsx

* Update index.jsx

* Update SpeechSynthesis.tsx

* Update SpeechRecognition.tsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update TextChat.jsx

* Squashed commit of the following:

commit 28230d9305
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Sun Sep 3 02:44:26 2023 +0200

    feat: delete button confirm (#875)

    * base for confirm delete

    * more like OpenAI

commit 2b54e3f9fe
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Fri Sep 1 14:20:51 2023 -0400

    update: install script (#858)

commit 1cd0fd9d5a
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Fri Sep 1 08:12:35 2023 -0400

    doc: Hugging Face Deployment (#867)

    * docs: update ToC

    * docs: update ToC

    * update huggingface.md

    * update render.md

    * update huggingface.md

    * update mongodb.md

    * update huggingface.md

    * update README.md

commit aeeb3d3050
Author: Mu Yuan <yuanmu.email@gmail.com>
Date:   Thu Aug 31 07:21:27 2023 +0800

    Update Zh.tsx (#862)

    * Update Zh.tsx

    Changed the translation of several words to make it more relevant to Chinese usage habits.

    * Update Zh.tsx

    Changed the translation of several words to make it more relevant to Chinese usage habits

commit 80e2e2675b
Author: Raí <140329135+itzraiss@users.noreply.github.com>
Date:   Mon Aug 28 18:05:46 2023 -0300

    Translation of 'com_ui_pay_per_call:' to Spanish and Portuguese that were missing. (#857)

    * Update Br.tsx

    * Update Es.tsx

    * Update Br.tsx

    * Update Es.tsx

commit 3574d0b823
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 14:49:26 2023 -0400

    docs: make_your_own.md formatting fix for mkdocs (#855)

commit d672ac690d
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 14:24:10 2023 -0400

    Release v0.5.8 (#854)

    * chore: add 'api' image to tag release workflow

    * docs: update DO deployment docs to include instruction about latest stable release, as well as security best practices

    * Release v0.5.8

    * docs: Update digitalocean.md with firewall section images

    * docs: make_your_own.md formatting fix for mkdocs

commit d3e7627046
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 12:03:08 2023 -0400

    refactor(plugins): Improve OpenAPI handling, Show Multiple Plugins, & Other Improvements (#845)

    * feat(PluginsClient.js): add conversationId to options object in the constructor
    feat(PluginsClient.js): add support for Code Interpreter plugin
    feat(PluginsClient.js): add support for Code Interpreter plugin in the availableTools manifest
    feat(CodeInterpreter.js): add CodeInterpreterTools module
    feat(CodeInterpreter.js): add RunCommand class
    feat(CodeInterpreter.js): add ReadFile class
    feat(CodeInterpreter.js): add WriteFile class
    feat(handleTools.js): add support for loading Code Interpreter plugin

    * chore(api): update langchain dependency to version 0.0.123

    * fix(CodeInterpreter.js): add support for extracting environment from code
    fix(WriteFile.js): add support for extracting environment from data
    fix(extractionChain.js): add utility functions for creating extraction chain from Zod schema
    fix(handleTools.js): refactor getOpenAIKey function to handle user-provided API key
    fix(handleTools.js): pass model and openAIApiKey to CodeInterpreter constructor

    * fix(tools): rename CodeInterpreterTools to E2BTools
    fix(tools): rename code_interpreter pluginKey to e2b_code_interpreter

    * chore(PluginsClient.js): comment out unused import and function findMessageContent
    feat(PluginsClient.js): add support for CodeSherpa plugin
    feat(PluginsClient.js): add CodeSherpaTools to available tools
    feat(PluginsClient.js): update manifest.json to include CodeSherpa plugin
    feat(CodeSherpaTools.js): create RunCode and RunCommand classes for CodeSherpa plugin

    feat(E2BTools.js): Add E2BTools module for extracting environment from code and running commands, reading and writing files
    fix(codesherpa.js): Remove codesherpa module as it is no longer needed

    feat(handleTools.js): add support for CodeSherpaTools in loadTools function
    feat(loadToolSuite.js): create loadToolSuite utility function to load a suite of tools

    * feat(PluginsClient.js): add support for CodeSherpa v2 plugin
    feat(PluginsClient.js): add CodeSherpa v1 plugin to available tools
    feat(PluginsClient.js): add CodeSherpa v2 plugin to available tools
    feat(PluginsClient.js): update manifest.json for CodeSherpa v1 plugin
    feat(PluginsClient.js): update manifest.json for CodeSherpa v2 plugin
    feat(CodeSherpa.js): implement CodeSherpa plugin for interactive code and shell command execution
    feat(CodeSherpaTools.js): implement RunCode and RunCommand plugins for CodeSherpa v1
    feat(CodeSherpaTools.js): update RunCode and RunCommand plugins for CodeSherpa v2

    fix(handleTools.js): add CodeSherpa import statement
    fix(handleTools.js): change pluginKey from 'codesherpa' to 'codesherpa_tools'
    fix(handleTools.js): remove model and openAIApiKey from options object in e2b_code_interpreter tool
    fix(handleTools.js): remove openAIApiKey from options object in codesherpa_tools tool
    fix(loadToolSuite.js): remove model and openAIApiKey parameters from loadToolSuite function

    * feat(initializeFunctionsAgent.js): add prefix to agentArgs in initializeFunctionsAgent function

    The prefix is added to the agentArgs in the initializeFunctionsAgent function. This prefix is used to provide instructions to the agent when it receives any instructions from a webpage, plugin, or other tool. The agent will notify the user immediately and ask them if they wish to carry out or ignore the instructions.

    * feat(PluginsClient.js): add ChatTool to the list of tools if it meets the conditions
    feat(tools/index.js): import and export ChatTool
    feat(ChatTool.js): create ChatTool class with necessary properties and methods

    * fix(initializeFunctionsAgent.js): update PREFIX message to include sharing all output from the tool
    fix(E2BTools.js): update descriptions for RunCommand, ReadFile, and WriteFile plugins to provide more clarity and context

    * chore: rebuild package-lock after rebase

    * chore: remove deleted file from rebase

    * wip: refactor plugin message handling to mirror chat.openai.com, handle incoming stream for plugin use

    * wip: new plugin handling

    * wip: show multiple plugins handling

    * feat(plugins): save new plugins array

    * chore: bump langchain

    * feat(experimental): support streaming in between plugins

    * refactor(PluginsClient): factor out helper methods to avoid bloating the class, refactor(gptPlugins): use agent action for mapping the name of action

    * fix(handleTools): fix tests by adding condition to return original toolFunctions map

    * refactor(MessageContent): Allow the last index to be last in case it has text (may change with streaming)

    * feat(Plugins): add handleParsingErrors, useful when LLM does not invoke function params

    * chore: edit out experimental codesherpa integration

    * refactor(OpenAPIPlugin): rework tool to be 'function-first', as the spec functions are explicitly passed to agent model

    * refactor(initializeFunctionsAgent): improve error handling and system message

    * refactor(CodeSherpa, Wolfram): optimize token usage by delegating bulk of instructions to system message

    * style(Plugins): match official style with input/outputs

    * chore: remove unnecessary console logs used for testing

    * fix(abortMiddleware): render markdown when message is aborted

    * feat(plugins): add BrowserOp

    * refactor(OpenAPIPlugin): improve prompt handling

    * fix(useGenerations): hide edit button when message is submitting/streaming

    * refactor(loadSpecs): optimize OpenAPI spec loading by only loading requested specs instead of all of them

    * fix(loadSpecs): will retain original behavior when no tools are passed to the function

    * fix(MessageContent): ensure cursor only shows up for last message and last display index
    fix(Message): show legacy plugin and pass isLast to Content

    * chore: remove console.logs

    * docs: update docs based on breaking changes and new features
    refactor(structured/SD): use description_for_model for detailed prompting

    * docs(azure): make plugins section more clear

    * refactor(structured/SD): change default payload to SD-WebUI to prefer realism and config for SDXL

    * refactor(structured/SD): further improve system message prompt

    * docs: update breaking changes after rebase

    * refactor(MessageContent): factor out EditMessage, types, Container to separate files, rename Content -> Markdown

    * fix(CodeInterpreter): linting errors

    * chore: reduce browser console logs from message streams

    * chore: re-enable debug logs for plugins/langchain to help with user troubleshooting

    * chore(manifest.json): add [Experimental] tag to CodeInterpreter plugins, which are not intended as the end-all be-all implementation of this feature for Librechat

commit 66b8580487
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Mon Aug 28 09:18:25 2023 -0400

    docs: third-party tools (#848)

    * docs: third-party tools

    * docs: third-party tools

    * Update third-party.md

    * Update third-party.md

    ---------

    Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>

commit 9791a78161
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Mon Aug 28 15:14:05 2023 +0200

    adjust the animation (#843)

commit 3797ec6082
Author: Ronith <87087292+ronith256@users.noreply.github.com>
Date:   Mon Aug 28 18:43:50 2023 +0530

    feat: Add Code Interpreter Plugin (#837)

    * feat: Add Code Interpreter Plugin

    Adds a Simple Code Interpreter Plugin.
    ## Features:
    - Runs code using local Python Environment

    ## Issues
    - Code execution is not sandboxed.

    * Add Docker Sandbox for Python Server

commit e2397076a2
Author: Alex Zhang <ztc2011@gmail.com>
Date:   Mon Aug 28 00:55:34 2023 +0800

    🌐: Chinese Translation (#846)

commit 50c15c704f
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Sat Aug 26 19:36:59 2023 -0400

    Language translation: Polish (#840)

    * Language translation: Polish

    * Language translation: Polish

    * Revert changes in language-contributions.md

commit 29d3640546
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Sat Aug 26 19:36:25 2023 -0400

    docs: updates (#841)

commit 39c626aa8e
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 25 09:29:19 2023 -0400

    fix: isEdited edge case where latest Message is not saved due to aborting too quickly

commit ae5c06f381
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 25 09:13:50 2023 -0400

    fix(chatGPTBrowser): render markdown formatting by setting isCreatedByUser, fix(useMessageHandler): avoid double appearance of cursor by setting latest message at initial response creation time

commit 9ef1686e18
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Thu Aug 24 20:24:47 2023 -0400

    Update mkdocs.yml

commit 5bbe411569
Author: Flynn <dev@flynnbuckingham.com>
Date:   Thu Aug 24 20:20:37 2023 -0400

    Add podman installation instructions. Update dockerfile to stub env (#819)

    * Added podman container installation docs. Updated dockerfile to stub env file if not present in source

    * Fix typos

commit 887fec99ca
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 25 02:11:27 2023 +0200

    🌐: Russian Translation (#830)

commit 007d51ede1
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 25 02:10:48 2023 +0200

    feat: facebook login (#820)

    * Facebook strategy

    * Update user_auth_system.md

    * Update user_auth_system.md

commit a569020312
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Thu Aug 24 21:59:11 2023 +0200

    Fix Meilisearch error and refactor of the server index.js (#832)

    * fix meilisearch error at startup

    * limit the nesting

    * disable useless console log

    * fix(indexSync.js): removed redundant searchEnabled

    * refactor(index.js): moved configureSocialLogins to a new file

    * refactor(socialLogins.js): removed unnecessary conditional

commit 37347d4683
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Wed Aug 23 16:14:17 2023 -0400

    fix(registration): Make Username optional (#831)

    * fix(User.js): update validation schema for username field, allow empty string as a valid value
    fix(validators.js): update validation schema for username field, allow empty string as a valid value
    fix(Registration.tsx, validators.js): update validation rules for name and username fields, change minimum length to 2 and maximum length to 80, assure they match and allow empty string as a valid value
    fix(Eng.tsx): update localization string for com_auth_username, indicate that it is optional

    * fix(User.js): update regex pattern for username validation to allow special characters @#$%&*()
    fix(validators.js): update regex pattern for username validation to allow special characters @#$%&*()

    * fix(Registration.spec.tsx): fix validation error message for username length requirement

commit d38e463d34
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Wed Aug 23 13:44:40 2023 -0400

    fix(bingAI): markdown and error formatting for final stream response (#829)

    * fix(bingAI): markdown formatting for final stream response due to new strict payload validation on the frontend

    * fix: add missing prop to bing Error response

commit 7dc27b10f1
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Tue Aug 22 18:44:59 2023 -0400

    feat: Edit AI Messages, Edit Messages in Place (#825)

    * refactor: replace lodash import with specific function import

    fix(api): esm imports to cjs

    * refactor(Messages.tsx): convert to TS, out-source scrollToDiv logic to a custom hook
    fix(ScreenshotContext.tsx): change Ref to RefObject in ScreenshotContextType
    feat(useScrollToRef.ts): add useScrollToRef hook for scrolling to a ref with throttle
    fix(Chat.tsx): update import path for Messages component
    fix(Search.tsx): update import path for Messages component

    * chore(types.ts): add TAskProps and TOptions types
    refactor(useMessageHandler.ts): use TAskFunction type for ask function signature

    * refactor(Message/Content): convert to TS, move Plugin component to Content dir

    * feat(MessageContent.tsx): add MessageContent component for displaying and editing message content
    feat(index.ts): export MessageContent component from Messages/Content directory

    * wip(Message.jsx): conversion and use of new component in progress

    * refactor: convert Message.jsx to TS and fix typing/imports based on changes

    * refactor: add typed props and refactor MultiMessage to TS, fix typing issues resulting from the conversion

    * edit message in progress

    * feat: complete edit AI message logic, refactor continue logic

    * feat(middleware): add validateMessageReq middleware
    feat(routes): add validation for message requests using validateMessageReq middleware
    feat(routes): add create, read, update, and delete routes for messages

    * feat: complete frontend logic for editing messages in place
    feat(messages.js): update route for updating a specific message
    - Change the route for updating a message to include the messageId in the URL
    - Update the request handler to use the messageId from the request parameters and the text from the request body
    - Call the updateMessage function with the updated parameters

    feat(MessageContent.tsx): add functionality to update a message
    - Import the useUpdateMessageMutation hook from the data provider
    - Destructure the conversationId, parentMessageId, and messageId from the message object
    - Create a mutation function using the useUpdateMessageMutation hook
    - Implement the updateMessage function to call the mutation function with the updated message parameters
    - Update the messages state to reflect the updated message text

    feat(api-endpoints.ts): update messages endpoint to include messageId
    - Update the messages endpoint to include the messageId as an optional parameter

    feat(data-service.ts): add updateMessage function
    - Implement the updateMessage function to make a PUT request to

    * fix(messages.js): make updateMessage function asynchronous and await its execution

    * style(EditIcon): make icon active for AI message

    * feat(gptPlugins/anthropic): add edit support

    * fix(validateMessageReq.js): handle case when conversationId is 'new' and return empty array
    feat(Message.tsx): pass message prop to SiblingSwitch component
    refactor(SiblingSwitch.tsx): convert to TS

    * fix(useMessageHandler.ts): remove message from currentMessages if isContinued is true
    feat(useMessageHandler.ts): add support for submission messages in setMessages
    fix(useServerStream.ts): remove unnecessary conditional in setMessages
    fix(useServerStream.ts): remove isContinued variable from submission

    * fix(continue): switch to continued message generation when continuing an earlier branch in conversation

    * fix(abortMiddleware.js): fix condition to check partialText length
    chore(abortMiddleware.js): add error logging when abortMessage fails

    * refactor(MessageHeader.tsx): convert to TS
    fix(Plugin.tsx): add default value for className prop in Plugin component

    * refactor(MultiMessage.tsx): remove commented out code
    docs(MultiMessage.tsx): update comment to clarify when siblingIdx is reset

    * fix(GenerationButtons): optimistic state for continue button

    * fix(MessageContent.tsx): add data-testid attribute to message text editor
    fix(messages.spec.ts): update waitForServerStream function to include edit endpoint check
    feat(messages.spec.ts): add test case for editing messages

    * fix(HoverButtons & Message & useGenerations): Refactor edit functionality and related conditions

    - Update enterEdit function signature and prop
    - Create and utilize hideEditButton variable
    - Enhance conditions for edit button visibility and active state
    - Update button event handlers
    - Introduce isEditableEndpoint in useGenerations and refine continueSupported condition.

    * fix(useGenerations.ts): fix condition for hideEditButton to include error and searchResult
    chore(data-provider): bump version to 0.1.6
    fix(types.ts): add status property to TError type

    * chore: bump @dqbd/tiktoken to 1.0.7

    * fix(abortMiddleware.js): add required isCreatedByUser property to the error response object

    * refactor(Message.tsx): remove unnecessary props from SiblingSwitch component, as setLatestMessage is firing on every switch already
    refactor(SiblingSwitch.tsx): remove unused imports and code

    * chore(BaseClient.js): move console.debug statements back inside if block

commit db77163f5d
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Tue Aug 22 14:15:14 2023 +0200

    docs: update chimeragpt (#826)

    * Update free_ai_apis.md

    * Update free_ai_apis.md

commit 4a4e803df3
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Mon Aug 21 20:15:18 2023 +0200

    style(Dialog): Improved Close Button ("X") position (#824)

commit 909b00c752
Author: Daniel Avila <messagedaniel@protonmail.com>
Date:   Sun Aug 20 21:04:36 2023 -0400

    fix(HoverButtons): light/dark styling to match official site

commit 61dcb4d307
Author: Naosuke Yokoe <ankerasoy@gmail.com>
Date:   Sat Aug 19 20:11:31 2023 +0900

    feat: Azure Cognitive Search Plugin (#815)

    * feat(AzureCognitiveSearchPlugin)

    * feat(tools/AzureCognitiveSearch.js): Add a new plugin (not structured
      version)
    * feat(tools/structured/AzureCognitiveSearch.js): Add a new plugin (structured version)
    * feat(tools/manifest.json, tools/index.js, tools/util/handleTools.js):
      Add configurations for the plugin
    * feat(api/package.json, package-lock.json): Installed a new package for the
      plugin (@azure/search-documents)
    * feat(.env.example): Add new environment variables for the plugin

    Here is the link to the corresponding discussion page:
    https://github.com/danny-avila/LibreChat/discussions/567

    * docs(AzureCognitiveSearchPlugin)

    * docs(features/plugins/azure_cognitive_search.md): Add a new document
      for the plugin

    * (fix:.env.example)

    * reverted extra whitespaces removed by the editor

    * docs(mkdocs.yml)

    * Add the Azure Cognitive Search Plugin's documentation item to
    mkdocs.yml.

commit 3c7f67fa76
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 18 12:40:33 2023 -0400

    fix(abortMiddleware): handle early abort error where userMessage.conversationId is undefined. In this case, the userId will be used as the abortKey

commit c74c68a135
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 18 12:10:30 2023 -0400

    refactor(MessageHandler -> useServerStream): convert all relating files to TS and correct typings based on this change: properly refactor MessageHandler to a custom hook, where it's passed a submission object to instantiate the stream. This is the bare minimum groundwork for potentially having multiple streams running, which would be a big project to modularize a lot of the global state into maps/multiple streams, particular useful for having multiple views in place

commit 8b4d3c2c21
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 18 12:04:29 2023 -0400

    refactor(routes): convert to TS

commit d612cfcb45
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 18 12:02:39 2023 -0400

    chore(Auth): reorder exports in Auth component
    fix(PluginAuthForm): handle case when pluginKey is null or undefined
    fix(PluginStoreDialog): handle case when getAvailablePluginFromKey is null or undefined
    fix(AuthContext): make authConfig optional in AuthContextProvider
    feat(hooks): add useServerStream hook
    fix(conversation): setSubmission to null instead of empty object
    fix(preset): specify type for presets atom
    fix(search): specify type for isSearchEnabled atom
    fix(submission): specify type for submission atom

commit c40b95f424
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 18 16:11:00 2023 +0200

    feat: Disable Registration with social login (#813)

    * Google, Github and Discord

    * update .env.example with ALLOW_SOCIAL_REGISTRATION

    * fix some conflict

    * refactor strategy

    * Update user_auth_system.md

    * Update user_auth_system.md

commit 46ed5aaccd
Author: Patrick <psarnowski@gmail.com>
Date:   Fri Aug 18 09:38:24 2023 -0400

    Show the response scores from Bing. (#814)

commit 1dacfa49f0
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Thu Aug 17 20:32:31 2023 +0200

    update profile picture (#792)

commit afd43afb60
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Thu Aug 17 12:50:05 2023 -0400

    feat(GPT/Anthropic): Continue Regenerating & Generation Buttons (#808)

    * feat(useMessageHandler.js/ts): Refactor and add features to handle user messages, support multiple endpoints/models, generate placeholder responses, regeneration, and stopGeneration function

    fix(conversation.ts, buildTree.ts): Import TMessage type, handle null parentMessageId

    feat(schemas.ts): Update and add schemas for various AI services, add default values, optional fields, and endpoint-to-schema mapping, create parseConvo function

    chore(useMessageHandler.js, schemas.ts): Remove unused imports, variables, and chatGPT enum

    * wip: add generation buttons

    * refactor(cleanupPreset.ts): simplify cleanupPreset function
    refactor(getDefaultConversation.js): remove unused code and simplify getDefaultConversation function

    feat(utils): add getDefaultConversation function

    This commit adds a new utility function called `getDefaultConversation` to the `client/src/utils/getDefaultConversation.ts` file. This function is responsible for generating a default conversation object based on the provided parameters.

    The `getDefaultConversation` function takes in an object with the following properties:
    - `conversation`: The conversation object to be used as a base.
    - `endpointsConfig`: The configuration object containing information about the available endpoints.
    - `preset`: An optional preset object that can be used to override the default behavior.

    The function first tries to determine the target endpoint based on the preset object. If a valid endpoint is found, it is used as the target endpoint. If not, the function tries to retrieve the last conversation setup from the local storage and uses its endpoint if it is valid. If neither the preset nor the local storage contains a valid endpoint, the function falls back to a default endpoint.

    Once the target endpoint is determined,

    * fix(utils): remove console.error statement in buildDefaultConversation function
    fix(schemas): add default values for catch blocks in openAISchema, googleSchema, bingAISchema, anthropicSchema, chatGPTBrowserSchema, and gptPluginsSchema

    * fix: endpoint not changing on change of preset from other endpoint, wip: refactor

    * refactor: preset items to TSX

    * refactor: convert resetConvo to TS

    * refactor(getDefaultConversation.ts): move defaultEndpoints array to the top of the file for better readability
    refactor(getDefaultConversation.ts): extract getDefaultEndpoint function for better code organization and reusability

    * feat(svg): add ContinueIcon component
    feat(svg): add RegenerateIcon component
    feat(svg): add ContinueIcon and RegenerateIcon components to index.ts

    * feat(Button.tsx): add onClick and className props to Button component
    feat(GenerationButtons.tsx): add logic to display Regenerate or StopGenerating button based on isSubmitting and messages
    feat(Regenerate.tsx): create Regenerate component with RegenerateIcon and handleRegenerate function
    feat(StopGenerating.tsx): create StopGenerating component with StopGeneratingIcon and handleStopGenerating function

    * fix(TextChat.jsx): reorder imports and variables for better readability
    fix(TextChat.jsx): fix typo in condition for isNotAppendable variable
    fix(TextChat.jsx): remove unused handleStopGenerating function
    fix(ContinueIcon.tsx): remove unnecessary closing tags for polygon elements
    fix(useMessageHandler.ts): add missing type annotations for handleStopGenerating and handleRegenerate functions
    fix(useMessageHandler.ts): remove unused variables in return statement

    * fix(getDefaultConversation.ts): refactor code to use getLocalStorageItems function
    feat(getLocalStorageItems.ts): add utility function to retrieve items from local storage

    * fix(OpenAIClient.js): add support for streaming result in sendCompletion method
    feat(OpenAIClient.js): add finish_reason metadata to opts in sendCompletion method
    feat(Message.js): add finish_reason field to Message model
    feat(messageSchema.js): add finish_reason field to messageSchema
    feat(openAI.js): parse chatGptLabel and promptPrefix from req.body and pass rest of the modelOptions to endpointOption
    feat(openAI.js): add addMetadata function to store metadata in ask function
    feat(openAI.js): add metadata to response if available
    feat(schemas.ts): add finish_reason field to tMessageSchema

    * feat(types.ts): add TOnClick and TGenButtonProps types for button components
    feat(Continue.tsx): create Continue component for generating button
    feat(GenerationButtons.tsx): update GenerationButtons component to use Continue component
    feat(Regenerate.tsx): create Regenerate component for regenerating button
    feat(Stop.tsx): create Stop component for stop generating button

    * feat(MessageHandler.jsx): add MessageHandler component to handle messages and conversations
    fix(Root.jsx): fix import paths for Nav and MessageHandler components

    * feat(useMessageHandler.ts): add support for generation parameter in ask function
    feat(useMessageHandler.ts): add support for isEdited parameter in ask function
    feat(useMessageHandler.ts): add support for continueGeneration function
    fix(createPayload.ts): replace endpoint URL when isEdited parameter is true

    * chore(client): set skipLibCheck to true in tsconfig.json

    * fix(useMessageHandler.ts): remove unused clientId variable
    fix(schemas.ts): make clientId field in tMessageSchema nullable and optional

    * wip: edit route for continue generation

    * refactor(api): move handlers to root of routes dir

    * fix(useMessageHandler.ts): initialize currentMessages to an empty array if messages is null
    fix(useMessageHandler.ts): update initialResponse text to use responseText variable
    fix(useMessageHandler.ts): update setMessages logic for isRegenerate case
    fix(MessageHandler.jsx): update setMessages logic for cancelHandler, createdHandler, and finalHandler

    * fix(schemas.ts): make createdAt and updatedAt fields optional and set default values using new Date().toISOString()
    fix(schemas.ts): change type annotation of TMessage from infer to input

    * refactor(useMessageHandler.ts): rename AskProps type to TAskProps
    refactor(useMessageHandler.ts): remove generation property from ask function arguments
    refactor(useMessageHandler.ts): use nullish coalescing operator (??) instead of logical OR (||)
    refactor(useMessageHandler.ts): pass the responseMessageId to message prop of submission

    * fix(BaseClient.js): use nullish coalescing operator (??) instead of logical OR (||) for default values

    * fix(BaseClient.js): fix responseMessageId assignment in handleStartMethods method
    feat(BaseClient.js): add support for isEdited flag in sendMessage method
    feat(BaseClient.js): add generation to responseMessage text in sendMessage method

    * fix(openAI.js): remove unused imports and commented out code
    feat(openAI.js): add support for generation parameter in request body
    fix(openAI.js): remove console.log statement
    fix(openAI.js): remove unused variables and parameters
    fix(openAI.js): update response text in case of error
    fix(openAI.js): handle error and abort message in case of error
    fix(handlers.js): add generation parameter to createOnProgress function
    fix(useMessageHandler.ts): update responseText variable to use generation parameter

    * refactor(api/middleware): move inside server dir

    * refactor: add endpoint specific, modular functions to build options and initialize clients, create server/utils, move middleware, separate utils into api general utils and server specific utils

    * fix(abortMiddleware.js): import getConvo and getConvoTitle functions from models
    feat(abortMiddleware.js): add abortAsk function to abortController to handle aborting of requests
    fix(openAI.js): import buildOptions and initializeClient functions from endpoints/openAI
    refactor(openAI.js): use getAbortData function to get data for abortAsk function

    * refactor: move endpoint specific logic to an endpoints dir

    * refactor(PluginService.js): fix import path for encrypt and decrypt functions in PluginService.js

    * feat(openAI): add new endpoint for adding a title to a conversation

    - Added a new file `addTitle.js` in the `api/server/routes/endpoints/openAI` directory.
    - The `addTitle.js` file exports a function `addTitle` that takes in request parameters and performs the following actions:
      - If the `parentMessageId` is `'00000000-0000-0000-0000-000000000000'` and `newConvo` is true, it proceeds with the following steps:
        - Calls the `titleConvo` function from the `titleConvo` module, passing in the necessary parameters.
        - Calls the `saveConvo` function from the `saveConvo` module, passing in the user ID and conversation details.
    - Updated the `index.js` file in the `api/server/routes/endpoints/openAI` directory to export the `addTitle` function.
    - This change adds

    * fix(abortMiddleware.js): remove console.log statement
    refactor(gptPlugins.js): update imports and function parameters
    feat(gptPlugins.js): add support for abortController and getAbortData
    refactor(openAI.js): update imports and function parameters
    feat(openAI.js): add support for abortController and getAbortData

    fix(openAI.js): refactor code to use modularized functions and middleware
    fix(buildOptions.js): refactor code to use destructuring and update variable names

    * refactor(askChatGPTBrowser.js, bingAI.js, google.js): remove duplicate code for setting response headers
    feat(askChatGPTBrowser.js, bingAI.js, google.js): add setHeaders middleware to set response headers

    * feat(middleware): validateEndpoint, refactor buildOption to only be concerned of endpointOption

    * fix(abortMiddleware.js): add 'finish_reason' property with value 'incomplete' to responseMessage object
    fix(abortMessage.js): remove console.log statement for aborted message
    fix(handlers.js): modify tokens assignment to handle empty generation string and trailing space

    * fix(BaseClient.js): import addSpaceIfNeeded function from server/utils
    fix(BaseClient.js): add space before generation in text property
    fix(index.js): remove getCitations and citeText exports
    feat(buildEndpointOption.js): add buildEndpointOption middleware
    fix(index.js): import buildEndpointOption middleware
    fix(anthropic.js): remove buildOptions function and use endpointOption from req.body
    fix(gptPlugins.js): remove buildOptions function and use endpointOption from req.body
    fix(openAI.js): remove buildOptions function and use endpointOption from req.body

    feat(utils): add citations.js and handleText.js modules
    fix(utils): fix import statements in index.js module

    * refactor(gptPlugins.js): use getResponseSender function from librechat-data-provider

    * feat(gptPlugins): complete 'continue generating'

    * wip: anthropic continue regen

    * feat(middleware): add validateRegistration middleware

    A new middleware function called `validateRegistration` has been added to the list of exported middleware functions in `index.js`. This middleware is responsible for validating registration data before allowing the registration process to proceed.

    * feat(Anthropic): complete continue regen

    * chore: add librechat-data-provider to api/package.json

    * fix(ci): backend-review will mock meilisearch, also installs data-provider as now needed

    * chore(ci): remove unneeded SEARCH env var

    * style(GenerationButtons): make text shorter for sake of space economy, even though this diverges from chat.openai.com

    * style(GenerationButtons/ScrollToBottom): adjust visibility/position based on screen size

    * chore(client): 'Editting' typo

    * feat(GenerationButtons.tsx): add support for endpoint prop in GenerationButtons component
    feat(OptionsBar.tsx): pass endpoint prop to GenerationButtons component
    feat(useGenerations.ts): create useGenerations hook to handle generation logic
    fix(schemas.ts): add searchResult field to tMessageSchema

    * refactor(HoverButtons): convert to TSX and utilize new useGenerations hook

    * fix(abortMiddleware): handle error with res headers set, or abortController not found, to ensure proper API error is sent to the client, chore(BaseClient): remove console log for onStart message meant for debugging

    * refactor(api): remove librechat-data-provider dep for now as it complicates deployed docker build stage, re-use code in CJS, located in server/endpoints/schemas

    * chore: remove console.logs from test files

    * ci: add backend tests for AnthropicClient, focusing on new buildMessages logic

    * refactor(FakeClient): use actual BaseClient sendMessage method for testing

    * test(BaseClient.test.js): add test for loading chat history
    test(BaseClient.test.js): add test for sendMessage logic with isEdited flag

    * fix(buildEndpointOption.js): add support for azureOpenAI in buildFunction object
    wip(endpoints.js): fetch Azure models from Azure OpenAI API if opts.azure is true

    * fix(Button.tsx): add data-testid attribute to button component
    fix(SelectDropDown.tsx): add data-testid attribute to Listbox.Button component
    fix(messages.spec.ts): add waitForServerStream function to consolidate logic for awaiting the server response
    feat(messages.spec.ts): add test for stopping and continuing message and improve browser/page context order and closing

    * refactor(onProgress): speed up time to save initial message for editable routes

    * chore: disable AI message editing (for now), was accidentally allowed

    * refactor: ensure continue is only supported for latest message style: improve styling in dark mode and across all hover buttons/icons, including making edit icon for AI invisible (for now)

    * fix: add test id to generation buttons so they never resolve to 2+ items

    * chore(package.json): add 'packages/' to the list of ignored directories
    chore(data-provider/package.json): bump version to 0.1.5

commit ae5b7d3d53
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Tue Aug 15 18:27:54 2023 -0400

    fix(PluginsClient.js): fix ChatOpenAI Azure Config Issue (#812)

    * fix(PluginsClient.js): fix issue with creating LLM when using Azure

    * chore(PluginsClient.js): omit azure logging

    * refactor(PluginsClient.js): simplify assignment of azure variable

    The code was simplified by directly assigning the value of `this.azure` to the `azure` variable using object destructuring. This makes the code cleaner and more concise.

commit b85f3bf91e
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Tue Aug 15 18:42:24 2023 +0200

    update from lang to localize (#810)

commit 80aab73bf6
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Mon Aug 14 19:19:04 2023 -0400

    chore: rebuilt package-lock file

commit bbe4931a97
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Mon Aug 14 19:13:24 2023 -0400

    refactor(ScreenshotContext): use html-to-image for lighter bundle, faster processing

commit 74802dd720
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Mon Aug 14 17:51:03 2023 +0200

    chore: Translation Fixes, Lint Error Corrections, and Additional Translations (#788)

    * fix translation and small lint error

    * changed from localize to useLocalize hook

    * changed to useLocalize

commit b64cc71d88
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 14 10:23:00 2023 -0400

    chore(docker-compose.yml): comment out meilisearch ports in docker-compose.yml (#807)

commit 89f260bc78
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 14 10:12:00 2023 -0400

    fix(CodeBlock.tsx): fix copy-to-clipboard functionality. The code has been updated to use the copy function from the copy-to-clipboard library instead of the (#806)

    avigator.clipboard.writeText method. This should fix the issue with browser incompatibility with navigator SDK and allow users to copy code from the CodeBlock component successfully.

commit d00c7354cd
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 14 09:45:44 2023 -0400

    fix: Corrected Registration Validation, Case-Insensitive Variable Handling, Playwright workflow (#805)

    * feat(auth.js): add validation for registration endpoint using validateRegistration middleware
    feat(validateRegistration.js): add middleware to validate registration based on ALLOW_REGISTRATION environment variable

    * fix(config.js): fix registrationEnabled and socialLoginEnabled variables to handle case-insensitive environment variable values

    * refactor(validateRegistration.js): remove console.log statement

    * chore(playwright.yml): skip browser download during yarn install
    chore(playwright.yml): place Playwright binaries to node_modules/@playwright/test
    chore(playwright.yml): install Playwright dependencies using npx playwright install-deps
    chore(playwright.yml): install Playwright chromium browser using npx playwright install chromium
    chore(playwright.yml): install @playwright/test@latest using npm install -D @playwright/test@latest
    chore(playwright.yml): run Playwright tests using npm run e2e:ci

    * chore(playwright.yml): change npm install order and update comment

    The order of the npm install commands in the "Install Playwright Browsers" step has been changed to first install @playwright/test@latest and then install chromium. Additionally, the comment explaining the PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD variable has been updated to mention npm install instead of yarn install.

    * chore(playwright.yml): remove commented out code for caching and add separate steps for installing Playwright dependencies and browsers

commit 1aa4b34dc6
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 11 19:02:52 2023 +0200

    added the dot (.) username rules (#787)

* Create VolumeMuteIcon.tsx

* Create VolumeIcon.tsx

* Update index.ts

* Update SubmitButton.jsx

* Update SubmitButton.jsx

* Update TextChat.jsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update TextChat.jsx

* Update SpeechRecognition.tsx

* Update TextChat.jsx

* Update HoverButtons.tsx

* Update useServerStream.ts

* Update useServerStream.ts

* Update HoverButtons.tsx

* Update useServerStream.ts

* Update useServerStream.ts

* Update HoverButtons.tsx

* Update VolumeIcon.tsx

* Update VolumeMuteIcon.tsx

* Update HoverButtons.tsx

* Update SpeechSynthesis.tsx

* Update HoverButtons.tsx

* Update HoverButtons.tsx

* Update SpeechSynthesis.tsx

* Update SpeechSynthesis.tsx

* Update HoverButtons.tsx

* Update SpeechSynthesis.tsx

* Update package.json

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Update SpeechRecognition.tsx

* Squashed commit of the following:

commit 1019529634
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 23:12:14 2023 -0500

    Update SpeechRecognition.tsx

commit 67f111ccd0
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 23:08:48 2023 -0500

    Update SpeechRecognition.tsx

commit 0b35dbe196
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 23:04:50 2023 -0500

    Update SpeechRecognition.tsx

commit 6686126dc0
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:49:08 2023 -0500

    Update SpeechRecognition.tsx

commit 5b80ddfba7
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:45:02 2023 -0500

    Update package.json

commit 39e84efa81
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:35:48 2023 -0500

    Update SpeechSynthesis.tsx

commit 4c6d067cb9
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:24:29 2023 -0500

    Update HoverButtons.tsx

commit c5ce576fb8
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:13:20 2023 -0500

    Update SpeechSynthesis.tsx

commit d95fa19539
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:11:38 2023 -0500

    Update SpeechSynthesis.tsx

commit c794f07678
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 22:03:34 2023 -0500

    Update HoverButtons.tsx

commit 7ae0e7e97c
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:59:45 2023 -0500

    Update HoverButtons.tsx

commit e9882dedad
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:58:07 2023 -0500

    Update SpeechSynthesis.tsx

commit 95cf300782
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:44:49 2023 -0500

    Update HoverButtons.tsx

commit 37c828d7fb
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:30:34 2023 -0500

    Update VolumeMuteIcon.tsx

commit 6133531737
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:29:54 2023 -0500

    Update VolumeIcon.tsx

commit 4b4afcdd37
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 21:20:14 2023 -0500

    Update HoverButtons.tsx

commit 609d1dfefb
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 20:49:52 2023 -0500

    Update useServerStream.ts

commit 875ce4b77e
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 20:48:26 2023 -0500

    Update useServerStream.ts

commit 8ed04e496b
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 20:37:59 2023 -0500

    Update HoverButtons.tsx

commit 4b30c132df
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 20:14:01 2023 -0500

    Update useServerStream.ts

commit c041c329cf
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 20:07:14 2023 -0500

    Update useServerStream.ts

commit 3e36c16817
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:36:21 2023 -0500

    Update HoverButtons.tsx

commit c7eea96759
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:28:03 2023 -0500

    Update TextChat.jsx

commit 5542f8e85d
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:21:50 2023 -0500

    Update SpeechRecognition.tsx

commit 9a27e56f8b
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:16:01 2023 -0500

    Update TextChat.jsx

commit 7f101bd122
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:09:51 2023 -0500

    Update SpeechRecognition.tsx

commit d405454bf5
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:03:34 2023 -0500

    Update SpeechRecognition.tsx

commit 6033eb3ed1
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 19:01:06 2023 -0500

    Update TextChat.jsx

commit 9a3e67fcd2
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 18:53:19 2023 -0500

    Update TextChat.jsx

commit 6583877cb3
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 17:53:18 2023 -0500

    Update SubmitButton.jsx

commit 8d5114bfae
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 17:39:20 2023 -0500

    Update SubmitButton.jsx

commit 29a5b55883
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 17:28:03 2023 -0500

    Update index.ts

commit b03001d01d
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 17:25:43 2023 -0500

    Create VolumeIcon.tsx

commit 863af2c959
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 17:21:43 2023 -0500

    Create VolumeMuteIcon.tsx

commit ad3c78f867
Merge: ed4b25b2 28230d93
Author: bsu3338 <bsu3338@users.noreply.github.com>
Date:   Sun Sep 3 16:49:56 2023 -0500

    Merge branch 'danny-avila:main' into Speech-September

commit ed4b25b2c1
Author: bsu3338 <bsu3338@yahoo.com>
Date:   Sun Sep 3 16:49:03 2023 -0500

    Squashed commit of the following:

    commit 28230d9305
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Sun Sep 3 02:44:26 2023 +0200

        feat: delete button confirm (#875)

        * base for confirm delete

        * more like OpenAI

    commit 2b54e3f9fe
    Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
    Date:   Fri Sep 1 14:20:51 2023 -0400

        update: install script (#858)

    commit 1cd0fd9d5a
    Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
    Date:   Fri Sep 1 08:12:35 2023 -0400

        doc: Hugging Face Deployment (#867)

        * docs: update ToC

        * docs: update ToC

        * update huggingface.md

        * update render.md

        * update huggingface.md

        * update mongodb.md

        * update huggingface.md

        * update README.md

    commit aeeb3d3050
    Author: Mu Yuan <yuanmu.email@gmail.com>
    Date:   Thu Aug 31 07:21:27 2023 +0800

        Update Zh.tsx (#862)

        * Update Zh.tsx

        Changed the translation of several words to make it more relevant to Chinese usage habits.

        * Update Zh.tsx

        Changed the translation of several words to make it more relevant to Chinese usage habits

    commit 80e2e2675b
    Author: Raí <140329135+itzraiss@users.noreply.github.com>
    Date:   Mon Aug 28 18:05:46 2023 -0300

        Translation of 'com_ui_pay_per_call:' to Spanish and Portuguese that were missing. (#857)

        * Update Br.tsx

        * Update Es.tsx

        * Update Br.tsx

        * Update Es.tsx

    commit 3574d0b823
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 28 14:49:26 2023 -0400

        docs: make_your_own.md formatting fix for mkdocs (#855)

    commit d672ac690d
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 28 14:24:10 2023 -0400

        Release v0.5.8 (#854)

        * chore: add 'api' image to tag release workflow

        * docs: update DO deployment docs to include instruction about latest stable release, as well as security best practices

        * Release v0.5.8

        * docs: Update digitalocean.md with firewall section images

        * docs: make_your_own.md formatting fix for mkdocs

    commit d3e7627046
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 28 12:03:08 2023 -0400

        refactor(plugins): Improve OpenAPI handling, Show Multiple Plugins, & Other Improvements (#845)

        * feat(PluginsClient.js): add conversationId to options object in the constructor
        feat(PluginsClient.js): add support for Code Interpreter plugin
        feat(PluginsClient.js): add support for Code Interpreter plugin in the availableTools manifest
        feat(CodeInterpreter.js): add CodeInterpreterTools module
        feat(CodeInterpreter.js): add RunCommand class
        feat(CodeInterpreter.js): add ReadFile class
        feat(CodeInterpreter.js): add WriteFile class
        feat(handleTools.js): add support for loading Code Interpreter plugin

        * chore(api): update langchain dependency to version 0.0.123

        * fix(CodeInterpreter.js): add support for extracting environment from code
        fix(WriteFile.js): add support for extracting environment from data
        fix(extractionChain.js): add utility functions for creating extraction chain from Zod schema
        fix(handleTools.js): refactor getOpenAIKey function to handle user-provided API key
        fix(handleTools.js): pass model and openAIApiKey to CodeInterpreter constructor

        * fix(tools): rename CodeInterpreterTools to E2BTools
        fix(tools): rename code_interpreter pluginKey to e2b_code_interpreter

        * chore(PluginsClient.js): comment out unused import and function findMessageContent
        feat(PluginsClient.js): add support for CodeSherpa plugin
        feat(PluginsClient.js): add CodeSherpaTools to available tools
        feat(PluginsClient.js): update manifest.json to include CodeSherpa plugin
        feat(CodeSherpaTools.js): create RunCode and RunCommand classes for CodeSherpa plugin

        feat(E2BTools.js): Add E2BTools module for extracting environment from code and running commands, reading and writing files
        fix(codesherpa.js): Remove codesherpa module as it is no longer needed

        feat(handleTools.js): add support for CodeSherpaTools in loadTools function
        feat(loadToolSuite.js): create loadToolSuite utility function to load a suite of tools

        * feat(PluginsClient.js): add support for CodeSherpa v2 plugin
        feat(PluginsClient.js): add CodeSherpa v1 plugin to available tools
        feat(PluginsClient.js): add CodeSherpa v2 plugin to available tools
        feat(PluginsClient.js): update manifest.json for CodeSherpa v1 plugin
        feat(PluginsClient.js): update manifest.json for CodeSherpa v2 plugin
        feat(CodeSherpa.js): implement CodeSherpa plugin for interactive code and shell command execution
        feat(CodeSherpaTools.js): implement RunCode and RunCommand plugins for CodeSherpa v1
        feat(CodeSherpaTools.js): update RunCode and RunCommand plugins for CodeSherpa v2

        fix(handleTools.js): add CodeSherpa import statement
        fix(handleTools.js): change pluginKey from 'codesherpa' to 'codesherpa_tools'
        fix(handleTools.js): remove model and openAIApiKey from options object in e2b_code_interpreter tool
        fix(handleTools.js): remove openAIApiKey from options object in codesherpa_tools tool
        fix(loadToolSuite.js): remove model and openAIApiKey parameters from loadToolSuite function

        * feat(initializeFunctionsAgent.js): add prefix to agentArgs in initializeFunctionsAgent function

        The prefix is added to the agentArgs in the initializeFunctionsAgent function. This prefix is used to provide instructions to the agent when it receives any instructions from a webpage, plugin, or other tool. The agent will notify the user immediately and ask them if they wish to carry out or ignore the instructions.

        * feat(PluginsClient.js): add ChatTool to the list of tools if it meets the conditions
        feat(tools/index.js): import and export ChatTool
        feat(ChatTool.js): create ChatTool class with necessary properties and methods

        * fix(initializeFunctionsAgent.js): update PREFIX message to include sharing all output from the tool
        fix(E2BTools.js): update descriptions for RunCommand, ReadFile, and WriteFile plugins to provide more clarity and context

        * chore: rebuild package-lock after rebase

        * chore: remove deleted file from rebase

        * wip: refactor plugin message handling to mirror chat.openai.com, handle incoming stream for plugin use

        * wip: new plugin handling

        * wip: show multiple plugins handling

        * feat(plugins): save new plugins array

        * chore: bump langchain

        * feat(experimental): support streaming in between plugins

        * refactor(PluginsClient): factor out helper methods to avoid bloating the class, refactor(gptPlugins): use agent action for mapping the name of action

        * fix(handleTools): fix tests by adding condition to return original toolFunctions map

        * refactor(MessageContent): Allow the last index to be last in case it has text (may change with streaming)

        * feat(Plugins): add handleParsingErrors, useful when LLM does not invoke function params

        * chore: edit out experimental codesherpa integration

        * refactor(OpenAPIPlugin): rework tool to be 'function-first', as the spec functions are explicitly passed to agent model

        * refactor(initializeFunctionsAgent): improve error handling and system message

        * refactor(CodeSherpa, Wolfram): optimize token usage by delegating bulk of instructions to system message

        * style(Plugins): match official style with input/outputs

        * chore: remove unnecessary console logs used for testing

        * fix(abortMiddleware): render markdown when message is aborted

        * feat(plugins): add BrowserOp

        * refactor(OpenAPIPlugin): improve prompt handling

        * fix(useGenerations): hide edit button when message is submitting/streaming

        * refactor(loadSpecs): optimize OpenAPI spec loading by only loading requested specs instead of all of them

        * fix(loadSpecs): will retain original behavior when no tools are passed to the function

        * fix(MessageContent): ensure cursor only shows up for last message and last display index
        fix(Message): show legacy plugin and pass isLast to Content

        * chore: remove console.logs

        * docs: update docs based on breaking changes and new features
        refactor(structured/SD): use description_for_model for detailed prompting

        * docs(azure): make plugins section more clear

        * refactor(structured/SD): change default payload to SD-WebUI to prefer realism and config for SDXL

        * refactor(structured/SD): further improve system message prompt

        * docs: update breaking changes after rebase

        * refactor(MessageContent): factor out EditMessage, types, Container to separate files, rename Content -> Markdown

        * fix(CodeInterpreter): linting errors

        * chore: reduce browser console logs from message streams

        * chore: re-enable debug logs for plugins/langchain to help with user troubleshooting

        * chore(manifest.json): add [Experimental] tag to CodeInterpreter plugins, which are not intended as the end-all be-all implementation of this feature for Librechat

    commit 66b8580487
    Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
    Date:   Mon Aug 28 09:18:25 2023 -0400

        docs: third-party tools (#848)

        * docs: third-party tools

        * docs: third-party tools

        * Update third-party.md

        * Update third-party.md

        ---------

        Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>

    commit 9791a78161
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Mon Aug 28 15:14:05 2023 +0200

        adjust the animation (#843)

    commit 3797ec6082
    Author: Ronith <87087292+ronith256@users.noreply.github.com>
    Date:   Mon Aug 28 18:43:50 2023 +0530

        feat: Add Code Interpreter Plugin (#837)

        * feat: Add Code Interpreter Plugin

        Adds a Simple Code Interpreter Plugin.
        ## Features:
        - Runs code using local Python Environment

        ## Issues
        - Code execution is not sandboxed.

        * Add Docker Sandbox for Python Server

    commit e2397076a2
    Author: Alex Zhang <ztc2011@gmail.com>
    Date:   Mon Aug 28 00:55:34 2023 +0800

        🌐: Chinese Translation (#846)

    commit 50c15c704f
    Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
    Date:   Sat Aug 26 19:36:59 2023 -0400

        Language translation: Polish (#840)

        * Language translation: Polish

        * Language translation: Polish

        * Revert changes in language-contributions.md

    commit 29d3640546
    Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
    Date:   Sat Aug 26 19:36:25 2023 -0400

        docs: updates (#841)

    commit 39c626aa8e
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 25 09:29:19 2023 -0400

        fix: isEdited edge case where latest Message is not saved due to aborting too quickly

    commit ae5c06f381
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 25 09:13:50 2023 -0400

        fix(chatGPTBrowser): render markdown formatting by setting isCreatedByUser, fix(useMessageHandler): avoid double appearance of cursor by setting latest message at initial response creation time

    commit 9ef1686e18
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Thu Aug 24 20:24:47 2023 -0400

        Update mkdocs.yml

    commit 5bbe411569
    Author: Flynn <dev@flynnbuckingham.com>
    Date:   Thu Aug 24 20:20:37 2023 -0400

        Add podman installation instructions. Update dockerfile to stub env (#819)

        * Added podman container installation docs. Updated dockerfile to stub env file if not present in source

        * Fix typos

    commit 887fec99ca
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Fri Aug 25 02:11:27 2023 +0200

        🌐: Russian Translation (#830)

    commit 007d51ede1
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Fri Aug 25 02:10:48 2023 +0200

        feat: facebook login (#820)

        * Facebook strategy

        * Update user_auth_system.md

        * Update user_auth_system.md

    commit a569020312
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Thu Aug 24 21:59:11 2023 +0200

        Fix Meilisearch error and refactor of the server index.js (#832)

        * fix meilisearch error at startup

        * limit the nesting

        * disable useless console log

        * fix(indexSync.js): removed redundant searchEnabled

        * refactor(index.js): moved configureSocialLogins to a new file

        * refactor(socialLogins.js): removed unnecessary conditional

    commit 37347d4683
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Wed Aug 23 16:14:17 2023 -0400

        fix(registration): Make Username optional (#831)

        * fix(User.js): update validation schema for username field, allow empty string as a valid value
        fix(validators.js): update validation schema for username field, allow empty string as a valid value
        fix(Registration.tsx, validators.js): update validation rules for name and username fields, change minimum length to 2 and maximum length to 80, assure they match and allow empty string as a valid value
        fix(Eng.tsx): update localization string for com_auth_username, indicate that it is optional

        * fix(User.js): update regex pattern for username validation to allow special characters @#$%&*()
        fix(validators.js): update regex pattern for username validation to allow special characters @#$%&*()

        * fix(Registration.spec.tsx): fix validation error message for username length requirement

    commit d38e463d34
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Wed Aug 23 13:44:40 2023 -0400

        fix(bingAI): markdown and error formatting for final stream response (#829)

        * fix(bingAI): markdown formatting for final stream response due to new strict payload validation on the frontend

        * fix: add missing prop to bing Error response

    commit 7dc27b10f1
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Tue Aug 22 18:44:59 2023 -0400

        feat: Edit AI Messages, Edit Messages in Place (#825)

        * refactor: replace lodash import with specific function import

        fix(api): esm imports to cjs

        * refactor(Messages.tsx): convert to TS, out-source scrollToDiv logic to a custom hook
        fix(ScreenshotContext.tsx): change Ref to RefObject in ScreenshotContextType
        feat(useScrollToRef.ts): add useScrollToRef hook for scrolling to a ref with throttle
        fix(Chat.tsx): update import path for Messages component
        fix(Search.tsx): update import path for Messages component

        * chore(types.ts): add TAskProps and TOptions types
        refactor(useMessageHandler.ts): use TAskFunction type for ask function signature

        * refactor(Message/Content): convert to TS, move Plugin component to Content dir

        * feat(MessageContent.tsx): add MessageContent component for displaying and editing message content
        feat(index.ts): export MessageContent component from Messages/Content directory

        * wip(Message.jsx): conversion and use of new component in progress

        * refactor: convert Message.jsx to TS and fix typing/imports based on changes

        * refactor: add typed props and refactor MultiMessage to TS, fix typing issues resulting from the conversion

        * edit message in progress

        * feat: complete edit AI message logic, refactor continue logic

        * feat(middleware): add validateMessageReq middleware
        feat(routes): add validation for message requests using validateMessageReq middleware
        feat(routes): add create, read, update, and delete routes for messages

        * feat: complete frontend logic for editing messages in place
        feat(messages.js): update route for updating a specific message
        - Change the route for updating a message to include the messageId in the URL
        - Update the request handler to use the messageId from the request parameters and the text from the request body
        - Call the updateMessage function with the updated parameters

        feat(MessageContent.tsx): add functionality to update a message
        - Import the useUpdateMessageMutation hook from the data provider
        - Destructure the conversationId, parentMessageId, and messageId from the message object
        - Create a mutation function using the useUpdateMessageMutation hook
        - Implement the updateMessage function to call the mutation function with the updated message parameters
        - Update the messages state to reflect the updated message text

        feat(api-endpoints.ts): update messages endpoint to include messageId
        - Update the messages endpoint to include the messageId as an optional parameter

        feat(data-service.ts): add updateMessage function
        - Implement the updateMessage function to make a PUT request to

        * fix(messages.js): make updateMessage function asynchronous and await its execution

        * style(EditIcon): make icon active for AI message

        * feat(gptPlugins/anthropic): add edit support

        * fix(validateMessageReq.js): handle case when conversationId is 'new' and return empty array
        feat(Message.tsx): pass message prop to SiblingSwitch component
        refactor(SiblingSwitch.tsx): convert to TS

        * fix(useMessageHandler.ts): remove message from currentMessages if isContinued is true
        feat(useMessageHandler.ts): add support for submission messages in setMessages
        fix(useServerStream.ts): remove unnecessary conditional in setMessages
        fix(useServerStream.ts): remove isContinued variable from submission

        * fix(continue): switch to continued message generation when continuing an earlier branch in conversation

        * fix(abortMiddleware.js): fix condition to check partialText length
        chore(abortMiddleware.js): add error logging when abortMessage fails

        * refactor(MessageHeader.tsx): convert to TS
        fix(Plugin.tsx): add default value for className prop in Plugin component

        * refactor(MultiMessage.tsx): remove commented out code
        docs(MultiMessage.tsx): update comment to clarify when siblingIdx is reset

        * fix(GenerationButtons): optimistic state for continue button

        * fix(MessageContent.tsx): add data-testid attribute to message text editor
        fix(messages.spec.ts): update waitForServerStream function to include edit endpoint check
        feat(messages.spec.ts): add test case for editing messages

        * fix(HoverButtons & Message & useGenerations): Refactor edit functionality and related conditions

        - Update enterEdit function signature and prop
        - Create and utilize hideEditButton variable
        - Enhance conditions for edit button visibility and active state
        - Update button event handlers
        - Introduce isEditableEndpoint in useGenerations and refine continueSupported condition.

        * fix(useGenerations.ts): fix condition for hideEditButton to include error and searchResult
        chore(data-provider): bump version to 0.1.6
        fix(types.ts): add status property to TError type

        * chore: bump @dqbd/tiktoken to 1.0.7

        * fix(abortMiddleware.js): add required isCreatedByUser property to the error response object

        * refactor(Message.tsx): remove unnecessary props from SiblingSwitch component, as setLatestMessage is firing on every switch already
        refactor(SiblingSwitch.tsx): remove unused imports and code

        * chore(BaseClient.js): move console.debug statements back inside if block

    commit db77163f5d
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Tue Aug 22 14:15:14 2023 +0200

        docs: update chimeragpt (#826)

        * Update free_ai_apis.md

        * Update free_ai_apis.md

    commit 4a4e803df3
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Mon Aug 21 20:15:18 2023 +0200

        style(Dialog): Improved Close Button ("X") position (#824)

    commit 909b00c752
    Author: Daniel Avila <messagedaniel@protonmail.com>
    Date:   Sun Aug 20 21:04:36 2023 -0400

        fix(HoverButtons): light/dark styling to match official site

    commit 61dcb4d307
    Author: Naosuke Yokoe <ankerasoy@gmail.com>
    Date:   Sat Aug 19 20:11:31 2023 +0900

        feat: Azure Cognitive Search Plugin (#815)

        * feat(AzureCognitiveSearchPlugin)

        * feat(tools/AzureCognitiveSearch.js): Add a new plugin (not structured
          version)
        * feat(tools/structured/AzureCognitiveSearch.js): Add a new plugin (structured version)
        * feat(tools/manifest.json, tools/index.js, tools/util/handleTools.js):
          Add configurations for the plugin
        * feat(api/package.json, package-lock.json): Installed a new package for the
          plugin (@azure/search-documents)
        * feat(.env.example): Add new environment variables for the plugin

        Here is the link to the corresponding discussion page:
        https://github.com/danny-avila/LibreChat/discussions/567

        * docs(AzureCognitiveSearchPlugin)

        * docs(features/plugins/azure_cognitive_search.md): Add a new document
          for the plugin

        * (fix:.env.example)

        * reverted extra whitespaces removed by the editor

        * docs(mkdocs.yml)

        * Add the Azure Cognitive Search Plugin's documentation item to
        mkdocs.yml.

    commit 3c7f67fa76
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 18 12:40:33 2023 -0400

        fix(abortMiddleware): handle early abort error where userMessage.conversationId is undefined. In this case, the userId will be used as the abortKey

    commit c74c68a135
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 18 12:10:30 2023 -0400

        refactor(MessageHandler -> useServerStream): convert all relating files to TS and correct typings based on this change: properly refactor MessageHandler to a custom hook, where it's passed a submission object to instantiate the stream. This is the bare minimum groundwork for potentially having multiple streams running, which would be a big project to modularize a lot of the global state into maps/multiple streams, particular useful for having multiple views in place

    commit 8b4d3c2c21
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 18 12:04:29 2023 -0400

        refactor(routes): convert to TS

    commit d612cfcb45
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Fri Aug 18 12:02:39 2023 -0400

        chore(Auth): reorder exports in Auth component
        fix(PluginAuthForm): handle case when pluginKey is null or undefined
        fix(PluginStoreDialog): handle case when getAvailablePluginFromKey is null or undefined
        fix(AuthContext): make authConfig optional in AuthContextProvider
        feat(hooks): add useServerStream hook
        fix(conversation): setSubmission to null instead of empty object
        fix(preset): specify type for presets atom
        fix(search): specify type for isSearchEnabled atom
        fix(submission): specify type for submission atom

    commit c40b95f424
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Fri Aug 18 16:11:00 2023 +0200

        feat: Disable Registration with social login (#813)

        * Google, Github and Discord

        * update .env.example with ALLOW_SOCIAL_REGISTRATION

        * fix some conflict

        * refactor strategy

        * Update user_auth_system.md

        * Update user_auth_system.md

    commit 46ed5aaccd
    Author: Patrick <psarnowski@gmail.com>
    Date:   Fri Aug 18 09:38:24 2023 -0400

        Show the response scores from Bing. (#814)

    commit 1dacfa49f0
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Thu Aug 17 20:32:31 2023 +0200

        update profile picture (#792)

    commit afd43afb60
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Thu Aug 17 12:50:05 2023 -0400

        feat(GPT/Anthropic): Continue Regenerating & Generation Buttons (#808)

        * feat(useMessageHandler.js/ts): Refactor and add features to handle user messages, support multiple endpoints/models, generate placeholder responses, regeneration, and stopGeneration function

        fix(conversation.ts, buildTree.ts): Import TMessage type, handle null parentMessageId

        feat(schemas.ts): Update and add schemas for various AI services, add default values, optional fields, and endpoint-to-schema mapping, create parseConvo function

        chore(useMessageHandler.js, schemas.ts): Remove unused imports, variables, and chatGPT enum

        * wip: add generation buttons

        * refactor(cleanupPreset.ts): simplify cleanupPreset function
        refactor(getDefaultConversation.js): remove unused code and simplify getDefaultConversation function

        feat(utils): add getDefaultConversation function

        This commit adds a new utility function called `getDefaultConversation` to the `client/src/utils/getDefaultConversation.ts` file. This function is responsible for generating a default conversation object based on the provided parameters.

        The `getDefaultConversation` function takes in an object with the following properties:
        - `conversation`: The conversation object to be used as a base.
        - `endpointsConfig`: The configuration object containing information about the available endpoints.
        - `preset`: An optional preset object that can be used to override the default behavior.

        The function first tries to determine the target endpoint based on the preset object. If a valid endpoint is found, it is used as the target endpoint. If not, the function tries to retrieve the last conversation setup from the local storage and uses its endpoint if it is valid. If neither the preset nor the local storage contains a valid endpoint, the function falls back to a default endpoint.

        Once the target endpoint is determined,

        * fix(utils): remove console.error statement in buildDefaultConversation function
        fix(schemas): add default values for catch blocks in openAISchema, googleSchema, bingAISchema, anthropicSchema, chatGPTBrowserSchema, and gptPluginsSchema

        * fix: endpoint not changing on change of preset from other endpoint, wip: refactor

        * refactor: preset items to TSX

        * refactor: convert resetConvo to TS

        * refactor(getDefaultConversation.ts): move defaultEndpoints array to the top of the file for better readability
        refactor(getDefaultConversation.ts): extract getDefaultEndpoint function for better code organization and reusability

        * feat(svg): add ContinueIcon component
        feat(svg): add RegenerateIcon component
        feat(svg): add ContinueIcon and RegenerateIcon components to index.ts

        * feat(Button.tsx): add onClick and className props to Button component
        feat(GenerationButtons.tsx): add logic to display Regenerate or StopGenerating button based on isSubmitting and messages
        feat(Regenerate.tsx): create Regenerate component with RegenerateIcon and handleRegenerate function
        feat(StopGenerating.tsx): create StopGenerating component with StopGeneratingIcon and handleStopGenerating function

        * fix(TextChat.jsx): reorder imports and variables for better readability
        fix(TextChat.jsx): fix typo in condition for isNotAppendable variable
        fix(TextChat.jsx): remove unused handleStopGenerating function
        fix(ContinueIcon.tsx): remove unnecessary closing tags for polygon elements
        fix(useMessageHandler.ts): add missing type annotations for handleStopGenerating and handleRegenerate functions
        fix(useMessageHandler.ts): remove unused variables in return statement

        * fix(getDefaultConversation.ts): refactor code to use getLocalStorageItems function
        feat(getLocalStorageItems.ts): add utility function to retrieve items from local storage

        * fix(OpenAIClient.js): add support for streaming result in sendCompletion method
        feat(OpenAIClient.js): add finish_reason metadata to opts in sendCompletion method
        feat(Message.js): add finish_reason field to Message model
        feat(messageSchema.js): add finish_reason field to messageSchema
        feat(openAI.js): parse chatGptLabel and promptPrefix from req.body and pass rest of the modelOptions to endpointOption
        feat(openAI.js): add addMetadata function to store metadata in ask function
        feat(openAI.js): add metadata to response if available
        feat(schemas.ts): add finish_reason field to tMessageSchema

        * feat(types.ts): add TOnClick and TGenButtonProps types for button components
        feat(Continue.tsx): create Continue component for generating button
        feat(GenerationButtons.tsx): update GenerationButtons component to use Continue component
        feat(Regenerate.tsx): create Regenerate component for regenerating button
        feat(Stop.tsx): create Stop component for stop generating button

        * feat(MessageHandler.jsx): add MessageHandler component to handle messages and conversations
        fix(Root.jsx): fix import paths for Nav and MessageHandler components

        * feat(useMessageHandler.ts): add support for generation parameter in ask function
        feat(useMessageHandler.ts): add support for isEdited parameter in ask function
        feat(useMessageHandler.ts): add support for continueGeneration function
        fix(createPayload.ts): replace endpoint URL when isEdited parameter is true

        * chore(client): set skipLibCheck to true in tsconfig.json

        * fix(useMessageHandler.ts): remove unused clientId variable
        fix(schemas.ts): make clientId field in tMessageSchema nullable and optional

        * wip: edit route for continue generation

        * refactor(api): move handlers to root of routes dir

        * fix(useMessageHandler.ts): initialize currentMessages to an empty array if messages is null
        fix(useMessageHandler.ts): update initialResponse text to use responseText variable
        fix(useMessageHandler.ts): update setMessages logic for isRegenerate case
        fix(MessageHandler.jsx): update setMessages logic for cancelHandler, createdHandler, and finalHandler

        * fix(schemas.ts): make createdAt and updatedAt fields optional and set default values using new Date().toISOString()
        fix(schemas.ts): change type annotation of TMessage from infer to input

        * refactor(useMessageHandler.ts): rename AskProps type to TAskProps
        refactor(useMessageHandler.ts): remove generation property from ask function arguments
        refactor(useMessageHandler.ts): use nullish coalescing operator (??) instead of logical OR (||)
        refactor(useMessageHandler.ts): pass the responseMessageId to message prop of submission

        * fix(BaseClient.js): use nullish coalescing operator (??) instead of logical OR (||) for default values

        * fix(BaseClient.js): fix responseMessageId assignment in handleStartMethods method
        feat(BaseClient.js): add support for isEdited flag in sendMessage method
        feat(BaseClient.js): add generation to responseMessage text in sendMessage method

        * fix(openAI.js): remove unused imports and commented out code
        feat(openAI.js): add support for generation parameter in request body
        fix(openAI.js): remove console.log statement
        fix(openAI.js): remove unused variables and parameters
        fix(openAI.js): update response text in case of error
        fix(openAI.js): handle error and abort message in case of error
        fix(handlers.js): add generation parameter to createOnProgress function
        fix(useMessageHandler.ts): update responseText variable to use generation parameter

        * refactor(api/middleware): move inside server dir

        * refactor: add endpoint specific, modular functions to build options and initialize clients, create server/utils, move middleware, separate utils into api general utils and server specific utils

        * fix(abortMiddleware.js): import getConvo and getConvoTitle functions from models
        feat(abortMiddleware.js): add abortAsk function to abortController to handle aborting of requests
        fix(openAI.js): import buildOptions and initializeClient functions from endpoints/openAI
        refactor(openAI.js): use getAbortData function to get data for abortAsk function

        * refactor: move endpoint specific logic to an endpoints dir

        * refactor(PluginService.js): fix import path for encrypt and decrypt functions in PluginService.js

        * feat(openAI): add new endpoint for adding a title to a conversation

        - Added a new file `addTitle.js` in the `api/server/routes/endpoints/openAI` directory.
        - The `addTitle.js` file exports a function `addTitle` that takes in request parameters and performs the following actions:
          - If the `parentMessageId` is `'00000000-0000-0000-0000-000000000000'` and `newConvo` is true, it proceeds with the following steps:
            - Calls the `titleConvo` function from the `titleConvo` module, passing in the necessary parameters.
            - Calls the `saveConvo` function from the `saveConvo` module, passing in the user ID and conversation details.
        - Updated the `index.js` file in the `api/server/routes/endpoints/openAI` directory to export the `addTitle` function.
        - This change adds

        * fix(abortMiddleware.js): remove console.log statement
        refactor(gptPlugins.js): update imports and function parameters
        feat(gptPlugins.js): add support for abortController and getAbortData
        refactor(openAI.js): update imports and function parameters
        feat(openAI.js): add support for abortController and getAbortData

        fix(openAI.js): refactor code to use modularized functions and middleware
        fix(buildOptions.js): refactor code to use destructuring and update variable names

        * refactor(askChatGPTBrowser.js, bingAI.js, google.js): remove duplicate code for setting response headers
        feat(askChatGPTBrowser.js, bingAI.js, google.js): add setHeaders middleware to set response headers

        * feat(middleware): validateEndpoint, refactor buildOption to only be concerned of endpointOption

        * fix(abortMiddleware.js): add 'finish_reason' property with value 'incomplete' to responseMessage object
        fix(abortMessage.js): remove console.log statement for aborted message
        fix(handlers.js): modify tokens assignment to handle empty generation string and trailing space

        * fix(BaseClient.js): import addSpaceIfNeeded function from server/utils
        fix(BaseClient.js): add space before generation in text property
        fix(index.js): remove getCitations and citeText exports
        feat(buildEndpointOption.js): add buildEndpointOption middleware
        fix(index.js): import buildEndpointOption middleware
        fix(anthropic.js): remove buildOptions function and use endpointOption from req.body
        fix(gptPlugins.js): remove buildOptions function and use endpointOption from req.body
        fix(openAI.js): remove buildOptions function and use endpointOption from req.body

        feat(utils): add citations.js and handleText.js modules
        fix(utils): fix import statements in index.js module

        * refactor(gptPlugins.js): use getResponseSender function from librechat-data-provider

        * feat(gptPlugins): complete 'continue generating'

        * wip: anthropic continue regen

        * feat(middleware): add validateRegistration middleware

        A new middleware function called `validateRegistration` has been added to the list of exported middleware functions in `index.js`. This middleware is responsible for validating registration data before allowing the registration process to proceed.

        * feat(Anthropic): complete continue regen

        * chore: add librechat-data-provider to api/package.json

        * fix(ci): backend-review will mock meilisearch, also installs data-provider as now needed

        * chore(ci): remove unneeded SEARCH env var

        * style(GenerationButtons): make text shorter for sake of space economy, even though this diverges from chat.openai.com

        * style(GenerationButtons/ScrollToBottom): adjust visibility/position based on screen size

        * chore(client): 'Editting' typo

        * feat(GenerationButtons.tsx): add support for endpoint prop in GenerationButtons component
        feat(OptionsBar.tsx): pass endpoint prop to GenerationButtons component
        feat(useGenerations.ts): create useGenerations hook to handle generation logic
        fix(schemas.ts): add searchResult field to tMessageSchema

        * refactor(HoverButtons): convert to TSX and utilize new useGenerations hook

        * fix(abortMiddleware): handle error with res headers set, or abortController not found, to ensure proper API error is sent to the client, chore(BaseClient): remove console log for onStart message meant for debugging

        * refactor(api): remove librechat-data-provider dep for now as it complicates deployed docker build stage, re-use code in CJS, located in server/endpoints/schemas

        * chore: remove console.logs from test files

        * ci: add backend tests for AnthropicClient, focusing on new buildMessages logic

        * refactor(FakeClient): use actual BaseClient sendMessage method for testing

        * test(BaseClient.test.js): add test for loading chat history
        test(BaseClient.test.js): add test for sendMessage logic with isEdited flag

        * fix(buildEndpointOption.js): add support for azureOpenAI in buildFunction object
        wip(endpoints.js): fetch Azure models from Azure OpenAI API if opts.azure is true

        * fix(Button.tsx): add data-testid attribute to button component
        fix(SelectDropDown.tsx): add data-testid attribute to Listbox.Button component
        fix(messages.spec.ts): add waitForServerStream function to consolidate logic for awaiting the server response
        feat(messages.spec.ts): add test for stopping and continuing message and improve browser/page context order and closing

        * refactor(onProgress): speed up time to save initial message for editable routes

        * chore: disable AI message editing (for now), was accidentally allowed

        * refactor: ensure continue is only supported for latest message style: improve styling in dark mode and across all hover buttons/icons, including making edit icon for AI invisible (for now)

        * fix: add test id to generation buttons so they never resolve to 2+ items

        * chore(package.json): add 'packages/' to the list of ignored directories
        chore(data-provider/package.json): bump version to 0.1.5

    commit ae5b7d3d53
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Tue Aug 15 18:27:54 2023 -0400

        fix(PluginsClient.js): fix ChatOpenAI Azure Config Issue (#812)

        * fix(PluginsClient.js): fix issue with creating LLM when using Azure

        * chore(PluginsClient.js): omit azure logging

        * refactor(PluginsClient.js): simplify assignment of azure variable

        The code was simplified by directly assigning the value of `this.azure` to the `azure` variable using object destructuring. This makes the code cleaner and more concise.

    commit b85f3bf91e
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Tue Aug 15 18:42:24 2023 +0200

        update from lang to localize (#810)

    commit 80aab73bf6
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Mon Aug 14 19:19:04 2023 -0400

        chore: rebuilt package-lock file

    commit bbe4931a97
    Author: Danny Avila <messagedaniel@protonmail.com>
    Date:   Mon Aug 14 19:13:24 2023 -0400

        refactor(ScreenshotContext): use html-to-image for lighter bundle, faster processing

    commit 74802dd720
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Mon Aug 14 17:51:03 2023 +0200

        chore: Translation Fixes, Lint Error Corrections, and Additional Translations (#788)

        * fix translation and small lint error

        * changed from localize to useLocalize hook

        * changed to useLocalize

    commit b64cc71d88
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 14 10:23:00 2023 -0400

        chore(docker-compose.yml): comment out meilisearch ports in docker-compose.yml (#807)

    commit 89f260bc78
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 14 10:12:00 2023 -0400

        fix(CodeBlock.tsx): fix copy-to-clipboard functionality. The code has been updated to use the copy function from the copy-to-clipboard library instead of the (#806)

        avigator.clipboard.writeText method. This should fix the issue with browser incompatibility with navigator SDK and allow users to copy code from the CodeBlock component successfully.

    commit d00c7354cd
    Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
    Date:   Mon Aug 14 09:45:44 2023 -0400

        fix: Corrected Registration Validation, Case-Insensitive Variable Handling, Playwright workflow (#805)

        * feat(auth.js): add validation for registration endpoint using validateRegistration middleware
        feat(validateRegistration.js): add middleware to validate registration based on ALLOW_REGISTRATION environment variable

        * fix(config.js): fix registrationEnabled and socialLoginEnabled variables to handle case-insensitive environment variable values

        * refactor(validateRegistration.js): remove console.log statement

        * chore(playwright.yml): skip browser download during yarn install
        chore(playwright.yml): place Playwright binaries to node_modules/@playwright/test
        chore(playwright.yml): install Playwright dependencies using npx playwright install-deps
        chore(playwright.yml): install Playwright chromium browser using npx playwright install chromium
        chore(playwright.yml): install @playwright/test@latest using npm install -D @playwright/test@latest
        chore(playwright.yml): run Playwright tests using npm run e2e:ci

        * chore(playwright.yml): change npm install order and update comment

        The order of the npm install commands in the "Install Playwright Browsers" step has been changed to first install @playwright/test@latest and then install chromium. Additionally, the comment explaining the PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD variable has been updated to mention npm install instead of yarn install.

        * chore(playwright.yml): remove commented out code for caching and add separate steps for installing Playwright dependencies and browsers

    commit 1aa4b34dc6
    Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
    Date:   Fri Aug 11 19:02:52 2023 +0200

        added the dot (.) username rules (#787)

commit 28230d9305
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Sun Sep 3 02:44:26 2023 +0200

    feat: delete button confirm (#875)

    * base for confirm delete

    * more like OpenAI

commit 2b54e3f9fe
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Fri Sep 1 14:20:51 2023 -0400

    update: install script (#858)

commit 1cd0fd9d5a
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Fri Sep 1 08:12:35 2023 -0400

    doc: Hugging Face Deployment (#867)

    * docs: update ToC

    * docs: update ToC

    * update huggingface.md

    * update render.md

    * update huggingface.md

    * update mongodb.md

    * update huggingface.md

    * update README.md

commit aeeb3d3050
Author: Mu Yuan <yuanmu.email@gmail.com>
Date:   Thu Aug 31 07:21:27 2023 +0800

    Update Zh.tsx (#862)

    * Update Zh.tsx

    Changed the translation of several words to make it more relevant to Chinese usage habits.

    * Update Zh.tsx

    Changed the translation of several words to make it more relevant to Chinese usage habits

commit 80e2e2675b
Author: Raí <140329135+itzraiss@users.noreply.github.com>
Date:   Mon Aug 28 18:05:46 2023 -0300

    Translation of 'com_ui_pay_per_call:' to Spanish and Portuguese that were missing. (#857)

    * Update Br.tsx

    * Update Es.tsx

    * Update Br.tsx

    * Update Es.tsx

commit 3574d0b823
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 14:49:26 2023 -0400

    docs: make_your_own.md formatting fix for mkdocs (#855)

commit d672ac690d
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 14:24:10 2023 -0400

    Release v0.5.8 (#854)

    * chore: add 'api' image to tag release workflow

    * docs: update DO deployment docs to include instruction about latest stable release, as well as security best practices

    * Release v0.5.8

    * docs: Update digitalocean.md with firewall section images

    * docs: make_your_own.md formatting fix for mkdocs

commit d3e7627046
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Mon Aug 28 12:03:08 2023 -0400

    refactor(plugins): Improve OpenAPI handling, Show Multiple Plugins, & Other Improvements (#845)

    * feat(PluginsClient.js): add conversationId to options object in the constructor
    feat(PluginsClient.js): add support for Code Interpreter plugin
    feat(PluginsClient.js): add support for Code Interpreter plugin in the availableTools manifest
    feat(CodeInterpreter.js): add CodeInterpreterTools module
    feat(CodeInterpreter.js): add RunCommand class
    feat(CodeInterpreter.js): add ReadFile class
    feat(CodeInterpreter.js): add WriteFile class
    feat(handleTools.js): add support for loading Code Interpreter plugin

    * chore(api): update langchain dependency to version 0.0.123

    * fix(CodeInterpreter.js): add support for extracting environment from code
    fix(WriteFile.js): add support for extracting environment from data
    fix(extractionChain.js): add utility functions for creating extraction chain from Zod schema
    fix(handleTools.js): refactor getOpenAIKey function to handle user-provided API key
    fix(handleTools.js): pass model and openAIApiKey to CodeInterpreter constructor

    * fix(tools): rename CodeInterpreterTools to E2BTools
    fix(tools): rename code_interpreter pluginKey to e2b_code_interpreter

    * chore(PluginsClient.js): comment out unused import and function findMessageContent
    feat(PluginsClient.js): add support for CodeSherpa plugin
    feat(PluginsClient.js): add CodeSherpaTools to available tools
    feat(PluginsClient.js): update manifest.json to include CodeSherpa plugin
    feat(CodeSherpaTools.js): create RunCode and RunCommand classes for CodeSherpa plugin

    feat(E2BTools.js): Add E2BTools module for extracting environment from code and running commands, reading and writing files
    fix(codesherpa.js): Remove codesherpa module as it is no longer needed

    feat(handleTools.js): add support for CodeSherpaTools in loadTools function
    feat(loadToolSuite.js): create loadToolSuite utility function to load a suite of tools

    * feat(PluginsClient.js): add support for CodeSherpa v2 plugin
    feat(PluginsClient.js): add CodeSherpa v1 plugin to available tools
    feat(PluginsClient.js): add CodeSherpa v2 plugin to available tools
    feat(PluginsClient.js): update manifest.json for CodeSherpa v1 plugin
    feat(PluginsClient.js): update manifest.json for CodeSherpa v2 plugin
    feat(CodeSherpa.js): implement CodeSherpa plugin for interactive code and shell command execution
    feat(CodeSherpaTools.js): implement RunCode and RunCommand plugins for CodeSherpa v1
    feat(CodeSherpaTools.js): update RunCode and RunCommand plugins for CodeSherpa v2

    fix(handleTools.js): add CodeSherpa import statement
    fix(handleTools.js): change pluginKey from 'codesherpa' to 'codesherpa_tools'
    fix(handleTools.js): remove model and openAIApiKey from options object in e2b_code_interpreter tool
    fix(handleTools.js): remove openAIApiKey from options object in codesherpa_tools tool
    fix(loadToolSuite.js): remove model and openAIApiKey parameters from loadToolSuite function

    * feat(initializeFunctionsAgent.js): add prefix to agentArgs in initializeFunctionsAgent function

    The prefix is added to the agentArgs in the initializeFunctionsAgent function. This prefix is used to provide instructions to the agent when it receives any instructions from a webpage, plugin, or other tool. The agent will notify the user immediately and ask them if they wish to carry out or ignore the instructions.

    * feat(PluginsClient.js): add ChatTool to the list of tools if it meets the conditions
    feat(tools/index.js): import and export ChatTool
    feat(ChatTool.js): create ChatTool class with necessary properties and methods

    * fix(initializeFunctionsAgent.js): update PREFIX message to include sharing all output from the tool
    fix(E2BTools.js): update descriptions for RunCommand, ReadFile, and WriteFile plugins to provide more clarity and context

    * chore: rebuild package-lock after rebase

    * chore: remove deleted file from rebase

    * wip: refactor plugin message handling to mirror chat.openai.com, handle incoming stream for plugin use

    * wip: new plugin handling

    * wip: show multiple plugins handling

    * feat(plugins): save new plugins array

    * chore: bump langchain

    * feat(experimental): support streaming in between plugins

    * refactor(PluginsClient): factor out helper methods to avoid bloating the class, refactor(gptPlugins): use agent action for mapping the name of action

    * fix(handleTools): fix tests by adding condition to return original toolFunctions map

    * refactor(MessageContent): Allow the last index to be last in case it has text (may change with streaming)

    * feat(Plugins): add handleParsingErrors, useful when LLM does not invoke function params

    * chore: edit out experimental codesherpa integration

    * refactor(OpenAPIPlugin): rework tool to be 'function-first', as the spec functions are explicitly passed to agent model

    * refactor(initializeFunctionsAgent): improve error handling and system message

    * refactor(CodeSherpa, Wolfram): optimize token usage by delegating bulk of instructions to system message

    * style(Plugins): match official style with input/outputs

    * chore: remove unnecessary console logs used for testing

    * fix(abortMiddleware): render markdown when message is aborted

    * feat(plugins): add BrowserOp

    * refactor(OpenAPIPlugin): improve prompt handling

    * fix(useGenerations): hide edit button when message is submitting/streaming

    * refactor(loadSpecs): optimize OpenAPI spec loading by only loading requested specs instead of all of them

    * fix(loadSpecs): will retain original behavior when no tools are passed to the function

    * fix(MessageContent): ensure cursor only shows up for last message and last display index
    fix(Message): show legacy plugin and pass isLast to Content

    * chore: remove console.logs

    * docs: update docs based on breaking changes and new features
    refactor(structured/SD): use description_for_model for detailed prompting

    * docs(azure): make plugins section more clear

    * refactor(structured/SD): change default payload to SD-WebUI to prefer realism and config for SDXL

    * refactor(structured/SD): further improve system message prompt

    * docs: update breaking changes after rebase

    * refactor(MessageContent): factor out EditMessage, types, Container to separate files, rename Content -> Markdown

    * fix(CodeInterpreter): linting errors

    * chore: reduce browser console logs from message streams

    * chore: re-enable debug logs for plugins/langchain to help with user troubleshooting

    * chore(manifest.json): add [Experimental] tag to CodeInterpreter plugins, which are not intended as the end-all be-all implementation of this feature for Librechat

commit 66b8580487
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Mon Aug 28 09:18:25 2023 -0400

    docs: third-party tools (#848)

    * docs: third-party tools

    * docs: third-party tools

    * Update third-party.md

    * Update third-party.md

    ---------

    Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>

commit 9791a78161
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Mon Aug 28 15:14:05 2023 +0200

    adjust the animation (#843)

commit 3797ec6082
Author: Ronith <87087292+ronith256@users.noreply.github.com>
Date:   Mon Aug 28 18:43:50 2023 +0530

    feat: Add Code Interpreter Plugin (#837)

    * feat: Add Code Interpreter Plugin

    Adds a Simple Code Interpreter Plugin.
    ## Features:
    - Runs code using local Python Environment

    ## Issues
    - Code execution is not sandboxed.

    * Add Docker Sandbox for Python Server

commit e2397076a2
Author: Alex Zhang <ztc2011@gmail.com>
Date:   Mon Aug 28 00:55:34 2023 +0800

    🌐: Chinese Translation (#846)

commit 50c15c704f
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Sat Aug 26 19:36:59 2023 -0400

    Language translation: Polish (#840)

    * Language translation: Polish

    * Language translation: Polish

    * Revert changes in language-contributions.md

commit 29d3640546
Author: Fuegovic <32828263+fuegovic@users.noreply.github.com>
Date:   Sat Aug 26 19:36:25 2023 -0400

    docs: updates (#841)

commit 39c626aa8e
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 25 09:29:19 2023 -0400

    fix: isEdited edge case where latest Message is not saved due to aborting too quickly

commit ae5c06f381
Author: Danny Avila <messagedaniel@protonmail.com>
Date:   Fri Aug 25 09:13:50 2023 -0400

    fix(chatGPTBrowser): render markdown formatting by setting isCreatedByUser, fix(useMessageHandler): avoid double appearance of cursor by setting latest message at initial response creation time

commit 9ef1686e18
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Thu Aug 24 20:24:47 2023 -0400

    Update mkdocs.yml

commit 5bbe411569
Author: Flynn <dev@flynnbuckingham.com>
Date:   Thu Aug 24 20:20:37 2023 -0400

    Add podman installation instructions. Update dockerfile to stub env (#819)

    * Added podman container installation docs. Updated dockerfile to stub env file if not present in source

    * Fix typos

commit 887fec99ca
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 25 02:11:27 2023 +0200

    🌐: Russian Translation (#830)

commit 007d51ede1
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Fri Aug 25 02:10:48 2023 +0200

    feat: facebook login (#820)

    * Facebook strategy

    * Update user_auth_system.md

    * Update user_auth_system.md

commit a569020312
Author: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Date:   Thu Aug 24 21:59:11 2023 +0200

    Fix Meilisearch error and refactor of the server index.js (#832)

    * fix meilisearch error at startup

    * limit the nesting

    * disable useless console log

    * fix(indexSync.js): removed redundant searchEnabled

    * refactor(index.js): moved configureSocialLogins to a new file

    * refactor(socialLogins.js): removed unnecessary conditional

commit 37347d4683
Author: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Date:   Wed Aug 23 16:14:17 2023 -0400

    fix(registration): Make Username optional (#831)

    * fix(User.js): update validation schema for username field, allow empty string as a valid value
    fix(validators.js): update validation schema for username field, allow empty string as a valid value
    fix(Registration.tsx, validators.js): update validation rules for name and username fields, change minimum length to 2 and maximum length to 80, assure they match and allow empty string as a valid value
    fix(Eng.tsx): update localization string for com_auth_username, indicate that it is optional

    * fix(User.js): update regex …

* Update package-lock.json

* Update SubmitButton.tsx

* Update SpeechRecognition.tsx

* fix: typescript error

* style: moved to new UI

* fix:(SpeechRecognition) lint error

* moved everything to hooks

* feat: support stt external

* fix(useExternalSpeechRecognition): recording the audio

* feat: whisper api support

* refactor(SpeechReecognition); fix(HoverButtons): set isSpeakling correctly

* fix: spelling errors

* fix: renamed files

* BIG FIX

* feat: whisper support

* fixed some ChatForm bugs and added the tts route

* handling more errors

* Fix audio stream initialization and cleanup in useSpeechToTextExternal

* feat: Elevenlabs TTS

* fixed some req issues

* fix: stt not activating on Mac

* fix: send audio blob to frontend

* fix(ChatForm): startupConfig var

* Update text-to-speech and speech-to-text services

* handle more errors correctly

* Remove console.log statements

* feat: added manual trigger with button

* fix: SpeechToText and SpeechToTextExernal + AudioRecorder

* refactor: TTS component

* chore: removed unused variable

* feat: azure stt

* feat: dedicated speech panel

* feat: STT button switch: fix: TextArea pr value adapted

* refactor: textToSpeech function and useTextToSpeechMutation

* fix: typo data-service

* fix: blob backend to frontend

* feat: TTS button for external

* feat: librechat.yaml

* style: spinner when loading TTS

* feat: hold click to download file

* style: disabled when apiKey not provided

* fix: typo startupConfig?.speechToTextExternal

* style: update icons

* fix(useTextToSpeech): set isSpeaking when audio finish

* fix: small issues with local TTS

* style: update settings dark theme

* docs: STT & TTS

* WIP: chat audio automatic; docs(custom_config): update to new .yaml version; chore: updated librechat.yaml version

* fix: send button disabled

* fix: interval update

* localization

* removed unused test code

* revert interval update to 100

* feat: auto-send message

* fix: chat audio automatic, default false

* refactor: moved all logic to hooks

* chore: renamed ChatAudio to conversationMode

* refactor: organized Speech panel

* feat: autoSendText switch

* feat: moved chataudio to conversationMode and improved error handling; docs: update localai model

* refactor: Auto transcribe audio

* test: AutoSendTextSwitch, AutoTranscribeAudioSwitch and ConversationModeSwitch.spec: refactor: removed hark

* fix: various speechTab fixes

* refactor(useSpeechToTextBrowser):: handle more errors

* feat: engine select

* feat: advanced mode

* chore: converted hooks to TS

* feat: cache TTS

* feat: delete cache; fix: cache issues

* refactor(useTextToSpeechExternal): removed unused import

* feat: cache switch; refactor: moved to dir STT/TTS

* tests: CacheTTS, TextToSpeech, SpeechToText

* feat: custom elevenlabs compatibility

* fix(useTextToSpeechExternal): cache switch not working

* feat: animation for STT

* fix: settings var not working

* chore: remove unused var

* feat: voice dropdown; refactor: yaml changes

* fix(textToSpeech): remove undefined properties

* refactor: Remove console logs and unused variable

* fix: TTS; feat: support coqui and piper

* fix: some STT issues

* fix: stt test

* fix: STT backend sending wrong data

* BREAKING: switch to react-speech-recognition, add regenerator-runtime/runtime in main.jsx

* feat: websocket backend

* foundations for websocket

* first pass elevenlabs streaming

* streaming audio

* stream changes

* input streaming implementation

* fix: client build errors

* WIP: streaming rewrite

* audio stream working but not the loop

* WIP: looping audio stream working

* WIP tts routes rewrite

* feat: track SSE runs by runId, which enables us to better track audio streams per message request

* chore: set activeRunId on data.created

* rate limit tts and only allow once

* WIP: streaming audio

* refactor(useSSE): simplify messageId/parentMessageId assignment in message stream

* delete unused component

* streaming working

* first pass but need to investigate forever pending bug

* optimize audio stream handling client and initial request

* fix(StreamAudio): null exception

* refactor(tts): add limiters for db polling and timeout promise by intervals and not elapsed time

* refactor(textToSpeech): reduce polling delay

* feat(StreamAudio): add caching

* refactor: rename global variable, add setIsPlaying, remove mediasource ref

* feat: use custom hook for audioRef to help determine audio end state

* fix: voices mutation -> query

* fix: voices mutation -> query 2/2

* feat: successful TTS for manual playback

* fix: tts voice init

* feat: playback rate

* feat: global audio toggles

* chore: Add renderIcon function for chat message hover buttons, update schemas with notes

* chore: add debug logging instead of console.logs

* fix: edge case undefined user id

* feat: Automatic Playback switch

* feat: add caching bump data-provider

* chore: tts add auth

* use global state for audio run

* feat: assistants support for TTS read aloud

* ci: uncomment tests for now until they are refactored

* stream audio tests are WIP

* refactor: make automatic playback false as default

---------

Co-authored-by: bsu3338 <bsu3338@users.noreply.github.com>
Co-authored-by: bsu3338 <bsu3338@yahoo.com>
Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
Co-authored-by: Berry-13 <marco13beretta@gmail.com>
Co-authored-by: Super User <root@Berry>
Co-authored-by: Marco Beretta <marco13beretta@proton.me>
2024-05-22 17:19:55 -04:00
Aman
9d8fd92dd3 🇬 refactor: Update default Google Models and Parameters (#2782)
* Update Google default model and parameters

* Update .env.example Vertex AI Models to reflect latest version and deprecate bison family

* Update Vertex AI model list in .env.example
2024-05-22 10:20:35 -04:00
Danny Avila
f00a8f87f7 🔧 fix(StableDiffusion): Temporarily Remove sampler_index (#2815) 2024-05-21 09:51:29 -04:00
Danny Avila
79840763e7 🔧 fix: Assistants App Error on Parameters Render (#2805) 2024-05-20 11:10:38 -04:00
Danny Avila
1a452121fa 🤖 feat: OpenAI Assistants v2 (initial support) (#2781)
* 🤖 Assistants V2 Support: Part 1

- Separated Azure Assistants to its own endpoint
- File Search / Vector Store integration is incomplete, but can toggle and use storage from playground
- Code Interpreter resource files can be added but not deleted
- GPT-4o is supported
- Many improvements to the Assistants Endpoint overall

data-provider v2 changes

copy existing route as v1

chore: rename new endpoint to reduce comparison operations and add new azure filesource

api: add azureAssistants part 1

force use of version for assistants/assistantsAzure

chore: switch name back to azureAssistants

refactor type version: string | number

Ensure assistants endpoints have version set

fix: isArchived type issue in ConversationListParams

refactor: update assistants mutations/queries with endpoint/version definitions, update Assistants Map structure

chore:  FilePreview component ExtendedFile type assertion

feat: isAssistantsEndpoint helper

chore: remove unused useGenerations

chore(buildTree): type issue

chore(Advanced): type issue (unused component, maybe in future)

first pass for multi-assistant endpoint rewrite

fix(listAssistants): pass params correctly

feat: list separate assistants by endpoint

fix(useTextarea): access assistantMap correctly

fix: assistant endpoint switching, resetting ID

fix: broken during rewrite, selecting assistant mention

fix: set/invalidate assistants endpoint query data correctly

feat: Fix issue with assistant ID not being reset correctly

getOpenAIClient helper function

feat: add toast for assistant deletion

fix: assistants delete right after create issue for azure

fix: assistant patching

refactor: actions to use getOpenAIClient

refactor: consolidate logic into helpers file

fix: issue where conversation data was not initially available

v1 chat support

refactor(spendTokens): only early return if completionTokens isNaN

fix(OpenAIClient): ensure spendTokens has all necessary params

refactor: route/controller logic

fix(assistants/initializeClient): use defaultHeaders field

fix: sanitize default operation id

chore: bump openai package

first pass v2 action service

feat: retroactive domain parsing for actions added via v1

feat: delete db records of actions/assistants on openai assistant deletion

chore: remove vision tools from v2 assistants

feat: v2 upload and delete assistant vision images

WIP first pass, thread attachments

fix: show assistant vision files (save local/firebase copy)

v2 image continue

fix: annotations

fix: refine annotations

show analyze as error if is no longer submitting before progress reaches 1 and show file_search as retrieval tool

fix: abort run, undefined endpoint issue

refactor: consolidate capabilities logic and anticipate versioning

frontend version 2 changes

fix: query selection and filter

add endpoint to unknown filepath

add file ids to resource, deleting in progress

enable/disable file search

remove version log

* 🤖 Assistants V2 Support: Part 2

🎹 fix: Autocompletion Chrome Bug on Action API Key Input

chore: remove `useOriginNavigate`

chore: set correct OpenAI Storage Source

fix: azure file deletions, instantiate clients by source for deletion

update code interpret files info

feat: deleteResourceFileId

chore: increase poll interval as azure easily rate limits

fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records

file source icons

update table file filters

chore: file search info and versioning

fix: retrieval update with necessary tool_resources if specified

fix(useMentions): add optional chaining in case listMap value is undefined

fix: force assistant avatar roundedness

fix: azure assistants, check correct flag

chore: bump data-provider

* fix: merge conflict

* ci: fix backend tests due to new updates

* chore: update .env.example

* meilisearch improvements

* localization updates

* chore: update comparisons

* feat: add additional metadata: endpoint, author ID

* chore: azureAssistants ENDPOINTS exclusion warning
2024-05-19 12:56:55 -04:00
Elijah Shackelford
af8bcb08d6 👨‍🔧 fix: recognize command+click on macos (#2786)
Fixes an issue where the "command+click" was not being recognized on
MacOS. The desired behavior was working fine on Windows using
"ctrl+click", but the MacOS equivalent was broken.

This was preventing new tabs from opening while holding "command" (meta
key) on MacOS and clicking.

I verified this change fixes the issue by building locally and testing.
2024-05-19 02:44:14 -04:00
312 changed files with 11409 additions and 4211 deletions

View File

@@ -64,6 +64,8 @@ PROXY=
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
# COHERE_API_KEY=
# DATABRICKS_API_KEY=
# FIREWORKS_API_KEY=
# GROQ_API_KEY=
# HUGGINGFACE_TOKEN=
@@ -119,7 +121,7 @@ GOOGLE_KEY=user_provided
# GOOGLE_MODELS=gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision
# Vertex AI
# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0409,gemini-1.0-pro-vision-001,gemini-pro,gemini-pro-vision,chat-bison,chat-bison-32k,codechat-bison,codechat-bison-32k,text-bison,text-bison-32k,text-unicorn,code-gecko,code-bison,code-bison-32k
# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0514,gemini-1.0-pro-vision-001,gemini-1.0-pro-002,gemini-1.0-pro-001,gemini-pro-vision,gemini-1.0-pro
# Google Gemini Safety Settings
# NOTE (Vertex AI): You do not have access to the BLOCK_NONE setting by default.
@@ -257,6 +259,14 @@ MEILI_NO_ANALYTICS=true
MEILI_HOST=http://0.0.0.0:7700
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
#==================================================#
# Speech to Text & Text to Speech #
#==================================================#
STT_API_KEY=
TTS_API_KEY=
#===================================================#
# User System #
#===================================================#
@@ -311,6 +321,9 @@ ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
SESSION_EXPIRY=1000 * 60 * 15
REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7
@@ -352,6 +365,14 @@ OPENID_REQUIRED_ROLE_PARAMETER_PATH=
OPENID_BUTTON_LABEL=
OPENID_IMAGE_URL=
# LDAP
LDAP_URL=
LDAP_BIND_DN=
LDAP_BIND_CREDENTIALS=
LDAP_USER_SEARCH_BASE=
LDAP_SEARCH_FILTER=mail={{username}}
LDAP_CA_CERT_PATH=
#========================#
# Email Password Reset #
#========================#
@@ -378,6 +399,13 @@ FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=
#========================#
# Shared Links #
#========================#
ALLOW_SHARED_LINKS=true
ALLOW_SHARED_LINKS_PUBLIC=true
#===================================================#
# UI #
#===================================================#
@@ -388,6 +416,9 @@ HELP_AND_FAQ_URL=https://librechat.ai
# SHOW_BIRTHDAY_ICON=true
# Google tag manager id
#ANALYTICS_GTM_ID=user provided google tag manager id
#==================================================#
# Others #
#==================================================#

View File

@@ -126,6 +126,18 @@ Apply the following naming conventions to branches, labels, and other Git-relate
- **Current Stance**: At present, this backend transition is of lower priority and might not be pursued.
## 7. Module Import Conventions
- `npm` packages first,
- from shortest line (top) to longest (bottom)
- Followed by typescript types (pertains to data-provider and client workspaces)
- longest line (top) to shortest (bottom)
- types from package come first
- Lastly, local imports
- longest line (top) to shortest (bottom)
- imports with alias `~` treated the same as relative import with respect to line length
---

View File

@@ -1,4 +1,4 @@
# v0.7.2
# v0.7.3
# Base node image
FROM node:20-alpine AS node

View File

@@ -1,4 +1,4 @@
# v0.7.2
# v0.7.3
# Build API, Client and Data Provider
FROM node:20-alpine AS base

View File

@@ -58,9 +58,13 @@
- 🌎 Multilingual UI:
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro,
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
- 🎨 Customizable Dropdown & Interface: Adapts to both power users and newcomers.
- 🎨 Customizable Dropdown & Interface: Adapts to both power users and newcomers
- 📧 Verify your email to ensure secure access
- 🗣️ Chat hands-free with Speech-to-Text and Text-to-Speech magic
- Automatically send and play Audio
- Supports OpenAI, Azure OpenAI, and Elevenlabs
- 📥 Import Conversations from LibreChat, ChatGPT, Chatbot UI
- 📤 Export conversations as screenshots, markdown, text, json.
- 📤 Export conversations as screenshots, markdown, text, json
- 🔍 Search all messages/conversations
- 🔌 Plugins, including web access, image generation with DALL-E-3 and more
- 👥 Multi-User, Secure Authentication with Moderation and Token spend tools
@@ -77,7 +81,7 @@ LibreChat brings together the future of assistant AIs with the revolutionary tec
With LibreChat, you no longer need to opt for ChatGPT Plus and can instead use free or pay-per-call APIs. We welcome contributions, cloning, and forking to enhance the capabilities of this advanced chatbot platform.
[![Watch the video](https://img.youtube.com/vi/YLVUW5UP9N0/maxresdefault.jpg)](https://www.youtube.com/watch?v=YLVUW5UP9N0)
[![Watch the video](https://img.youtube.com/vi/bSVHEbVPNl4/maxresdefault.jpg)](https://www.youtube.com/watch?v=bSVHEbVPNl4)
Click on the thumbnail to open the video☝
---

View File

@@ -1,4 +1,5 @@
const Anthropic = require('@anthropic-ai/sdk');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
const {
getResponseSender,
@@ -123,9 +124,14 @@ class AnthropicClient extends BaseClient {
getClient() {
/** @type {Anthropic.default.RequestOptions} */
const options = {
fetch: this.fetch,
apiKey: this.apiKey,
};
if (this.options.proxy) {
options.httpAgent = new HttpsProxyAgent(this.options.proxy);
}
if (this.options.reverseProxyUrl) {
options.baseURL = this.options.reverseProxyUrl;
}

View File

@@ -1,4 +1,5 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const { supportsBalanceCheck, Constants } = require('librechat-data-provider');
const { getConvo, getMessages, saveMessage, updateMessage, saveConvo } = require('~/models');
const { addSpaceIfNeeded, isEnabled } = require('~/server/utils');
@@ -17,6 +18,7 @@ class BaseClient {
month: 'long',
day: 'numeric',
});
this.fetch = this.fetch.bind(this);
}
setOptions() {
@@ -54,6 +56,25 @@ class BaseClient {
});
}
/**
* Makes an HTTP request and logs the process.
*
* @param {RequestInfo} url - The URL to make the request to. Can be a string or a Request object.
* @param {RequestInit} [init] - Optional init options for the request.
* @returns {Promise<Response>} - A promise that resolves to the response of the fetch request.
*/
async fetch(_url, init) {
let url = _url;
if (this.options.directEndpoint) {
url = this.options.reverseProxyUrl;
}
logger.debug(`Making request to ${url}`);
if (typeof Bun !== 'undefined') {
return await fetch(url, init);
}
return await fetch(url, init);
}
getBuildMessagesOptions() {
throw new Error('Subclasses must implement getBuildMessagesOptions');
}
@@ -373,6 +394,14 @@ class BaseClient {
const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } =
await this.handleStartMethods(message, opts);
if (opts.progressCallback) {
opts.onProgress = opts.progressCallback.call(null, {
...(opts.progressOptions ?? {}),
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
}
const { generation = '' } = opts;
// It's not necessary to push to currentMessages

View File

@@ -438,9 +438,17 @@ class ChatGPTClient extends BaseClient {
if (message.eventType === 'text-generation' && message.text) {
onTokenProgress(message.text);
} else if (message.eventType === 'stream-end' && message.response) {
reply += message.text;
}
/*
Cohere API Chinese Unicode character replacement hotfix.
Should be un-commented when the following issue is resolved:
https://github.com/cohere-ai/cohere-typescript/issues/151
else if (message.eventType === 'stream-end' && message.response) {
reply = message.response.text;
}
*/
}
return reply;

View File

@@ -27,6 +27,7 @@ const {
createContextHandlers,
} = require('./prompts');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { updateTokenWebsocket } = require('~/server/services/Files/Audio');
const { isEnabled, sleep } = require('~/server/utils');
const { handleOpenAIErrors } = require('./tools/util');
const spendTokens = require('~/models/spendTokens');
@@ -588,12 +589,13 @@ class OpenAIClient extends BaseClient {
let streamResult = null;
this.modelOptions.user = this.user;
const invalidBaseUrl = this.completionsUrl && extractBaseURL(this.completionsUrl) === null;
const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion || typeof Bun !== 'undefined');
const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion);
if (typeof opts.onProgress === 'function' && useOldMethod) {
const completionResult = await this.getCompletion(
payload,
(progressMessage) => {
if (progressMessage === '[DONE]') {
updateTokenWebsocket('[DONE]');
return;
}
@@ -827,7 +829,7 @@ class OpenAIClient extends BaseClient {
const instructionsPayload = [
{
role: 'system',
role: this.options.titleMessageRole ?? 'system',
content: `Please generate ${titleInstruction}
${convo}
@@ -1106,7 +1108,12 @@ ${convo}
}
if (this.azure || this.options.azure) {
// Azure does not accept `model` in the body, so we need to remove it.
/* Azure Bug, extremely short default `max_tokens` response */
if (!modelOptions.max_tokens && modelOptions.model === 'gpt-4-vision-preview') {
modelOptions.max_tokens = 4000;
}
/* Azure does not accept `model` in the body, so we need to remove it. */
delete modelOptions.model;
opts.baseURL = this.langchainProxy
@@ -1127,6 +1134,7 @@ ${convo}
let chatCompletion;
/** @type {OpenAI} */
const openai = new OpenAI({
fetch: this.fetch,
apiKey: this.apiKey,
...opts,
});
@@ -1216,6 +1224,7 @@ ${convo}
});
const azureDelay = this.modelOptions.model?.includes('gpt-4') ? 30 : 17;
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || '';
intermediateReply += token;

View File

@@ -250,6 +250,7 @@ class PluginsClient extends OpenAIClient {
this.setOptions(opts);
return super.sendMessage(message, opts);
}
logger.debug('[PluginsClient] sendMessage', { userMessageText: message, opts });
const {
user,
@@ -264,6 +265,14 @@ class PluginsClient extends OpenAIClient {
onToolEnd,
} = await this.handleStartMethods(message, opts);
if (opts.progressCallback) {
opts.onProgress = opts.progressCallback.call(null, {
...(opts.progressOptions ?? {}),
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
}
this.currentMessages.push(userMessage);
let {

View File

@@ -8,8 +8,6 @@ In your response, remember to follow these guidelines:
- If you don't know the answer, simply say that you don't know.
- If you are unsure how to answer, ask for clarification.
- Avoid mentioning that you obtained the information from the context.
Answer appropriately in the user's language.
`;
function createContextHandlers(req, userMessageContent) {
@@ -94,37 +92,40 @@ function createContextHandlers(req, userMessageContent) {
const resolvedQueries = await Promise.all(queryPromises);
const context = resolvedQueries
.map((queryResult, index) => {
const file = processedFiles[index];
let contextItems = queryResult.data;
const context =
resolvedQueries.length === 0
? '\n\tThe semantic search did not return any results.'
: resolvedQueries
.map((queryResult, index) => {
const file = processedFiles[index];
let contextItems = queryResult.data;
const generateContext = (currentContext) =>
`
const generateContext = (currentContext) =>
`
<file>
<filename>${file.filename}</filename>
<context>${currentContext}
</context>
</file>`;
if (useFullContext) {
return generateContext(`\n${contextItems}`);
}
if (useFullContext) {
return generateContext(`\n${contextItems}`);
}
contextItems = queryResult.data
.map((item) => {
const pageContent = item[0].page_content;
return `
contextItems = queryResult.data
.map((item) => {
const pageContent = item[0].page_content;
return `
<contextItem>
<![CDATA[${pageContent?.trim()}]]>
</contextItem>`;
})
.join('');
return generateContext(contextItems);
})
.join('');
return generateContext(contextItems);
})
.join('');
if (useFullContext) {
const prompt = `${header}
${context}

View File

@@ -28,7 +28,7 @@ ${convo}`,
};
const titleInstruction =
'a concise, 5-word-or-less title for the conversation, using its same language, with no punctuation. Apply title case conventions appropriate for the language. For English, use AP Stylebook Title Case. Never directly mention the language name or the word "title"';
'a concise, 5-word-or-less title for the conversation, using its same language, with no punctuation. Apply title case conventions appropriate for the language. Never directly mention the language name or the word "title"';
const titleFunctionPrompt = `In this environment you have access to a set of tools you can use to generate the conversation title.
You may call them like this:

View File

@@ -80,13 +80,18 @@ class StableDiffusionAPI extends StructuredTool {
const payload = {
prompt,
negative_prompt,
sampler_index: 'DPM++ 2M Karras',
cfg_scale: 4.5,
steps: 22,
width: 1024,
height: 1024,
};
const generationResponse = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
let generationResponse;
try {
generationResponse = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
} catch (error) {
logger.error('[StableDiffusion] Error while generating image:', error);
return 'Error making API request.';
}
const image = generationResponse.data.images[0];
/** @type {{ height: number, width: number, seed: number, infotexts: string[] }} */

View File

@@ -7,6 +7,7 @@ const keyvMongo = require('./keyvMongo');
const { BAN_DURATION, USE_REDIS } = process.env ?? {};
const THIRTY_MINUTES = 1800000;
const TEN_MINUTES = 600000;
const duration = math(BAN_DURATION, 7200000);
@@ -24,6 +25,10 @@ const config = isEnabled(USE_REDIS)
? new Keyv({ store: keyvRedis })
: new Keyv({ namespace: CacheKeys.CONFIG_STORE });
const audioRuns = isEnabled(USE_REDIS) // ttl: 30 minutes
? new Keyv({ store: keyvRedis, ttl: TEN_MINUTES })
: new Keyv({ namespace: CacheKeys.AUDIO_RUNS, ttl: TEN_MINUTES });
const tokenConfig = isEnabled(USE_REDIS) // ttl: 30 minutes
? new Keyv({ store: keyvRedis, ttl: THIRTY_MINUTES })
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: THIRTY_MINUTES });
@@ -55,7 +60,13 @@ const namespaces = {
message_limit: createViolationInstance('message_limit'),
token_balance: createViolationInstance(ViolationTypes.TOKEN_BALANCE),
registrations: createViolationInstance('registrations'),
[ViolationTypes.TTS_LIMIT]: createViolationInstance(ViolationTypes.TTS_LIMIT),
[ViolationTypes.STT_LIMIT]: createViolationInstance(ViolationTypes.STT_LIMIT),
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
[ViolationTypes.VERIFY_EMAIL_LIMIT]: createViolationInstance(ViolationTypes.VERIFY_EMAIL_LIMIT),
[ViolationTypes.RESET_PASSWORD_LIMIT]: createViolationInstance(
ViolationTypes.RESET_PASSWORD_LIMIT,
),
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: createViolationInstance(
ViolationTypes.ILLEGAL_MODEL_REQUEST,
),
@@ -64,6 +75,7 @@ const namespaces = {
[CacheKeys.TOKEN_CONFIG]: tokenConfig,
[CacheKeys.GEN_TITLE]: genTitle,
[CacheKeys.MODEL_QUERIES]: modelQueries,
[CacheKeys.AUDIO_RUNS]: audioRuns,
};
/**

View File

@@ -1,6 +1,6 @@
const { isEnabled } = require('~/server/utils');
const getLogStores = require('./getLogStores');
const banViolation = require('./banViolation');
const { isEnabled } = require('../server/utils');
/**
* Logs the violation.

View File

@@ -27,26 +27,25 @@ function getMatchingSensitivePatterns(valueStr) {
}
/**
* Redacts sensitive information from a console message.
*
* Redacts sensitive information from a console message and trims it to a specified length if provided.
* @param {string} str - The console message to be redacted.
* @returns {string} - The redacted console message.
* @param {number} [trimLength] - The optional length at which to trim the redacted message.
* @returns {string} - The redacted and optionally trimmed console message.
*/
function redactMessage(str) {
function redactMessage(str, trimLength) {
if (!str) {
return '';
}
const patterns = getMatchingSensitivePatterns(str);
if (patterns.length === 0) {
return str;
}
patterns.forEach((pattern) => {
str = str.replace(pattern, '$1[REDACTED]');
});
if (trimLength !== undefined && str.length > trimLength) {
return `${str.substring(0, trimLength)}...`;
}
return str;
}

View File

@@ -14,7 +14,7 @@ const Assistant = mongoose.model('assistant', assistantSchema);
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
* @returns {Promise<Object>} The updated or newly created assistant document as a plain object.
*/
const updateAssistant = async (searchParams, updateData, session = null) => {
const updateAssistantDoc = async (searchParams, updateData, session = null) => {
const options = { new: true, upsert: true, session };
return await Assistant.findOneAndUpdate(searchParams, updateData, options).lean();
};
@@ -52,7 +52,7 @@ const deleteAssistant = async (searchParams) => {
};
module.exports = {
updateAssistant,
updateAssistantDoc,
deleteAssistant,
getAssistants,
getAssistant,

View File

@@ -21,7 +21,7 @@ module.exports = {
Conversation,
saveConvo: async (user, { conversationId, newConversationId, ...convo }) => {
try {
const messages = await getMessages({ conversationId });
const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user };
if (newConversationId) {
update.conversationId = newConversationId;

View File

@@ -97,8 +97,12 @@ const deleteFileByFilter = async (filter) => {
* @param {Array<string>} file_ids - The unique identifiers of the files to delete.
* @returns {Promise<Object>} A promise that resolves to the result of the deletion operation.
*/
const deleteFiles = async (file_ids) => {
return await File.deleteMany({ file_id: { $in: file_ids } });
const deleteFiles = async (file_ids, user) => {
let deleteQuery = { file_id: { $in: file_ids } };
if (user) {
deleteQuery = { user: user };
}
return await File.deleteMany(deleteQuery);
};
module.exports = {

View File

@@ -129,6 +129,14 @@ module.exports = {
throw new Error('Failed to save message.');
}
},
async updateMessageText({ messageId, text }) {
try {
await Message.updateOne({ messageId }, { text });
} catch (err) {
logger.error('Error updating message text:', err);
throw new Error('Failed to update message text.');
}
},
async updateMessage(message) {
try {
const { messageId, ...update } = message;
@@ -171,8 +179,18 @@ module.exports = {
}
},
async getMessages(filter) {
/**
* Retrieves messages from the database.
* @param {Record<string, unknown>} filter
* @param {string | undefined} [select]
* @returns
*/
async getMessages(filter, select) {
try {
if (select) {
return await Message.find(filter).select(select).sort({ createdAt: 1 }).lean();
}
return await Message.find(filter).sort({ createdAt: 1 }).lean();
} catch (err) {
logger.error('Error getting messages:', err);

View File

@@ -86,4 +86,21 @@ module.exports = {
}
return await SharedLink.findOneAndDelete({ shareId, user });
},
/**
* Deletes all shared links for a specific user.
* @param {string} user - The user ID.
* @returns {Promise<{ message: string, deletedCount?: number }>} A result object indicating success or error message.
*/
deleteAllSharedLinks: async (user) => {
try {
const result = await SharedLink.deleteMany({ user });
return {
message: 'All shared links have been deleted successfully',
deletedCount: result.deletedCount,
};
} catch (error) {
logger.error('[deleteAllSharedLinks] Error deleting shared links', error);
return { message: 'Error deleting shared links' };
}
},
};

View File

@@ -1,61 +1,5 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const signPayload = require('../server/services/signPayload');
const userSchema = require('./schema/userSchema.js');
const { SESSION_EXPIRY } = process.env ?? {};
const expires = eval(SESSION_EXPIRY) ?? 1000 * 60 * 15;
userSchema.methods.toJSON = function () {
return {
id: this._id,
provider: this.provider,
email: this.email,
name: this.name,
username: this.username,
avatar: this.avatar,
role: this.role,
emailVerified: this.emailVerified,
plugins: this.plugins,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
};
userSchema.methods.generateToken = async function () {
return await signPayload({
payload: {
id: this._id,
username: this.username,
provider: this.provider,
email: this.email,
},
secret: process.env.JWT_SECRET,
expirationTime: expires / 1000,
});
};
userSchema.methods.comparePassword = function (candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) {
return callback(err);
}
callback(null, isMatch);
});
};
module.exports.hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) {
reject(err);
} else {
resolve(hash);
}
});
});
return hashedPassword;
};
const userSchema = require('~/models/schema/userSchema');
const User = mongoose.model('User', userSchema);

View File

@@ -6,9 +6,18 @@ const {
deleteMessagesSince,
deleteMessages,
} = require('./Message');
const {
comparePassword,
deleteUserById,
generateToken,
getUserById,
updateUser,
createUser,
countUsers,
findUser,
} = require('./userMethods');
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
const { hashPassword, getUser, updateUser } = require('./userMethods');
const {
findFileById,
createFile,
@@ -29,9 +38,14 @@ module.exports = {
Session,
Balance,
hashPassword,
comparePassword,
deleteUserById,
generateToken,
getUserById,
countUsers,
createUser,
updateUser,
getUser,
findUser,
getMessages,
saveMessage,

View File

@@ -3,9 +3,9 @@ const mongoose = require('mongoose');
/**
* @typedef {Object} MongoFile
* @property {mongoose.Schema.Types.ObjectId} [_id] - MongoDB Document ID
* @property {ObjectId} [_id] - MongoDB Document ID
* @property {number} [__v] - MongoDB Version Key
* @property {mongoose.Schema.Types.ObjectId} user - User ID
* @property {ObjectId} user - User ID
* @property {string} [conversationId] - Optional conversation ID
* @property {string} file_id - File identifier
* @property {string} [temp_file_id] - Temporary File identifier
@@ -14,17 +14,19 @@ const mongoose = require('mongoose');
* @property {string} filepath - Location of the file
* @property {'file'} object - Type of object, always 'file'
* @property {string} type - Type of file
* @property {number} usage - Number of uses of the file
* @property {number} [usage=0] - Number of uses of the file
* @property {string} [context] - Context of the file origin
* @property {boolean} [embedded] - Whether or not the file is embedded in vector db
* @property {boolean} [embedded=false] - Whether or not the file is embedded in vector db
* @property {string} [model] - The model to identify the group region of the file (for Azure OpenAI hosting)
* @property {string} [source] - The source of the file
* @property {string} [source] - The source of the file (e.g., from FileSources)
* @property {number} [width] - Optional width of the file
* @property {number} [height] - Optional height of the file
* @property {Date} [expiresAt] - Optional height of the file
* @property {Date} [expiresAt] - Optional expiration date of the file
* @property {Date} [createdAt] - Date when the file was created
* @property {Date} [updatedAt] - Date when the file was updated
*/
/** @type {MongooseSchema<MongoFile>} */
const fileSchema = mongoose.Schema(
{
user: {
@@ -91,7 +93,7 @@ const fileSchema = mongoose.Schema(
height: Number,
expiresAt: {
type: Date,
expires: 3600,
expires: 3600, // 1 hour in seconds
},
},
{

View File

@@ -11,6 +11,7 @@ const messageSchema = mongoose.Schema(
},
conversationId: {
type: String,
index: true,
required: true,
meiliIndex: true,
},

View File

@@ -7,6 +7,9 @@ const tokenSchema = new Schema({
required: true,
ref: 'user',
},
email: {
type: String,
},
token: {
type: String,
required: true,

View File

@@ -1,5 +1,35 @@
const mongoose = require('mongoose');
/**
* @typedef {Object} MongoSession
* @property {string} [refreshToken] - The refresh token
*/
/**
* @typedef {Object} MongoUser
* @property {ObjectId} [_id] - MongoDB Document ID
* @property {string} [name] - The user's name
* @property {string} [username] - The user's username, in lowercase
* @property {string} email - The user's email address
* @property {boolean} emailVerified - Whether the user's email is verified
* @property {string} [password] - The user's password, trimmed with 8-128 characters
* @property {string} [avatar] - The URL of the user's avatar
* @property {string} provider - The provider of the user's account (e.g., 'local', 'google')
* @property {string} [role='USER'] - The role of the user
* @property {string} [googleId] - Optional Google ID for the user
* @property {string} [facebookId] - Optional Facebook ID for the user
* @property {string} [openidId] - Optional OpenID ID for the user
* @property {string} [ldapId] - Optional LDAP ID for the user
* @property {string} [githubId] - Optional GitHub ID for the user
* @property {string} [discordId] - Optional Discord ID for the user
* @property {Array} [plugins=[]] - List of plugins used by the user
* @property {Array.<MongoSession>} [refreshToken] - List of sessions with refresh tokens
* @property {Date} [expiresAt] - Optional expiration date of the file
* @property {Date} [createdAt] - Date when the user was created (added by timestamps)
* @property {Date} [updatedAt] - Date when the user was last updated (added by timestamps)
*/
/** @type {MongooseSchema<MongoSession>} */
const Session = mongoose.Schema({
refreshToken: {
type: String,
@@ -7,6 +37,7 @@ const Session = mongoose.Schema({
},
});
/** @type {MongooseSchema<MongoUser>} */
const userSchema = mongoose.Schema(
{
name: {
@@ -64,6 +95,11 @@ const userSchema = mongoose.Schema(
unique: true,
sparse: true,
},
ldapId: {
type: String,
unique: true,
sparse: true,
},
githubId: {
type: String,
unique: true,
@@ -81,6 +117,10 @@ const userSchema = mongoose.Schema(
refreshToken: {
type: [Session],
},
expiresAt: {
type: Date,
expires: 604800, // 7 days in seconds
},
},
{ timestamps: true },
);

View File

@@ -1,28 +1,37 @@
const bcrypt = require('bcryptjs');
const signPayload = require('~/server/services/signPayload');
const User = require('./User');
const hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) {
reject(err);
} else {
resolve(hash);
}
});
});
return hashedPassword;
};
/**
* Retrieve a user by ID and convert the found user document to a plain object.
*
* @param {string} userId - The ID of the user to find and return as a plain object.
* @returns {Promise<Object>} A plain object representing the user document, or `null` if no user is found.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
* @returns {Promise<MongoUser>} A plain object representing the user document, or `null` if no user is found.
*/
const getUser = async function (userId) {
return await User.findById(userId).lean();
const getUserById = async function (userId, fieldsToSelect = null) {
const query = User.findById(userId);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return await query.lean();
};
/**
* Search for a single user based on partial data and return matching user document as plain object.
* @param {Partial<MongoUser>} searchCriteria - The partial data to use for searching the user.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
* @returns {Promise<MongoUser>} A plain object representing the user document, or `null` if no user is found.
*/
const findUser = async function (searchCriteria, fieldsToSelect = null) {
const query = User.findOne(searchCriteria);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return await query.lean();
};
/**
@@ -30,17 +39,136 @@ const getUser = async function (userId) {
*
* @param {string} userId - The ID of the user to update.
* @param {Object} updateData - An object containing the properties to update.
* @returns {Promise<Object>} The updated user document as a plain object, or `null` if no user is found.
* @returns {Promise<MongoUser>} The updated user document as a plain object, or `null` if no user is found.
*/
const updateUser = async function (userId, updateData) {
return await User.findByIdAndUpdate(userId, updateData, {
const updateOperation = {
$set: updateData,
$unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
};
return await User.findByIdAndUpdate(userId, updateOperation, {
new: true,
runValidators: true,
}).lean();
};
module.exports = {
hashPassword,
updateUser,
getUser,
/**
* Creates a new user, optionally with a TTL of 1 week.
* @param {MongoUser} data - The user data to be created, must contain user_id.
* @param {boolean} [disableTTL=true] - Whether to disable the TTL. Defaults to `true`.
* @param {boolean} [returnUser=false] - Whether to disable the TTL. Defaults to `true`.
* @returns {Promise<ObjectId>} A promise that resolves to the created user document ID.
* @throws {Error} If a user with the same user_id already exists.
*/
const createUser = async (data, disableTTL = true, returnUser = false) => {
const userData = {
...data,
expiresAt: disableTTL ? null : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
};
if (disableTTL) {
delete userData.expiresAt;
}
try {
const user = await User.create(userData);
if (returnUser) {
return user.toObject();
}
return user._id;
} catch (error) {
if (error.code === 11000) {
// Duplicate key error code
throw new Error(`User with \`_id\` ${data._id} already exists.`);
} else {
throw error;
}
}
};
/**
* Count the number of user documents in the collection based on the provided filter.
*
* @param {Object} [filter={}] - The filter to apply when counting the documents.
* @returns {Promise<number>} The count of documents that match the filter.
*/
const countUsers = async function (filter = {}) {
return await User.countDocuments(filter);
};
/**
* Delete a user by their unique ID.
*
* @param {string} userId - The ID of the user to delete.
* @returns {Promise<{ deletedCount: number }>} An object indicating the number of deleted documents.
*/
const deleteUserById = async function (userId) {
try {
const result = await User.deleteOne({ _id: userId });
if (result.deletedCount === 0) {
return { deletedCount: 0, message: 'No user found with that ID.' };
}
return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' };
} catch (error) {
throw new Error('Error deleting user: ' + error.message);
}
};
const { SESSION_EXPIRY } = process.env ?? {};
const expires = eval(SESSION_EXPIRY) ?? 1000 * 60 * 15;
/**
* Generates a JWT token for a given user.
*
* @param {MongoUser} user - ID of the user for whom the token is being generated.
* @returns {Promise<string>} A promise that resolves to a JWT token.
*/
const generateToken = async (user) => {
if (!user) {
throw new Error('No user provided');
}
return await signPayload({
payload: {
id: user._id,
username: user.username,
provider: user.provider,
email: user.email,
},
secret: process.env.JWT_SECRET,
expirationTime: expires / 1000,
});
};
/**
* Compares the provided password with the user's password.
*
* @param {MongoUser} user - the user to compare password for.
* @param {string} candidatePassword - The password to test against the user's password.
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the password matches.
*/
const comparePassword = async (user, candidatePassword) => {
if (!user) {
throw new Error('No user provided');
}
return new Promise((resolve, reject) => {
bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
if (err) {
reject(err);
}
resolve(isMatch);
});
});
};
module.exports = {
comparePassword,
deleteUserById,
generateToken,
getUserById,
countUsers,
createUser,
updateUser,
findUser,
};

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "0.7.2",
"version": "0.7.3",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -40,8 +40,7 @@
"@keyv/redis": "^2.8.1",
"@langchain/community": "^0.0.46",
"@langchain/google-genai": "^0.0.11",
"@langchain/google-vertexai": "^0.0.5",
"agenda": "^5.0.0",
"@langchain/google-vertexai": "^0.0.17",
"axios": "^1.3.4",
"bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12",
@@ -86,6 +85,7 @@
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0",
"pino": "^8.12.1",
"sharp": "^0.32.6",
@@ -94,6 +94,7 @@
"ua-parser-js": "^1.0.36",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"ws": "^8.17.0",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -105,11 +105,12 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
getReqData,
onStart,
abortController,
onProgress: progressCallback.call(null, {
progressCallback,
progressOptions: {
res,
text,
parentMessageId: overrideParentMessageId || userMessageId,
}),
// parentMessageId: overrideParentMessageId || userMessageId,
},
};
let response = await client.sendMessage(text, messageOptions);

View File

@@ -1,45 +1,29 @@
const crypto = require('crypto');
const cookies = require('cookie');
const jwt = require('jsonwebtoken');
const { Session, User } = require('~/models');
const {
registerUser,
resetPassword,
setAuthTokens,
requestPasswordReset,
} = require('~/server/services/AuthService');
const { Session, getUserById } = require('~/models');
const { logger } = require('~/config');
const registrationController = async (req, res) => {
try {
const response = await registerUser(req.body);
if (response.status === 200) {
const { status, user } = response;
let newUser = await User.findOne({ _id: user._id });
if (!newUser) {
newUser = new User(user);
await newUser.save();
}
const token = await setAuthTokens(user._id, res);
res.setHeader('Authorization', `Bearer ${token}`);
res.status(status).send({ user });
} else {
const { status, message } = response;
res.status(status).send({ message });
}
const { status, message } = response;
res.status(status).send({ message });
} catch (err) {
logger.error('[registrationController]', err);
return res.status(500).json({ message: err.message });
}
};
const getUserController = async (req, res) => {
return res.status(200).send(req.user);
};
const resetPasswordRequestController = async (req, res) => {
try {
const resetService = await requestPasswordReset(req.body.email);
const resetService = await requestPasswordReset(req);
if (resetService instanceof Error) {
return res.status(400).json(resetService);
} else {
@@ -77,7 +61,7 @@ const refreshController = async (req, res) => {
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findOne({ _id: payload.id });
const user = await getUserById(payload.id, '-password -__v');
if (!user) {
return res.status(401).redirect('/login');
}
@@ -86,8 +70,7 @@ const refreshController = async (req, res) => {
if (process.env.NODE_ENV === 'CI') {
const token = await setAuthTokens(userId, res);
const userObj = user.toJSON();
return res.status(200).send({ token, user: userObj });
return res.status(200).send({ token, user });
}
// Hash the refresh token
@@ -98,8 +81,7 @@ const refreshController = async (req, res) => {
const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken });
if (session && session.expiration > new Date()) {
const token = await setAuthTokens(userId, res, session._id);
const userObj = user.toJSON();
res.status(200).send({ token, user: userObj });
res.status(200).send({ token, user });
} else if (req?.query?.retry) {
// Retrying from a refresh token request that failed (401)
res.status(403).send('No session found');
@@ -115,7 +97,6 @@ const refreshController = async (req, res) => {
};
module.exports = {
getUserController,
refreshController,
registrationController,
resetPasswordController,

View File

@@ -112,11 +112,12 @@ const EditController = async (req, res, next, initializeClient) => {
getReqData,
onStart,
abortController,
onProgress: progressCallback.call(null, {
progressCallback,
progressOptions: {
res,
text,
parentMessageId: overrideParentMessageId || userMessageId,
}),
// parentMessageId: overrideParentMessageId || userMessageId,
},
});
const conversation = await getConvo(user, conversationId);

View File

@@ -1,11 +1,37 @@
const { updateUserPluginsService } = require('~/server/services/UserService');
const {
Session,
Balance,
getFiles,
deleteFiles,
deleteConvos,
deletePresets,
deleteMessages,
deleteUserById,
} = require('~/models');
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
const { processDeleteRequest } = require('~/server/services/Files/process');
const { deleteAllSharedLinks } = require('~/models/Share');
const { Transaction } = require('~/models/Transaction');
const { logger } = require('~/config');
const getUserController = async (req, res) => {
res.status(200).send(req.user);
};
const deleteUserFiles = async (req) => {
try {
const userFiles = await getFiles({ user: req.user.id });
await processDeleteRequest({
req,
files: userFiles,
});
} catch (error) {
logger.error('[deleteUserFiles]', error);
}
};
const updateUserPluginsController = async (req, res) => {
const { user } = req;
const { pluginKey, action, auth, isAssistantTool } = req.body;
@@ -49,11 +75,68 @@ const updateUserPluginsController = async (req, res) => {
res.status(200).send();
} catch (err) {
logger.error('[updateUserPluginsController]', err);
res.status(500).json({ message: err.message });
return res.status(500).json({ message: 'Something went wrong.' });
}
};
const deleteUserController = async (req, res) => {
const { user } = req;
try {
await deleteMessages({ user: user.id }); // delete user messages
await Session.deleteMany({ user: user.id }); // delete user sessions
await Transaction.deleteMany({ user: user.id }); // delete user transactions
await deleteUserKey({ userId: user.id, all: true }); // delete user keys
await Balance.deleteMany({ user: user._id }); // delete user balances
await deletePresets(user.id); // delete user presets
/* TODO: Delete Assistant Threads */
await deleteConvos(user.id); // delete user convos
await deleteUserPluginAuth(user.id, null, true); // delete user plugin auth
await deleteUserById(user.id); // delete user
await deleteAllSharedLinks(user.id); // delete user shared links
await deleteUserFiles(req); // delete user files
await deleteFiles(null, user.id); // delete database files in case of orphaned files from previous steps
/* TODO: queue job for cleaning actions and assistants of non-existant users */
logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`);
res.status(200).send({ message: 'User deleted' });
} catch (err) {
logger.error('[deleteUserController]', err);
return res.status(500).json({ message: 'Something went wrong.' });
}
};
const verifyEmailController = async (req, res) => {
try {
const verifyEmailService = await verifyEmail(req);
if (verifyEmailService instanceof Error) {
return res.status(400).json(verifyEmailService);
} else {
return res.status(200).json(verifyEmailService);
}
} catch (e) {
logger.error('[verifyEmailController]', e);
return res.status(500).json({ message: 'Something went wrong.' });
}
};
const resendVerificationController = async (req, res) => {
try {
const result = await resendVerificationEmail(req);
if (result instanceof Error) {
return res.status(400).json(result);
} else {
return res.status(200).json(result);
}
} catch (e) {
logger.error('[verifyEmailController]', e);
return res.status(500).json({ message: 'Something went wrong.' });
}
};
module.exports = {
getUserController,
deleteUserController,
verifyEmailController,
updateUserPluginsController,
resendVerificationController,
};

View File

@@ -20,6 +20,7 @@ const {
} = require('~/server/services/Threads');
const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils');
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
@@ -31,15 +32,14 @@ const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
const { handleAbortError } = require('~/server/middleware');
const ten_minutes = 1000 * 60 * 10;
/**
* @route POST /
* @desc Chat with an assistant
* @access Public
* @param {Express.Request} req - The request object, containing the request data.
* @param {object} req - The request object, containing the request data.
* @param {object} req.body - The request payload.
* @param {Express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
@@ -60,30 +60,6 @@ const chatV1 = async (req, res) => {
parentMessageId: _parentId = Constants.NO_PARENT,
} = req.body;
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals?.[endpoint];
if (assistantsConfig) {
const { supportedIds, excludedIds } = assistantsConfig;
const error = { message: 'Assistant not supported' };
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId: convoId,
messageId: v4(),
parentMessageId: _messageId,
error,
});
} else if (excludedIds?.length && excludedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId: convoId,
messageId: v4(),
parentMessageId: _messageId,
});
}
}
/** @type {OpenAIClient} */
let openai;
/** @type {string|undefined} - the current thread id */
@@ -311,6 +287,7 @@ const chatV1 = async (req, res) => {
});
openai = _openai;
await validateAuthor({ req, openai });
if (previousMessages.length) {
parentMessageId = previousMessages[previousMessages.length - 1].messageId;

View File

@@ -20,6 +20,7 @@ const {
} = require('~/server/services/Threads');
const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils');
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { getTransactions } = require('~/models/Transaction');
@@ -30,8 +31,6 @@ const { getModelMaxTokens } = require('~/utils');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
const { handleAbortError } = require('~/server/middleware');
const ten_minutes = 1000 * 60 * 10;
/**
@@ -60,30 +59,6 @@ const chatV2 = async (req, res) => {
parentMessageId: _parentId = Constants.NO_PARENT,
} = req.body;
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals?.[endpoint];
if (assistantsConfig) {
const { supportedIds, excludedIds } = assistantsConfig;
const error = { message: 'Assistant not supported' };
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId: convoId,
messageId: v4(),
parentMessageId: _messageId,
error,
});
} else if (excludedIds?.length && excludedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId: convoId,
messageId: v4(),
parentMessageId: _messageId,
});
}
}
/** @type {OpenAIClient} */
let openai;
/** @type {string|undefined} - the current thread id */
@@ -309,6 +284,7 @@ const chatV2 = async (req, res) => {
});
openai = _openai;
await validateAuthor({ req, openai });
if (previousMessages.length) {
parentMessageId = previousMessages[previousMessages.length - 1].messageId;
@@ -520,6 +496,7 @@ const chatV2 = async (req, res) => {
handlers,
thread_id,
attachedFileIds,
parentMessageId: userMessageId,
responseMessage: openai.responseMessage,
// streamOptions: {
@@ -532,6 +509,7 @@ const chatV2 = async (req, res) => {
});
response = streamRunManager;
response.text = streamRunManager.intermediateText;
};
await processRun();
@@ -554,6 +532,7 @@ const chatV2 = async (req, res) => {
/** @type {ResponseMessage} */
const responseMessage = {
...(response.responseMessage ?? response.finalMessage),
text: response.text,
parentMessageId: userMessageId,
conversationId,
user: req.user.id,

View File

@@ -1,4 +1,9 @@
const { EModelEndpoint, CacheKeys, defaultAssistantsVersion } = require('librechat-data-provider');
const {
EModelEndpoint,
CacheKeys,
defaultAssistantsVersion,
defaultOrderQuery,
} = require('librechat-data-provider');
const {
initializeClient: initAzureClient,
} = require('~/server/services/Endpoints/azureAssistants');
@@ -35,6 +40,7 @@ const getCurrentVersion = async (req, endpoint) => {
* Initializes the client with the current request and response objects and lists assistants
* according to the query parameters. This function abstracts the logic for non-Azure paths.
*
* @deprecated
* @async
* @param {object} params - The parameters object.
* @param {object} params.req - The request object, used for initializing the client.
@@ -43,11 +49,65 @@ const getCurrentVersion = async (req, endpoint) => {
* @param {object} params.query - The query parameters to list assistants (e.g., limit, order).
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
*/
const listAssistants = async ({ req, res, version, query }) => {
const _listAssistants = async ({ req, res, version, query }) => {
const { openai } = await getOpenAIClient({ req, res, version });
return openai.beta.assistants.list(query);
};
/**
* Fetches all assistants based on provided query params, until `has_more` is `false`.
*
* @async
* @param {object} params - The parameters object.
* @param {object} params.req - The request object, used for initializing the client.
* @param {object} params.res - The response object, used for initializing the client.
* @param {string} params.version - The API version to use.
* @param {Omit<AssistantListParams, 'endpoint'>} params.query - The query parameters to list assistants (e.g., limit, order).
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
*/
const listAllAssistants = async ({ req, res, version, query }) => {
/** @type {{ openai: OpenAIClient }} */
const { openai } = await getOpenAIClient({ req, res, version });
const allAssistants = [];
let first_id;
let last_id;
let afterToken = query.after;
let hasMore = true;
while (hasMore) {
const response = await openai.beta.assistants.list({
...query,
after: afterToken,
});
const { body } = response;
allAssistants.push(...body.data);
hasMore = body.has_more;
if (!first_id) {
first_id = body.first_id;
}
if (hasMore) {
afterToken = body.last_id;
} else {
last_id = body.last_id;
}
}
return {
data: allAssistants,
body: {
data: allAssistants,
has_more: false,
first_id,
last_id,
},
};
};
/**
* Asynchronously lists assistants for Azure configured groups.
*
@@ -82,7 +142,7 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que
/* The specified model is only necessary to
fetch assistants for the shared instance */
req.body.model = currentModelTuples[0][0];
promises.push(listAssistants({ req, res, version, query }));
promises.push(listAllAssistants({ req, res, version, query }));
}
const resolvedQueries = await Promise.all(promises);
@@ -133,8 +193,27 @@ async function getOpenAIClient({ req, res, endpointOption, initAppClient, overri
return result;
}
const fetchAssistants = async (req, res) => {
const { limit = 100, order = 'desc', after, before, endpoint } = req.query;
/**
* Returns a list of assistants.
* @param {object} params
* @param {object} params.req - Express Request
* @param {AssistantListParams} [params.req.query] - The assistant list parameters for pagination and sorting.
* @param {object} params.res - Express Response
* @param {string} [params.overrideEndpoint] - The endpoint to override the request endpoint.
* @returns {Promise<AssistantListResponse>} 200 - success response - application/json
*/
const fetchAssistants = async ({ req, res, overrideEndpoint }) => {
const {
limit = 100,
order = 'desc',
after,
before,
endpoint,
} = req.query ?? {
endpoint: overrideEndpoint,
...defaultOrderQuery,
};
const version = await getCurrentVersion(req, endpoint);
const query = { limit, order, after, before };
@@ -142,15 +221,47 @@ const fetchAssistants = async (req, res) => {
let body;
if (endpoint === EModelEndpoint.assistants) {
({ body } = await listAssistants({ req, res, version, query }));
({ body } = await listAllAssistants({ req, res, version, query }));
} else if (endpoint === EModelEndpoint.azureAssistants) {
const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI];
body = await listAssistantsForAzure({ req, res, version, azureConfig, query });
}
if (req.user.role === 'ADMIN') {
return body;
} else if (!req.app.locals[endpoint]) {
return body;
}
body.data = filterAssistants({
userId: req.user.id,
assistants: body.data,
assistantsConfig: req.app.locals[endpoint],
});
return body;
};
/**
* Filter assistants based on configuration.
*
* @param {object} params - The parameters object.
* @param {string} params.userId - The user ID to filter private assistants.
* @param {Assistant[]} params.assistants - The list of assistants to filter.
* @param {Partial<TAssistantEndpoint>} params.assistantsConfig - The assistant configuration.
* @returns {Assistant[]} - The filtered list of assistants.
*/
function filterAssistants({ assistants, userId, assistantsConfig }) {
const { supportedIds, excludedIds, privateAssistants } = assistantsConfig;
if (privateAssistants) {
return assistants.filter((assistant) => userId === assistant.metadata?.author);
} else if (supportedIds?.length) {
return assistants.filter((assistant) => supportedIds.includes(assistant.id));
} else if (excludedIds?.length) {
return assistants.filter((assistant) => !excludedIds.includes(assistant.id));
}
return assistants;
}
module.exports = {
getOpenAIClient,
fetchAssistants,

View File

@@ -1,8 +1,9 @@
const { FileContext } = require('librechat-data-provider');
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { deleteAssistantActions } = require('~/server/services/ActionService');
const { updateAssistantDoc, getAssistants } = require('~/models/Assistant');
const { uploadImageBuffer } = require('~/server/services/Files/process');
const { updateAssistant, getAssistants } = require('~/models/Assistant');
const { getOpenAIClient, fetchAssistants } = require('./helpers');
const { deleteFileByFilter } = require('~/models/File');
const { logger } = require('~/config');
@@ -40,9 +41,11 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@@ -61,7 +64,6 @@ const retrieveAssistant = async (req, res) => {
try {
/* NOTE: not actually being used right now */
const { openai } = await getOpenAIClient({ req, res });
const assistant_id = req.params.id;
const assistant = await openai.beta.assistants.retrieve(assistant_id);
res.json(assistant);
@@ -83,6 +85,7 @@ const retrieveAssistant = async (req, res) => {
const patchAssistant = async (req, res) => {
try {
const { openai } = await getOpenAIClient({ req, res });
await validateAuthor({ req, openai });
const assistant_id = req.params.id;
const { endpoint: _e, ...updateData } = req.body;
@@ -119,6 +122,7 @@ const patchAssistant = async (req, res) => {
const deleteAssistant = async (req, res) => {
try {
const { openai } = await getOpenAIClient({ req, res });
await validateAuthor({ req, openai });
const assistant_id = req.params.id;
const deletionStatus = await openai.beta.assistants.del(assistant_id);
@@ -141,19 +145,7 @@ const deleteAssistant = async (req, res) => {
*/
const listAssistants = async (req, res) => {
try {
const body = await fetchAssistants(req, res);
if (req.app.locals?.[req.query.endpoint]) {
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals[req.query.endpoint];
const { supportedIds, excludedIds } = assistantsConfig;
if (supportedIds?.length) {
body.data = body.data.filter((assistant) => supportedIds.includes(assistant.id));
} else if (excludedIds?.length) {
body.data = body.data.filter((assistant) => !excludedIds.includes(assistant.id));
}
}
const body = await fetchAssistants({ req, res });
res.json(body);
} catch (error) {
logger.error('[/assistants] Error listing assistants', error);
@@ -195,6 +187,7 @@ const uploadAssistantAvatar = async (req, res) => {
let { metadata: _metadata = '{}' } = req.body;
const { openai } = await getOpenAIClient({ req, res });
await validateAuthor({ req, openai });
const image = await uploadImageBuffer({
req,
@@ -229,7 +222,7 @@ const uploadAssistantAvatar = async (req, res) => {
const promises = [];
promises.push(
updateAssistant(
updateAssistantDoc(
{ assistant_id },
{
avatar: {

View File

@@ -1,5 +1,7 @@
const { ToolCallTypes } = require('librechat-data-provider');
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { validateAndUpdateTool } = require('~/server/services/ActionService');
const { updateAssistantDoc } = require('~/models/Assistant');
const { getOpenAIClient } = require('./helpers');
const { logger } = require('~/config');
@@ -37,9 +39,11 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@@ -58,6 +62,7 @@ const createAssistant = async (req, res) => {
* @returns {Promise<Assistant>} The updated assistant.
*/
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
await validateAuthor({ req, openai });
const tools = [];
let hasFileSearch = false;

View File

@@ -1,26 +1,22 @@
const User = require('~/models/User');
const { setAuthTokens } = require('~/server/services/AuthService');
const { logger } = require('~/config');
const loginController = async (req, res) => {
try {
const user = await User.findById(req.user._id);
// If user doesn't exist, return error
if (!user) {
// typeof user !== User) { // this doesn't seem to resolve the User type ??
if (!req.user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
const token = await setAuthTokens(user._id, res);
const { password: _, __v, ...user } = req.user;
user.id = user._id.toString();
const token = await setAuthTokens(req.user._id, res);
return res.status(200).send({ token, user });
} catch (err) {
logger.error('[loginController]', err);
return res.status(500).json({ message: 'Something went wrong' });
}
// Generic error messages are safer
return res.status(500).json({ message: 'Something went wrong' });
};
module.exports = {

View File

@@ -6,16 +6,16 @@ const axios = require('axios');
const express = require('express');
const passport = require('passport');
const mongoSanitize = require('express-mongo-sanitize');
const { jwtLogin, passportLogin } = require('~/strategies');
const { connectDb, indexSync } = require('~/lib/db');
const { isEnabled } = require('~/server/utils');
const { ldapLogin } = require('~/strategies');
const { logger } = require('~/config');
const validateImageRequest = require('./middleware/validateImageRequest');
const errorController = require('./controllers/ErrorController');
const { jwtLogin, passportLogin } = require('~/strategies');
const configureSocialLogins = require('./socialLogins');
const { connectDb, indexSync } = require('~/lib/db');
const AppService = require('./services/AppService');
const noIndex = require('./middleware/noIndex');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const routes = require('./routes');
const { PORT, HOST, ALLOW_SOCIAL_LOGIN } = process.env ?? {};
@@ -60,6 +60,11 @@ const startServer = async () => {
passport.use(await jwtLogin());
passport.use(passportLogin());
// LDAP Auth
if (process.env.LDAP_URL && process.env.LDAP_BIND_DN && process.env.LDAP_USER_SEARCH_BASE) {
passport.use(ldapLogin);
}
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
configureSocialLogins(app);
}
@@ -88,7 +93,7 @@ const startServer = async () => {
app.use('/api/share', routes.share);
app.use((req, res) => {
res.status(404).sendFile(path.join(app.locals.paths.dist, 'index.html'));
res.sendFile(path.join(app.locals.paths.dist, 'index.html'));
});
app.listen(port, host, () => {

View File

@@ -1,6 +1,7 @@
const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider');
const { initializeClient } = require('~/server/services/Endpoints/assistants');
const { checkMessageGaps, recordUsage } = require('~/server/services/Threads');
const { deleteMessages } = require('~/models/Message');
const { getConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { sendMessage } = require('~/server/utils');
@@ -66,13 +67,19 @@ async function abortRun(req, res) {
logger.error('[abortRun] Error fetching or processing run', error);
}
/* TODO: a reconciling strategy between the existing intermediate message would be more optimal than deleting it */
await deleteMessages({
user: req.user.id,
unfinished: true,
conversationId,
});
runMessages = await checkMessageGaps({
openai,
run_id,
endpoint,
thread_id,
run_id,
latestMessageId,
conversationId,
latestMessageId,
});
const finalEvent = {

View File

@@ -0,0 +1,43 @@
const { v4 } = require('uuid');
const { handleAbortError } = require('~/server/middleware/abortMiddleware');
/**
* Checks if the assistant is supported or excluded
* @param {object} req - Express Request
* @param {object} req.body - The request payload.
* @param {object} res - Express Response
* @param {function} next - Express next middleware function.
* @returns {Promise<void>}
*/
const validateAssistant = async (req, res, next) => {
const { endpoint, conversationId, assistant_id, messageId } = req.body;
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals?.[endpoint];
if (!assistantsConfig) {
return next();
}
const { supportedIds, excludedIds } = assistantsConfig;
const error = { message: 'Assistant not supported' };
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId,
messageId: v4(),
parentMessageId: messageId,
error,
});
} else if (excludedIds?.length && excludedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',
conversationId,
messageId: v4(),
parentMessageId: messageId,
});
}
return next();
};
module.exports = validateAssistant;

View File

@@ -0,0 +1,42 @@
const { getAssistant } = require('~/models/Assistant');
/**
* Checks if the assistant is supported or excluded
* @param {object} params
* @param {object} params.req - Express Request
* @param {object} params.req.body - The request payload.
* @param {string} params.overrideEndpoint - The override endpoint
* @param {string} params.overrideAssistantId - The override assistant ID
* @param {OpenAIClient} params.openai - OpenAI API Client
* @returns {Promise<void>}
*/
const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistantId }) => {
if (req.user.role === 'ADMIN') {
return;
}
const endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint;
const assistant_id =
overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id;
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = req.app.locals?.[endpoint];
if (!assistantsConfig) {
return;
}
if (!assistantsConfig.privateAssistants) {
return;
}
const assistantDoc = await getAssistant({ assistant_id, user: req.user.id });
if (assistantDoc) {
return;
}
const assistant = await openai.beta.assistants.retrieve(assistant_id);
if (req.user.id !== assistant?.metadata?.author) {
throw new Error(`Assistant ${assistant_id} is not authored by the user.`);
}
};
module.exports = validateAuthor;

View File

@@ -0,0 +1,27 @@
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
/**
* Checks if the user can delete their account
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Next middleware function
*
* @returns {Promise<function|Object>} - Returns a Promise which when resolved calls next middleware if the user can delete their account
*/
const canDeleteAccount = async (req, res, next = () => {}) => {
const { user } = req;
const { ALLOW_ACCOUNT_DELETION = true } = process.env;
if (user?.role === 'ADMIN' || isEnabled(ALLOW_ACCOUNT_DELETION)) {
return next();
} else {
logger.error(`[User] [Delete Account] [User cannot delete account] [User: ${user?.id}]`);
return res.status(403).send({ message: 'You do not have permission to delete this account' });
}
};
module.exports = canDeleteAccount;

View File

@@ -1,15 +1,13 @@
const Keyv = require('keyv');
const uap = require('ua-parser-js');
const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, removePorts } = require('../utils');
const keyvRedis = require('~/cache/keyvRedis');
const { isEnabled, removePorts } = require('~/server/utils');
const keyvMongo = require('~/cache/keyvMongo');
const denyRequest = require('./denyRequest');
const { getLogStores } = require('~/cache');
const User = require('~/models/User');
const { findUser } = require('~/models');
const banCache = isEnabled(process.env.USE_REDIS)
? new Keyv({ store: keyvRedis })
: new Keyv({ namespace: ViolationTypes.BAN, ttl: 0 });
const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 });
const message = 'Your account has been temporarily banned due to violations of our service.';
/**
@@ -57,7 +55,7 @@ const checkBan = async (req, res, next = () => {}) => {
let userId = req.user?.id ?? req.user?._id ?? null;
if (!userId && req?.body?.email) {
const user = await User.findOne({ email: req.body.email }, '_id').lean();
const user = await findUser({ email: req.body.email }, '_id');
userId = user?._id ? user._id.toString() : userId;
}

View File

@@ -1,45 +1,43 @@
const abortMiddleware = require('./abortMiddleware');
const checkBan = require('./checkBan');
const checkDomainAllowed = require('./checkDomainAllowed');
const uaParser = require('./uaParser');
const setHeaders = require('./setHeaders');
const loginLimiter = require('./loginLimiter');
const validateModel = require('./validateModel');
const requireJwtAuth = require('./requireJwtAuth');
const uploadLimiters = require('./uploadLimiters');
const registerLimiter = require('./registerLimiter');
const messageLimiters = require('./messageLimiters');
const requireLocalAuth = require('./requireLocalAuth');
const validateEndpoint = require('./validateEndpoint');
const concurrentLimiter = require('./concurrentLimiter');
const validateMessageReq = require('./validateMessageReq');
const buildEndpointOption = require('./buildEndpointOption');
const validatePasswordReset = require('./validatePasswordReset');
const validateRegistration = require('./validateRegistration');
const validateImageRequest = require('./validateImageRequest');
const buildEndpointOption = require('./buildEndpointOption');
const validateMessageReq = require('./validateMessageReq');
const checkDomainAllowed = require('./checkDomainAllowed');
const concurrentLimiter = require('./concurrentLimiter');
const validateEndpoint = require('./validateEndpoint');
const requireLocalAuth = require('./requireLocalAuth');
const canDeleteAccount = require('./canDeleteAccount');
const requireLdapAuth = require('./requireLdapAuth');
const abortMiddleware = require('./abortMiddleware');
const requireJwtAuth = require('./requireJwtAuth');
const validateModel = require('./validateModel');
const moderateText = require('./moderateText');
const setHeaders = require('./setHeaders');
const limiters = require('./limiters');
const uaParser = require('./uaParser');
const checkBan = require('./checkBan');
const noIndex = require('./noIndex');
const importLimiters = require('./importLimiters');
module.exports = {
...uploadLimiters,
...abortMiddleware,
...messageLimiters,
...limiters,
noIndex,
checkBan,
uaParser,
setHeaders,
loginLimiter,
moderateText,
validateModel,
requireJwtAuth,
registerLimiter,
requireLdapAuth,
requireLocalAuth,
canDeleteAccount,
validateEndpoint,
concurrentLimiter,
checkDomainAllowed,
validateMessageReq,
buildEndpointOption,
validateRegistration,
validateImageRequest,
validateModel,
moderateText,
noIndex,
...importLimiters,
checkDomainAllowed,
validatePasswordReset,
};

View File

@@ -0,0 +1,22 @@
const createTTSLimiters = require('./ttsLimiters');
const createSTTLimiters = require('./sttLimiters');
const loginLimiter = require('./loginLimiter');
const importLimiters = require('./importLimiters');
const uploadLimiters = require('./uploadLimiters');
const registerLimiter = require('./registerLimiter');
const messageLimiters = require('./messageLimiters');
const verifyEmailLimiter = require('./verifyEmailLimiter');
const resetPasswordLimiter = require('./resetPasswordLimiter');
module.exports = {
...uploadLimiters,
...importLimiters,
...messageLimiters,
loginLimiter,
registerLimiter,
createTTSLimiters,
createSTTLimiters,
verifyEmailLimiter,
resetPasswordLimiter,
};

View File

@@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { logViolation } = require('../../cache');
const { removePorts } = require('../utils');
const { removePorts } = require('~/server/utils');
const { logViolation } = require('~/cache');
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
const windowMs = LOGIN_WINDOW * 60 * 1000;

View File

@@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { logViolation } = require('../../cache');
const denyRequest = require('./denyRequest');
const denyRequest = require('~/server/middleware/denyRequest');
const { logViolation } = require('~/cache');
const {
MESSAGE_IP_MAX = 40,

View File

@@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { logViolation } = require('../../cache');
const { removePorts } = require('../utils');
const { removePorts } = require('~/server/utils');
const { logViolation } = require('~/cache');
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
const windowMs = REGISTER_WINDOW * 60 * 1000;

View File

@@ -0,0 +1,35 @@
const rateLimit = require('express-rate-limit');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { logViolation } = require('~/cache');
const {
RESET_PASSWORD_WINDOW = 2,
RESET_PASSWORD_MAX = 2,
RESET_PASSWORD_VIOLATION_SCORE: score,
} = process.env;
const windowMs = RESET_PASSWORD_WINDOW * 60 * 1000;
const max = RESET_PASSWORD_MAX;
const windowInMinutes = windowMs / 60000;
const message = `Too many attempts, please try again after ${windowInMinutes} minute(s)`;
const handler = async (req, res) => {
const type = ViolationTypes.RESET_PASSWORD_LIMIT;
const errorMessage = {
type,
max,
windowInMinutes,
};
await logViolation(req, res, type, errorMessage, score);
return res.status(429).json({ message });
};
const resetPasswordLimiter = rateLimit({
windowMs,
max,
handler,
keyGenerator: removePorts,
});
module.exports = resetPasswordLimiter;

View File

@@ -0,0 +1,68 @@
const rateLimit = require('express-rate-limit');
const { ViolationTypes } = require('librechat-data-provider');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
const STT_IP_MAX = parseInt(process.env.STT_IP_MAX) || 100;
const STT_IP_WINDOW = parseInt(process.env.STT_IP_WINDOW) || 1;
const STT_USER_MAX = parseInt(process.env.STT_USER_MAX) || 50;
const STT_USER_WINDOW = parseInt(process.env.STT_USER_WINDOW) || 1;
const sttIpWindowMs = STT_IP_WINDOW * 60 * 1000;
const sttIpMax = STT_IP_MAX;
const sttIpWindowInMinutes = sttIpWindowMs / 60000;
const sttUserWindowMs = STT_USER_WINDOW * 60 * 1000;
const sttUserMax = STT_USER_MAX;
const sttUserWindowInMinutes = sttUserWindowMs / 60000;
return {
sttIpWindowMs,
sttIpMax,
sttIpWindowInMinutes,
sttUserWindowMs,
sttUserMax,
sttUserWindowInMinutes,
};
};
const createSTTHandler = (ip = true) => {
const { sttIpMax, sttIpWindowInMinutes, sttUserMax, sttUserWindowInMinutes } =
getEnvironmentVariables();
return async (req, res) => {
const type = ViolationTypes.STT_LIMIT;
const errorMessage = {
type,
max: ip ? sttIpMax : sttUserMax,
limiter: ip ? 'ip' : 'user',
windowInMinutes: ip ? sttIpWindowInMinutes : sttUserWindowInMinutes,
};
await logViolation(req, res, type, errorMessage);
res.status(429).json({ message: 'Too many STT requests. Try again later' });
};
};
const createSTTLimiters = () => {
const { sttIpWindowMs, sttIpMax, sttUserWindowMs, sttUserMax } = getEnvironmentVariables();
const sttIpLimiter = rateLimit({
windowMs: sttIpWindowMs,
max: sttIpMax,
handler: createSTTHandler(),
});
const sttUserLimiter = rateLimit({
windowMs: sttUserWindowMs,
max: sttUserMax,
handler: createSTTHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
},
});
return { sttIpLimiter, sttUserLimiter };
};
module.exports = createSTTLimiters;

View File

@@ -0,0 +1,68 @@
const rateLimit = require('express-rate-limit');
const { ViolationTypes } = require('librechat-data-provider');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100;
const TTS_IP_WINDOW = parseInt(process.env.TTS_IP_WINDOW) || 1;
const TTS_USER_MAX = parseInt(process.env.TTS_USER_MAX) || 50;
const TTS_USER_WINDOW = parseInt(process.env.TTS_USER_WINDOW) || 1;
const ttsIpWindowMs = TTS_IP_WINDOW * 60 * 1000;
const ttsIpMax = TTS_IP_MAX;
const ttsIpWindowInMinutes = ttsIpWindowMs / 60000;
const ttsUserWindowMs = TTS_USER_WINDOW * 60 * 1000;
const ttsUserMax = TTS_USER_MAX;
const ttsUserWindowInMinutes = ttsUserWindowMs / 60000;
return {
ttsIpWindowMs,
ttsIpMax,
ttsIpWindowInMinutes,
ttsUserWindowMs,
ttsUserMax,
ttsUserWindowInMinutes,
};
};
const createTTSHandler = (ip = true) => {
const { ttsIpMax, ttsIpWindowInMinutes, ttsUserMax, ttsUserWindowInMinutes } =
getEnvironmentVariables();
return async (req, res) => {
const type = ViolationTypes.TTS_LIMIT;
const errorMessage = {
type,
max: ip ? ttsIpMax : ttsUserMax,
limiter: ip ? 'ip' : 'user',
windowInMinutes: ip ? ttsIpWindowInMinutes : ttsUserWindowInMinutes,
};
await logViolation(req, res, type, errorMessage);
res.status(429).json({ message: 'Too many TTS requests. Try again later' });
};
};
const createTTSLimiters = () => {
const { ttsIpWindowMs, ttsIpMax, ttsUserWindowMs, ttsUserMax } = getEnvironmentVariables();
const ttsIpLimiter = rateLimit({
windowMs: ttsIpWindowMs,
max: ttsIpMax,
handler: createTTSHandler(),
});
const ttsUserLimiter = rateLimit({
windowMs: ttsUserWindowMs,
max: ttsUserMax,
handler: createTTSHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
},
});
return { ttsIpLimiter, ttsUserLimiter };
};
module.exports = createTTSLimiters;

View File

@@ -0,0 +1,35 @@
const rateLimit = require('express-rate-limit');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { logViolation } = require('~/cache');
const {
VERIFY_EMAIL_WINDOW = 2,
VERIFY_EMAIL_MAX = 2,
VERIFY_EMAIL_VIOLATION_SCORE: score,
} = process.env;
const windowMs = VERIFY_EMAIL_WINDOW * 60 * 1000;
const max = VERIFY_EMAIL_MAX;
const windowInMinutes = windowMs / 60000;
const message = `Too many attempts, please try again after ${windowInMinutes} minute(s)`;
const handler = async (req, res) => {
const type = ViolationTypes.VERIFY_EMAIL_LIMIT;
const errorMessage = {
type,
max,
windowInMinutes,
};
await logViolation(req, res, type, errorMessage, score);
return res.status(429).json({ message });
};
const verifyEmailLimiter = rateLimit({
windowMs,
max,
handler,
keyGenerator: removePorts,
});
module.exports = verifyEmailLimiter;

View File

@@ -0,0 +1,22 @@
const passport = require('passport');
const requireLdapAuth = (req, res, next) => {
passport.authenticate('ldapauth', (err, user, info) => {
if (err) {
console.log({
title: '(requireLdapAuth) Error at passport.authenticate',
parameters: [{ name: 'error', value: err }],
});
return next(err);
}
if (!user) {
console.log({
title: '(requireLdapAuth) Error: No user',
});
return res.status(422).send(info);
}
req.user = user;
next();
})(req, res, next);
};
module.exports = requireLdapAuth;

View File

@@ -21,7 +21,13 @@ const requireLocalAuth = (req, res, next) => {
log({
title: '(requireLocalAuth) Error: No user',
});
return res.status(422).send(info);
return res.status(404).send(info);
}
if (info && info.message) {
log({
title: '(requireLocalAuth) Error: ' + info.message,
});
return res.status(422).send({ message: info.message });
}
req.user = user;
next();

View File

@@ -0,0 +1,13 @@
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
function validatePasswordReset(req, res, next) {
if (isEnabled(process.env.ALLOW_PASSWORD_RESET)) {
next();
} else {
logger.warn(`Password reset attempt while not allowed. IP: ${req.ip}`);
res.status(403).send('Password reset is not allowed.');
}
}
module.exports = validatePasswordReset;

View File

@@ -1,6 +1,7 @@
const { isEnabled } = require('~/server/utils');
function validateRegistration(req, res, next) {
const setting = process.env.ALLOW_REGISTRATION?.toLowerCase();
if (setting === 'true') {
if (isEnabled(process.env.ALLOW_REGISTRATION)) {
next();
} else {
res.status(403).send('Registration is not allowed.');

View File

@@ -25,6 +25,12 @@ afterEach(() => {
delete process.env.DOMAIN_SERVER;
delete process.env.ALLOW_REGISTRATION;
delete process.env.ALLOW_SOCIAL_LOGIN;
delete process.env.ALLOW_PASSWORD_RESET;
delete process.env.LDAP_URL;
delete process.env.LDAP_BIND_DN;
delete process.env.LDAP_BIND_CREDENTIALS;
delete process.env.LDAP_USER_SEARCH_BASE;
delete process.env.LDAP_SEARCH_FILTER;
});
//TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why.
@@ -50,6 +56,12 @@ describe.skip('GET /', () => {
process.env.DOMAIN_SERVER = 'http://test-server.com';
process.env.ALLOW_REGISTRATION = 'true';
process.env.ALLOW_SOCIAL_LOGIN = 'true';
process.env.ALLOW_PASSWORD_RESET = 'true';
process.env.LDAP_URL = 'Test LDAP URL';
process.env.LDAP_BIND_DN = 'Test LDAP Bind DN';
process.env.LDAP_BIND_CREDENTIALS = 'Test LDAP Bind Credentials';
process.env.LDAP_USER_SEARCH_BASE = 'Test LDAP User Search Base';
process.env.LDAP_SEARCH_FILTER = 'Test LDAP Search Filter';
const response = await request(app).get('/');
@@ -64,9 +76,11 @@ describe.skip('GET /', () => {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
ldapLoginEnabled: true,
serverDomain: 'http://test-server.com',
emailLoginEnabled: 'true',
registrationEnabled: 'true',
passwordResetEnabled: 'true',
socialLoginEnabled: 'true',
});
});

View File

@@ -106,7 +106,11 @@ router.post(
const pluginMap = new Map();
const onAgentAction = async (action, runId) => {
pluginMap.set(runId, action.tool);
sendIntermediateMessage(res, { plugins });
sendIntermediateMessage(res, {
plugins,
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
};
const onToolStart = async (tool, input, runId, parentRunId) => {
@@ -124,7 +128,11 @@ router.post(
}
const extraTokens = ':::plugin:::\n';
plugins.push(latestPlugin);
sendIntermediateMessage(res, { plugins }, extraTokens);
sendIntermediateMessage(
res,
{ plugins, parentMessageId: userMessage.messageId, messageId: responseMessageId },
extraTokens,
);
};
const onToolEnd = async (output, runId) => {
@@ -142,7 +150,11 @@ router.post(
const onChainEnd = () => {
saveMessage({ ...userMessage, user });
sendIntermediateMessage(res, { plugins });
sendIntermediateMessage(res, {
plugins,
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
};
const getAbortData = () => ({
@@ -174,12 +186,13 @@ router.post(
onStart,
getPartialText,
...endpointOption,
onProgress: progressCallback.call(null, {
progressCallback,
progressOptions: {
res,
text,
parentMessageId: overrideParentMessageId || userMessageId,
// parentMessageId: overrideParentMessageId || userMessageId,
plugins,
}),
},
abortController,
});

View File

@@ -4,7 +4,7 @@ const { encryptMetadata, domainParser } = require('~/server/services/ActionServi
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { updateAssistant, getAssistant } = require('~/models/Assistant');
const { updateAssistantDoc, getAssistant } = require('~/models/Assistant');
const { logger } = require('~/config');
const router = express.Router();
@@ -109,7 +109,7 @@ router.post('/:assistant_id', async (req, res) => {
let updatedAssistant = await openai.beta.assistants.update(assistant_id, { tools });
const promises = [];
promises.push(
updateAssistant(
updateAssistantDoc(
{ assistant_id },
{
actions,
@@ -186,7 +186,7 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => {
const promises = [];
promises.push(
updateAssistant(
updateAssistantDoc(
{ assistant_id },
{
actions: updatedActions,

View File

@@ -8,6 +8,7 @@ const {
// validateEndpoint,
buildEndpointOption,
} = require('~/server/middleware');
const validateAssistant = require('~/server/middleware/assistants/validate');
const chatController = require('~/server/controllers/assistants/chatV1');
router.post('/abort', handleAbort());
@@ -20,6 +21,6 @@ router.post('/abort', handleAbort());
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post('/', validateModel, buildEndpointOption, setHeaders, chatController);
router.post('/', validateModel, buildEndpointOption, validateAssistant, setHeaders, chatController);
module.exports = router;

View File

@@ -8,6 +8,7 @@ const {
// validateEndpoint,
buildEndpointOption,
} = require('~/server/middleware');
const validateAssistant = require('~/server/middleware/assistants/validate');
const chatController = require('~/server/controllers/assistants/chatV2');
router.post('/abort', handleAbort());
@@ -20,6 +21,6 @@ router.post('/abort', handleAbort());
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post('/', validateModel, buildEndpointOption, setHeaders, chatController);
router.post('/', validateModel, buildEndpointOption, validateAssistant, setHeaders, chatController);
module.exports = router;

View File

@@ -1,29 +1,46 @@
const express = require('express');
const {
resetPasswordRequestController,
resetPasswordController,
refreshController,
registrationController,
} = require('../controllers/AuthController');
const { loginController } = require('../controllers/auth/LoginController');
const { logoutController } = require('../controllers/auth/LogoutController');
resetPasswordController,
resetPasswordRequestController,
} = require('~/server/controllers/AuthController');
const { loginController } = require('~/server/controllers/auth/LoginController');
const { logoutController } = require('~/server/controllers/auth/LogoutController');
const {
checkBan,
loginLimiter,
registerLimiter,
requireJwtAuth,
registerLimiter,
requireLdapAuth,
requireLocalAuth,
resetPasswordLimiter,
validateRegistration,
} = require('../middleware');
validatePasswordReset,
} = require('~/server/middleware');
const router = express.Router();
const ldapAuth =
!!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE;
//Local
router.post('/logout', requireJwtAuth, logoutController);
router.post('/login', loginLimiter, checkBan, requireLocalAuth, loginController);
router.post(
'/login',
loginLimiter,
checkBan,
ldapAuth ? requireLdapAuth : requireLocalAuth,
loginController,
);
router.post('/refresh', refreshController);
router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController);
router.post('/requestPasswordReset', resetPasswordRequestController);
router.post('/resetPassword', resetPasswordController);
router.post(
'/requestPasswordReset',
resetPasswordLimiter,
checkBan,
validatePasswordReset,
resetPasswordRequestController,
);
router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController);
module.exports = router;

View File

@@ -6,6 +6,15 @@ const { logger } = require('~/config');
const router = express.Router();
const emailLoginEnabled =
process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN);
const passwordResetEnabled = isEnabled(process.env.ALLOW_PASSWORD_RESET);
const sharedLinksEnabled =
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
const publicSharedLinksEnabled =
sharedLinksEnabled &&
(process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined ||
isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC));
router.get('/', async function (req, res) {
const isBirthday = () => {
@@ -13,6 +22,8 @@ router.get('/', async function (req, res) {
return today.getMonth() === 1 && today.getDate() === 11;
};
const ldapLoginEnabled =
!!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE;
try {
/** @type {TStartupConfig} */
const payload = {
@@ -30,15 +41,17 @@ router.get('/', async function (req, res) {
!!process.env.OPENID_SESSION_SECRET,
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
openidImageUrl: process.env.OPENID_IMAGE_URL,
ldapLoginEnabled,
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
emailLoginEnabled,
registrationEnabled: isEnabled(process.env.ALLOW_REGISTRATION),
registrationEnabled: !ldapLoginEnabled && isEnabled(process.env.ALLOW_REGISTRATION),
socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN),
emailEnabled:
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM,
passwordResetEnabled,
checkBalance: isEnabled(process.env.CHECK_BALANCE),
showBirthdayIcon:
isBirthday() ||
@@ -47,6 +60,9 @@ router.get('/', async function (req, res) {
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
interface: req.app.locals.interfaceConfig,
modelSpecs: req.app.locals.modelSpecs,
sharedLinksEnabled,
publicSharedLinksEnabled,
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
};
if (typeof process.env.CUSTOM_FOOTER === 'string') {

View File

@@ -3,12 +3,11 @@ const express = require('express');
const { CacheKeys } = require('librechat-data-provider');
const { initializeClient } = require('~/server/services/Endpoints/assistants');
const { getConvosByPage, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation');
const { IMPORT_CONVERSATION_JOB_NAME } = require('~/server/utils/import/jobDefinition');
const { storage, importFileFilter } = require('~/server/routes/files/multer');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { forkConversation } = require('~/server/utils/import/fork');
const { importConversations } = require('~/server/utils/import');
const { createImportLimiters } = require('~/server/middleware');
const jobScheduler = require('~/server/utils/jobScheduler');
const getLogStores = require('~/cache/getLogStores');
const { sleep } = require('~/server/utils');
const { logger } = require('~/config');
@@ -129,10 +128,9 @@ router.post(
upload.single('file'),
async (req, res) => {
try {
const filepath = req.file.path;
const job = await jobScheduler.now(IMPORT_CONVERSATION_JOB_NAME, filepath, req.user.id);
res.status(201).json({ message: 'Import started', jobId: job.id });
/* TODO: optimize to return imported conversations and add manually */
await importConversations({ filepath: req.file.path, requestUserId: req.user.id });
res.status(201).json({ message: 'Conversation(s) imported successfully' });
} catch (error) {
logger.error('Error processing file', error);
res.status(500).send('Error processing file');
@@ -169,24 +167,4 @@ router.post('/fork', async (req, res) => {
}
});
// Get the status of an import job for polling
router.get('/import/jobs/:jobId', async (req, res) => {
try {
const { jobId } = req.params;
const { userId, ...jobStatus } = await jobScheduler.getJobStatus(jobId);
if (!jobStatus) {
return res.status(404).json({ message: 'Job not found.' });
}
if (userId !== req.user.id) {
return res.status(403).json({ message: 'Unauthorized' });
}
res.json(jobStatus);
} catch (error) {
logger.error('Error getting job details', error);
res.status(500).send('Error getting job details');
}
});
module.exports = router;

View File

@@ -110,7 +110,11 @@ router.post(
if (!start) {
saveMessage({ ...userMessage, user });
}
sendIntermediateMessage(res, { plugin });
sendIntermediateMessage(res, {
plugin,
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
// logger.debug('PLUGIN ACTION', formattedAction);
};
@@ -119,7 +123,11 @@ router.post(
plugin.outputs = steps && steps[0].action ? formatSteps(steps) : 'An error occurred.';
plugin.loading = false;
saveMessage({ ...userMessage, user });
sendIntermediateMessage(res, { plugin });
sendIntermediateMessage(res, {
plugin,
parentMessageId: userMessage.messageId,
messageId: responseMessageId,
});
// logger.debug('CHAIN END', plugin.outputs);
};
@@ -153,12 +161,13 @@ router.post(
onChainEnd,
onStart,
...endpointOption,
onProgress: progressCallback.call(null, {
progressCallback,
progressOptions: {
res,
text,
plugin,
parentMessageId: overrideParentMessageId || userMessageId,
}),
// parentMessageId: overrideParentMessageId || userMessageId,
},
abortController,
});

View File

@@ -1,10 +1,19 @@
const express = require('express');
const { uaParser, checkBan, requireJwtAuth, createFileLimiters } = require('~/server/middleware');
const {
uaParser,
checkBan,
requireJwtAuth,
createFileLimiters,
createTTSLimiters,
createSTTLimiters,
} = require('~/server/middleware');
const { createMulterInstance } = require('./multer');
const files = require('./files');
const images = require('./images');
const avatar = require('./avatar');
const stt = require('./stt');
const tts = require('./tts');
const initialize = async () => {
const router = express.Router();
@@ -12,6 +21,12 @@ const initialize = async () => {
router.use(checkBan);
router.use(uaParser);
/* Important: stt/tts routes must be added before the upload limiters */
const { sttIpLimiter, sttUserLimiter } = createSTTLimiters();
const { ttsIpLimiter, ttsUserLimiter } = createTTSLimiters();
router.use('/stt', sttIpLimiter, sttUserLimiter, stt);
router.use('/tts', ttsIpLimiter, ttsUserLimiter, tts);
const upload = await createMulterInstance();
const { fileUploadIpLimiter, fileUploadUserLimiter } = createFileLimiters();
router.post('*', fileUploadIpLimiter, fileUploadUserLimiter);

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const { requireJwtAuth } = require('~/server/middleware/');
const { speechToText } = require('~/server/services/Files/Audio');
const upload = multer();
router.post('/', requireJwtAuth, upload.single('audio'), async (req, res) => {
await speechToText(req, res);
});
module.exports = router;

View File

@@ -0,0 +1,42 @@
const multer = require('multer');
const express = require('express');
const { CacheKeys } = require('librechat-data-provider');
const { getVoices, streamAudio, textToSpeech } = require('~/server/services/Files/Audio');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const router = express.Router();
const upload = multer();
router.post('/manual', upload.none(), async (req, res) => {
await textToSpeech(req, res);
});
const logDebugMessage = (req, message) =>
logger.debug(`[streamAudio] user: ${req?.user?.id ?? 'UNDEFINED_USER'} | ${message}`);
// TODO: test caching
router.post('/', async (req, res) => {
try {
const audioRunsCache = getLogStores(CacheKeys.AUDIO_RUNS);
const audioRun = await audioRunsCache.get(req.body.runId);
logDebugMessage(req, 'start stream audio');
if (audioRun) {
logDebugMessage(req, 'stream audio already running');
return res.status(401).json({ error: 'Audio stream already running' });
}
audioRunsCache.set(req.body.runId, true);
await streamAudio(req, res);
logDebugMessage(req, 'end stream audio');
res.status(200).end();
} catch (error) {
logger.error(`[streamAudio] user: ${req.user.id} | Failed to stream audio: ${error}`);
res.status(500).json({ error: 'Failed to stream audio' });
}
});
router.get('/voices', async (req, res) => {
await getVoices(req, res);
});
module.exports = router;

View File

@@ -14,7 +14,7 @@ router.use(requireJwtAuth);
router.get('/:conversationId', validateMessageReq, async (req, res) => {
const { conversationId } = req.params;
res.status(200).send(await getMessages({ conversationId }));
res.status(200).send(await getMessages({ conversationId }, '-_id -__v -user'));
});
// CREATE
@@ -28,7 +28,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => {
// READ
router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
const { conversationId, messageId } = req.params;
res.status(200).send(await getMessages({ conversationId, messageId }));
res.status(200).send(await getMessages({ conversationId, messageId }, '-_id -__v -user'));
});
// UPDATE

View File

@@ -1,12 +1,12 @@
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
const passport = require('passport');
const express = require('express');
const router = express.Router();
const { setAuthTokens } = require('~/server/services/AuthService');
const passport = require('passport');
const { loginLimiter, checkBan, checkDomainAllowed } = require('~/server/middleware');
const { setAuthTokens } = require('~/server/services/AuthService');
const { logger } = require('~/config');
const router = express.Router();
const domains = {
client: process.env.DOMAIN_CLIENT,
server: process.env.DOMAIN_SERVER,

View File

@@ -8,21 +8,33 @@ const {
deleteSharedLink,
} = require('~/models/Share');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { isEnabled } = require('~/server/utils');
const router = express.Router();
/**
* Shared messages
* this route does not require authentication
*/
router.get('/:shareId', async (req, res) => {
const share = await getSharedMessages(req.params.shareId);
const allowSharedLinks =
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
if (share) {
res.status(200).json(share);
} else {
res.status(404).end();
}
});
if (allowSharedLinks) {
const allowSharedLinksPublic =
process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined ||
isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC);
router.get(
'/:shareId',
allowSharedLinksPublic ? (req, res, next) => next() : requireJwtAuth,
async (req, res) => {
const share = await getSharedMessages(req.params.shareId);
if (share) {
res.status(200).json(share);
} else {
res.status(404).end();
}
},
);
}
/**
* Shared links

View File

@@ -1,10 +1,19 @@
const express = require('express');
const requireJwtAuth = require('../middleware/requireJwtAuth');
const { getUserController, updateUserPluginsController } = require('../controllers/UserController');
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
const {
getUserController,
deleteUserController,
verifyEmailController,
updateUserPluginsController,
resendVerificationController,
} = require('~/server/controllers/UserController');
const router = express.Router();
router.get('/', requireJwtAuth, getUserController);
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController);
router.post('/verify', verifyEmailController);
router.post('/verify/resend', verifyEmailLimiter, resendVerificationController);
module.exports = router;

View File

@@ -78,6 +78,7 @@ const AppService = async (app) => {
if (config?.endpoints?.[EModelEndpoint.azureAssistants]) {
endpointLocals[EModelEndpoint.azureAssistants] = assistantsConfigSetup(
config,
EModelEndpoint.azureAssistants,
endpointLocals[EModelEndpoint.azureAssistants],
);
}
@@ -85,6 +86,7 @@ const AppService = async (app) => {
if (config?.endpoints?.[EModelEndpoint.assistants]) {
endpointLocals[EModelEndpoint.assistants] = assistantsConfigSetup(
config,
EModelEndpoint.assistants,
endpointLocals[EModelEndpoint.assistants],
);
}

View File

@@ -218,6 +218,7 @@ describe('AppService', () => {
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: ['id1', 'id2'],
privateAssistants: false,
},
},
}),
@@ -232,6 +233,7 @@ describe('AppService', () => {
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: expect.arrayContaining(['id1', 'id2']),
privateAssistants: false,
}),
);
});
@@ -505,7 +507,31 @@ describe('AppService updating app.locals and issuing warnings', () => {
const { logger } = require('~/config');
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Both `supportedIds` and `excludedIds` are defined'),
expect.stringContaining(
'The \'assistants\' endpoint has both \'supportedIds\' and \'excludedIds\' defined.',
),
);
});
it('should log a warning when privateAssistants and supportedIds or excludedIds are provided', async () => {
const mockConfig = {
endpoints: {
assistants: {
privateAssistants: true,
supportedIds: ['id1'],
},
},
};
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
const app = { locals: {} };
await require('./AppService')(app);
const { logger } = require('~/config');
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'The \'assistants\' endpoint has both \'privateAssistants\' and \'supportedIds\' or \'excludedIds\' defined.',
),
);
});

View File

@@ -1,13 +1,21 @@
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const { errorsToString } = require('librechat-data-provider');
const {
findUser,
countUsers,
createUser,
updateUser,
getUserById,
generateToken,
deleteUserById,
} = require('~/models/userMethods');
const { sendEmail, checkEmailConfig } = require('~/server/utils');
const { registerSchema } = require('~/strategies/validators');
const isDomainAllowed = require('./isDomainAllowed');
const Token = require('~/models/schema/tokenSchema');
const { sendEmail } = require('~/server/utils');
const Session = require('~/models/Session');
const { logger } = require('~/config');
const User = require('~/models/User');
const domains = {
client: process.env.DOMAIN_CLIENT,
@@ -15,6 +23,7 @@ const domains = {
};
const isProduction = process.env.NODE_ENV === 'production';
const genericVerificationMessage = 'Please check your email to verify your email address.';
/**
* Logout user
@@ -45,10 +54,72 @@ const logoutUser = async (userId, refreshToken) => {
};
/**
* Register a new user
*
* @param {Object} user <email, password, name, username>
* @returns
* Send Verification Email
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
* @returns {Promise<void>}
*/
const sendVerificationEmail = async (user) => {
let verifyToken = crypto.randomBytes(32).toString('hex');
const hash = bcrypt.hashSync(verifyToken, 10);
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
await sendEmail({
email: user.email,
subject: 'Verify your email',
payload: {
appName: process.env.APP_TITLE || 'LibreChat',
name: user.name,
verificationLink: verificationLink,
year: new Date().getFullYear(),
},
template: 'verifyEmail.handlebars',
});
await new Token({
userId: user._id,
email: user.email,
token: hash,
createdAt: Date.now(),
}).save();
logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
};
/**
* Verify Email
* @param {Express.Request} req
*/
const verifyEmail = async (req) => {
const { email, token } = req.body;
let emailVerificationData = await Token.findOne({ email: decodeURIComponent(email) });
if (!emailVerificationData) {
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`);
return new Error('Invalid or expired password reset token');
}
const isValid = bcrypt.compareSync(token, emailVerificationData.token);
if (!isValid) {
logger.warn(`[verifyEmail] [Invalid or expired email verification token] [Email: ${email}]`);
return new Error('Invalid or expired email verification token');
}
const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true });
if (!updatedUser) {
logger.warn(`[verifyEmail] [User not found] [Email: ${email}]`);
return new Error('User not found');
}
await emailVerificationData.deleteOne();
logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`);
return { message: 'Email verification was successful' };
};
/**
* Register a new user.
* @param {MongoUser} user <email, password, name, username>
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
*/
const registerUser = async (user) => {
const { error } = registerSchema.safeParse(user);
@@ -60,13 +131,14 @@ const registerUser = async (user) => {
{ name: 'Validation error:', value: errorMessage },
);
return { status: 422, message: errorMessage };
return { status: 404, message: errorMessage };
}
const { email, password, name, username } = user;
let newUserId;
try {
const existingUser = await User.findOne({ email }).lean();
const existingUser = await findUser({ email }, 'email _id');
if (existingUser) {
logger.info(
@@ -77,51 +149,71 @@ const registerUser = async (user) => {
// Sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
// TODO: We should change the process to always email and be generic is signup works or fails (user enum)
return { status: 500, message: 'Something went wrong' };
return { status: 200, message: genericVerificationMessage };
}
if (!(await isDomainAllowed(email))) {
const errorMessage = 'Registration from this domain is not allowed.';
const errorMessage =
'The email address provided cannot be used. Please use a different email address.';
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
return { status: 403, message: errorMessage };
}
//determine if this is the first registered user (not counting anonymous_user)
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
const isFirstRegisteredUser = (await countUsers()) === 0;
const newUser = await new User({
const salt = bcrypt.genSaltSync(10);
const newUserData = {
provider: 'local',
email,
password,
username,
name,
avatar: null,
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
});
password: bcrypt.hashSync(password, salt),
};
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(newUser.password, salt);
newUser.password = hash;
await newUser.save();
const emailEnabled = checkEmailConfig();
newUserId = await createUser(newUserData, false);
if (emailEnabled) {
await sendVerificationEmail({
_id: newUserId,
email,
name,
});
} else {
await updateUser(newUserId, { emailVerified: true });
}
return { status: 200, user: newUser };
return { status: 200, message: genericVerificationMessage };
} catch (err) {
return { status: 500, message: err?.message || 'Something went wrong' };
logger.error('[registerUser] Error in registering user:', err);
if (newUserId) {
const result = await deleteUserById(newUserId);
logger.warn(
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
);
}
return { status: 500, message: 'Something went wrong' };
}
};
/**
* Request password reset
*
* @param {String} email
* @returns
* @param {Express.Request} req
*/
const requestPasswordReset = async (email) => {
const user = await User.findOne({ email }).lean();
const requestPasswordReset = async (req) => {
const { email } = req.body;
const user = await findUser({ email }, 'email _id');
const emailEnabled = checkEmailConfig();
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
if (!user) {
return new Error('Email does not exist');
logger.warn(`[requestPasswordReset] [No user found] [Email: ${email}] [IP: ${req.ip}]`);
return {
message: 'If an account with that email exists, a password reset link has been sent to it.',
};
}
let token = await Token.findOne({ userId: user._id });
@@ -140,28 +232,31 @@ const requestPasswordReset = async (email) => {
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
const emailEnabled =
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
!!process.env.EMAIL_USERNAME &&
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM;
if (emailEnabled) {
sendEmail(
user.email,
'Password Reset Request',
{
await sendEmail({
email: user.email,
subject: 'Password Reset Request',
payload: {
appName: process.env.APP_TITLE || 'LibreChat',
name: user.name,
link: link,
year: new Date().getFullYear(),
},
'requestPasswordReset.handlebars',
template: 'requestPasswordReset.handlebars',
});
logger.info(
`[requestPasswordReset] Link emailed. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
);
return { link: '' };
} else {
logger.info(
`[requestPasswordReset] Link issued. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
);
return { link };
}
return {
message: 'If an account with that email exists, a password reset link has been sent to it.',
};
};
/**
@@ -186,39 +281,38 @@ const resetPassword = async (userId, token, password) => {
}
const hash = bcrypt.hashSync(password, 10);
const user = await updateUser(userId, { password: hash });
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
const user = await User.findById({ _id: userId });
sendEmail(
user.email,
'Password Reset Successfully',
{
appName: process.env.APP_TITLE || 'LibreChat',
name: user.name,
year: new Date().getFullYear(),
},
'passwordReset.handlebars',
);
if (checkEmailConfig()) {
await sendEmail({
email: user.email,
subject: 'Password Reset Successfully',
payload: {
appName: process.env.APP_TITLE || 'LibreChat',
name: user.name,
year: new Date().getFullYear(),
},
template: 'passwordReset.handlebars',
});
}
await passwordResetToken.deleteOne();
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
return { message: 'Password reset was successful' };
};
/**
* Set Auth Tokens
*
* @param {String} userId
* @param {String | ObjectId} userId
* @param {Object} res
* @param {String} sessionId
* @returns
*/
const setAuthTokens = async (userId, res, sessionId = null) => {
try {
const user = await User.findOne({ _id: userId });
const token = await user.generateToken();
const user = await getUserById(userId);
const token = await generateToken(user);
let session;
let refreshTokenExpires;
@@ -248,11 +342,70 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
}
};
/**
* Resend Verification Email
* @param {Object} req
* @param {Object} req.body
* @param {String} req.body.email
* @returns {Promise<{status: number, message: string}>}
*/
const resendVerificationEmail = async (req) => {
try {
const { email } = req.body;
await Token.deleteMany({ email });
const user = await findUser({ email }, 'email _id name');
if (!user) {
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
return { status: 200, message: genericVerificationMessage };
}
let verifyToken = crypto.randomBytes(32).toString('hex');
const hash = bcrypt.hashSync(verifyToken, 10);
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
await sendEmail({
email: user.email,
subject: 'Verify your email',
payload: {
appName: process.env.APP_TITLE || 'LibreChat',
name: user.name,
verificationLink: verificationLink,
year: new Date().getFullYear(),
},
template: 'verifyEmail.handlebars',
});
await new Token({
userId: user._id,
email: user.email,
token: hash,
createdAt: Date.now(),
}).save();
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
return {
status: 200,
message: genericVerificationMessage,
};
} catch (error) {
logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`);
return {
status: 500,
message: 'Something went wrong.',
};
}
};
module.exports = {
registerUser,
logoutUser,
verifyEmail,
registerUser,
setAuthTokens,
resetPassword,
isDomainAllowed,
requestPasswordReset,
resetPassword,
setAuthTokens,
resendVerificationEmail,
};

View File

@@ -1,3 +1,5 @@
const { RateLimitPrefix } = require('librechat-data-provider');
/**
*
* @param {TCustomConfig['rateLimits'] | undefined} rateLimits
@@ -6,24 +8,41 @@ const handleRateLimits = (rateLimits) => {
if (!rateLimits) {
return;
}
const { fileUploads, conversationsImport } = rateLimits;
if (fileUploads) {
process.env.FILE_UPLOAD_IP_MAX = fileUploads.ipMax ?? process.env.FILE_UPLOAD_IP_MAX;
process.env.FILE_UPLOAD_IP_WINDOW =
fileUploads.ipWindowInMinutes ?? process.env.FILE_UPLOAD_IP_WINDOW;
process.env.FILE_UPLOAD_USER_MAX = fileUploads.userMax ?? process.env.FILE_UPLOAD_USER_MAX;
process.env.FILE_UPLOAD_USER_WINDOW =
fileUploads.userWindowInMinutes ?? process.env.FILE_UPLOAD_USER_WINDOW;
}
if (conversationsImport) {
process.env.IMPORT_IP_MAX = conversationsImport.ipMax ?? process.env.IMPORT_IP_MAX;
process.env.IMPORT_IP_WINDOW =
conversationsImport.ipWindowInMinutes ?? process.env.IMPORT_IP_WINDOW;
process.env.IMPORT_USER_MAX = conversationsImport.userMax ?? process.env.IMPORT_USER_MAX;
process.env.IMPORT_USER_WINDOW =
conversationsImport.userWindowInMinutes ?? process.env.IMPORT_USER_WINDOW;
}
const rateLimitKeys = {
fileUploads: RateLimitPrefix.FILE_UPLOAD,
conversationsImport: RateLimitPrefix.IMPORT,
tts: RateLimitPrefix.TTS,
stt: RateLimitPrefix.STT,
};
Object.entries(rateLimitKeys).forEach(([key, prefix]) => {
const rateLimit = rateLimits[key];
if (rateLimit) {
setRateLimitEnvVars(prefix, rateLimit);
}
});
};
/**
* Set environment variables for rate limit configurations
*
* @param {string} prefix - Prefix for environment variable names
* @param {object} rateLimit - Rate limit configuration object
*/
const setRateLimitEnvVars = (prefix, rateLimit) => {
const envVarsMapping = {
ipMax: `${prefix}_IP_MAX`,
ipWindowInMinutes: `${prefix}_IP_WINDOW`,
userMax: `${prefix}_USER_MAX`,
userWindowInMinutes: `${prefix}_USER_WINDOW`,
};
Object.entries(envVarsMapping).forEach(([key, envVar]) => {
if (rateLimit[key] !== undefined) {
process.env[envVar] = rateLimit[key];
}
});
};
module.exports = handleRateLimits;

View File

@@ -112,6 +112,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
modelDisplayLabel: endpointConfig.modelDisplayLabel,
titleMethod: endpointConfig.titleMethod ?? 'completion',
contextStrategy: endpointConfig.summarize ? 'summarize' : null,
directEndpoint: endpointConfig.directEndpoint,
titleMessageRole: endpointConfig.titleMessageRole,
endpointTokenConfig,
};

View File

@@ -0,0 +1,48 @@
const { logger } = require('~/config');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { getProvider } = require('./textToSpeech');
/**
* This function retrieves the available voices for the current TTS provider
* It first fetches the TTS configuration and determines the provider
* Then, based on the provider, it sends the corresponding voices as a JSON response
*
* @param {Object} req - The request object
* @param {Object} res - The response object
* @returns {Promise<void>}
* @throws {Error} - If the provider is not 'openai' or 'elevenlabs', an error is thrown
*/
async function getVoices(req, res) {
try {
const customConfig = await getCustomConfig();
if (!customConfig || !customConfig?.tts) {
throw new Error('Configuration or TTS schema is missing');
}
const ttsSchema = customConfig?.tts;
const provider = getProvider(ttsSchema);
let voices;
switch (provider) {
case 'openai':
voices = ttsSchema.openai?.voices;
break;
case 'elevenlabs':
voices = ttsSchema.elevenlabs?.voices;
break;
case 'localai':
voices = ttsSchema.localai?.voices;
break;
default:
throw new Error('Invalid provider');
}
res.json(voices);
} catch (error) {
logger.error(`Failed to get voices: ${error.message}`);
res.status(500).json({ error: 'Failed to get voices' });
}
}
module.exports = getVoices;

View File

@@ -0,0 +1,11 @@
const getVoices = require('./getVoices');
const textToSpeech = require('./textToSpeech');
const speechToText = require('./speechToText');
const { updateTokenWebsocket } = require('./webSocket');
module.exports = {
getVoices,
speechToText,
...textToSpeech,
updateTokenWebsocket,
};

View File

@@ -0,0 +1,211 @@
const axios = require('axios');
const { Readable } = require('stream');
const { logger } = require('~/config');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { extractEnvVariable } = require('librechat-data-provider');
/**
* Handle the response from the STT API
* @param {Object} response - The response from the STT API
*
* @returns {string} The text from the response data
*
* @throws Will throw an error if the response status is not 200 or the response data is missing
*/
async function handleResponse(response) {
if (response.status !== 200) {
throw new Error('Invalid response from the STT API');
}
if (!response.data || !response.data.text) {
throw new Error('Missing data in response from the STT API');
}
return response.data.text.trim();
}
function getProvider(sttSchema) {
if (sttSchema.openai) {
return 'openai';
}
throw new Error('Invalid provider');
}
function removeUndefined(obj) {
Object.keys(obj).forEach((key) => {
if (obj[key] && typeof obj[key] === 'object') {
removeUndefined(obj[key]);
if (Object.keys(obj[key]).length === 0) {
delete obj[key];
}
} else if (obj[key] === undefined) {
delete obj[key];
}
});
}
/**
* This function prepares the necessary data and headers for making a request to the OpenAI API
* It uses the provided speech-to-text schema and audio stream to create the request
*
* @param {Object} sttSchema - The speech-to-text schema containing the OpenAI configuration
* @param {Stream} audioReadStream - The audio data to be transcribed
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* If an error occurs, it returns an array with three null values and logs the error with logger
*/
function openAIProvider(sttSchema, audioReadStream) {
try {
const url = sttSchema.openai?.url || 'https://api.openai.com/v1/audio/transcriptions';
const apiKey = sttSchema.openai.apiKey ? extractEnvVariable(sttSchema.openai.apiKey) : '';
let data = {
file: audioReadStream,
model: sttSchema.openai.model,
};
let headers = {
'Content-Type': 'multipart/form-data',
};
[headers].forEach(removeUndefined);
if (apiKey) {
headers.Authorization = 'Bearer ' + apiKey;
}
return [url, data, headers];
} catch (error) {
logger.error('An error occurred while preparing the OpenAI API STT request: ', error);
return [null, null, null];
}
}
/**
* This function prepares the necessary data and headers for making a request to the Azure API
* It uses the provided request and audio stream to create the request
*
* @param {Object} req - The request object, which should contain the endpoint in its body
* @param {Stream} audioReadStream - The audio data to be transcribed
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* If an error occurs, it returns an array with three null values and logs the error with logger
*/
function azureProvider(req, audioReadStream) {
try {
const { endpoint } = req.body;
const azureConfig = req.app.locals[endpoint];
if (!azureConfig) {
throw new Error(`No configuration found for endpoint: ${endpoint}`);
}
const { apiKey, instanceName, whisperModel, apiVersion } = Object.entries(
azureConfig.groupMap,
).reduce((acc, [, value]) => {
if (acc) {
return acc;
}
const whisperKey = Object.keys(value.models).find((modelKey) =>
modelKey.startsWith('whisper'),
);
if (whisperKey) {
return {
apiVersion: value.version,
apiKey: value.apiKey,
instanceName: value.instanceName,
whisperModel: value.models[whisperKey]['deploymentName'],
};
}
return null;
}, null);
if (!apiKey || !instanceName || !whisperModel || !apiVersion) {
throw new Error('Required Azure configuration values are missing');
}
const baseURL = `https://${instanceName}.openai.azure.com`;
const url = `${baseURL}/openai/deployments/${whisperModel}/audio/transcriptions?api-version=${apiVersion}`;
let data = {
file: audioReadStream,
filename: 'audio.wav',
contentType: 'audio/wav',
knownLength: audioReadStream.length,
};
const headers = {
...data.getHeaders(),
'Content-Type': 'multipart/form-data',
'api-key': apiKey,
};
return [url, data, headers];
} catch (error) {
logger.error('An error occurred while preparing the Azure API STT request: ', error);
return [null, null, null];
}
}
/**
* Convert speech to text
* @param {Object} req - The request object
* @param {Object} res - The response object
*
* @returns {Object} The response object with the text from the STT API
*
* @throws Will throw an error if an error occurs while processing the audio
*/
async function speechToText(req, res) {
const customConfig = await getCustomConfig();
if (!customConfig) {
return res.status(500).send('Custom config not found');
}
if (!req.file || !req.file.buffer) {
return res.status(400).json({ message: 'No audio file provided in the FormData' });
}
const audioBuffer = req.file.buffer;
const audioReadStream = Readable.from(audioBuffer);
audioReadStream.path = 'audio.wav';
const provider = getProvider(customConfig.stt);
let [url, data, headers] = [];
switch (provider) {
case 'openai':
[url, data, headers] = openAIProvider(customConfig.stt, audioReadStream);
break;
case 'azure':
[url, data, headers] = azureProvider(req, audioReadStream);
break;
default:
throw new Error('Invalid provider');
}
if (!Readable.from) {
const audioBlob = new Blob([audioBuffer], { type: req.file.mimetype });
delete data['file'];
data['file'] = audioBlob;
}
try {
const response = await axios.post(url, data, { headers: headers });
const text = await handleResponse(response);
res.json({ text });
} catch (error) {
logger.error('An error occurred while processing the audio:', error);
res.sendStatus(500);
}
}
module.exports = speechToText;

View File

@@ -0,0 +1,371 @@
const WebSocket = require('ws');
const { Message } = require('~/models/Message');
/**
* @param {string[]} voiceIds - Array of voice IDs
* @returns {string}
*/
function getRandomVoiceId(voiceIds) {
const randomIndex = Math.floor(Math.random() * voiceIds.length);
return voiceIds[randomIndex];
}
/**
* @typedef {Object} VoiceSettings
* @property {number} similarity_boost
* @property {number} stability
* @property {boolean} use_speaker_boost
*/
/**
* @typedef {Object} GenerateAudioBulk
* @property {string} model_id
* @property {string} text
* @property {VoiceSettings} voice_settings
*/
/**
* @typedef {Object} TextToSpeechClient
* @property {function(Object): Promise<stream.Readable>} generate
*/
/**
* @typedef {Object} AudioChunk
* @property {string} audio
* @property {boolean} isFinal
* @property {Object} alignment
* @property {number[]} alignment.char_start_times_ms
* @property {number[]} alignment.chars_durations_ms
* @property {string[]} alignment.chars
* @property {Object} normalizedAlignment
* @property {number[]} normalizedAlignment.char_start_times_ms
* @property {number[]} normalizedAlignment.chars_durations_ms
* @property {string[]} normalizedAlignment.chars
*/
/**
*
* @param {Record<string, unknown | undefined>} parameters
* @returns
*/
function assembleQuery(parameters) {
let query = '';
let hasQuestionMark = false;
for (const [key, value] of Object.entries(parameters)) {
if (value == null) {
continue;
}
if (!hasQuestionMark) {
query += '?';
hasQuestionMark = true;
} else {
query += '&';
}
query += `${key}=${value}`;
}
return query;
}
const SEPARATORS = ['.', '?', '!', '۔', '。', '‥', ';', '¡', '¿', '\n'];
/**
*
* @param {string} text
* @param {string[] | undefined} [separators]
* @returns
*/
function findLastSeparatorIndex(text, separators = SEPARATORS) {
let lastIndex = -1;
for (const separator of separators) {
const index = text.lastIndexOf(separator);
if (index > lastIndex) {
lastIndex = index;
}
}
return lastIndex;
}
const MAX_NOT_FOUND_COUNT = 6;
const MAX_NO_CHANGE_COUNT = 10;
/**
* @param {string} messageId
* @returns {() => Promise<{ text: string, isFinished: boolean }[]>}
*/
function createChunkProcessor(messageId) {
let notFoundCount = 0;
let noChangeCount = 0;
let processedText = '';
if (!messageId) {
throw new Error('Message ID is required');
}
/**
* @returns {Promise<{ text: string, isFinished: boolean }[] | string>}
*/
async function processChunks() {
if (notFoundCount >= MAX_NOT_FOUND_COUNT) {
return `Message not found after ${MAX_NOT_FOUND_COUNT} attempts`;
}
if (noChangeCount >= MAX_NO_CHANGE_COUNT) {
return `No change in message after ${MAX_NO_CHANGE_COUNT} attempts`;
}
const message = await Message.findOne({ messageId }, 'text unfinished').lean();
if (!message || !message.text) {
notFoundCount++;
return [];
}
const { text, unfinished } = message;
if (text === processedText) {
noChangeCount++;
}
const remainingText = text.slice(processedText.length);
const chunks = [];
if (unfinished && remainingText.length >= 20) {
const separatorIndex = findLastSeparatorIndex(remainingText);
if (separatorIndex !== -1) {
const chunkText = remainingText.slice(0, separatorIndex + 1);
chunks.push({ text: chunkText, isFinished: false });
processedText += chunkText;
} else {
chunks.push({ text: remainingText, isFinished: false });
processedText = text;
}
} else if (!unfinished && remainingText.trim().length > 0) {
chunks.push({ text: remainingText.trim(), isFinished: true });
processedText = text;
}
return chunks;
}
return processChunks;
}
/**
* @param {string} text
* @param {number} [chunkSize=4000]
* @returns {{ text: string, isFinished: boolean }[]}
*/
function splitTextIntoChunks(text, chunkSize = 4000) {
if (!text) {
throw new Error('Text is required');
}
const chunks = [];
let startIndex = 0;
const textLength = text.length;
while (startIndex < textLength) {
let endIndex = Math.min(startIndex + chunkSize, textLength);
let chunkText = text.slice(startIndex, endIndex);
if (endIndex < textLength) {
let lastSeparatorIndex = -1;
for (const separator of SEPARATORS) {
const index = chunkText.lastIndexOf(separator);
if (index !== -1) {
lastSeparatorIndex = Math.max(lastSeparatorIndex, index);
}
}
if (lastSeparatorIndex !== -1) {
endIndex = startIndex + lastSeparatorIndex + 1;
chunkText = text.slice(startIndex, endIndex);
} else {
const nextSeparatorIndex = text.slice(endIndex).search(/\S/);
if (nextSeparatorIndex !== -1) {
endIndex += nextSeparatorIndex;
chunkText = text.slice(startIndex, endIndex);
}
}
}
chunkText = chunkText.trim();
if (chunkText) {
chunks.push({
text: chunkText,
isFinished: endIndex >= textLength,
});
} else if (chunks.length > 0) {
chunks[chunks.length - 1].isFinished = true;
}
startIndex = endIndex;
while (startIndex < textLength && text[startIndex].trim() === '') {
startIndex++;
}
}
return chunks;
}
/**
* Input stream text to speech
* @param {Express.Response} res
* @param {AsyncIterable<string>} textStream
* @param {(token: string) => Promise<boolean>} callback - Whether to continue the stream or not
* @returns {AsyncGenerator<AudioChunk>}
*/
function inputStreamTextToSpeech(res, textStream, callback) {
const model = 'eleven_monolingual_v1';
const wsUrl = `wss://api.elevenlabs.io/v1/text-to-speech/${getRandomVoiceId()}/stream-input${assembleQuery(
{
model_id: model,
// flush: true,
// optimize_streaming_latency: this.settings.optimizeStreamingLatency,
optimize_streaming_latency: 1,
// output_format: this.settings.outputFormat,
},
)}`;
const socket = new WebSocket(wsUrl);
socket.onopen = function () {
const streamStart = {
text: ' ',
voice_settings: {
stability: 0.5,
similarity_boost: 0.8,
},
xi_api_key: process.env.ELEVENLABS_API_KEY,
// generation_config: { chunk_length_schedule: [50, 90, 120, 150, 200] },
};
socket.send(JSON.stringify(streamStart));
// send stream until done
const streamComplete = new Promise((resolve, reject) => {
(async () => {
let textBuffer = '';
let shouldContinue = true;
for await (const textDelta of textStream) {
textBuffer += textDelta;
// using ". " as separator: sending in full sentences improves the quality
// of the audio output significantly.
const separatorIndex = findLastSeparatorIndex(textBuffer);
// Callback for textStream (will return false if signal is aborted)
shouldContinue = await callback(textDelta);
if (separatorIndex === -1) {
continue;
}
if (!shouldContinue) {
break;
}
const textToProcess = textBuffer.slice(0, separatorIndex);
textBuffer = textBuffer.slice(separatorIndex + 1);
const request = {
text: textToProcess,
try_trigger_generation: true,
};
socket.send(JSON.stringify(request));
}
// send remaining text:
if (shouldContinue && textBuffer.length > 0) {
socket.send(
JSON.stringify({
text: `${textBuffer} `, // append space
try_trigger_generation: true,
}),
);
}
})()
.then(resolve)
.catch(reject);
});
streamComplete
.then(() => {
const endStream = {
text: '',
};
socket.send(JSON.stringify(endStream));
})
.catch((e) => {
console.error('Error streaming text to speech:', e);
throw e;
});
};
return (async function* audioStream() {
let isDone = false;
let chunks = [];
let resolve;
let waitForMessage = new Promise((r) => (resolve = r));
socket.onmessage = function (event) {
// console.log(event);
const audioChunk = JSON.parse(event.data);
if (audioChunk.audio && audioChunk.alignment) {
res.write(`event: audio\ndata: ${event.data}\n\n`);
chunks.push(audioChunk);
resolve(null);
waitForMessage = new Promise((r) => (resolve = r));
} else if (audioChunk.isFinal) {
isDone = true;
resolve(null);
} else if (audioChunk.message) {
console.warn('Received Elevenlabs message:', audioChunk.message);
resolve(null);
}
};
socket.onerror = function (error) {
console.error('WebSocket error:', error);
// throw error;
};
socket.onclose = function () {
isDone = true;
resolve(null);
};
while (!isDone) {
await waitForMessage;
yield* chunks;
chunks = [];
}
res.write('event: end\ndata: \n\n');
})();
}
/**
*
* @param {AsyncIterable<string>} llmStream
*/
async function* llmMessageSource(llmStream) {
for await (const chunk of llmStream) {
const message = chunk.choices[0].delta.content;
if (message) {
yield message;
}
}
}
module.exports = {
inputStreamTextToSpeech,
findLastSeparatorIndex,
createChunkProcessor,
splitTextIntoChunks,
llmMessageSource,
getRandomVoiceId,
};

View File

@@ -0,0 +1,137 @@
const { createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
const { Message } = require('~/models/Message');
jest.mock('~/models/Message', () => ({
Message: {
findOne: jest.fn().mockReturnValue({
lean: jest.fn(),
}),
},
}));
describe('processChunks', () => {
let processChunks;
beforeEach(() => {
processChunks = createChunkProcessor('message-id');
Message.findOne.mockClear();
Message.findOne().lean.mockClear();
});
it('should return an empty array when the message is not found', async () => {
Message.findOne().lean.mockResolvedValueOnce(null);
const result = await processChunks();
expect(result).toEqual([]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalled();
});
it('should return an empty array when the message does not have a text property', async () => {
Message.findOne().lean.mockResolvedValueOnce({ unfinished: true });
const result = await processChunks();
expect(result).toEqual([]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalled();
});
it('should return chunks for an unfinished message with separators', async () => {
const messageText = 'This is a long message. It should be split into chunks. Lol hi mom';
Message.findOne().lean.mockResolvedValueOnce({ text: messageText, unfinished: true });
const result = await processChunks();
expect(result).toEqual([
{ text: 'This is a long message. It should be split into chunks.', isFinished: false },
]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalled();
});
it('should return chunks for an unfinished message without separators', async () => {
const messageText = 'This is a long message without separators hello there my friend';
Message.findOne().lean.mockResolvedValueOnce({ text: messageText, unfinished: true });
const result = await processChunks();
expect(result).toEqual([{ text: messageText, isFinished: false }]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalled();
});
it('should return the remaining text as a chunk for a finished message', async () => {
const messageText = 'This is a finished message.';
Message.findOne().lean.mockResolvedValueOnce({ text: messageText, unfinished: false });
const result = await processChunks();
expect(result).toEqual([{ text: messageText, isFinished: true }]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalled();
});
it('should return an empty array for a finished message with no remaining text', async () => {
const messageText = 'This is a finished message.';
Message.findOne().lean.mockResolvedValueOnce({ text: messageText, unfinished: false });
await processChunks();
Message.findOne().lean.mockResolvedValueOnce({ text: messageText, unfinished: false });
const result = await processChunks();
expect(result).toEqual([]);
expect(Message.findOne).toHaveBeenCalledWith({ messageId: 'message-id' }, 'text unfinished');
expect(Message.findOne().lean).toHaveBeenCalledTimes(2);
});
});
describe('splitTextIntoChunks', () => {
test('splits text into chunks of specified size with default separators', () => {
const text = 'This is a test. This is only a test! Make sure it works properly? Okay.';
const chunkSize = 20;
const expectedChunks = [
{ text: 'This is a test.', isFinished: false },
{ text: 'This is only a test!', isFinished: false },
{ text: 'Make sure it works p', isFinished: false },
{ text: 'roperly? Okay.', isFinished: true },
];
const result = splitTextIntoChunks(text, chunkSize);
expect(result).toEqual(expectedChunks);
});
test('splits text into chunks with default size', () => {
const text = 'A'.repeat(8000) + '. The end.';
const expectedChunks = [
{ text: 'A'.repeat(4000), isFinished: false },
{ text: 'A'.repeat(4000), isFinished: false },
{ text: '. The end.', isFinished: true },
];
const result = splitTextIntoChunks(text);
expect(result).toEqual(expectedChunks);
});
test('returns a single chunk if text length is less than chunk size', () => {
const text = 'Short text.';
const expectedChunks = [{ text: 'Short text.', isFinished: true }];
const result = splitTextIntoChunks(text, 4000);
expect(result).toEqual(expectedChunks);
});
test('handles text with no separators correctly', () => {
const text = 'ThisTextHasNoSeparatorsAndIsVeryLong'.repeat(100);
const chunkSize = 4000;
const expectedChunks = [{ text: text, isFinished: true }];
const result = splitTextIntoChunks(text, chunkSize);
expect(result).toEqual(expectedChunks);
});
test('throws an error when text is empty', () => {
expect(() => splitTextIntoChunks('')).toThrow('Text is required');
});
});

View File

@@ -0,0 +1,416 @@
const axios = require('axios');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
const { extractEnvVariable } = require('librechat-data-provider');
const { logger } = require('~/config');
/**
* getProvider function
* This function takes the ttsSchema object and returns the name of the provider
* If more than one provider is set or no provider is set, it throws an error
*
* @param {Object} ttsSchema - The TTS schema containing the provider configuration
* @returns {string} The name of the provider
* @throws {Error} Throws an error if multiple providers are set or no provider is set
*/
function getProvider(ttsSchema) {
if (!ttsSchema) {
throw new Error(`No TTS schema is set. Did you configure TTS in the custom config (librechat.yaml)?
https://www.librechat.ai/docs/configuration/stt_tts#tts`);
}
const providers = Object.entries(ttsSchema).filter(([, value]) => Object.keys(value).length > 0);
if (providers.length > 1) {
throw new Error('Multiple providers are set. Please set only one provider.');
} else if (providers.length === 0) {
throw new Error('No provider is set. Please set a provider.');
} else {
return providers[0][0];
}
}
/**
* removeUndefined function
* This function takes an object and removes all keys with undefined values
* It also removes keys with empty objects as values
*
* @param {Object} obj - The object to be cleaned
* @returns {void} This function does not return a value. It modifies the input object directly
*/
function removeUndefined(obj) {
Object.keys(obj).forEach((key) => {
if (obj[key] && typeof obj[key] === 'object') {
removeUndefined(obj[key]);
if (Object.keys(obj[key]).length === 0) {
delete obj[key];
}
} else if (obj[key] === undefined) {
delete obj[key];
}
});
}
/**
* This function prepares the necessary data and headers for making a request to the OpenAI TTS
* It uses the provided TTS schema, input text, and voice to create the request
*
* @param {TCustomConfig['tts']['openai']} ttsSchema - The TTS schema containing the OpenAI configuration
* @param {string} input - The text to be converted to speech
* @param {string} voice - The voice to be used for the speech
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* If an error occurs, it throws an error with a message indicating that the selected voice is not available
*/
function openAIProvider(ttsSchema, input, voice) {
const url = ttsSchema?.url || 'https://api.openai.com/v1/audio/speech';
if (
ttsSchema?.voices &&
ttsSchema.voices.length > 0 &&
!ttsSchema.voices.includes(voice) &&
!ttsSchema.voices.includes('ALL')
) {
throw new Error(`Voice ${voice} is not available.`);
}
let data = {
input,
model: ttsSchema?.model,
voice: ttsSchema?.voices && ttsSchema.voices.length > 0 ? voice : undefined,
backend: ttsSchema?.backend,
};
let headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + extractEnvVariable(ttsSchema?.apiKey),
};
[data, headers].forEach(removeUndefined);
return [url, data, headers];
}
/**
* elevenLabsProvider function
* This function prepares the necessary data and headers for making a request to the Eleven Labs TTS
* It uses the provided TTS schema, input text, and voice to create the request
*
* @param {TCustomConfig['tts']['elevenLabs']} ttsSchema - The TTS schema containing the Eleven Labs configuration
* @param {string} input - The text to be converted to speech
* @param {string} voice - The voice to be used for the speech
* @param {boolean} stream - Whether to stream the audio or not
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* @throws {Error} Throws an error if the selected voice is not available
*/
function elevenLabsProvider(ttsSchema, input, voice, stream) {
let url =
ttsSchema?.url ||
`https://api.elevenlabs.io/v1/text-to-speech/{voice_id}${stream ? '/stream' : ''}`;
if (!ttsSchema?.voices.includes(voice) && !ttsSchema?.voices.includes('ALL')) {
throw new Error(`Voice ${voice} is not available.`);
}
url = url.replace('{voice_id}', voice);
let data = {
model_id: ttsSchema?.model,
text: input,
// voice_id: voice,
voice_settings: {
similarity_boost: ttsSchema?.voice_settings?.similarity_boost,
stability: ttsSchema?.voice_settings?.stability,
style: ttsSchema?.voice_settings?.style,
use_speaker_boost: ttsSchema?.voice_settings?.use_speaker_boost || undefined,
},
pronunciation_dictionary_locators: ttsSchema?.pronunciation_dictionary_locators,
};
let headers = {
'Content-Type': 'application/json',
'xi-api-key': extractEnvVariable(ttsSchema?.apiKey),
Accept: 'audio/mpeg',
};
[data, headers].forEach(removeUndefined);
return [url, data, headers];
}
/**
* localAIProvider function
* This function prepares the necessary data and headers for making a request to the LocalAI TTS
* It uses the provided TTS schema, input text, and voice to create the request
*
* @param {TCustomConfig['tts']['localai']} ttsSchema - The TTS schema containing the LocalAI configuration
* @param {string} input - The text to be converted to speech
* @param {string} voice - The voice to be used for the speech
*
* @returns {Array} An array containing the URL for the API request, the data to be sent, and the headers for the request
* @throws {Error} Throws an error if the selected voice is not available
*/
function localAIProvider(ttsSchema, input, voice) {
let url = ttsSchema?.url;
if (
ttsSchema?.voices &&
ttsSchema.voices.length > 0 &&
!ttsSchema.voices.includes(voice) &&
!ttsSchema.voices.includes('ALL')
) {
throw new Error(`Voice ${voice} is not available.`);
}
let data = {
input,
model: ttsSchema?.voices && ttsSchema.voices.length > 0 ? voice : undefined,
backend: ttsSchema?.backend,
};
let headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + extractEnvVariable(ttsSchema?.apiKey),
};
[data, headers].forEach(removeUndefined);
if (extractEnvVariable(ttsSchema.apiKey) === '') {
delete headers.Authorization;
}
return [url, data, headers];
}
/**
*
* Returns provider and its schema for use with TTS requests
* @param {TCustomConfig} customConfig
* @param {string} _voice
* @returns {Promise<[string, TProviderSchema]>}
*/
async function getProviderSchema(customConfig) {
const provider = getProvider(customConfig.tts);
return [provider, customConfig.tts[provider]];
}
/**
*
* Returns a tuple of the TTS schema as well as the voice for the TTS request
* @param {TProviderSchema} providerSchema
* @param {string} requestVoice
* @returns {Promise<string>}
*/
async function getVoice(providerSchema, requestVoice) {
const voices = providerSchema.voices.filter((voice) => voice && voice.toUpperCase() !== 'ALL');
let voice = requestVoice;
if (!voice || !voices.includes(voice) || (voice.toUpperCase() === 'ALL' && voices.length > 1)) {
voice = getRandomVoiceId(voices);
}
return voice;
}
/**
*
* @param {string} provider
* @param {TProviderSchema} ttsSchema
* @param {object} params
* @param {string} params.voice
* @param {string} params.input
* @param {boolean} [params.stream]
* @returns {Promise<ArrayBuffer>}
*/
async function ttsRequest(provider, ttsSchema, { input, voice, stream = true } = { stream: true }) {
let [url, data, headers] = [];
switch (provider) {
case 'openai':
[url, data, headers] = openAIProvider(ttsSchema, input, voice);
break;
case 'elevenlabs':
[url, data, headers] = elevenLabsProvider(ttsSchema, input, voice, stream);
break;
case 'localai':
[url, data, headers] = localAIProvider(ttsSchema, input, voice);
break;
default:
throw new Error('Invalid provider');
}
if (stream) {
return await axios.post(url, data, { headers, responseType: 'stream' });
}
return await axios.post(url, data, { headers, responseType: 'arraybuffer' });
}
/**
* Handles a text-to-speech request. Extracts input and voice from the request, retrieves the TTS configuration,
* and sends a request to the appropriate provider. The resulting audio data is sent in the response
*
* @param {Object} req - The request object, which should contain the input text and voice in its body
* @param {Object} res - The response object, used to send the audio data or an error message
*
* @returns {Promise<void>} This function does not return a value. It sends the audio data or an error message in the response
*
* @throws {Error} Throws an error if the provider is invalid
*/
async function textToSpeech(req, res) {
const { input } = req.body;
if (!input) {
return res.status(400).send('Missing text in request body');
}
const customConfig = await getCustomConfig();
if (!customConfig) {
res.status(500).send('Custom config not found');
}
try {
res.setHeader('Content-Type', 'audio/mpeg');
const [provider, ttsSchema] = await getProviderSchema(customConfig);
const voice = await getVoice(ttsSchema, req.body.voice);
if (input.length < 4096) {
const response = await ttsRequest(provider, ttsSchema, { input, voice });
response.data.pipe(res);
return;
}
const textChunks = splitTextIntoChunks(input, 1000);
for (const chunk of textChunks) {
try {
const response = await ttsRequest(provider, ttsSchema, {
voice,
input: chunk.text,
stream: true,
});
logger.debug(`[textToSpeech] user: ${req?.user?.id} | writing audio stream`);
await new Promise((resolve) => {
response.data.pipe(res, { end: chunk.isFinished });
response.data.on('end', () => {
resolve();
});
});
if (chunk.isFinished) {
break;
}
} catch (innerError) {
logger.error('Error processing manual update:', chunk, innerError);
if (!res.headersSent) {
res.status(500).end();
}
return;
}
}
if (!res.headersSent) {
res.end();
}
} catch (error) {
logger.error(
'Error creating the audio stream. Suggestion: check your provider quota. Error:',
error,
);
res.status(500).send('An error occurred');
}
}
async function streamAudio(req, res) {
res.setHeader('Content-Type', 'audio/mpeg');
const customConfig = await getCustomConfig();
if (!customConfig) {
return res.status(500).send('Custom config not found');
}
const [provider, ttsSchema] = await getProviderSchema(customConfig);
const voice = await getVoice(ttsSchema, req.body.voice);
try {
let shouldContinue = true;
req.on('close', () => {
logger.warn('[streamAudio] Audio Stream Request closed by client');
shouldContinue = false;
});
const processChunks = createChunkProcessor(req.body.messageId);
while (shouldContinue) {
// example updates
// const updates = [
// { text: 'This is a test.', isFinished: false },
// { text: 'This is only a test.', isFinished: false },
// { text: 'Your voice is like a combination of Fergie and Jesus!', isFinished: true },
// ];
const updates = await processChunks();
if (typeof updates === 'string') {
logger.error(`Error processing audio stream updates: ${JSON.stringify(updates)}`);
res.status(500).end();
return;
}
if (updates.length === 0) {
await new Promise((resolve) => setTimeout(resolve, 1250));
continue;
}
for (const update of updates) {
try {
const response = await ttsRequest(provider, ttsSchema, {
voice,
input: update.text,
stream: true,
});
if (!shouldContinue) {
break;
}
logger.debug(`[streamAudio] user: ${req?.user?.id} | writing audio stream`);
await new Promise((resolve) => {
response.data.pipe(res, { end: update.isFinished });
response.data.on('end', () => {
resolve();
});
});
if (update.isFinished) {
shouldContinue = false;
break;
}
} catch (innerError) {
logger.error('Error processing update:', update, innerError);
if (!res.headersSent) {
res.status(500).end();
}
return;
}
}
if (!shouldContinue) {
break;
}
}
if (!res.headersSent) {
res.end();
}
} catch (error) {
logger.error('Failed to fetch audio:', error);
if (!res.headersSent) {
res.status(500).end();
}
}
}
module.exports = {
textToSpeech,
getProvider,
streamAudio,
};

View File

@@ -0,0 +1,31 @@
let token = '';
function updateTokenWebsocket(newToken) {
console.log('Token:', newToken);
token = newToken;
}
function sendTextToWebsocket(ws, onDataReceived) {
if (token === '[DONE]') {
ws.send(' ');
return;
}
if (ws.readyState === WebSocket.OPEN) {
ws.send(token);
ws.onmessage = function (event) {
console.log('Received:', event.data);
if (onDataReceived) {
onDataReceived(event.data); // Pass the received data to the callback function
}
};
} else {
console.error('WebSocket is not open. Ready state is: ' + ws.readyState);
}
}
module.exports = {
updateTokenWebsocket,
sendTextToWebsocket,
};

View File

@@ -88,7 +88,17 @@ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
}
};
const deleteUserPluginAuth = async (userId, authField) => {
const deleteUserPluginAuth = async (userId, authField, all = false) => {
if (all) {
try {
const response = await PluginAuth.deleteMany({ userId });
return response;
} catch (err) {
logger.error('[deleteUserPluginAuth]', err);
return err;
}
}
try {
return await PluginAuth.deleteOne({ userId, authField });
} catch (err) {

View File

@@ -1,3 +1,4 @@
const throttle = require('lodash/throttle');
const {
StepTypes,
ContentTypes,
@@ -8,6 +9,7 @@ const {
} = require('librechat-data-provider');
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
const { processRequiredActions } = require('~/server/services/ToolService');
const { saveMessage, updateMessageText } = require('~/models/Message');
const { createOnProgress, sendMessage } = require('~/server/utils');
const { processMessages } = require('~/server/services/Threads');
const { logger } = require('~/config');
@@ -43,6 +45,8 @@ class StreamRunManager {
/** @type {string} */
this.apiKey = this.openai.apiKey;
/** @type {string} */
this.parentMessageId = fields.parentMessageId;
/** @type {string} */
this.thread_id = fields.thread_id;
/** @type {RunCreateAndStreamParams} */
this.initialRunBody = fields.runBody;
@@ -58,10 +62,14 @@ class StreamRunManager {
this.messages = [];
/** @type {string} */
this.text = '';
/** @type {string} */
this.intermediateText = '';
/** @type {Set<string>} */
this.attachedFileIds = fields.attachedFileIds;
/** @type {undefined | Promise<ChatCompletion>} */
this.visionPromise = fields.visionPromise;
/** @type {boolean} */
this.savedInitialMessage = false;
/**
* @type {Object.<AssistantStreamEvents, (event: AssistantStreamEvent) => Promise<void>>}
@@ -123,6 +131,33 @@ class StreamRunManager {
sendMessage(this.res, contentData);
}
/* <------------------ Misc. Helpers ------------------> */
/** Returns the latest intermediate text
* @returns {string}
*/
getText() {
return this.intermediateText;
}
/** Saves the initial intermediate message
* @returns {Promise<void>}
*/
async saveInitialMessage() {
return saveMessage({
conversationId: this.finalMessage.conversationId,
messageId: this.finalMessage.messageId,
parentMessageId: this.parentMessageId,
model: this.req.body.assistant_id,
endpoint: this.req.body.endpoint,
isCreatedByUser: false,
user: this.req.user.id,
text: this.getText(),
sender: 'Assistant',
unfinished: true,
error: false,
});
}
/* <------------------ Main Event Handlers ------------------> */
/**
@@ -407,6 +442,7 @@ class StreamRunManager {
const content = message.delta.content?.[0];
if (content && content.type === MessageContentTypes.TEXT) {
this.intermediateText += content.text.value;
onProgress(content.text.value);
}
}
@@ -461,6 +497,34 @@ class StreamRunManager {
return `${stepId}_tool_call_${toolCall.index}_${toolCall.type}`;
}
/**
* Check Missing Outputs
* @param {ToolOutput[]} tool_outputs - The tool outputs.
* @param {RequiredAction[]} actions - The required actions.
* @returns {ToolOutput[]} completeOutputs - The complete outputs.
*/
checkMissingOutputs(tool_outputs, actions) {
const missingOutputs = [];
for (const item of actions) {
const { tool, toolCallId, run_id, thread_id } = item;
const outputExists = tool_outputs.some((output) => output.tool_call_id === toolCallId);
if (!outputExists) {
logger.warn(
`The "${tool}" tool (ID: ${toolCallId}) failed to produce an output. run_id: ${run_id} thread_id: ${thread_id}`,
);
missingOutputs.push({
tool_call_id: toolCallId,
output:
'The tool failed to produce an output. The tool may not be currently available or experienced an unhandled error.',
});
}
}
return [...tool_outputs, ...missingOutputs];
}
/* <------------------ Run Event handlers ------------------> */
/**
@@ -483,7 +547,8 @@ class StreamRunManager {
};
});
const { tool_outputs } = await processRequiredActions(this, actions);
const { tool_outputs: preliminaryOutputs } = await processRequiredActions(this, actions);
const tool_outputs = this.checkMissingOutputs(preliminaryOutputs, actions);
/** @type {AssistantStream | undefined} */
let toolRun;
try {
@@ -523,10 +588,24 @@ class StreamRunManager {
const stepKey = message_creation.message_id;
const index = this.getStepIndex(stepKey);
this.orderedRunSteps.set(index, message_creation);
// Create the Factory Function to stream the message
const { onProgress: progressCallback } = createOnProgress({
// todo: add option to save partialText to db
// onProgress: () => {},
onProgress: throttle(
() => {
if (!this.savedInitialMessage) {
this.saveInitialMessage();
this.savedInitialMessage = true;
} else {
updateMessageText({
messageId: this.finalMessage.messageId,
text: this.getText(),
});
}
},
2000,
{ trailing: false },
),
});
// This creates a function that attaches all of the parameters

View File

@@ -121,6 +121,7 @@ async function saveUserMessage(params) {
* @param {Object} params - The parameters of the Assistant message
* @param {string} params.user - The user's ID.
* @param {string} params.messageId - The message Id.
* @param {string} params.text - The concatenated text of the message.
* @param {string} params.assistant_id - The assistant Id.
* @param {string} params.thread_id - The thread Id.
* @param {string} params.model - The model used by the assistant.
@@ -134,14 +135,6 @@ async function saveUserMessage(params) {
* @return {Promise<Run>} A promise that resolves to the created run object.
*/
async function saveAssistantMessage(params) {
const text = params.content.reduce((acc, part) => {
if (!part.value) {
return acc;
}
return acc + ' ' + part.value;
}, '');
// const tokenCount = // TODO: need to count each content part
const message = await recordMessage({
@@ -156,7 +149,8 @@ async function saveAssistantMessage(params) {
content: params.content,
sender: 'Assistant',
isCreatedByUser: false,
text: text.trim(),
text: params.text,
unfinished: false,
// tokenCount,
});
@@ -302,6 +296,7 @@ async function syncMessages({
aggregateMessages: [{ id: apiMessage.id }],
model: apiMessage.role === 'user' ? null : apiMessage.assistant_id,
user: openai.req.user.id,
unfinished: false,
};
if (apiMessage.file_ids?.length) {

View File

@@ -340,29 +340,26 @@ async function processRequiredActions(client, requiredActions) {
currentAction.toolInput = currentAction.toolInput.input;
}
try {
const promise = tool
._call(currentAction.toolInput)
.then(handleToolOutput)
.catch((error) => {
logger.error(`Error processing tool ${currentAction.tool}`, error);
return {
tool_call_id: currentAction.toolCallId,
output: `Error processing tool ${currentAction.tool}: ${redactMessage(error.message)}`,
};
});
promises.push(promise);
} catch (error) {
const handleToolError = (error) => {
logger.error(
`tool_call_id: ${currentAction.toolCallId} | Error processing tool ${currentAction.tool}`,
error,
);
promises.push(
Promise.resolve({
tool_call_id: currentAction.toolCallId,
error: error.message,
}),
);
return {
tool_call_id: currentAction.toolCallId,
output: `Error processing tool ${currentAction.tool}: ${redactMessage(error.message, 256)}`,
};
};
try {
const promise = tool
._call(currentAction.toolInput)
.then(handleToolOutput)
.catch(handleToolError);
promises.push(promise);
} catch (error) {
const toolOutputError = handleToolError(error);
promises.push(Promise.resolve(toolOutputError));
}
}

View File

@@ -1,6 +1,6 @@
const { ErrorTypes } = require('librechat-data-provider');
const { encrypt, decrypt } = require('~/server/utils');
const { User, Key } = require('~/models');
const { updateUser, Key } = require('~/models');
const { logger } = require('~/config');
/**
@@ -16,16 +16,13 @@ const { logger } = require('~/config');
*/
const updateUserPluginsService = async (user, pluginKey, action) => {
try {
const userPlugins = user.plugins || [];
if (action === 'install') {
return await User.updateOne(
{ _id: user._id },
{ $set: { plugins: [...user.plugins, pluginKey] } },
);
return await updateUser(user._id, { plugins: [...userPlugins, pluginKey] });
} else if (action === 'uninstall') {
return await User.updateOne(
{ _id: user._id },
{ $set: { plugins: user.plugins.filter((plugin) => plugin !== pluginKey) } },
);
return await updateUser(user._id, {
plugins: userPlugins.filter((plugin) => plugin !== pluginKey),
});
}
} catch (err) {
logger.error('[updateUserPluginsService]', err);
@@ -167,11 +164,11 @@ const checkUserKeyExpiry = (expiresAt, endpoint) => {
};
module.exports = {
updateUserPluginsService,
getUserKey,
getUserKeyValues,
getUserKeyExpiry,
updateUserKey,
deleteUserKey,
getUserKeyValues,
getUserKeyExpiry,
checkUserKeyExpiry,
updateUserPluginsService,
};

View File

@@ -1,6 +1,5 @@
const {
Capabilities,
EModelEndpoint,
assistantEndpointSchema,
defaultAssistantsVersion,
} = require('librechat-data-provider');
@@ -20,16 +19,25 @@ function azureAssistantsDefaults() {
/**
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
* @param {TCustomConfig} config - The loaded custom configuration.
* @param {Partial<TAssistantEndpoint>} [prevConfig]
* @param {EModelEndpoint.assistants|EModelEndpoint.azureAssistants} assistantsEndpoint - The Assistants endpoint name.
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
* @param {Partial<TAssistantEndpoint>} [prevConfig]
* @returns {Partial<TAssistantEndpoint>} The Assistants endpoint configuration.
*/
function assistantsConfigSetup(config, prevConfig = {}) {
const assistantsConfig = config.endpoints[EModelEndpoint.assistants];
function assistantsConfigSetup(config, assistantsEndpoint, prevConfig = {}) {
const assistantsConfig = config.endpoints[assistantsEndpoint];
const parsedConfig = assistantEndpointSchema.parse(assistantsConfig);
if (assistantsConfig.supportedIds?.length && assistantsConfig.excludedIds?.length) {
logger.warn(
`Both \`supportedIds\` and \`excludedIds\` are defined for the ${EModelEndpoint.assistants} endpoint; \`excludedIds\` field will be ignored.`,
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'supportedIds' and 'excludedIds' defined. The 'excludedIds' will be ignored.`,
);
}
if (
assistantsConfig.privateAssistants &&
(assistantsConfig.supportedIds?.length || assistantsConfig.excludedIds?.length)
) {
logger.warn(
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined. The 'supportedIds' and 'excludedIds' will be ignored.`,
);
}
@@ -41,6 +49,7 @@ function assistantsConfigSetup(config, prevConfig = {}) {
supportedIds: parsedConfig.supportedIds,
capabilities: parsedConfig.capabilities,
excludedIds: parsedConfig.excludedIds,
privateAssistants: parsedConfig.privateAssistants,
timeoutMs: parsedConfig.timeoutMs,
};
}

View File

@@ -3,6 +3,7 @@ const {
deprecatedAzureVariables,
conflictingAzureVariables,
} = require('librechat-data-provider');
const { isEnabled, checkEmailConfig } = require('~/server/utils');
const { logger } = require('~/config');
const secretDefaults = {
@@ -49,6 +50,8 @@ function checkVariables() {
Please use the config (\`librechat.yaml\`) file for setting up OpenRouter, and use \`OPENROUTER_KEY\` or another environment variable instead.`,
);
}
checkPasswordReset();
}
/**
@@ -107,4 +110,25 @@ Latest version: ${Constants.CONFIG_VERSION}
}
}
function checkPasswordReset() {
const emailEnabled = checkEmailConfig();
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
if (!emailEnabled && passwordResetAllowed) {
logger.warn(
`❗❗❗
Password reset is enabled with \`ALLOW_PASSWORD_RESET\` but email service is not configured.
This setup is insecure as password reset links will be issued with a recognized email.
Please configure email service for secure password reset functionality.
https://www.librechat.ai/docs/configuration/authentication/password_reset
❗❗❗`,
);
}
}
module.exports = { checkVariables, checkHealth, checkConfig, checkAzureVariables };

View File

@@ -1,14 +1,15 @@
const Redis = require('ioredis');
const passport = require('passport');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const passport = require('passport');
const {
setupOpenId,
googleLogin,
githubLogin,
discordLogin,
facebookLogin,
setupOpenId,
} = require('../strategies');
const client = require('../cache/redis');
} = require('~/strategies');
const { logger } = require('~/config');
/**
*
@@ -40,6 +41,11 @@ const configureSocialLogins = (app) => {
saveUninitialized: false,
};
if (process.env.USE_REDIS) {
const client = new Redis(process.env.REDIS_URI);
client
.on('error', (err) => logger.error('ioredis error:', err))
.on('ready', () => logger.info('ioredis successfully initialized.'))
.on('reconnecting', () => logger.info('ioredis reconnecting...'));
sessionOptions.store = new RedisStore({ client, prefix: 'librechat' });
}
app.use(session(sessionOptions));

View File

@@ -1,9 +1,11 @@
<!DOCTYPE HTML
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<html
xmlns='http://www.w3.org/1999/xhtml'
xmlns:v='urn:schemas-microsoft-com:vml'
xmlns:o='urn:schemas-microsoft-com:office:office'
>
<head>
<!--[if gte mso 9]>
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
@@ -11,176 +13,184 @@
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta name="color-scheme" content="light dark">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<title></title>
<style type="text/css">
@media (prefers-color-scheme: dark) {
.darkmode {
background-color: #212121 !important;
}
.darkmode p {
color: #ffffff !important;
}
}
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-100 {
width: 500px !important;
}
}
@media (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row .u-col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.u-row {
width: 100% !important;
}
.u-col {
width: 100% !important;
}
.u-col>div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}
table,
tr,
td {
vertical-align: top;
border-collapse: collapse;
}
.ie-container table,
.mso-container table {
table-layout: fixed;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors='true'] {
color: inherit !important;
text-decoration: none !important;
}
table,
td {
color: #ffffff;
}
</style>
</head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='x-apple-disable-message-reformatting' />
<meta name='color-scheme' content='light dark' />
<!--[if !mso]><!-->
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<!--<![endif]-->
<title></title>
<style type='text/css'>
@media (prefers-color-scheme: dark) { .darkmode { background-color: #212121 !important; }
.darkmode p { color: #ffffff !important; } } @media only screen and (min-width: 520px) {
.u-row { width: 500px !important; } .u-row .u-col { vertical-align: top; } .u-row .u-col-100 {
width: 500px !important; } } @media (max-width: 520px) { .u-row-container { max-width: 100%
!important; padding-left: 0px !important; padding-right: 0px !important; } .u-row .u-col {
min-width: 320px !important; max-width: 100% !important; display: block !important; } .u-row {
width: 100% !important; } .u-col { width: 100% !important; } .u-col>div { margin: 0 auto; } }
body { margin: 0; padding: 0; } table, tr, td { vertical-align: top; border-collapse:
collapse; } .ie-container table, .mso-container table { table-layout: fixed; } * {
line-height: inherit; } a[x-apple-data-detectors='true'] { color: inherit !important;
text-decoration: none !important; } table, td { color: #ffffff; }
</style>
</head>
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff">
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%" cellpadding="0" cellspacing="0">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;">
<div style="background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--<![endif]-->
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>Hi {{name}},</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>
<div>Your password has been updated successfully! </div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;">
<div>
<div><sub>© {{year}} {{appName}}. All rights
reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
<body
class='clean-body u_body'
style='margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff'
>
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table
style='border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%'
cellpadding='0'
cellspacing='0'
>
<tbody>
<tr style='vertical-align: top'>
<td
style='word-break: break-word;border-collapse: collapse !important;vertical-align: top'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class='u-row-container' style='padding: 0px;background-color: transparent'>
<div
class='u-row'
style='margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;'
>
<div
style='border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div
class='u-col u-col-100'
style='max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;'
>
<div
style='background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--[if (!mso)&(!IE)]><!-->
<div
style='box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--<![endif]-->
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>Hi {{name}},</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>
<div>Your password has been updated successfully! </div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;'
>
<div>
<div><sub>©
{{year}}
{{appName}}. All rights reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>

View File

@@ -1,9 +1,11 @@
<!DOCTYPE HTML
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<html
xmlns='http://www.w3.org/1999/xhtml'
xmlns:v='urn:schemas-microsoft-com:vml'
xmlns:o='urn:schemas-microsoft-com:office:office'
>
<head>
<!--[if gte mso 9]>
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
@@ -11,229 +13,272 @@
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta name="color-scheme" content="light dark">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<title></title>
<style type="text/css">
@media (prefers-color-scheme: dark) {
.darkmode {
background-color: #212121 !important;
}
.darkmode p {
color: #ffffff !important;
}
}
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-100 {
width: 500px !important;
}
}
@media (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row .u-col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.u-row {
width: 100% !important;
}
.u-col {
width: 100% !important;
}
.u-col>div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}
table,
tr,
td {
vertical-align: top;
border-collapse: collapse;
}
p {
margin: 0;
}
.ie-container table,
.mso-container table {
table-layout: fixed;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors='true'] {
color: inherit !important;
text-decoration: none !important;
}
table,
td {
color: #ffffff;
}
#u_body a {
color: #0000ee;
text-decoration: underline;
}
</style>
</head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='x-apple-disable-message-reformatting' />
<meta name='color-scheme' content='light dark' />
<!--[if !mso]><!-->
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<!--<![endif]-->
<title></title>
<style type='text/css'>
@media (prefers-color-scheme: dark) { .darkmode { background-color: #212121 !important; }
.darkmode p { color: #ffffff !important; } } @media only screen and (min-width: 520px) {
.u-row { width: 500px !important; } .u-row .u-col { vertical-align: top; } .u-row .u-col-100 {
width: 500px !important; } } @media (max-width: 520px) { .u-row-container { max-width: 100%
!important; padding-left: 0px !important; padding-right: 0px !important; } .u-row .u-col {
min-width: 320px !important; max-width: 100% !important; display: block !important; } .u-row {
width: 100% !important; } .u-col { width: 100% !important; } .u-col>div { margin: 0 auto; } }
body { margin: 0; padding: 0; } table, tr, td { vertical-align: top; border-collapse:
collapse; } p { margin: 0; } .ie-container table, .mso-container table { table-layout: fixed;
} * { line-height: inherit; } a[x-apple-data-detectors='true'] { color: inherit !important;
text-decoration: none !important; } table, td { color: #ffffff; } #u_body a { color: #0000ee;
text-decoration: underline; }
</style>
</head>
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff">
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%" cellpadding="0" cellspacing="0">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;">
<div style="background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--<![endif]-->
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<!--[if mso]><table width="100%"><tr><td><![endif]-->
<h1 style="margin: 0px; line-height: 140%; text-align: left; word-wrap: break-word; font-size: 22px; font-weight: 700;">
<div>
<div>You have requested to reset your password.
</div>
</div>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>Hi {{name}},</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<p style="line-height: 140%;">Please click the button below to reset your password.</p>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<!--[if mso]><style>.v-button {background: transparent !important;}</style><![endif]-->
<div align="left">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{link}}" style="height:37px; v-text-anchor:middle; width:142px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
<a href="{{link}}" target="_blank" class="v-button" style="box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #10a37f; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;"> <span style="display:block;padding:10px 20px;line-height:120%;"><span
style="line-height: 16.8px;">Reset
Password</span></span>
</a>
<!--[if mso]></center></v:roundrect><![endif]-->
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>
<div>If you did not request a password reset, please ignore this email.</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;">
<div>
<div><sub>© {{year}} {{appName}}. All rights
reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
<body
class='clean-body u_body'
style='margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff'
>
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table
id='u_body'
style='border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%'
cellpadding='0'
cellspacing='0'
>
<tbody>
<tr style='vertical-align: top'>
<td
style='word-break: break-word;border-collapse: collapse !important;vertical-align: top'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class='u-row-container' style='padding: 0px;background-color: transparent'>
<div
class='u-row'
style='margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;'
>
<div
style='border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div
class='u-col u-col-100'
style='max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;'
>
<div
style='background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--[if (!mso)&(!IE)]><!-->
<div
style='box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--<![endif]-->
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<!--[if mso]><table width="100%"><tr><td><![endif]-->
<h1
style='margin: 0px; line-height: 140%; text-align: left; word-wrap: break-word; font-size: 22px; font-weight: 700;'
>
<div>
<div>You have requested to reset your password.
</div>
</div>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>Hi {{name}},</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<p style='line-height: 140%;'>Please click the button below to
reset your password.</p>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<!--[if mso]><style>.v-button {background: transparent !important;}</style><![endif]-->
<div align='left'>
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{link}}" style="height:37px; v-text-anchor:middle; width:142px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
<a
href='{{link}}'
target='_blank'
class='v-button'
style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #10a37f; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'
>
<span
style='display:block;padding:10px 20px;line-height:120%;'
><span style='line-height: 16.8px;'>Reset Password</span></span>
</a>
<!--[if mso]></center></v:roundrect><![endif]-->
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>
<div>If you did not request a password reset, please ignore this
email.</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;'
>
<div>
<div><sub>©
{{year}}
{{appName}}. All rights reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>

View File

@@ -1,9 +1,11 @@
<!DOCTYPE HTML
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<html
xmlns='http://www.w3.org/1999/xhtml'
xmlns:v='urn:schemas-microsoft-com:vml'
xmlns:o='urn:schemas-microsoft-com:office:office'
>
<head>
<!--[if gte mso 9]>
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
@@ -11,229 +13,278 @@
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta name="color-scheme" content="light dark">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<title></title>
<style type="text/css">
@media (prefers-color-scheme: dark) {
.darkmode {
background-color: #212121 !important;
}
.darkmode p {
color: #ffffff !important;
}
}
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-100 {
width: 500px !important;
}
}
@media (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row .u-col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.u-row {
width: 100% !important;
}
.u-col {
width: 100% !important;
}
.u-col>div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}
table,
tr,
td {
vertical-align: top;
border-collapse: collapse;
}
.ie-container table,
.mso-container table {
table-layout: fixed;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors='true'] {
color: inherit !important;
text-decoration: none !important;
}
table,
td {
color: #ffffff;
}
#u_body a {
color: #0000ee;
text-decoration: underline;
}
</style>
</head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='x-apple-disable-message-reformatting' />
<meta name='color-scheme' content='light dark' />
<!--[if !mso]><!-->
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<!--<![endif]-->
<title></title>
<style type='text/css'>
@media (prefers-color-scheme: dark) { .darkmode { background-color: #212121 !important; }
.darkmode p { color: #ffffff !important; } } @media only screen and (min-width: 520px) {
.u-row { width: 500px !important; } .u-row .u-col { vertical-align: top; } .u-row .u-col-100 {
width: 500px !important; } } @media (max-width: 520px) { .u-row-container { max-width: 100%
!important; padding-left: 0px !important; padding-right: 0px !important; } .u-row .u-col {
min-width: 320px !important; max-width: 100% !important; display: block !important; } .u-row {
width: 100% !important; } .u-col { width: 100% !important; } .u-col>div { margin: 0 auto; } }
body { margin: 0; padding: 0; } table, tr, td { vertical-align: top; border-collapse:
collapse; } .ie-container table, .mso-container table { table-layout: fixed; } * {
line-height: inherit; } a[x-apple-data-detectors='true'] { color: inherit !important;
text-decoration: none !important; } table, td { color: #ffffff; } #u_body a { color: #0000ee;
text-decoration: underline; }
</style>
</head>
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff">
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%" cellpadding="0" cellspacing="0">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;">
<div style="background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;">
<!--<![endif]-->
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<!--[if mso]><table width="100%"><tr><td><![endif]-->
<h1 style="margin: 0px; line-height: 140%; text-align: left; word-wrap: break-word; font-size: 22px; font-weight: 700;">
<div>
<div>Welcome to {{appName}}!</div>
</div>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>
<div>Dear {{name}},</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>
<div>Thank you for registering with {{appName}}. To complete your registration and verify your email address, please click the button below:</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<!--[if mso]><style>.v-button {background: transparent !important;}</style><![endif]-->
<div align="left">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="href=&quot;{{verificationLink}}&quot;" style="height:37px; v-text-anchor:middle; width:114px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
<a href="href=&quot;{{verificationLink}}&quot;" target="_blank" class="v-button" style="box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #10a37f; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;"> <span style="display:block;padding:10px 20px;line-height:120%;">
<div>
<div>Verify Email</div>
</div>
</span> </a>
<!--[if mso]></center></v:roundrect><![endif]-->
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>
<div>If you did not create an account with {{appName}}, please ignore this email.</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;">
<div>
<div><sub>© {{year}} {{appName}}. All rights
reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
<body
class='clean-body u_body'
style='margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #212121;color: #ffffff'
>
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table
id='u_body'
style='border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #212121;width:100%'
cellpadding='0'
cellspacing='0'
>
<tbody>
<tr style='vertical-align: top'>
<td
style='word-break: break-word;border-collapse: collapse !important;vertical-align: top'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #212121;"><![endif]-->
<div class='u-row-container' style='padding: 0px;background-color: transparent'>
<div
class='u-row'
style='margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;'
>
<div
style='border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;'
>
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="500" style="background-color: #212121;width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;" valign="top"><![endif]-->
<div
class='u-col u-col-100'
style='max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;'
>
<div
style='background-color: #212121;height: 100%;width: 100% !important;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--[if (!mso)&(!IE)]><!-->
<div
style='box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;border-radius: 0px;-webkit-border-radius: 0px; -moz-border-radius: 0px;'
>
<!--<![endif]-->
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<!--[if mso]><table width="100%"><tr><td><![endif]-->
<h1
style='margin: 0px; line-height: 140%; text-align: left; word-wrap: break-word; font-size: 22px; font-weight: 700;'
>
<div>
<div>Welcome to {{appName}}!</div>
</div>
</h1>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>
<div>Dear {{name}},</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>
<div>Thank you for registering with
{{appName}}. To complete your registration and verify your
email address, please click the button below:</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<!--[if mso]><style>.v-button {background: transparent !important;}</style><![endif]-->
<div align='left'>
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="href=&quot;{{verificationLink}}&quot;" style="height:37px; v-text-anchor:middle; width:114px;" arcsize="11%" stroke="f" fillcolor="#10a37f"><w:anchorlock/><center style="color:#FFFFFF;"><![endif]-->
<a
href='{{verificationLink}}'
target='_blank'
class='v-button'
style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #10a37f; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'
>
<span style='display:block;padding:10px 20px;line-height:120%;'>
<div>
<div>Verify Email</div>
</div>
</span>
</a>
<!--[if mso]></center></v:roundrect><![endif]-->
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>
<div>If you did not create an account with
{{appName}}, please ignore this email.</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;'
>
<div>Best regards,</div>
<div>The {{appName}} Team</div>
</div>
</td>
</tr>
</tbody>
</table>
<table
style='font-family:arial,helvetica,sans-serif;'
role='presentation'
cellpadding='0'
cellspacing='0'
width='100%'
border='0'
>
<tbody>
<tr>
<td
style='overflow-wrap:break-word;word-break:break-word;padding:0px 10px 10px;font-family:arial,helvetica,sans-serif;'
align='left'
>
<div
style='font-size: 14px; line-height: 140%; text-align: right; word-wrap: break-word;'
>
<div>
<div><sub>©
{{year}}
{{appName}}. All rights reserved.</sub></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>

View File

@@ -19,7 +19,7 @@
"endpoint": "openAI",
"title": "VW Transporter 2014 Fuel Consumption. Web Search"
},
"messagesTree": [
"messages": [
{
"_id": "6615516574dc2ddcdebe40b6",
"messageId": "b123942f-ca1a-4b16-9e1f-ea4af5171168",

Some files were not shown because too many files have changed in this diff Show More