From 2291ee31b750a5289b8c7af37f50c82b614314ee Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 10 Sep 2025 09:08:31 +0200 Subject: [PATCH 01/30] feat: initial acr build and push action MIA-85 --- .../acr-build-and-push-libre-chat.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/acr-build-and-push-libre-chat.yml diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml new file mode 100644 index 00000000000..0fc4d1b2826 --- /dev/null +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -0,0 +1,40 @@ +name: Build and Push MIA - LibreChat Container to ACR + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./ + file: ./Dockerfile.multi + push: true + tags: | + ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:${{ github.sha }} + ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest + + + # - name: Build and push Docker image + # run: | + # az acr login --name ${{ secrets.ACR_NAME }} + # docker build -f ./Dockerfile.multi -t ${{ secrets.ACR_NAME }}.azurecr.io/mia-libre-chat:${{ github.sha }} . + # docker push ${{ secrets.ACR_NAME }}.azurecr.io/mia-libre-chat:${{ github.sha }} \ No newline at end of file From 0577dc906696fd9b0dc47f1767656b35b21de7c9 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 10 Sep 2025 09:14:13 +0200 Subject: [PATCH 02/30] feat: add working branch for testing action MIA-85 --- .github/workflows/acr-build-and-push-libre-chat.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index 0fc4d1b2826..f68ab3af32f 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - feat/MIA-85-librechat-build-pipeline-gh-action workflow_dispatch: jobs: From 8bd241122ba1a2a70d6a23a6bf5661ae832dd76b Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 10 Sep 2025 13:50:43 +0200 Subject: [PATCH 03/30] feat: permissions id-token write MIA-85 --- .github/workflows/acr-build-and-push-libre-chat.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index f68ab3af32f..dcba8c6b18e 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -7,6 +7,9 @@ on: - feat/MIA-85-librechat-build-pipeline-gh-action workflow_dispatch: +permissions: + id-token: write + jobs: build-and-push: runs-on: ubuntu-latest From b31feb14c7a7c40b7c5af556584ccb9dcee10d16 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 10 Sep 2025 17:18:14 +0200 Subject: [PATCH 04/30] feat: login to acr before build and push docker image MIA-85 --- .github/workflows/acr-build-and-push-libre-chat.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index dcba8c6b18e..fcd6321c07c 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -26,12 +26,17 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} enable-AzPSSession: true + - name: Login to ACR + run: | + az acr login --name ${{ vars.ACR_NAME }} + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: ./ file: ./Dockerfile.multi push: true + registries: ${{ vars.ACR_REGISTRY_SERVER }} tags: | ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:${{ github.sha }} ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest From eb1cc62c14118840909d0ddc6d90d530522b87ef Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 10 Sep 2025 17:36:30 +0200 Subject: [PATCH 05/30] refactor: remove not needed code MIA-85 --- .github/workflows/acr-build-and-push-libre-chat.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index fcd6321c07c..effd7b7c0b3 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -39,11 +39,4 @@ jobs: registries: ${{ vars.ACR_REGISTRY_SERVER }} tags: | ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:${{ github.sha }} - ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest - - - # - name: Build and push Docker image - # run: | - # az acr login --name ${{ secrets.ACR_NAME }} - # docker build -f ./Dockerfile.multi -t ${{ secrets.ACR_NAME }}.azurecr.io/mia-libre-chat:${{ github.sha }} . - # docker push ${{ secrets.ACR_NAME }}.azurecr.io/mia-libre-chat:${{ github.sha }} \ No newline at end of file + ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest \ No newline at end of file From 66a6282086a121ad2724c68bda08a90fe80765b5 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 11 Sep 2025 10:57:17 +0200 Subject: [PATCH 06/30] feat: only run action on main branch MIA-85 --- .github/workflows/acr-build-and-push-libre-chat.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index effd7b7c0b3..95fd80449dc 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - feat/MIA-85-librechat-build-pipeline-gh-action workflow_dispatch: permissions: From 6aeb5c6cfbfa8f81ced673a20cb99a6c73a14df7 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Tue, 21 Oct 2025 14:57:29 +0200 Subject: [PATCH 07/30] fix: Implement hotfix for Android Edge browser keyboard overlay issue in ChatForm - Added functionality to adjust form position when the virtual keyboard appears on Android Edge, ensuring the input field remains visible. - Implemented event handlers to manage margin adjustments on focus and blur of the textarea. - Relates to LibreChat issue #10074 MIA-113 --- client/src/components/Chat/Input/ChatForm.tsx | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 0736c7dc61f..f2f97d48e3e 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -121,6 +121,41 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { } }, [isCollapsed]); + /** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix + * + * Problem: On Android Edge browser, the virtual keyboard covers the input field + * when it appears, making it impossible to see what the user is typing. + * + * Solution: When the textarea gains focus, push the form up by adding + * margin-bottom to make the input field visible above the keyboard. + */ + const handleAndroidEdgeKeyboardFocusFix = useCallback(() => { + if (isAndroidEdgeBrowser()) { + const formElement = document.querySelector('form'); + if (formElement) { + // Add margin-bottom to push the form up by the keyboard height + formElement.style.marginBottom = `240px`; + } + } + }, []); + + /** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix - Restore Position + * + * When the textarea loses focus (keyboard hides), restore the original + * form position by removing the margin-bottom. + */ + const handleAndroidEdgeKeyboardBlurFix = useCallback(() => { + if (isAndroidEdgeBrowser()) { + const formElement = document.querySelector('form'); + if (formElement) { + // Remove margin-bottom to restore original form position + formElement.style.marginBottom = `0px`; + } + } + }, []); + useAutoSave({ files, setFiles, @@ -274,9 +309,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { rows={1} onFocus={() => { handleFocusOrClick(); + handleAndroidEdgeKeyboardFocusFix(); setIsTextAreaFocused(true); }} - onBlur={setIsTextAreaFocused.bind(null, false)} + onBlur={() => { + handleAndroidEdgeKeyboardBlurFix(); + setIsTextAreaFocused(false); + }} onClick={handleFocusOrClick} style={{ height: 44, overflowY: 'auto' }} className={cn( @@ -344,4 +383,14 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { ); }); +/** + * Needed for HOTFIX: Android Edge Browser Keyboard Overlay Fix (can be removed when Edge Android is fixed) + * @returns true if the browser is Android Edge, false otherwise + */ +function isAndroidEdgeBrowser(): boolean { + // https://learn.microsoft.com/de-de/microsoft-edge/web-platform/user-agent-guidance#identifiers-for-microsoft-edge-on-various-platforms + // EdgA -> Edge Android + return navigator.userAgent.includes('EdgA'); +} + export default ChatForm; From 3cb2b4f119222ec22165cdf6463f2f1d37a5e610 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 22 Oct 2025 09:43:15 +0200 Subject: [PATCH 08/30] fix: Refine Android Edge keyboard overlay hotfix in ChatForm - Updated the keyboard overlay fix to specifically target Android Edge versions 141 and greater. - Relates to #10074 MIA-113 --- client/src/components/Chat/Input/ChatForm.tsx | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index f2f97d48e3e..9bf72caba0b 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -131,11 +131,14 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { * margin-bottom to make the input field visible above the keyboard. */ const handleAndroidEdgeKeyboardFocusFix = useCallback(() => { - if (isAndroidEdgeBrowser()) { + const ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT = '240px'; + + // Only apply fix for Android Edge version 141 or greater + if (isAndroidEdgeWithVersion141OrGreater()) { const formElement = document.querySelector('form'); if (formElement) { // Add margin-bottom to push the form up by the keyboard height - formElement.style.marginBottom = `240px`; + formElement.style.marginBottom = ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT; } } }, []); @@ -147,7 +150,8 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { * form position by removing the margin-bottom. */ const handleAndroidEdgeKeyboardBlurFix = useCallback(() => { - if (isAndroidEdgeBrowser()) { + // Only apply fix for Android Edge version 141 or greater + if (isAndroidEdgeWithVersion141OrGreater()) { const formElement = document.querySelector('form'); if (formElement) { // Remove margin-bottom to restore original form position @@ -384,13 +388,42 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { }); /** - * Needed for HOTFIX: Android Edge Browser Keyboard Overlay Fix (can be removed when Edge Android is fixed) - * @returns true if the browser is Android Edge, false otherwise + * Get the Edge browser version from user agent string + * @returns the version number (e.g., 141) or null if not Edge + */ +function getEdgeVersion(): number | null { + const userAgent = navigator.userAgent; + + // Edge on Android pattern: "EdgA/141.0.0.0" + const edgeAndroidMatch = userAgent.match(/EdgA\/(\d+)/); + if (edgeAndroidMatch) { + return parseInt(edgeAndroidMatch[1], 10); + } + + return null; +} + +/** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix + * + * Problem: On Android Edge browser, the virtual keyboard covers the input field + * when it appears, making it impossible to see what the user is typing. + * + * Solution: When the textarea gains focus, push the form up by adding + * margin-bottom to make the input field visible above the keyboard. + * + * This can be reproduced on Edge Version 141 or greater + * + * Check if browser is Android Edge with version 141 or greater + * @returns true if Android Edge version >= 141, false otherwise */ -function isAndroidEdgeBrowser(): boolean { +function isAndroidEdgeWithVersion141OrGreater(): boolean { // https://learn.microsoft.com/de-de/microsoft-edge/web-platform/user-agent-guidance#identifiers-for-microsoft-edge-on-various-platforms // EdgA -> Edge Android - return navigator.userAgent.includes('EdgA'); + const isAndroidEdge = navigator.userAgent.includes('EdgA'); + const version = getEdgeVersion(); + + return isAndroidEdge && version !== null && version >= 141; } export default ChatForm; From 91cab50ff6e33228a7f4dca96c648cf01e509c06 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Wed, 22 Oct 2025 16:41:11 +0200 Subject: [PATCH 09/30] fix: Implement hotfix for Android Edge browser keyboard overlay issue in ChatForm component based on v0.8.0 MIA-113 --- client/src/components/Chat/Input/ChatForm.tsx | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 0736c7dc61f..ec2b6ef003a 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -121,6 +121,45 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { } }, [isCollapsed]); + /** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix + * + * Problem: On Android Edge browser, the virtual keyboard covers the input field + * when it appears, making it impossible to see what the user is typing. + * + * Solution: When the textarea gains focus, push the form up by adding + * margin-bottom to make the input field visible above the keyboard. + */ + const handleAndroidEdgeKeyboardFocusFix = useCallback(() => { + const ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT = '240px'; + + // Only apply fix for Android Edge version 141 or greater + if (isAndroidEdgeWithVersion141OrGreater()) { + const formElement = document.querySelector('form'); + if (formElement) { + // Add margin-bottom to push the form up by the keyboard height + formElement.style.marginBottom = ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT; + } + } + }, []); + + /** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix - Restore Position + * + * When the textarea loses focus (keyboard hides), restore the original + * form position by removing the margin-bottom. + */ + const handleAndroidEdgeKeyboardBlurFix = useCallback(() => { + // Only apply fix for Android Edge version 141 or greater + if (isAndroidEdgeWithVersion141OrGreater()) { + const formElement = document.querySelector('form'); + if (formElement) { + // Remove margin-bottom to restore original form position + formElement.style.marginBottom = `0px`; + } + } + }, []); + useAutoSave({ files, setFiles, @@ -274,9 +313,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { rows={1} onFocus={() => { handleFocusOrClick(); + handleAndroidEdgeKeyboardFocusFix(); setIsTextAreaFocused(true); }} - onBlur={setIsTextAreaFocused.bind(null, false)} + onBlur={() => { + handleAndroidEdgeKeyboardBlurFix(); + setIsTextAreaFocused(false); + }} onClick={handleFocusOrClick} style={{ height: 44, overflowY: 'auto' }} className={cn( @@ -344,4 +387,43 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { ); }); -export default ChatForm; +/** + * Get the Edge browser version from user agent string + * @returns the version number (e.g., 141) or null if not Edge + */ +function getEdgeVersion(): number | null { + const userAgent = navigator.userAgent; + + // Edge on Android pattern: "EdgA/141.0.0.0" + const edgeAndroidMatch = userAgent.match(/EdgA\/(\d+)/); + if (edgeAndroidMatch) { + return parseInt(edgeAndroidMatch[1], 10); + } + + return null; +} + +/** + * HOTFIX: Android Edge Browser Keyboard Overlay Fix + * + * Problem: On Android Edge browser, the virtual keyboard covers the input field + * when it appears, making it impossible to see what the user is typing. + * + * Solution: When the textarea gains focus, push the form up by adding + * margin-bottom to make the input field visible above the keyboard. + * + * This can be reproduced on Edge Version 141 or greater + * + * Check if browser is Android Edge with version 141 or greater + * @returns true if Android Edge version >= 141, false otherwise + */ +function isAndroidEdgeWithVersion141OrGreater(): boolean { + // https://learn.microsoft.com/de-de/microsoft-edge/web-platform/user-agent-guidance#identifiers-for-microsoft-edge-on-various-platforms + // EdgA -> Edge Android + const isAndroidEdge = navigator.userAgent.includes('EdgA'); + const version = getEdgeVersion(); + + return isAndroidEdge && version !== null && version >= 141; +} + +export default ChatForm; \ No newline at end of file From 5a2bf024c1f7828dae637d77366df778c997a471 Mon Sep 17 00:00:00 2001 From: Andre Roggenkamp Date: Wed, 5 Nov 2025 17:23:09 +0100 Subject: [PATCH 10/30] build: add custom pp assets --- client/index.html | 4 +- client/manifest.webmanifest | 1 + .../assets/apple-touch-icon-180x180.png | Bin 4948 -> 6696 bytes client/public/assets/favicon-16x16.png | Bin 709 -> 603 bytes client/public/assets/favicon-32x32.png | Bin 1712 -> 939 bytes client/public/assets/icon-192x192.png | Bin 5291 -> 6794 bytes client/public/assets/logo.svg | 64 ++++++++++-------- client/public/assets/maskable-icon.png | Bin 12133 -> 20390 bytes 8 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 client/manifest.webmanifest diff --git a/client/index.html b/client/index.html index c94c3981b41..2bd9c91e5e1 100644 --- a/client/index.html +++ b/client/index.html @@ -7,8 +7,8 @@ - - LibreChat + + MIA diff --git a/client/manifest.webmanifest b/client/manifest.webmanifest new file mode 100644 index 00000000000..fe94b58b809 --- /dev/null +++ b/client/manifest.webmanifest @@ -0,0 +1 @@ +{"name":"MIA","short_name":"MIA","description":"","start_url":"/","display":"standalone","background_color":"#000000","theme_color":"#009688","lang":"en","scope":"/","icons":[{"src":"/assets/favicon-32x32.png","sizes":"32x32","type":"image/png"},{"src":"/assets/favicon-16x16.png","sizes":"16x16","type":"image/png"},{"src":"/assets/apple-touch-icon-180x180.png","sizes":"180x180","type":"image/png"},{"src":"/assets/icon-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/maskable-icon.png","sizes":"512x512","type":"image/png","purpose":"maskable"}]} diff --git a/client/public/assets/apple-touch-icon-180x180.png b/client/public/assets/apple-touch-icon-180x180.png index 57c4637c934e1fd4796c0d39ac85bf9ddfd827e8..3a1468e7d55f024cd36f9c0b40cf520e60bcab56 100644 GIT binary patch literal 6696 zcmaJ`1y>wR(_JK3U~wmS9v}n@ws?Rbi!7Gl?hYH=VX@%u4grF@ySrO}po=F!aDwf( z?;rT)oSr&Ur@HIRboag0cOq0Fa(FmYH~;_u4=n#l9cdf?*RW8L_xhRy3#7qtRflLG zE%I_K!Aw9JY-f2L7XW~h>3G~NsPY5#u4f0Z0M-HdcMaz>YNd6mDqd`or#KP#a(m^%Uh z>;)!P$NXQik2t8c!ovSCVVT;{abTD>?9LwFu&5xs_XcOaw(aKPW1)8u|thMO6|AJ|KifdQ42 zS;TVEW*s(ozm*+8&^jR~E2eU`@Q=^I&SG``_7EB=l&!%qDPn4fKJwwEo3=#YI^=i6 z4RR4nCUUsHp4nEpx(>vbG~@p$|W= zh4}UH?Kn=I46@TN{Wm=?UlG^o@pWcisLx#x|9|b+F3Dp-F~csn1)qWbE(h~tP64N9 z2{HF93GqE%1Cco@HFbSu!`uQKu?&%lziGXm_+j@NgTvuY=WDHwWySjSME0)Q|Cpp0 z&d=(G_F@*t(3yZ?Y9@ny-0z}66tba&m;*n!ca14XWodAW`o9I6e5QbMN8|i|LG_Jj zWYPsRl~s0Cq7VQ5{Y7~Ud3qwE{*lN??>;F_ZUDoztjmV|~S`o)9Ik!`lB( zSd${zR1^;fudON1*pOtRImCZx$L#P!{aG{?Iv*vvWJ&1#>91z@{dygHGK`@Tw+|UZ zb8}0N7X|13;P$em;{s|qHCIdhpIL~1bS4b`voU(uNg6cF;F9kLPtQdfQye*s-J^Ym zjaDNMEP_J~t4Yqb5`BY`iOiCT0KjGS%@8-bB#l^x^IpZ3ZOk$*X94TOU$m=DFF%pI zJH0k_qUdWp6f6KhCLw}fqfQ+f&s??wN5lDI=cD4HXysz3r4m=EwJedZYQrZ?Gt#8{ zY@KV@*q&G~%!|+uLNRnVWuQlqj7Jx$r+Sk$C9C$i_i$|;I}|myBC04YJnD`_OM(tR z;?tx_gW4jM*2m?yz%JUFN}KVi8stK+{NZSCbE=u4`^`$Kx1U>3aHLFflP95%*Hqe! zF}SLns`|^fVCJ#S?w9LHk+S1|Uav?`->B2fkDH?Iu+~N4;_HJ1y~p}NbWVbH6oK#G z1B=yje8mn|CIorw&weZ&q%&$uutt88!;q-;oj&<;gkOZS@Z?kxPi>d)@L$n+tp=58zDQZXd}fCWQ;@No#Ry+P)iavuuM7*t!Cb^klM zI-%9jqAlB}> zbru7>Wz;qLR0E>Q5;63HJ3|cH_R}VkfJRPtaht1RF2j5ehLUNZYwxEHx*Ff7xbqZ+ ztO^_l0d8C$Q+T9SE+Q&Ew~AMHkSAZY>*ggQF96&C!)qQ>(YHV9!_Upy-N)JZ;0twi zzd7^2NO~%K*J?>Q+`BOF@2F3UspM5J|7#r@ezz(g!wNW4|q z^b_!3wP#hEb&h?pJ<)YPdrVgg8&WGLw%t=^R@W&5Tx0%7z(-xmYvq$z`b z5kj?hm_~@lx<1JmYzJk+k*UhvJfiBX+4w%jJ#tO|J2)vZ6%_}t`0L*;70B_8QufxT z-6*p|sx0AX2a84!#P{m8OSQmG{2jaa{S#j-b<0;C&gYi;_T!&ruVwjzUcn0Aql7c< znTP_1O@bmcB|~SAp-qrd_20=<{Y1}>mrdF1D~>g~;#uCr@ImOcIqV-}2SLaoeeIP? z1HLn{3Y4r>xE7rTU4{@Hz}=UO0ssUug8}mDHOuA2kwF4B0S2X=-5N`8S*8*<

57 z;1XOq6eIifYyv#HpPM`DO`cPqE#(dO6+)&7yB+ILOr5NvCvu&0(7(ukJc52ntL&)l zeuMzaHGG=l+vr<}{f>DS2!XDNF3HTneV7$l4c@ zm_+tzo>hv_*L)2tNF?BT{CL+qIUe?1^c3)PoI^VjaL<-e2x7fK0xGB8m9K%{daECG zMDLRLpNPlP=Z$>5+@l|lB%~6*jyRlx95Y7X<|Q<4wdB;kiSs`feyuIdT3?ezN_rBh z9hHPee>lTdVZ>Q&?PchF({9nEo0X#n*#|4^wkBueRY2g}xBk@&cUA6Kw}E!$@M7yy~7P+^IfX z_<)nFFz{LhZMPT7*z@BAVuQzA0|4|2^$UJo|Jpno8EK@$ixq}ZnR`qp@upDR{Hu(& zWkO%K*&;SuEL^9}Vx*@#K1a)PE$CdV4&OOylW>OU=R^OXwmj^|!?C8FtWf3K>fo-U zIsQ8*zo#IcOV)Fa*yZN4IDEgnuOIQs*?D`N*?5zmHO2A6^QwzDAo}9*6m0;yJ1_(BaKDiX zcndHdhdC#Nzm2Mbd_cazcBYQ&V57d$ILL#+?S6f0Fd;?5d;i4Y-U%&`W6z5#B0GLg zNHba^y+t~o#kB!Ryuz1oXSyI%BrS;Ey1^hSiiGkrZ3Q%Sc<5H?U3ICg!xVJkwH`Qw1F$ z4--=p*vsPQ|6t*v*45dqxKQ(nAcTcbQA*T+(T|4~^DJr@JA$|?rK-zfY8*mYFtUnT z{c~jX?V8aM&?|MD6aKjQl2pC(O|n9ytFI5c?IZH-ZbiZ z1A7}#3h@O>+aOKwyOph~roHMxzobz4VI&IBan84BH0}|FhwE)FFalh=oSL4&voLW* zuS`o_G9j#cJ@JkzKTDQ-k;`9rg?8HhbE#@tu9D46HRE~8Yz~`nNWva&)$J=UYc0V^rfw5$QD9|} zmsDvDD2ek8?_w(Rw`FRJ7%iOh_NIl(>tsI7ycwFuBK79Gl$>@9_Iet4y%)q?RVgOF zxQhBTVV((y#Bdn#@1@i)OMaz&+h)vQffAB2>Z2|TPu#}HzVd*LK{kC>-dV;fmC4{6 z!=Tq3yGJKy(l%>5+d^0cAx%3}eu}m{L#yHi3xY=Mv(?vpBWfj-aZ) zbBM@K_D>mJ0>dNB@$Qj&?#tOOBxw z8G$uP%Nu#&e`fx^U#_nI+DcD$+tO+A3M=w5$0nmI4o`28*PWTPmEx`_1_LsKA(%EH znUlTU3PSC|fowS*{vO|@gfE~)$_)1jD`62Q^d}_|VfyRpA?xR74JMzx z#qVGJD;IbksU0cSJi3q&+h`UELiZAudr&2XDQv{Nwof{2C~)$NT~3!8b^LY4Zx5iG7lhb?8s6jBkOc`zhAbH!XNvF z&_tJ^k?u6T6F(ASS)@7vDwKE+chwI2S^nK%w~g(h_dL!1R%)V2q3Sl~8h`K-5HbS) zB!^z7{oel8Z-<A-ASUbG zMjz~uHM7|Qx_y%2_r;+cLRr03C*v#PWc5hz5sfBPuA;ea^5=k>l(8RGKl_a~AP`e? zvMt?}*n2dtvKdY5H@6vCA%-N+qIfUfvS^-WD^Th#qtCTRMt4~$d^KEi3QZbi*u_%T z+Y2Ne5M)?QG=wH@u>oRRlBGF;|86Yp87Alf7efL?sAG>xtP7&?vjzX z2RhoxQY!0tHscnsoFwJHq5=jL9@%j@>U7fI-t|zv;7Il+Wx?-GH@`{Y{T<B=6zu)a>PUmFUCI>?v=2~NRJLDgm zbsEuvZ+iMWr?ptOyB>#cyF`}bCDXN!Q?^s*s7rf10Z@WargiJsz>`0!jj< zRe|~Wl3n7l5MY0VhkWhKbSYhgkwDZ)$6Y~7kRGwEt)6DJ56XDc0!7-JCbPH75j_Gh z*^)`bAy=@$LIEop zfPiG1zj1*d17nbZ;BUXwbG|1UK-Fy`N`-kKlD?5mKC$v)Wh$4*I<0v+lhDywZz2#x zfUd(Qh>nT6AD>GV~& ze~|waH7qckPWvOW_W&VPPMSM%f4m{5{@!7W&7`JO@&;HI;LXXS7y z=i4Q=Ysxxwq=Cb6djriA;bjzSu=X|FKqF$&bxf9@$V8>fJto?h6FWDnN6JSZHB_c% zbS~S0nZWM?l?F;sBJj=pjw0LYO!qb0wH+Yf4o!xc!LWP6j0W|Wg9alrxLkODYURxJ zU`e$MU`Az{KrghDpT%Cx?N<)QRwN&K_2BbmWuB-G3qX%u-_luJQ@;?vl)P zt*u`X%8aF>?6Y1KdA2JH6VSoEFn-dH90x4=00F&2-?JVKMLroFUdVCN>JjZ7lbkVO zU+jTDv4$zv&p$9jj!0~E48j{5!MdA;#Vo~8bazve{tPt#WEYwkd83na6@3RBB84wk(1Er!7Rz+da8IKh5pobR3vZRJ`0`^(J+xf0#Qt2v zZQ^+J9YwGk{+5aXW$%w%G=MW)bB@A;0-dLVpWEms3i;w*&k(nN4 zm$j=~!k+*)_6efm6bhx_5qRgzrG{_!+}kImRW9v_()LeFd=<0p(LS3w<7m=AGYt~9;@VTTuX#hlKgg7mEm>V)ib1$ z@f+*Q4x6=;<+i2V000cNq!Y0v(gyE9aqR55wimENul%n@|6Lb*H5BD*2mcq#KvZMieY>27hIRl7w4?~qW(K_s*{ME6GtfsNH}cBt z?KpM#YaI5BRudN@l5MxW1}zwxiQ(RBBN_l8MZs@-yJhRyGc*I19S4&0TJ#{ zT%^S2vv>c`1<_qiWv>2bBubn4=R+DMqW#8%@H;z5Za`@l0ElPS`K<1P)NZ0DML
X$oow4$L;a9 zhV$2l7w;+Uth}JOl8N~(H4+!&RSkzVnx6%BEy3-Lw4bE9duk4&k4>V;2W4f^*P=p~ z2^h^db}3}}e~e9WN%*|j%37)+zKirqtA&J`Aa(R<$ljjyA@2}}efd_SYEQf|os|`4 zQm~j!|H&W^Iyg~zDkowr13QA!&5c8twIzNYJFMWmtIn(un<;IA4C@{X_P5oZMb0NE z@X(lx=&zS%?u%J5GGEagDajWDc_kIdRLbpm)v_MhYxZeThgCLI*GdDL0i@z;Lm7&k zzk&0WxSA|FO)H1(`Zv*8FSoJE)c^MksY(nfJEt8WCTCz06Xo<|l zU6<=#`)TU+PKPOOch{0_MCSGsP$h2p#6PFhqg>^kUvJ!zA4V%Ky4%(x8uTAGQXMrk zu^7L}Uc7#atu{Q8>qC0cbgQasTZP~9-+fhZ*HC16-+*-Dql1^1oAebYI?`Pm%A38O zbi9#X26V|q!Db_snV!r{J?BydvlG%(q?;EVe0x2r&WlrBynnE#b$xq7y_8KDuKbZ8 zM5!97mUd>Qrln}C{aOQC+>U=6I8bH9WUt7o-WctGHq=&kyG3{6$kH@NK}wyh&stNy z_@2rdLT##)#4mf}2r{E9L|CTGLXTMYO=nfznvO4=7bbMsBG@(dSS h4ZA$TJ23)|0Fc!xlx!M$i~sM34_F5Bsanby_J4GN>$(5{ literal 4948 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 diff --git a/client/public/assets/icon-192x192.png b/client/public/assets/icon-192x192.png index b8dfe0eae57b401edfc440ed9165428c28629aff..1f9c84dbfbb7c03f71a402a80889f479bf8847d5 100644 GIT binary patch literal 6794 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 a?JAgFST-s87F`j5D2lRbGWAmCU;YnZ91Umy literal 5291 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 diff --git a/client/public/assets/logo.svg b/client/public/assets/logo.svg index 36a536d654b..b683603b7db 100644 --- a/client/public/assets/logo.svg +++ b/client/public/assets/logo.svg @@ -1,32 +1,38 @@ - + + - - - - - - - - - - - - - - - - - - - + - - - - - - - - + + + + + + + + - + + + + + + + \ No newline at end of file diff --git a/client/public/assets/maskable-icon.png b/client/public/assets/maskable-icon.png index 90e48f870bd916514d904041b7f86a3c026e0ff1..1fbcba6ec6ed56a454ae108e17f0a07f4d80ab98 100644 GIT binary patch literal 20390 zcmdp8gM%75|Yv)0wUc=N;gP%cQ<>7@4NS( zxc-o3&(54P^Ulm0&-=U^@=QY!8{-iM1VPx!N>5)v5EA$k2|~Yr@p&Xy1YRDvz0lAC zui&3U9$E}|dFZ61?+QU&^!NW^p1IQQ5JU?pKb6(?Ox>BadPV%Q<-x+&X;tt2XoY>M z9QtqH0s>H{r+iu~zgO4RZf#Aq2mTXhYmj5fBqUIJLhwRqze~NLq5m9NIwy|LqyNkK zrRC$AV}e+({Zvs`oIV~bR9bg?`@Ha0R0erRalCckxMOc{m|qYa-}#Qv2)ahVy?@>OQzsea<3A8u{CyrjO&gMr|zzs{huM zG54kl)iNhTZJ4Og(&(`5jd@@3K74Zcm9=xtjIk$ba^5?!z7tk@nrb_T`8i2FO>`Mb z45+PR)df*0W;vNyNI_vJK|K)Pw4ie0y*o2WnC`pL@oTVoujvs4{|3ouV}!EZov!J2 z@?qAV6(e3)Cr5^&du_#}ko2ahjR=I5V9DTNKu-hBk_0Kv8ZYN?)%)yC#6GGNBuQHx zVGhhJt{0vU(?QiAksw~DTBCjclkM{+hsxe1-!QXks&nD|JAn8uHoUV z7;a|aznz&SQp7E4-AKfLhM395Rp7t7>)4eJ_sGjbRJ&08fuPy~kVZKEwH&`SVnD8KhVE7|_Q~>)-WF zJGysjuT&+YYs@_!Z4yCgK}gV|evPe_y)oe?5!{??ZG>BQ*?|Wg2LnRscwGU%T~-)o z?N4<5Z86Yjo}&f1-GA`rd@j7!o4VmvdU--xOGy#hpd^COdy^x4lohM1krg!rs>>hxC?4xge!vQ#0R3Zs63XY$8)7;a-VGxl@(C;;`pe7F*hnYT+*Myqbf*Vk zNZGU2HnwZm0x8449}VL}Vi>4U;Ls~;I;zEP6nugO0r1G6E{prOYE0~#=n&N>Bu;o@q+q!4n5>X1jNt4E0yf_u_va>Liu66 z2D+0F7*f~w#anQ@)C=mEKeBiC;=ONaae{6_+ME<~dp{7A`(^?D4>RfB=V%!Xe+g;5 zm-qeE5S4;z_baktRxU1C5rr!670gy!(fY zshkgOV1TiXjYQ4zeKUPLiKX&sDQ$x0@O&ihf43}Kn$GcEO4N0X_+_`n3H|xN`q(Ub zfQBqAAb^pvjpKTLyN&@C|Ef9CZMjF zA$C-xOPzJ;M2hsS&^pLW$qL#i?wiJyzFq2c7bm6o4Q}DH_V0Yo(o9ap7OKF#R?DHAt z1`YOr-A!0AIx3VkJoxWuXuYY#B*WI+N5uMX^NLl5o`CzPOk>d#*q|kl8Aa7}8N%tT zGXoJ5@7lfV?twt1Z~5TZ_R(GxVj`rKRy5-Gq&x z$DDUx)~Au>*DaF~Xw7KgZe?o;XQQ`Y5x%SZ`6FYKB~-%R3uT%dkNAF(=6MI|nK2=M z)_?YG9(zJ_)O|C3_xqw>Fiy)PO&b;}FfGqnfYAwg0)rqWu-71&tY_do0XPj%Gz$>~ z1w73J?-{`H`Tx)~mzD|&u+%9mnziOlE_(SAyW%uRL9VcGZ=+>2YIy%6`|!_Qc_ zIG7I)PEQX{huDZ*#*vYupYUVJi6P5WCH0Pv3u$>-+Rt_U&RXj3-U><NMRLq40Y8)eevylD3R{axb*GTyzudErx6liTO^_$j}%9ty$z5OXehaxV`u+PQdoHOs@dsrh|d~6v6{gi~hBw7Pykd6gf5RVGBv9Z^bHu=RbD9I^V zQvW;BElc%9Vf=>&3=tHP2V2VYOFjylLnzITpfz{dWU{GVqj&4v7fD5mM}|tBnEyH`jKLR{d!Fn#Kyt~cbs%&t&587e-e;UveyXjtvT=C zb~8{TLE?J`$tc+SjRa>D88vW?G0wpQa17 zN$-)VrqGL$JL;5sS1{f-2 z#FJ8#;ajs6Y+9vP1v;qxu@$5{cjJBDXntX~O>1g8 zYktmMyP z9tn@QYTCR~PyJ0{`)fN}2Rj=14nX}doj>1(GocWrRLMg4b*Vm(OD<<`4 zWDd(K-QzkxWN?vK3$PU_?7E~Kx1}Bx7}G=nJi4o;HOVFKMT|deB0gKwVAkFaV`ExOOjL$4spkEv;e09?e5GE zKG@_*zO%i~&!5u)MfDFi821_UQIwKPPOGO;ZnEy2T0G(9ptUt)8hUe>b#iDZdYb20 z^uy%`5!6G{&GU*(txFzl-=M!vH8Y$=9bY6jS$!g@#bRC0!quPD z_e>}T+ac{`LL#e&o>No31!z%@>dN9yB0UxaJ!d#)CJ zX07$>ocE{HuldR_l{;YvObJ}*L>QOh1=%G1DSZCDw2aie$4o@`$H4XXx1G-TX(0#E zS1ociP#P&AvG;Tzzhj+qP)W&NH~f(pF6sjDHLfN6wlxT-r8pcUTF*xjwnHtO=d)gt zI`WoO7y*J@iQ=5}`}y(HIMiJ>cET}{$h2$%>WcC~y(yB1LEFqs9g1<0^aLyrNwv}5 z2Stsa7yhlKy+}5@?;^XTn`?eWH|z6v&N=r8K9tN>vci`jlBuemO%LzA8E7(aWst;m zy@ZJnQj-Wcp0yb|CR39VYKVmeWJ0xnm(3!rNd0eHJN(f2oufztK7aokowBZJDB^+P zfa`{h@r@k>NIJe%{+)Z}?%AK*NT7nSu!l{j9pu8?NkQ?7;+Uv|qCt6;rg#7DM!N0w z%FzO(t&_Lrg%3l5Ni@=V)C!`(z{ywwB7aX(vHA>4U%Q-kweSt=L;ZngG~g8K_^V8HrqdAX#)TBg`=>1cNc{lj$>P-P)et?_#n$ z13n*_(f~as#HYULj;+hrB>$B5pkD@d^C|N?p=iE|_iayVJpm9yGrI-fq;XZB!OcdW zlsD%lM|}X&HJ($;())BpD&3=D%V0}L1O8~)t+67#4k_5tf7gu-baY>Lfq|g+hN(y zF%B?KiFE1l9d8f~a_Xp^phSNMT2IQ~!`at}VUyNEsT|sIU#HmjM=U!UDx<-mmYZ8Y zDgWz&hs!~O0-G$Uaj#*cqvYwncdaMOmpLCGi2JX_Ntx!%)u80Dbx3@`3I;7bKVm3< zzR>Y49Tf_6v6@_{2*2rntvpMTFpi850&WuSOEN1sR6TfRY{OlD^=XnL)4qYx4fU6~ z?o=4U#KGEu{=TCf&_s@i>&0J#GQ+afv>`|bo=NeAT64bpXmJly&XJmlPxdJgn?3_) zpNSFf$4lH$@W^TVgq_s2ZNw}H22r7pbV6b~aUL1e60{+1c>4riuzx#|!gIHGRnjkv z960tKKxudcJBX$UnluSBvQ1hOhBc&LdiIWu*R9;}9lXPNe5jt8r}?}=m^y>+c{vsB z)cwaL^@pb!+wO5VkCp%B;I*|3=mOs$sy|gI*$uA*&kB~*H%@AL?Q*;3cP}2god!(c z3f``-qJfnd=sri-pjdeNPFO5D#mkP+8;9S!0L9NqgtB7Ojev0axg|bUl_v@`hy9}OUq!rk}Ga|H#0(Ktvue|XT*2dkv*uk2r9 zY!VqW`~`E79B3@xdFsHsN!!ep*%*dYV0d*E>W5aQl)r%?OVXUru46yxoa6>Vn9wYi zsLtcR{QElEV{tgcHZAM{J2P6Qh+i?_Q#`3N1uAu>?QKW+w!4p>kGux@PbfKgI>XC` z+~GzAa}PTp@t4?EHNW8UGWXr9W8wx)CDaACT9dnh#Qe}g;37GsU2+mXB}9MB;0bHx zG4*Y5*R_4HNt;HSgQRzOdsW1$sFV-6pg1S$;nDpOr_@-F)s^Nh`A$!jlZ~PU`J7Q9 zEH2p=`>Rl)yVv`*ML-#>#71%Gh})HHsJE~LD9CO1IR5-K`SDN^b*SYaQm^!?gKzuk z5OdEbXMPz7b-~@o1b#iuwpdb!66jn&C|N9>Y2J8ewtUw*2>^kw5XP(1pwWnb3#8Q_ z2_@a4Mm7zurV{%&6^3Pt1NKp$y)ZaVh)$e>ARD%lpFYd8`=0)LOYdO+b|Z5Um%nr4 z_c<{jr%V8u70>_9a0~5Odwk9o z2p|VgZE;P_ZADen;%?+i2~H@n=XPp({%fBO*!@tgrCoOnRow`{Kt$3>ntz=7oY4@h z9Q}YGD}XVbdNeMtuf_OsfGx5HwDi~eWpoEVTI2O0H<%R4K1P695~2}d?y4%jNejZF zOPkfRd+nu_8s&i+U5mJQ;IEmA1Z{ADl|O>(9N4!){=@-2tNz=!G+sWPl+d@HBg`JS zX<3+CsOv!?q*JWID)CZT3>6B{tp^XOH(>dqi291pQ2OHSJwgJ_4bKcW0=GU#>n1`G z_1D|W4E4V1b4=*<+1UBEoh>%F{)rnEL|5kR=M-SUp=z?T%0MSE<8z6tyq?uJyTojVi2n5+TSYoi?Rg`0#8@@xfKcN9#bp z)knEcvR_zh^W~f8J+Y9tig^ICYK!eGlF{G~ch+}ND0yFOLUNQmr9_0}Kn zd>k5_#U{xF4sS8W?WfL1W264Q!4oi0C`9%x|I_F%*@{F!U!Li&(x6Z<0bJIs7EcL{ zve_XA-j~8vBooO$Qr`A41>^S!x*NM`Z$wtm*jCsElB1Ib^sQC9|KYbJu0Y{O|MDbz zIOqbEK_2BaVk{W#JMu|+DyfH?491Lme$k}GPXU3|mt=*>Z`BoWrT1(xdU_D`+v(FK z^ke}}u$xM@)la%jv}7_)LHBU?(j+2$-dYct*B^}{ z&aY{qGaXkg6H8-*%!1m>6At?7FTjD7Oi5%VvX~pjXNRIX`4N?*WiA@{qGE_4fue_J z5oBqULJYDr_)20pGWom?nm%6UeVI3TGcLhH3Su~LzoVx131e5{7&*d}2i_W{Aqa=!QJh3?@gh*zwpnFnSswZxY=OQ3ssP-8msSCMPk%3Ks4aE2L?Y7n>bBTs2 zIx7NZ&o&%&HK|LN;uvRW5_|Cmqnhm7ClOe=CWPXauX=OtL{2|!*vQ?)I*?JkdpRE) zu^E)=5=FQU3(fitJn+Q$iL)L2u0!XCMzHeAM?nbM4~WjqFG{-1p*Fez9hw z+0Pf6F8D0a-9ZQr_0`6XR2Y+r@7!^Q-MN$R(_%-Y5+lahDnxmBVXz#U|uAx3MV^Pj@y zrAe&d0jfhm7dU=PwMikjAri#$shzDiH4egwiYw(%We%l-9lx{J)AWOghvb(#*k*D| zz*$@n|N3OkZn65#Lp^To=SGJAbjRwV2xowC0l5)%By-?(J5Kevt^IczpoIhRg#;Ss zrSoh9eU;HR$ZHkz&>=4c zNrM?%kf%@zGl}_DI@8sfVEo*H*2XY!|pN~edy1${> ze9U7S&w(+pxs|sPnuChaMCwrcjbTpuc5SSu$^IT{kz`|G3CbI2qyg_-irBpV{HrcR zQ?=?G*-Gx10sD5?icl>S#4fqFMSz`#z>C*I#@E$M@Y0%`D|V7rMjGhKPwvAQWo&fI~zaf!~F*T z&^f~*Q?EONalK#VNfEHu!8}zj{^r~vk{(|W(-*t>o0G9w3X@)2dNn|lIJzpf-iFtR z0mSCQyNJPbe0AhH6D}BNEh!@#eKR>(nITatP`oR*=oPNcWrQJ`i#q* z9Ss!HhgjCs7Xq04pm{Ez>T^MITGPPN-`iTwlHGC4p%yaK*ur?^{WR)PU36S_q|aD#X$BtEF6R$B zT2taad1x4cAb3yDt#8JT8Rfz6dZ7n)sIMwSL4^M6ZTpYVZZRE`XaqRz8S;em3XJ#h z`1A1TI7H9fRQ9Jp&!&qiuG6P3uXpZq3-5-V;$tf;Z}eG!YK_xkJ{LveU8>9XR5c zq`>}|g5OJ~mT&AIsItkfkIC>=pA->ur^GoUN{}xq3TkMZG&TG86F>Y+E-K$q3%|Hh6 zQCb1H@cij-lz}^ywAy(pEvGZI1zbPvXT#X$3>DTJG?X!=rw^U#ND!`RC zyj+OeCrh5Hb$v^hW$Ct`(rNBN3N?m2nqYqG`FN8kwY!VGW|M~7QC)O+^?j-aX+Y<) z*J+a}{P&G!)%0wI9EQQ@KPj`;oKv|6v_yUm>EAHp#*dSV?DIO){orG@qs8xJ)-jrQ z&u)(62Sn#8Vx@hv0^>--8Pkv;E{)>nXH9m=YRcZ83DfUcs-|xTbfs<&CyIIz6ts zFa@5rc&ah>xR-BopZtj1@XQdwWGK1}$_-%)y@HB!M8dexh!(f6o{_=qA8Gh1lC||) z{hJm=0Cw%6ZZk*~?cht$a#_ew0+(^{;lqiDXP4GW`Ly4D)E*Wv(!6tuydws3bLaNE z?O7UrBoz2C)~HA=SvWa%7?}xr{OxF@ec+L-7%I7_7bD_sUzqA^0sIq^UXvxfJKZhe z#k&WN{Z*w}54KvI_DN+t%L`GhKo;gdT!6=&XJ>I+3REdoG*{Wk{6gHi5Oz=k@Q~xq zE3uZP?Z2Bw+34*P%0z?HGF;EgCrT=w;gOs;zw9lM4qA;6lZIBWR0WwORY_kIWR`x~ zeMQy!VK~#$Z8&wv1$*xn3H_yvi^auf29QncxX!PxbRW5o>nkR?kFB`$As^f~MbqW; zm<1^Gw5)6v&oqJm|ILu#P1G}`Q+uPU!F;w2*sI@ogEVT#6>!dO!{$Ei%yd+Yj zBp^&_=(Fa|7ugr-e7f;bpnG~J9m88b{ z(i=LtjYii~`O|t^qtI~6(-+-p8t7 zA@{sD2d^_cTq?+g1Yl8jNS|LiHR)9}rMqFg9=D&F9(NGC=3ggjev}@Li2k9T97;Kx zGFS?0-Ugb_mw{V1XyIeK?R@#};AF;S9rV_{GyS$xB&v=6>OcnZ(Fv?%b68f#*<#P2 zLI8~iz3yxYzEMs)33OeR#HAI)?ArNq_l^| zm|KLvnL?)|gQ|&VgoCnAWxe>u-HLbXhK-7wNUm-QA&q$Lv2jzEb`e%|3Ia_rB*;m& zSm(iM$m){PO&x&U0H~I1s_lQ(gkTFm4le6jrPIudMmz5uX@Xwhi&Zmkq7%vE*F6=v zK`cwciJRpUL!xn-E_gm^sDl{Li2>{gZt4qWw~$|mLDT&UgaLKQx9@>!c>d^nJzb}? z+%>xz!9#1m|IOe`eAVp!;*zV8Jy;O$eUkNym%78UQnXJfzrEk{PHLXz)b^7`_GqCc zhUg#992P%;#!|XnOYEEv(as!Y&3jHiUaFwVKX!2beOUJFl0b$QiPOAZ7fHa7MVFYf?dU-xMrc;3 z3R{}S1aXtXPi=p(jPJC=tRhRl#SApY#;N)p*=?MXekNX-JLLDw+GZK3=WXQ$EiwZ(v9|uU{tRhd3E#BziV<=R(%ye{S@A5{=-IS zOkje#8<&cJM%eP3(CEA>-DRt=(9^sxKB)w=mp2w>}A{DDL}jp54PcU?7epOc$Sg$agyF|`H7wy`s|--N$gHlRiS)Cin@xV>{jcdxkRhK zT>HF!PQXg?J)fvoV?B?+wr4M(r}`K^lJ5uB^OJmhPHXfE4UK|Wpod__&u#?c*!Wb9 z$EU;=&!oKi?z5wgIpGLgibtA5OHH0jqE)l{wW8TB10iMmhwi}*eMT4Se?XfR)a5ow z%w}mRwch}!(OGu)u^^IvIUy8_qO;$j z2ITV3Bh-u9UPR|%9w)N4i(*GQd#MOfa$ej9^sL1T-brwI9A43Xf>TECu&_C=O(pbd z3R7r!+M!ofRC67dsqe04rLip&QmKi+h7>%Xuf3vj--p?G_!Nrn z2OB#)FDBO1y80|LO!wJFpuGDL_6-GM(aA{dizbxxBjpIL+|Wzuokc6QvrZ5c zQuaXqoG`QnmG0sh$HZFNY>ZnR6g!t2stDz}!i@@?^(o~Ganl;UBU^<|Bh-9T%)W~%`(sR4ae(UV^(35@Jv(7A1 z*5(h)F@iMmRp0Q1?Iss5m;dq4q5su2$NJnW6RD9xvyxrdJ)Il+d4eRKbJ8e#G6Zbc zhVqrk*Sc1{d_09zP*gs*yw;v9fz^7D6Z$l-=3A)S$ENc1AB6*3uv00jE5X=Ak26vcXZcHQZg-X@!3ftk$>?OaH%ppg($ic2*ca_8} zUEZ;bRTmM4^IkBcV0|rPXV_%u($eCjn|2C6JqM9diF-)2GhOrM*`sOzR^Z z2`{L%PYF;5f^07n^2PsVp#3F|4)<6#rEK&d8VOnh>6)joUy+XDq(_{Z9?{ac(RHs9 zHs39Ik^LwmtMXJ}6Yw?or$NCnOH9)6l%l1?u^>rjn0UnNTbYG-gBxb^F5W-{#}NfZ zntb>5Pwx<+wy3qfXM*Y@Q+U)3iyKV5X=q~d**aZ;AKH#_C-oiBHcO??E;>HNMji;y zSK|%iw<+XlA6T6m*e5k5DO^fC7Q=N@dG)P?Lm)|UpHh@5ucj9kf=}e=u#Uo@5F1JR z1g7kJS^R)WK030Q3GMJ0$~2v6hD`ON!Q4}sh7kul&(SvCW!7KhYhQ2kpsZlOZ^%+o zyik3Xp$GQl(>n>74eF=QzGZN5XxBCszODtn0-NtszI%V*xmj!Muk>JHl=Fy}TYtGt zzmM7kUU$Z5{9#J0?nkB~CE8%d#5!1>ReM$}!kJ}IO6#J{ci7Geh^CZ%VblaPUtdrQ zaR3S;+grWBa82Aaw~P-L#1P$LrYv9X6Kb;N67n~xrH60d7dZ^0ToXvk0wD1;1cxa_ zw(T`DF0D8zPzt4SXn0u7Vo@WzJV$nEjQuB{!=j%P5mR?0PNGs0j>J~xw+~P%oT1>u z`*;EsNhLvCb8)~B2x|#q+-i{*w14W${6123gPbv&g-u8-+>L;5$DU*uUk|8sT=IW$ zqeCCO6_0I&ey{)^R>>AcAul>?XIGlBBTlAMUjE}>mQWRqaJsY=4$4OOyGcqe=h+9} zti_!Bl^#cJ_Ph*jtI57R{uQft*%9>Acr5X6CVl70**0CJaF}lm(!$NTLauh()8I5N zNJ@Qc$z9=#{L^cL)I3Z~rF7}Y<+?xR;TqNcUmQ8o5lc^6Dq3%sYoze6+dnaxNXG(% zEPz{l+DVE~fJsb^E3pfVL}g)w7^>npb3a()ig!bP02cws$G*=sgF`|vMwELrhEwgK(Dv8DQF23ZB=f^E znxtohn2ZEFGN$TE(%dbASDotH`S5n3Zw0_7DfyMlKlrRok#x|vS9~MJxCFG%(XU8k zl36rE5^W+=MU`&=Np~)5wD5XHthBQ?lP@=LRyTol#aZjYS)FTWw{C4u$g!*w;vXHZ z>2}2Qe(gcP`||-TDuRJ4_G3jKFXT;h3kJrvi;ar=+cJ(Jy&}tNQ0tXO;rk?EcJ^7S zDtsd7qUXD2hJ6tA>iaiYToXcGzOO}aoUc!)#eQ{02N$pU7vpzM zskGnJ!O2@6oJ-x(z;D|a+B-j@TvH@Z{gr>5S5cjCfq^kc!uvy4#zp(`tT(7PcJHU( z_rU@=w9_3?*Y8PZ$oQ~|rsDnGxYnSyLaL|O^QcU%t9s=KEPu@_rp?( zyJS@x8!TZs+0?{@q6dh5xB@_u|F0#VNANsxXi(>_1J`#P3bv!?-^vPZPw}&30cz$$ z$>r)-*wAia&sV#nkvQ4U6F#g+ugh9a=bsEUa6QIDz73S?v2MC>0zbBA=!Ruo<8R9a zQ-1a-gqnf$&AWi~gq~4L3Ddh0}eCj5I^F z#{_15%!LduA*;XI81w(|I2LJNC)3iX<8jUQcX}qR1=GgKa*4iTH;hh}!X%4Z=?&_liz zZ9o!-Y0L%AiNLoeY0_P5qr%atMZz>UzatcA>WQmR35AGi$OdW!^W6v zfBh}PzQ@L7I8gt1Qu|n)q?9piP{40noxlGY1pD8{e(cxU3+cvO2AM!r197|z}jp)m-saoUO+WD`=j<+^qW=5!-W=1X|EV0 zbgTp8s~cmJDA(stHJd6)v`+8h7zm+&L}Et~6)_|jU?AN_%XX3a-z~*St@o!7vMZ+= z${3mKE+Uy2SHPgKx9T0^j~k1j#frnI=vN4|y>y=;#Jj_L{l+cjlW)Y8`AJrTUx$2( zodS@up_1>CMhi8sCfT{SMnz}s93?Zne1~%L&Ww&qOG1`|jKuxIs|XH>m(6}?x!$Kc z&uXZOw?15^QpcjTKFn4;G32p2%DvlX#0<#WgiF0M3}{->!MFL#Q6Z@8mhue3FO)V| zv95~N_`})&=q@#NUlQl=4qPy$f&Gv?J?P{6T22z|ByLnqj77^AKCM+Doalz3F(lA% zF_faYf9u1lD^fG>-xI!F(hxePjr7cwW!`4zsuGZJgd-*`k&fmG7&_v`GXK50W4LB^^wd)|xu!Qu!B4 z)Dtf3AJ0)lU8?T9kvVOY$R{U-L9YkD7@GYQ0OL*@Vcw18*c!<^yuI^VMwFfSQx$Z6 zPzHB?>2rJ!H155YP@6nk&e3vG%pa-0oua2QnyASZ_AIGe`vGtA%+T3hjc~mzcYkp@ zN9j{|pW@y~=;$3jb19IR$<|>{l=7GWEH$xnlc*W{)mO64XCFU2^D=W z$^oblw2^i6g`+St%?*QV*i_I^z1VbkM)>r zP>)q7EPyfLYP@@vSt)LMuyIf-9_}AK;K!CWX2vW zb=+h~&o5xQvlI61nk>(H*Q}~lUQE%-?G8n0#I0#g60Uc51Qd0hQ!|&;Dg`OU-7TrA zV>-M-f%DD~Dc5kAYFFnTMI^)$C03W1xx76R6Mazp>@VO&ZB95M{4P8FxIet9twQa- zlbbB5zkF%qWiMI$`WE-$8GP+fUU(p5JS`?^j|7r` z&J=f=Jpe~Iujjvfwh+LlNayJ-6% z#M_IK%m^#2=HD~4`mO%5I`(qWr)BwYRU>(V;wi^kGh0v3=}-tq=T-Yx%R z;qcz3hj+&2_O{#?w-7Zp@&binSi!4R|3A3589ryatnpEE($aj7|4~@MyM0h)o@=|j z*qJBgc$`dc`)KW7Z?%kJA6v9XQqOO>^4m4UKI>wU zfBAWSBo#i#8E`PzOkEmdB8j4}UPFuur#=xul{$oE0)WZ|_<*PL@`QMv8rj;eoM#op z@;nMDO~)hWmxZeseFt*}%(KVl_o{E)>jFncsuiQ12#-M`!LFWVX(BXK9$U}gGbWPq zZsW=LI@dOVG2(!M8FwoH?o6{^RE==^Fu(ssi)d`{`E}-!xNwS8ZQD!xX(sGg?=s+! zhmm5~Hj3!83pEl{QoY;XGLH**60tZ-#v@F)R`AXuOwR_WiE>bXEq@AF_*G8!R*`3{n>pSE z>tv_%sy_vLaP%knayB2R*$he&+DD~*)fY7voT28i;)zxT>EwQ5E zfQvQ+oH$(UhYM?ZH$yrv5)c2>uGOok;i3XcQIGL&(dD*KDxA0oHxdo?48GVhb7yS` zWDBP@&8tO8O8!lAfGNg1NCs_YLR40c{8rra29>8tqv@p++XDP0YkS&V&w)JO-Bv7% zij$N80d1~@@2nb9>4_12LpBJr9iBIy?u|RWt*LO@Bgz%_2oS~fK;`jO+W-6cRXKOt^B%^0bq=HL5uvN;ct>feQqC2TPXr1-_(=n6kO2{wO(yC;QyD!P-tq!Vk z6o4OF|A(iKnLUboTDykb@QY!c+n~CzT6~v?B=ZPXFrl^t^_zaqf%SAEMBI#S&e_Cy zk`#-J8jUFPz%}e3p=eUC^3?OT^mT}|c!4&{nIl4iLm;~6gUgfzq zUI2Po`IT0jY6I#lWZ_9!oQ!l2 z#2JuhE&uVC*Ka8g`^EJ(C~H{I0MwYxwUf0Q+pfrqjP*M11dUo-bU@lT0bN<((zmA* zM(u~Pm%WznwDqXE(4LY z>t(?94$9o~U8uMXE)$h1rXA#)DzVb*{dgzqXa2<-`@ZWhLGo>WjWKGja)H-()0ard zzPNjO#Y5k#rON}a<%R`L|K1+sQi}fZJ3pXVHk(q}npUOoeI1zHaf(efDz_T!gG{T9 z)vC8`Xoo{OYW3xx;bAdi$)UmKK}?yn{L1~bqk_LyS$Bohzuv>;_|tpw#} zcYHWBcxW3;iDM2w-~Lc%f7@N)l$=|UY?%LDySu?qim}2R<10W(Y_}s@qa=>0xsb%3 zfK&^UqC(`}ZX#as+1QI=|0JA?-es3*!@dZ8OYF|Di1-H*C;sx7Yij=I5uHj@X?h?t zy$3i!rq8rBRyL4wvClA-BC%-FS11U7j@td3{fPdphpej8tXybaSI{D%GI3m-H>1cL zF!d8&?lUt!CB&53YIdo)DK;$eJFHl(M&A&z)2gc|XgG6v&m}=i%ga}7`qOlPC)Xrm zh>o_!-gRo4vq)Z0otfP*Y&(tVr^mEtnN=jc=UG0w2{?GGE;Uy37e>Qq&@K z%4$l--dKA~I(1mZy(m!9M?5!D6PYAy1MZo^$VqJee)Vpnu4EEtvrzER5|07P)=m2w zNCMTUWS$KD2c8(>Gon#Sp5Aktq$H99XOub^^W3qf^i{vb{gJ!?&Wwlj(OSA+=Y0&Y z5U7(q*I=XXNj6~zH~CCT52=#O7dK0gK+!?Ldug4)Wzy|&NE>OC9uxL}rwf7lrOyZ7 zg7>~EinW`1K8J|pB`*)uXB>kdsJWCeZ2wuo#cy0%Q;cTClH0T6M5Ut7C{XizdCZb@ zaUUdr$u6scr5r2q7?^fU{x5a&;(C)*+8c9|Xno;czn)&mVJ`bTjn#lb&5ilgHKL%X z^S^x08{g9sZodF&WD7tZB@_ETQ6$_4ij945*Z^I)BZAsz67m zxN|0-Jy2yvM&qJpM)P%X?`f?6oX>a5&`u(l)0Q54>VgSeZd8;sxzW5qfd!zqoqzT{ zX+#BlRZ1F6pgZteucX`tBp&tXAq7NttscQSYdSj>RmJ|cAC^Nbq23$m6ine%Pk|37 zGchXw-ዓ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 S6sS`Ku{>vUw%pY7&i?^ij@AAE literal 12133 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 Date: Thu, 6 Nov 2025 10:46:11 +0100 Subject: [PATCH 11/30] build: add GitHub Actions workflow for building and pushing LibreChat container to ACR --- .../acr-build-and-push-libre-chat.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/acr-build-and-push-libre-chat.yml diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml new file mode 100644 index 00000000000..ca96d820733 --- /dev/null +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -0,0 +1,41 @@ +name: Build and Push MIA - LibreChat Container to ACR + +on: + push: + branches: + - release + workflow_dispatch: + +permissions: + id-token: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Login to ACR + run: | + az acr login --name ${{ vars.ACR_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./ + file: ./Dockerfile.multi + push: true + registries: ${{ vars.ACR_REGISTRY_SERVER }} + tags: | + ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:${{ github.sha }} + ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest From 7b5b224f909f46ca1ce6680f826bb44903cd5c6b Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 6 Nov 2025 12:11:57 +0100 Subject: [PATCH 12/30] chore: update GitHub Actions workflow for LibreChat container build and push to ACR - Simplified branch specification for push events. - Added environment variables for image name and tag. - Enhanced Azure login step to use OIDC. - Added sanity check for ACR before login. - Updated Docker image tags to use environment variables for better maintainability. --- .../acr-build-and-push-libre-chat.yml | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/acr-build-and-push-libre-chat.yml b/.github/workflows/acr-build-and-push-libre-chat.yml index ca96d820733..cd53fc693df 100644 --- a/.github/workflows/acr-build-and-push-libre-chat.yml +++ b/.github/workflows/acr-build-and-push-libre-chat.yml @@ -2,40 +2,46 @@ name: Build and Push MIA - LibreChat Container to ACR on: push: - branches: - - release + branches: ["release"] workflow_dispatch: permissions: id-token: write + contents: read jobs: build-and-push: runs-on: ubuntu-latest + env: + IMAGE_NAME: mia-libre-chat + TAG: ${{ github.sha }} + steps: - name: Checkout code uses: actions/checkout@v4 - - name: Azure login + - name: Azure login (OIDC) uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Login to ACR + - name: ACR sanity run: | - az acr login --name ${{ vars.ACR_NAME }} + az account show -o table + az acr show -n "${{ vars.ACR_NAME }}" -o table + + - name: Login to ACR + run: az acr login --name "${{ vars.ACR_NAME }}" - name: Build and push Docker image uses: docker/build-push-action@v5 with: - context: ./ + context: . file: ./Dockerfile.multi push: true - registries: ${{ vars.ACR_REGISTRY_SERVER }} tags: | - ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:${{ github.sha }} - ${{ vars.ACR_REGISTRY_SERVER }}/mia-libre-chat:latest + ${{ vars.ACR_REGISTRY_SERVER }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} + ${{ vars.ACR_REGISTRY_SERVER }}/${{ env.IMAGE_NAME }}:latest From 46c9096a5d281f2652e440f68d634eea77078c14 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 13 Nov 2025 14:01:16 +0100 Subject: [PATCH 13/30] chore: remove Android Edge keyboard overlay hotfix code - Eliminated the hotfix for the Android Edge browser keyboard overlay issue because Edge fixed it in their latest release - Relates to #10074 MIA-113 --- client/src/components/Chat/Input/ChatForm.tsx | 84 +------------------ 1 file changed, 1 insertion(+), 83 deletions(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index d48e228fc59..0d78474bad2 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -123,45 +123,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { } }, [isCollapsed]); - /** - * HOTFIX: Android Edge Browser Keyboard Overlay Fix - * - * Problem: On Android Edge browser, the virtual keyboard covers the input field - * when it appears, making it impossible to see what the user is typing. - * - * Solution: When the textarea gains focus, push the form up by adding - * margin-bottom to make the input field visible above the keyboard. - */ - const handleAndroidEdgeKeyboardFocusFix = useCallback(() => { - const ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT = '240px'; - - // Only apply fix for Android Edge version 141 or greater - if (isAndroidEdgeWithVersion141OrGreater()) { - const formElement = document.querySelector('form'); - if (formElement) { - // Add margin-bottom to push the form up by the keyboard height - formElement.style.marginBottom = ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT; - } - } - }, []); - - /** - * HOTFIX: Android Edge Browser Keyboard Overlay Fix - Restore Position - * - * When the textarea loses focus (keyboard hides), restore the original - * form position by removing the margin-bottom. - */ - const handleAndroidEdgeKeyboardBlurFix = useCallback(() => { - // Only apply fix for Android Edge version 141 or greater - if (isAndroidEdgeWithVersion141OrGreater()) { - const formElement = document.querySelector('form'); - if (formElement) { - // Remove margin-bottom to restore original form position - formElement.style.marginBottom = `0px`; - } - } - }, []); - useAutoSave({ files, setFiles, @@ -317,14 +278,10 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { rows={1} onFocus={() => { handleFocusOrClick(); - handleAndroidEdgeKeyboardFocusFix(); setIsTextAreaFocused(true); }} + onBlur={setIsTextAreaFocused.bind(null, false)} aria-label={localize('com_ui_message_input')} - onBlur={() => { - handleAndroidEdgeKeyboardBlurFix(); - setIsTextAreaFocused(false); - }} onClick={handleFocusOrClick} style={{ height: 44, overflowY: 'auto' }} className={cn( @@ -392,43 +349,4 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { ); }); -/** - * Get the Edge browser version from user agent string - * @returns the version number (e.g., 141) or null if not Edge - */ -function getEdgeVersion(): number | null { - const userAgent = navigator.userAgent; - - // Edge on Android pattern: "EdgA/141.0.0.0" - const edgeAndroidMatch = userAgent.match(/EdgA\/(\d+)/); - if (edgeAndroidMatch) { - return parseInt(edgeAndroidMatch[1], 10); - } - - return null; -} - -/** - * HOTFIX: Android Edge Browser Keyboard Overlay Fix - * - * Problem: On Android Edge browser, the virtual keyboard covers the input field - * when it appears, making it impossible to see what the user is typing. - * - * Solution: When the textarea gains focus, push the form up by adding - * margin-bottom to make the input field visible above the keyboard. - * - * This can be reproduced on Edge Version 141 or greater - * - * Check if browser is Android Edge with version 141 or greater - * @returns true if Android Edge version >= 141, false otherwise - */ -function isAndroidEdgeWithVersion141OrGreater(): boolean { - // https://learn.microsoft.com/de-de/microsoft-edge/web-platform/user-agent-guidance#identifiers-for-microsoft-edge-on-various-platforms - // EdgA -> Edge Android - const isAndroidEdge = navigator.userAgent.includes('EdgA'); - const version = getEdgeVersion(); - - return isAndroidEdge && version !== null && version >= 141; -} - export default ChatForm; \ No newline at end of file From 8e90a736b206a95c6f355cb6682859e87b5a8800 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 13 Nov 2025 14:31:16 +0100 Subject: [PATCH 14/30] chore: remove Android Edge keyboard overlay hotfix code - Eliminated the hotfix for the Android Edge browser keyboard overlay issue as it is no longer necessary. - This change simplifies the ChatForm component by removing unused code related to keyboard focus handling. MIA-113 --- client/src/components/Chat/Input/ChatForm.tsx | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 7038f976a55..f1dc1ef076a 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -123,45 +123,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { } }, [isCollapsed]); - /** - * HOTFIX: Android Edge Browser Keyboard Overlay Fix - * - * Problem: On Android Edge browser, the virtual keyboard covers the input field - * when it appears, making it impossible to see what the user is typing. - * - * Solution: When the textarea gains focus, push the form up by adding - * margin-bottom to make the input field visible above the keyboard. - */ - const handleAndroidEdgeKeyboardFocusFix = useCallback(() => { - const ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT = '240px'; - - // Only apply fix for Android Edge version 141 or greater - if (isAndroidEdgeWithVersion141OrGreater()) { - const formElement = document.querySelector('form'); - if (formElement) { - // Add margin-bottom to push the form up by the keyboard height - formElement.style.marginBottom = ANDROID_EDGE_HOTFIX_KEYBOARD_OVERLAY_HEIGHT; - } - } - }, []); - - /** - * HOTFIX: Android Edge Browser Keyboard Overlay Fix - Restore Position - * - * When the textarea loses focus (keyboard hides), restore the original - * form position by removing the margin-bottom. - */ - const handleAndroidEdgeKeyboardBlurFix = useCallback(() => { - // Only apply fix for Android Edge version 141 or greater - if (isAndroidEdgeWithVersion141OrGreater()) { - const formElement = document.querySelector('form'); - if (formElement) { - // Remove margin-bottom to restore original form position - formElement.style.marginBottom = `0px`; - } - } - }, []); - useAutoSave({ files, setFiles, @@ -317,14 +278,10 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { rows={1} onFocus={() => { handleFocusOrClick(); - handleAndroidEdgeKeyboardFocusFix(); setIsTextAreaFocused(true); }} + onBlur={setIsTextAreaFocused.bind(null, false)} aria-label={localize('com_ui_message_input')} - onBlur={() => { - handleAndroidEdgeKeyboardBlurFix(); - setIsTextAreaFocused(false); - }} onClick={handleFocusOrClick} style={{ height: 44, overflowY: 'auto' }} className={cn( From 97e10dba1120fd7db77da84a74dc0c49cc3680e1 Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 5 Mar 2026 19:31:00 +0100 Subject: [PATCH 15/30] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20Image=20Generat?= =?UTF-8?q?ion=20capability=20and=20UI=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented image generation functionality in Agent and loadAddedAgent models. - Introduced ImageGeneration component for user interface. - Updated BadgeRowContext to manage image generation state. - Enhanced ToolsDropdown to include image generation toggle. - Added support for image generation in agent capabilities and local storage management. - Updated relevant types and enums to accommodate new feature. MIA-131 --- api/models/Agent.js | 3 ++ api/models/loadAddedAgent.js | 3 ++ client/src/Providers/BadgeRowContext.tsx | 24 +++++++++++ client/src/components/Chat/Input/BadgeRow.tsx | 2 + .../components/Chat/Input/ImageGeneration.tsx | 26 +++++++++++ .../components/Chat/Input/ToolsDropdown.tsx | 43 ++++++++++++++++++- .../src/hooks/Agents/useAgentCapabilities.ts | 7 +++ packages/data-provider/src/config.ts | 4 ++ packages/data-provider/src/types.ts | 1 + .../data-provider/src/types/assistants.ts | 1 + 10 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 client/src/components/Chat/Input/ImageGeneration.tsx diff --git a/api/models/Agent.js b/api/models/Agent.js index 11789ca63b0..21f3523361b 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -127,6 +127,9 @@ const loadEphemeralAgent = async ({ req, spec, endpoint, model_parameters: _m }) if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { tools.push(Tools.web_search); } + if (ephemeralAgent?.image_generation === true) { + tools.push(Tools.gemini_image_gen); + } const addedServers = new Set(); if (mcpServers.size > 0) { diff --git a/api/models/loadAddedAgent.js b/api/models/loadAddedAgent.js index aa83375eae4..92ed50a56b4 100644 --- a/api/models/loadAddedAgent.js +++ b/api/models/loadAddedAgent.js @@ -139,6 +139,9 @@ const loadAddedAgent = async ({ req, conversation, primaryAgent }) => { if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) { tools.push(Tools.web_search); } + if (ephemeralAgent?.image_generation === true) { + tools.push(Tools.gemini_image_gen); + } const addedServers = new Set(); if (mcpServers.size > 0) { diff --git a/client/src/Providers/BadgeRowContext.tsx b/client/src/Providers/BadgeRowContext.tsx index 40df795aba9..fb568906a76 100644 --- a/client/src/Providers/BadgeRowContext.tsx +++ b/client/src/Providers/BadgeRowContext.tsx @@ -18,6 +18,7 @@ interface BadgeRowContextType { webSearch: ReturnType; artifacts: ReturnType; fileSearch: ReturnType; + imageGeneration: ReturnType; codeInterpreter: ReturnType; codeApiKeyForm: ReturnType; searchApiKeyForm: ReturnType; @@ -66,11 +67,13 @@ export default function BadgeRowProvider({ const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`; const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${key}`; const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${key}`; + const imageGenToggleKey = `${LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_}${key}`; const codeToggleValue = getTimestampedValue(codeToggleKey); const webSearchToggleValue = getTimestampedValue(webSearchToggleKey); const fileSearchToggleValue = getTimestampedValue(fileSearchToggleKey); const artifactsToggleValue = getTimestampedValue(artifactsToggleKey); + const imageGenToggleValue = getTimestampedValue(imageGenToggleKey); const initialValues: Record = {}; @@ -106,6 +109,14 @@ export default function BadgeRowProvider({ } } + if (imageGenToggleValue !== null) { + try { + initialValues[AgentCapabilities.image_generation] = JSON.parse(imageGenToggleValue); + } catch (e) { + console.error('Failed to parse image generation toggle value:', e); + } + } + /** * Always set values for all tools (use defaults if not in `localStorage`) * If `ephemeralAgent` is `null`, create a new object with just our tool values @@ -115,6 +126,8 @@ export default function BadgeRowProvider({ [Tools.web_search]: initialValues[Tools.web_search] ?? false, [Tools.file_search]: initialValues[Tools.file_search] ?? false, [AgentCapabilities.artifacts]: initialValues[AgentCapabilities.artifacts] ?? false, + [AgentCapabilities.image_generation]: + initialValues[AgentCapabilities.image_generation] ?? false, }; setEphemeralAgent((prev) => ({ @@ -131,6 +144,8 @@ export default function BadgeRowProvider({ storageKey = webSearchToggleKey; } else if (toolKey === Tools.file_search) { storageKey = fileSearchToggleKey; + } else if (toolKey === AgentCapabilities.image_generation) { + storageKey = imageGenToggleKey; } // Store the value and set timestamp for existing values localStorage.setItem(storageKey, JSON.stringify(value)); @@ -186,12 +201,21 @@ export default function BadgeRowProvider({ isAuthenticated: true, }); + /** Image Generation hook - using capability key */ + const imageGeneration = useToolToggle({ + conversationId, + toolKey: AgentCapabilities.image_generation, + localStorageKey: LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_, + isAuthenticated: true, + }); + const mcpServerManager = useMCPServerManager({ conversationId }); const value: BadgeRowContextType = { webSearch, artifacts, fileSearch, + imageGeneration, agentsConfig, conversationId, codeApiKeyForm, diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 5036dcd5e4f..c761f7c6303 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -21,6 +21,7 @@ import FileSearch from './FileSearch'; import Artifacts from './Artifacts'; import MCPSelect from './MCPSelect'; import WebSearch from './WebSearch'; +import ImageGeneration from './ImageGeneration'; import store from '~/store'; interface BadgeRowProps { @@ -367,6 +368,7 @@ function BadgeRow({ + diff --git a/client/src/components/Chat/Input/ImageGeneration.tsx b/client/src/components/Chat/Input/ImageGeneration.tsx new file mode 100644 index 00000000000..722b8103520 --- /dev/null +++ b/client/src/components/Chat/Input/ImageGeneration.tsx @@ -0,0 +1,26 @@ +import React, { memo } from 'react'; +import { ImageIcon } from 'lucide-react'; +import { CheckboxButton } from '@librechat/client'; +import { useLocalize } from '~/hooks'; +import { useBadgeRowContext } from '~/Providers'; + +function ImageGeneration() { + const localize = useLocalize(); + const { imageGeneration: imageGenerationData } = useBadgeRowContext(); + const { toggleState: imageGeneration, debouncedChange, isPinned } = imageGenerationData; + + return ( + (isPinned || imageGeneration === true) && ( +

+ ), + }); + } + if (canUseWebSearch && webSearchEnabled) { dropdownItems.push({ onClick: handleWebSearchToggle, diff --git a/client/src/hooks/Agents/useAgentCapabilities.ts b/client/src/hooks/Agents/useAgentCapabilities.ts index 8d2bd6ef87e..8d0a8ce491e 100644 --- a/client/src/hooks/Agents/useAgentCapabilities.ts +++ b/client/src/hooks/Agents/useAgentCapabilities.ts @@ -10,6 +10,7 @@ interface AgentCapabilitiesResult { fileSearchEnabled: boolean; webSearchEnabled: boolean; codeEnabled: boolean; + imageGenerationEnabled: boolean; } export default function useAgentCapabilities( @@ -55,6 +56,11 @@ export default function useAgentCapabilities( [capabilities], ); + const imageGenerationEnabled = useMemo( + () => capabilities?.includes(AgentCapabilities.image_generation) ?? false, + [capabilities], + ); + return { ocrEnabled, codeEnabled, @@ -64,5 +70,6 @@ export default function useAgentCapabilities( artifactsEnabled, webSearchEnabled, fileSearchEnabled, + imageGenerationEnabled, }; } diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index c27bf42c9c9..74d8812680f 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -180,6 +180,7 @@ export enum AgentCapabilities { execute_code = 'execute_code', file_search = 'file_search', web_search = 'web_search', + image_generation = 'image_generation', artifacts = 'artifacts', actions = 'actions', context = 'context', @@ -262,6 +263,7 @@ export const defaultAgentCapabilities = [ AgentCapabilities.execute_code, AgentCapabilities.file_search, AgentCapabilities.web_search, + AgentCapabilities.image_generation, AgentCapabilities.artifacts, AgentCapabilities.actions, AgentCapabilities.context, @@ -1795,6 +1797,8 @@ export enum LocalStorageKeys { LAST_FILE_SEARCH_TOGGLE_ = 'LAST_FILE_SEARCH_TOGGLE_', /** Last checked toggle for Artifacts per conversation ID */ LAST_ARTIFACTS_TOGGLE_ = 'LAST_ARTIFACTS_TOGGLE_', + /** Last checked toggle for Image Generation per conversation ID */ + LAST_IMAGE_GEN_TOGGLE_ = 'LAST_IMAGE_GEN_TOGGLE_', /** Key for the last selected agent provider */ LAST_AGENT_PROVIDER = 'lastAgentProvider', /** Key for the last selected agent model */ diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index d9050a07f03..03cf2badf8e 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -98,6 +98,7 @@ export type TEphemeralAgent = { web_search?: boolean; file_search?: boolean; execute_code?: boolean; + image_generation?: boolean; }; export type TPayload = Partial & diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index da773071e77..35200a0bd5c 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -20,6 +20,7 @@ export enum Tools { code_interpreter = 'code_interpreter', file_search = 'file_search', web_search = 'web_search', + gemini_image_gen = 'gemini_image_gen', retrieval = 'retrieval', function = 'function', memory = 'memory', From 736960e0a4fd374ec32c5cbced49202fd69ba6a7 Mon Sep 17 00:00:00 2001 From: Lionel Ringenbach Date: Fri, 27 Mar 2026 00:02:26 -0700 Subject: [PATCH 16/30] =?UTF-8?q?=F0=9F=95=90=20fix:=20Use=20client=20time?= =?UTF-8?q?zone=20for=20special=20variable=20date=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `replaceSpecialVars` uses `dayjs()` which defaults to the server's timezone (UTC in Docker), causing `{{current_datetime}}` and `{{current_date}}` to always show +00:00 regardless of the user's location. This is inconsistent with `append_current_datetime` for assistants which already handles client timezone via `clientTimestamp`. Add dayjs timezone plugin support and a `timezone` parameter to `replaceSpecialVars`. The client sends its IANA timezone string (`Intl.DateTimeFormat().resolvedOptions().timeZone`) alongside the existing `clientTimestamp`, and the agent initialization flow passes it through to produce timezone-aware date formatting. Co-Authored-By: Claude Opus 4.6 (1M context) --- client/src/hooks/Chat/useChatFunctions.ts | 1 + packages/api/src/agents/initialize.ts | 1 + packages/api/src/types/http.ts | 1 + packages/data-provider/specs/parsers.spec.ts | 65 +++++++++++++++++++- packages/data-provider/src/parsers.ts | 20 +++++- packages/data-provider/src/schemas.ts | 2 + 6 files changed, 86 insertions(+), 4 deletions(-) diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 7cf8c6bf250..53392cd9184 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -208,6 +208,7 @@ export default function useChatFunctions({ text, sender: 'User', clientTimestamp: new Date().toLocaleString('sv').replace(' ', 'T'), + clientTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, isCreatedByUser: true, parentMessageId, conversationId, diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 81bc89cac44..e66ff9eb9b2 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -403,6 +403,7 @@ export async function initializeAgent( agent.instructions = replaceSpecialVars({ text: agent.instructions, user: req.user ? (req.user as unknown as TUser) : null, + timezone: req.body?.clientTimezone, }); } diff --git a/packages/api/src/types/http.ts b/packages/api/src/types/http.ts index c304e9089ec..1a3cab3d492 100644 --- a/packages/api/src/types/http.ts +++ b/packages/api/src/types/http.ts @@ -16,6 +16,7 @@ export type RequestBody = { model?: string; key?: string; endpointOption?: Partial; + clientTimezone?: string; }; export type ServerRequest = Request & { diff --git a/packages/data-provider/specs/parsers.spec.ts b/packages/data-provider/specs/parsers.spec.ts index 83c3500922e..e30a5b72985 100644 --- a/packages/data-provider/specs/parsers.spec.ts +++ b/packages/data-provider/specs/parsers.spec.ts @@ -7,13 +7,13 @@ import type { TUser, TConversation } from '../src/types'; // Mock dayjs module with consistent date/time values regardless of environment jest.mock('dayjs', () => { - const mockDayjs = () => ({ + const makeMock = (offset: string) => ({ format: (format: string) => { if (format === 'YYYY-MM-DD') { return '2024-04-29'; } if (format === 'YYYY-MM-DD HH:mm:ss Z') { - return '2024-04-29 12:34:56 -04:00'; + return `2024-04-29 12:34:56 ${offset}`; } if (format === 'dddd') { return 'Monday'; @@ -23,8 +23,19 @@ jest.mock('dayjs', () => { ); }, toISOString: () => '2024-04-29T16:34:56.000Z', + isValid: () => true, + tz: (timezone: string) => { + if (timezone === 'America/New_York') { + return makeMock('-04:00'); + } + if (timezone === 'Asia/Tokyo') { + return makeMock('+09:00'); + } + return { isValid: () => false }; + }, }); + const mockDayjs = () => makeMock('-04:00'); mockDayjs.extend = jest.fn(); return mockDayjs; @@ -126,6 +137,56 @@ describe('replaceSpecialVars', () => { expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime expect(result).toContain('Test User'); // current_user }); + + test('should use provided timezone for date formatting', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 +09:00 (Monday)'); + }); + + test('should fall back to default when no timezone is provided', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)'); + }); + + test('should fall back to default for invalid timezone', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + timezone: 'Invalid/Timezone', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)'); + }); + + test('should apply timezone to {{current_date}} formatting', () => { + const result = replaceSpecialVars({ + text: 'Today is {{current_date}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('Today is 2024-04-29 (Monday)'); + }); + + test('should not affect {{iso_datetime}} regardless of timezone', () => { + const result = replaceSpecialVars({ + text: 'ISO: {{iso_datetime}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('ISO: 2024-04-29T16:34:56.000Z'); + }); + + test('should handle all variables with timezone and user combined', () => { + const result = replaceSpecialVars({ + text: '{{current_user}} - {{current_date}} - {{current_datetime}} - {{iso_datetime}}', + user: mockUser, + timezone: 'Asia/Tokyo', + }); + expect(result).toBe( + 'Test User - 2024-04-29 (Monday) - 2024-04-29 12:34:56 +09:00 (Monday) - 2024-04-29T16:34:56.000Z', + ); + }); }); describe('parseCompactConvo', () => { diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 3ec4221b62e..ee3a500fa0a 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -1,5 +1,10 @@ import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import tz from 'dayjs/plugin/timezone'; import type { ZodIssue } from 'zod'; + +dayjs.extend(utc); +dayjs.extend(tz); import type * as a from './types/assistants'; import type * as s from './schemas'; import type * as t from './types'; @@ -402,13 +407,24 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n return lastIndex; } -export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) { +export function replaceSpecialVars({ + text, + user, + timezone, +}: { + text: string; + user?: t.TUser | null; + timezone?: string; +}) { let result = text; if (!result) { return result; } - const now = dayjs(); + let now = timezone ? dayjs().tz(timezone) : dayjs(); + if (!now.isValid()) { + now = dayjs(); + } const weekdayName = now.format('dddd'); const currentDate = now.format('YYYY-MM-DD'); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 19ba804556c..60d40ffa477 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -611,6 +611,7 @@ export const tMessageSchema = z.object({ isCreatedByUser: z.boolean(), error: z.boolean().optional(), clientTimestamp: z.string().optional(), + clientTimezone: z.string().optional(), createdAt: z .string() .optional() @@ -686,6 +687,7 @@ export type TMessage = z.input & { siblingIndex?: number; attachments?: TAttachment[]; clientTimestamp?: string; + clientTimezone?: string; feedback?: TFeedback; }; From 13de39a7a9eb58e8c1c970a3b9f01e22369a504e Mon Sep 17 00:00:00 2001 From: ronniekolk Date: Thu, 23 Apr 2026 11:39:47 +0200 Subject: [PATCH 17/30] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20conversation=5F?= =?UTF-8?q?id=20support=20in=20special=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced `conversation_id` as a new special variable in the application, allowing it to be used in various contexts. - Updated the `replaceSpecialVars` function to handle `conversationId` for text replacements. - Enhanced the `specialVariableIcons` to include an icon for `conversation_id`. - Modified relevant hooks and components to utilize the new `conversationId` where applicable. - Updated translations to include descriptions for the new special variable. This addition improves the flexibility of the chat functionality by allowing conversation-specific data to be referenced dynamically. --- .../components/Prompts/utils/specialVariables.ts | 3 ++- client/src/hooks/Chat/useChatFunctions.ts | 1 + client/src/hooks/Messages/useSubmitMessage.ts | 6 ++++-- client/src/locales/en/translation.json | 2 ++ packages/api/src/agents/initialize.ts | 1 + packages/data-provider/specs/parsers.spec.ts | 8 ++++++-- packages/data-provider/src/config.ts | 1 + packages/data-provider/src/parsers.ts | 14 +++++++++++++- 8 files changed, 30 insertions(+), 6 deletions(-) diff --git a/client/src/components/Prompts/utils/specialVariables.ts b/client/src/components/Prompts/utils/specialVariables.ts index e9719038201..fee8cc8b84f 100644 --- a/client/src/components/Prompts/utils/specialVariables.ts +++ b/client/src/components/Prompts/utils/specialVariables.ts @@ -1,4 +1,4 @@ -import { Calendar, User, Clock, Globe, Sparkles } from 'lucide-react'; +import { Calendar, User, Clock, Globe, Hash, Sparkles } from 'lucide-react'; import type { specialVariables } from 'librechat-data-provider'; type SpecialVariableKey = keyof typeof specialVariables; @@ -11,6 +11,7 @@ export const specialVariableIcons: Record< current_datetime: Clock, current_user: User, iso_datetime: Globe, + conversation_id: Hash, }; export const getSpecialVariableIcon = (name: string) => diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 18aaf0daee3..4087cc1f5ec 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -132,6 +132,7 @@ export default function useChatFunctions({ conversation.promptPrefix = replaceSpecialVars({ text: conversation.promptPrefix, user, + conversationId, }); } diff --git a/client/src/hooks/Messages/useSubmitMessage.ts b/client/src/hooks/Messages/useSubmitMessage.ts index 8fd06e6003d..5c03c80131e 100644 --- a/client/src/hooks/Messages/useSubmitMessage.ts +++ b/client/src/hooks/Messages/useSubmitMessage.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useParams } from 'react-router-dom'; import { replaceSpecialVars } from 'librechat-data-provider'; import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers'; import { useAuthContext } from '~/hooks/AuthContext'; @@ -8,6 +9,7 @@ import store from '~/store'; export default function useSubmitMessage() { const { user } = useAuthContext(); + const { conversationId } = useParams(); const methods = useChatFormContext(); const { conversation: addedConvo } = useAddedChatContext(); const { ask, index, getMessages, setMessages } = useChatContext(); @@ -44,7 +46,7 @@ export default function useSubmitMessage() { const submitPrompt = useCallback( (text: string) => { - const parsedText = replaceSpecialVars({ text, user }); + const parsedText = replaceSpecialVars({ text, user, conversationId }); if (autoSendPrompts) { submitMessage({ text: parsedText }); return; @@ -55,7 +57,7 @@ export default function useSubmitMessage() { const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText; setActivePrompt(newText); }, - [autoSendPrompts, submitMessage, setActivePrompt, methods, user], + [autoSendPrompts, submitMessage, setActivePrompt, methods, user, conversationId], ); return { submitMessage, submitPrompt }; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 74c668038b1..b9f08e8801a 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1437,9 +1437,11 @@ "com_ui_special_var_current_date": "Current Date", "com_ui_special_var_current_datetime": "Current Date & Time", "com_ui_special_var_current_user": "Current User", + "com_ui_special_var_conversation_id": "Conversation ID", "com_ui_special_var_desc_current_date": "Today's date and day of the week", "com_ui_special_var_desc_current_datetime": "Local date and time in your timezone", "com_ui_special_var_desc_current_user": "Your account display name", + "com_ui_special_var_desc_conversation_id": "Current chat/conversation ID from the URL", "com_ui_special_var_desc_iso_datetime": "UTC datetime in ISO 8601 format", "com_ui_special_var_iso_datetime": "UTC ISO Datetime", "com_ui_special_variable_added": "{{0}} special variable added.", diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 5105b213053..efba62afeef 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -405,6 +405,7 @@ export async function initializeAgent( agent.instructions = replaceSpecialVars({ text: agent.instructions, user: req.user ? (req.user as unknown as TUser) : null, + conversationId, }); } diff --git a/packages/data-provider/specs/parsers.spec.ts b/packages/data-provider/specs/parsers.spec.ts index 83c3500922e..ea506081c54 100644 --- a/packages/data-provider/specs/parsers.spec.ts +++ b/packages/data-provider/specs/parsers.spec.ts @@ -36,6 +36,7 @@ describe('replaceSpecialVars', () => { name: 'Test User', id: 'user123', } as TUser; + const mockConversationId = '24fa3d2e-a576-44f7-a65c-23cc4d330726'; beforeEach(() => { jest.clearAllMocks(); @@ -87,11 +88,12 @@ describe('replaceSpecialVars', () => { test('should handle multiple replacements in the same text', () => { const result = replaceSpecialVars({ - text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}', + text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}. Convo: {{conversation_id}}', user: mockUser, + conversationId: mockConversationId, }); expect(result).toBe( - 'Hello Test User! Today is 2024-04-29 (Monday) and the time is 2024-04-29 12:34:56 -04:00 (Monday). ISO: 2024-04-29T16:34:56.000Z', + 'Hello Test User! Today is 2024-04-29 (Monday) and the time is 2024-04-29 12:34:56 -04:00 (Monday). ISO: 2024-04-29T16:34:56.000Z. Convo: 24fa3d2e-a576-44f7-a65c-23cc4d330726', ); }); @@ -112,6 +114,7 @@ describe('replaceSpecialVars', () => { const result = replaceSpecialVars({ text: specialVarsText, user: mockUser, + conversationId: mockConversationId, }); // Verify none of the original variable placeholders remain in the result @@ -125,6 +128,7 @@ describe('replaceSpecialVars', () => { expect(result).toContain('2024-04-29 12:34:56 -04:00 (Monday)'); // current_datetime expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime expect(result).toContain('Test User'); // current_user + expect(result).toContain(mockConversationId); // conversation_id }); }); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 7eb779ffc71..e1eb6240d6e 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2016,6 +2016,7 @@ export const specialVariables = { current_user: true, iso_datetime: true, current_datetime: true, + conversation_id: true, }; export type TSpecialVarLabel = `com_ui_special_var_${keyof typeof specialVariables}`; diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 3ec4221b62e..74a4c208be5 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -402,7 +402,15 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n return lastIndex; } -export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) { +export function replaceSpecialVars({ + text, + user, + conversationId, +}: { + text: string; + user?: t.TUser | null; + conversationId?: string | null; +}) { let result = text; if (!result) { return result; @@ -424,6 +432,10 @@ export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUse result = result.replace(/{{current_user}}/gi, user.name); } + if (typeof conversationId === 'string' && conversationId) { + result = result.replace(/{{conversation_id}}/gi, conversationId); + } + return result; } From 867f06653767afc8a59d43290fb3b96649d6a30c Mon Sep 17 00:00:00 2001 From: Nico Peham Date: Thu, 23 Apr 2026 13:56:51 +0200 Subject: [PATCH 18/30] Merge remote-tracking branch 'origin/release' into chore/main-v0.8.5 --- client/src/Providers/BadgeRowContext.tsx | 20 ++++++++++++++ client/src/components/Chat/Input/BadgeRow.tsx | 2 ++ .../components/Chat/Input/ImageGeneration.tsx | 26 +++++++++++++++++++ .../components/Chat/Input/ToolsDropdown.tsx | 2 +- .../src/hooks/Agents/useAgentCapabilities.ts | 7 +++++ packages/data-provider/src/config.ts | 4 +++ packages/data-provider/src/types.ts | 1 + .../data-provider/src/types/assistants.ts | 1 + 8 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 client/src/components/Chat/Input/ImageGeneration.tsx diff --git a/client/src/Providers/BadgeRowContext.tsx b/client/src/Providers/BadgeRowContext.tsx index 1bedcec66f6..eb8fee021ac 100644 --- a/client/src/Providers/BadgeRowContext.tsx +++ b/client/src/Providers/BadgeRowContext.tsx @@ -20,6 +20,7 @@ interface BadgeRowContextType { webSearch: ReturnType; artifacts: ReturnType; fileSearch: ReturnType; + imageGeneration: ReturnType; codeInterpreter: ReturnType; codeApiKeyForm: ReturnType; searchApiKeyForm: ReturnType; @@ -100,11 +101,13 @@ export default function BadgeRowProvider({ const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${storageSuffix}`; const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${storageSuffix}`; const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${storageSuffix}`; + const imageGenToggleKey = `${LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_}${key}`; const codeToggleValue = getTimestampedValue(codeToggleKey); const webSearchToggleValue = getTimestampedValue(webSearchToggleKey); const fileSearchToggleValue = getTimestampedValue(fileSearchToggleKey); const artifactsToggleValue = getTimestampedValue(artifactsToggleKey); + const imageGenToggleValue = getTimestampedValue(imageGenToggleKey); const initialValues: Record = {}; @@ -140,6 +143,14 @@ export default function BadgeRowProvider({ } } + if (imageGenToggleValue !== null) { + try { + initialValues[AgentCapabilities.image_generation] = JSON.parse(imageGenToggleValue); + } catch (e) { + console.error('Failed to parse image generation toggle value:', e); + } + } + const hasOverrides = Object.keys(initialValues).length > 0; /** Read persisted MCP values from localStorage */ @@ -238,12 +249,21 @@ export default function BadgeRowProvider({ isAuthenticated: true, }); + /** Image Generation hook - using capability key */ + const imageGeneration = useToolToggle({ + conversationId, + toolKey: AgentCapabilities.image_generation, + localStorageKey: LocalStorageKeys.LAST_IMAGE_GEN_TOGGLE_, + isAuthenticated: true, + }); + const mcpServerManager = useMCPServerManager({ conversationId, storageContextKey }); const value: BadgeRowContextType = { webSearch, artifacts, fileSearch, + imageGeneration, agentsConfig, conversationId, storageContextKey, diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 6fea6b0d58b..4d36cff127f 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -21,6 +21,7 @@ import FileSearch from './FileSearch'; import Artifacts from './Artifacts'; import MCPSelect from './MCPSelect'; import WebSearch from './WebSearch'; +import ImageGeneration from './ImageGeneration'; import store from '~/store'; interface BadgeRowProps { @@ -373,6 +374,7 @@ function BadgeRow({ + diff --git a/client/src/components/Chat/Input/ImageGeneration.tsx b/client/src/components/Chat/Input/ImageGeneration.tsx new file mode 100644 index 00000000000..722b8103520 --- /dev/null +++ b/client/src/components/Chat/Input/ImageGeneration.tsx @@ -0,0 +1,26 @@ +import React, { memo } from 'react'; +import { ImageIcon } from 'lucide-react'; +import { CheckboxButton } from '@librechat/client'; +import { useLocalize } from '~/hooks'; +import { useBadgeRowContext } from '~/Providers'; + +function ImageGeneration() { + const localize = useLocalize(); + const { imageGeneration: imageGenerationData } = useBadgeRowContext(); + const { toggleState: imageGeneration, debouncedChange, isPinned } = imageGenerationData; + + return ( + (isPinned || imageGeneration === true) && ( +