🎉 feat: Code Interpreter API and Agents Release (#4860)

* feat: Code Interpreter API & File Search Agent Uploads

chore: add back code files

wip: first pass, abstract key dialog

refactor: influence checkbox on key changes

refactor: update localization keys for 'execute code' to 'run code'

wip: run code button

refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions

feat: first pass, API tool calling

fix: handle missing toolId in callTool function and return 404 for non-existent tools

feat: show code outputs

fix: improve error handling in callTool function and log errors

fix: handle potential null value for filepath in attachment destructuring

fix: normalize language before rendering and prevent null return

fix: add loading indicator in RunCode component while executing code

feat: add support for conditional code execution in Markdown components

feat: attachments

refactor: remove bash

fix: pass abort signal to graph/run

refactor: debounce and rate limit tool call

refactor: increase debounce delay for execute function

feat: set code output attachments

feat: image attachments

refactor: apply message context

refactor: pass `partIndex`

feat: toolCall schema/model/methods

feat: block indexing

feat: get tool calls

chore: imports

chore: typing

chore: condense type imports

feat: get tool calls

fix: block indexing

chore: typing

refactor: update tool calls mapping to support multiple results

fix: add unique key to nav link for rendering

wip: first pass, tool call results

refactor: update query cache from successful tool call mutation

style: improve result switcher styling

chore: note on using \`.toObject()\`

feat: add agent_id field to conversation schema

chore: typing

refactor: rename agentMap to agentsMap for consistency

feat: Agent Name as chat input placeholder

chore: bump agents

📦 chore: update @langchain dependencies to latest versions to match agents package

📦 chore: update @librechat/agents dependency to version 1.8.0

fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label

refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads

feat: upload menu

feat: prime message_file resources

feat: implement conversation access validation in chat route

refactor: remove file parameter from processFileUpload and use req.file instead

feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db

feat: prevent duplicate message saves by checking savedMessageIds in AgentController

refactor: skip legacy RAG API handling for agents

feat: add files field to convoSchema

refactor: update request type annotations from Express.Request to ServerRequest in file processing functions

feat: track conversation files

fix: resendFiles, addPreviousAttachments handling

feat: add ID validation for session_id and file_id in download route

feat: entity_id for code file uploads/downloads

fix: code file edge cases

feat: delete related tool calls

feat: add stream rate handling for LLM configuration

feat: enhance system content with attached file information

fix: improve error logging in resource priming function

* WIP: PoC, sequential agents

WIP: PoC Sequential Agents, first pass content data + bump agents package

fix: package-lock

WIP: PoC, o1 support, refactor bufferString

feat: convertJsonSchemaToZod

fix: form issues and schema defining erroneous model

fix: max length issue on agent form instructions, limit conversation messages to sequential agents

feat: add abort signal support to createRun function and AgentClient

feat: PoC, hide prior sequential agent steps

fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data

refactor: use only last contentData, track model for usage data

chore: bump agents package

fix: content parts issue

refactor: filter contentParts to include tool calls and relevant indices

feat: show function calls

refactor: filter context messages to exclude tool calls when no tools are available to the agent

fix: ensure tool call content is not undefined in formatMessages

feat: add agent_id field to conversationPreset schema

feat: hide sequential agents

feat: increase upload toast duration to 10 seconds

* refactor: tool context handling & update Code API Key Dialog

feat: toolContextMap

chore: skipSpecs -> useSpecs

ci: fix handleTools tests

feat: API Key Dialog

* feat: Agent Permissions Admin Controls

feat: replace label with button for prompt permission toggle

feat: update agent permissions

feat: enable experimental agents and streamline capability configuration

feat: implement access control for agents and enhance endpoint menu items

feat: add welcome message for agent selection in localization

feat: add agents permission to access control and update version to 0.7.57

* fix: update types in useAssistantListMap and useMentions hooks for better null handling

* feat: mention agents

* fix: agent tool resource race conditions when deleting agent tool resource files

* feat: add error handling for code execution with user feedback

* refactor: rename AdminControls to AdminSettings for clarity

* style: add gap to button in AdminSettings for improved layout

* refactor: separate agent query hooks and check access to enable fetching

* fix: remove unused provider from agent initialization options, creates issue with custom endpoints

* refactor: remove redundant/deprecated modelOptions from AgentClient processes

* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json

* fix: minor styling issues + agent panel uniformity

* fix: agent edge cases when set endpoint is no longer defined

* refactor: remove unused cleanup function call from AppService

* fix: update link in ApiKeyDialog to point to pricing page

* fix: improve type handling and layout calculations in SidePanel component

* fix: add missing localization string for agent selection in SidePanel

* chore: form styling and localizations for upload filesearch/code interpreter

* fix: model selection placeholder logic in AgentConfig component

* style: agent capabilities

* fix: add localization for provider selection and improve dropdown styling in ModelPanel

* refactor: use gpt-4o-mini > gpt-3.5-turbo

* fix: agents configuration for loadDefaultInterface and update related tests

* feat: DALLE Agents support
This commit is contained in:
Danny Avila 2024-12-04 15:48:13 -05:00 committed by GitHub
parent affcebd48c
commit 1a815f5e19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
189 changed files with 5056 additions and 1815 deletions

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>C</title><path d="M16.5921 9.1962s-.354-3.298-3.627-3.39c-3.2741-.09-4.9552 2.474-4.9552 6.14 0 3.6651 1.858 6.5972 5.0451 6.5972 3.184 0 3.5381-3.665 3.5381-3.665l6.1041.365s.36 3.31-2.196 5.836c-2.552 2.5241-5.6901 2.9371-7.8762 2.9201-2.19-.017-5.2261.034-8.1602-2.97-2.938-3.0101-3.436-5.9302-3.436-8.8002 0-2.8701.556-6.6702 4.047-9.5502C7.444.72 9.849 0 12.254 0c10.0422 0 10.7172 9.2602 10.7172 9.2602z"/></svg>

After

Width:  |  Height:  |  Size: 496 B

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>C++</title><path d="M22.394 6c-.167-.29-.398-.543-.652-.69L12.926.22c-.509-.294-1.34-.294-1.848 0L2.26 5.31c-.508.293-.923 1.013-.923 1.6v10.18c0 .294.104.62.271.91.167.29.398.543.652.69l8.816 5.09c.508.293 1.34.293 1.848 0l8.816-5.09c.254-.147.485-.4.652-.69.167-.29.27-.616.27-.91V6.91c.003-.294-.1-.62-.268-.91zM12 19.11c-3.92 0-7.109-3.19-7.109-7.11 0-3.92 3.19-7.11 7.11-7.11a7.133 7.133 0 016.156 3.553l-3.076 1.78a3.567 3.567 0 00-3.08-1.78A3.56 3.56 0 008.444 12 3.56 3.56 0 0012 15.555a3.57 3.57 0 003.08-1.778l3.078 1.78A7.135 7.135 0 0112 19.11zm7.11-6.715h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79zm2.962 0h-.79v.79h-.79v-.79h-.79v-.79h.79v-.79h.79v.79h.79z"/></svg>

After

Width:  |  Height:  |  Size: 764 B

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Fortran</title><path d="M19.536 0H4.464A4.463 4.463 0 0 0 0 4.464v15.073A4.463 4.463 0 0 0 4.464 24h15.073A4.463 4.463 0 0 0 24 19.536V4.464A4.463 4.463 0 0 0 19.536 0zm1.193 6.493v3.871l-.922-.005c-.507-.003-.981-.021-1.052-.041-.128-.036-.131-.05-.192-.839-.079-1.013-.143-1.462-.306-2.136-.352-1.457-1.096-2.25-2.309-2.463-.509-.089-2.731-.176-4.558-.177L10.13 4.7v5.82l.662-.033c.757-.038 1.353-.129 1.64-.252.306-.131.629-.462.781-.799.158-.352.262-.815.345-1.542.033-.286.07-.572.083-.636.024-.116.028-.117 1.036-.117h1.012v9.3h-2.062l-.035-.536c-.063-.971-.252-1.891-.479-2.331-.311-.601-.922-.871-2.151-.95a11.422 11.422 0 0 1-.666-.059l-.172-.027.02 2.926c.021 3.086.03 3.206.265 3.465.241.266.381.284 2.827.368.05.002.065.246.065 1.041v1.039H3.271v-1.039c0-.954.007-1.039.091-1.041.05-.001.543-.023 1.097-.049.891-.042 1.033-.061 1.244-.167a.712.712 0 0 0 .345-.328c.106-.206.107-.254.107-6.78 0-6.133-.006-6.584-.09-6.737a.938.938 0 0 0-.553-.436c-.104-.032-.65-.07-1.215-.086l-1.026-.027V2.622h17.458v3.871z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Node.js</title><path d="M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PHP</title><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715l.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zM17.766 10.207h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Python</title><path d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Rust</title><path d="M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.49-1.0761a.3437.3437 0 00-.0274-.3361.3486.3486 0 00-.3006-.154l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.417-.4172l-1.1532.2724a14.0183 14.0183 0 00-.2278-.1873l.0415-1.1845a.3442.3442 0 00-.49-.328l-1.076.491c-.0872-.0476-.1742-.0952-.2623-.1407l-.1903-1.1673A.3483.3483 0 0016.256.955l-.9597.6905a8.4867 8.4867 0 00-.2855-.086l-.414-1.1066a.3483.3483 0 00-.5781-.1154l-.8069.8666a9.2936 9.2936 0 00-.2936-.0284L12.2946.1683a.3462.3462 0 00-.5892 0l-.6236 1.0073a13.7383 13.7383 0 00-.2936.0284L9.9803.3374a.3462.3462 0 00-.578.1154l-.4141 1.1065c-.0962.0274-.1903.0567-.2855.086L7.744.955a.3483.3483 0 00-.5447.2258L7.009 2.348a9.3574 9.3574 0 00-.2622.1407l-1.0762-.491a.3462.3462 0 00-.49.328l.0416 1.1845a7.9826 7.9826 0 00-.2278.1873L3.8413 3.425a.3472.3472 0 00-.4171.4171l.2713 1.1531c-.0628.075-.1255.1509-.1863.2268l-1.1845-.0415a.3462.3462 0 00-.328.49l.491 1.0761a9.167 9.167 0 00-.1407.2622l-1.1662.1894a.3483.3483 0 00-.2258.5446l.6904.9587a13.303 13.303 0 00-.087.2855l-1.1065.414a.3483.3483 0 00-.1155.5781l.8656.807a9.2936 9.2936 0 00-.0283.2935l-1.0073.6236a.3442.3442 0 000 .5892l1.0073.6236c.008.0982.0182.1964.0283.2936l-.8656.8079a.3462.3462 0 00.1155.578l1.1065.4141c.0273.0962.0567.1914.087.2855l-.6904.9587a.3452.3452 0 00.2268.5447l1.1662.1893c.0456.088.0922.1751.1408.2622l-.491 1.0762a.3462.3462 0 00.328.49l1.1834-.0415c.0618.0769.1235.1528.1873.2277l-.2713 1.1541a.3462.3462 0 00.4171.4161l1.153-.2713c.075.0638.151.1255.2279.1863l-.0415 1.1845a.3442.3442 0 00.49.327l1.0761-.49c.087.0486.1741.0951.2622.1407l.1903 1.1662a.3483.3483 0 00.5447.2268l.9587-.6904a9.299 9.299 0 00.2855.087l.414 1.1066a.3452.3452 0 00.5781.1154l.8079-.8656c.0972.0111.1954.0203.2936.0294l.6236 1.0073a.3472.3472 0 00.5892 0l.6236-1.0073c.0982-.0091.1964-.0183.2936-.0294l.8069.8656a.3483.3483 0 00.578-.1154l.4141-1.1066a8.4626 8.4626 0 00.2855-.087l.9587.6904a.3452.3452 0 00.5447-.2268l.1903-1.1662c.088-.0456.1751-.0931.2622-.1407l1.0762.49a.3472.3472 0 00.49-.327l-.0415-1.1845a6.7267 6.7267 0 00.2267-.1863l1.1531.2713a.3472.3472 0 00.4171-.416l-.2713-1.1542c.0628-.0749.1255-.1508.1863-.2278l1.1845.0415a.3442.3442 0 00.328-.49l-.49-1.076c.0475-.0872.0951-.1742.1407-.2623l1.1662-.1893a.3483.3483 0 00.2258-.5447l-.6904-.9587.087-.2855 1.1066-.414a.3462.3462 0 00.1154-.5781l-.8656-.8079c.0101-.0972.0202-.1954.0283-.2936l1.0073-.6236a.3442.3442 0 000-.5892zm-6.7413 8.3551a.7138.7138 0 01.2986-1.396.714.714 0 11-.2997 1.396zm-.3422-2.3142a.649.649 0 00-.7715.5l-.3573 1.6685c-1.1035.501-2.3285.7795-3.6193.7795a8.7368 8.7368 0 01-3.6951-.814l-.3574-1.6684a.648.648 0 00-.7714-.499l-1.473.3158a8.7216 8.7216 0 01-.7613-.898h7.1676c.081 0 .1356-.0141.1356-.088v-2.536c0-.074-.0536-.0881-.1356-.0881h-2.0966v-1.6077h2.2677c.2065 0 1.1065.0587 1.394 1.2088.0901.3533.2875 1.5044.4232 1.8729.1346.413.6833 1.2381 1.2685 1.2381h3.5716a.7492.7492 0 00.1296-.0131 8.7874 8.7874 0 01-.8119.9526zM6.8369 20.024a.714.714 0 11-.2997-1.396.714.714 0 01.2997 1.396zM4.1177 8.9972a.7137.7137 0 11-1.304.5791.7137.7137 0 011.304-.579zm-.8352 1.9813l1.5347-.6824a.65.65 0 00.33-.8585l-.3158-.7147h1.2432v5.6025H3.5669a8.7753 8.7753 0 01-.2834-3.348zm6.7343-.5437V8.7836h2.9601c.153 0 1.0792.1772 1.0792.8697 0 .575-.7107.7815-1.2948.7815zm10.7574 1.4862c0 .2187-.008.4363-.0243.651h-.9c-.09 0-.1265.0586-.1265.1477v.413c0 .973-.5487 1.1846-1.0296 1.2382-.4576.0517-.9648-.1913-1.0275-.4717-.2704-1.5186-.7198-1.8436-1.4305-2.4034.8817-.5599 1.799-1.386 1.799-2.4915 0-1.1936-.819-1.9458-1.3769-2.3153-.7825-.5163-1.6491-.6195-1.883-.6195H5.4682a8.7651 8.7651 0 014.907-2.7699l1.0974 1.151a.648.648 0 00.9182.0213l1.227-1.1743a8.7753 8.7753 0 016.0044 4.2762l-.8403 1.8982a.652.652 0 00.33.8585l1.6178.7188c.0283.2875.0425.577.0425.8717zm-9.3006-9.5993a.7128.7128 0 11.984 1.0316.7137.7137 0 01-.984-1.0316zm8.3389 6.71a.7107.7107 0 01.9395-.3625.7137.7137 0 11-.9405.3635z"/></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>ts-node</title><path d="M11.999 0c-.196 0-.392.05-.568.153L2.026 5.58a1.135 1.135 0 00-.568.983V17.43c0 .406.216.781.568.984l5.787 3.344v-7.344H4.748v-1.943h8.342v1.943h-3.065v8.622l1.406.812c.351.203.784.203 1.136 0l2.317-1.338a3.958 3.958 0 01-1.195-1.413l1.801-1.042c.361.59.806 1.06 1.48 1.25l2.174-1.256c-.127-.568-.698-.823-1.584-1.21l-.553-.238c-1.596-.68-2.655-1.532-2.655-3.334 0-1.658 1.265-2.922 3.24-2.922 1.406 0 2.417.49 3.144 1.77l-1.723 1.105c-.379-.68-.79-.948-1.421-.948-.648 0-1.06.41-1.06.948 0 .663.412.932 1.36 1.343l.553.237c1.336.573 2.255 1.155 2.676 2.107l.853-.493c.352-.203.568-.578.568-.984V6.565c0-.406-.216-.782-.568-.984L12.567.153A1.134 1.134 0 0011.999 0z"/></svg>

After

Width:  |  Height:  |  Size: 776 B

View file

@ -0,0 +1,34 @@
import { createContext, useContext, ReactNode, useCallback, useRef } from 'react';
type TCodeBlockContext = {
getNextIndex: (skip: boolean) => number;
resetCounter: () => void;
// codeBlocks: Map<number, string>;
};
export const CodeBlockContext = createContext<TCodeBlockContext>({} as TCodeBlockContext);
export const useCodeBlockContext = () => useContext(CodeBlockContext);
export function CodeBlockProvider({ children }: { children: ReactNode }) {
const counterRef = useRef(0);
// const codeBlocks = useRef(new Map<number, string>()).current;
const getNextIndex = useCallback((skip: boolean) => {
if (skip) {
return counterRef.current;
}
const nextIndex = counterRef.current;
counterRef.current += 1;
return nextIndex;
}, []);
const resetCounter = useCallback(() => {
counterRef.current = 0;
}, []);
return (
<CodeBlockContext.Provider value={{ getNextIndex, resetCounter }}>
{children}
</CodeBlockContext.Provider>
);
}

View file

@ -0,0 +1,9 @@
import { createContext, useContext } from 'react';
type MessageContext = {
messageId: string;
partIndex?: number;
conversationId?: string | null;
};
export const MessageContext = createContext<MessageContext>({} as MessageContext);
export const useMessageContext = () => useContext(MessageContext);

View file

@ -0,0 +1,21 @@
import { createContext, useContext } from 'react';
import useToolCallsMap from '~/hooks/Plugins/useToolCallsMap';
type ToolCallsMapContextType = ReturnType<typeof useToolCallsMap>;
export const ToolCallsMapContext = createContext<ToolCallsMapContextType>(
{} as ToolCallsMapContextType,
);
export const useToolCallsMapContext = () => useContext(ToolCallsMapContext);
interface ToolCallsMapProviderProps {
children: React.ReactNode;
conversationId: string;
}
export function ToolCallsMapProvider({ children, conversationId }: ToolCallsMapProviderProps) {
const toolCallsMap = useToolCallsMap({ conversationId });
return (
<ToolCallsMapContext.Provider value={toolCallsMap}>{children}</ToolCallsMapContext.Provider>
);
}

View file

@ -9,9 +9,12 @@ export * from './FileMapContext';
export * from './AddedChatContext';
export * from './ChatFormContext';
export * from './BookmarkContext';
export * from './MessageContext';
export * from './DashboardContext';
export * from './AssistantsContext';
export * from './AgentsContext';
export * from './AssistantsMapContext';
export * from './AnnouncerContext';
export * from './AgentsMapContext';
export * from './CodeBlockContext';
export * from './ToolCallsMapContext';

View file

@ -11,6 +11,8 @@ export type TAgentOption = OptionWithIcon &
export type TAgentCapabilities = {
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean;
[AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean;
};
export type AgentForm = {
@ -23,4 +25,5 @@ export type AgentForm = {
model_parameters: AgentModelParameters;
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
} & TAgentCapabilities;

View file

@ -1,5 +1,6 @@
export * from './a11y';
export * from './artifacts';
export * from './types';
export * from './tools';
export * from './assistants-types';
export * from './agents-types';

View file

@ -0,0 +1,6 @@
import type { AuthType } from 'librechat-data-provider';
export type ApiKeyFormData = {
apiKey: string;
authType?: string | AuthType;
};

View file

@ -1,36 +1,21 @@
import React from 'react';
import { RefObject } from 'react';
import { FileSources } from 'librechat-data-provider';
import type * as InputNumberPrimitive from 'rc-input-number';
import type { ColumnDef } from '@tanstack/react-table';
import type { SetterOrUpdater } from 'recoil';
import type {
TRole,
TUser,
Agent,
Action,
TPreset,
TPlugin,
TMessage,
Assistant,
TResPlugin,
TLoginUser,
AuthTypeEnum,
TModelsConfig,
TConversation,
TStartupConfig,
EModelEndpoint,
TEndpointsConfig,
ActionMetadata,
AssistantDocument,
AssistantsEndpoint,
TMessageContentParts,
AuthorizationTypeEnum,
TSetOption as SetOption,
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react';
export type CodeBarProps = {
lang: string;
error?: boolean;
plugin?: boolean;
blockIndex?: number;
allowExecution?: boolean;
codeRef: RefObject<HTMLElement>;
};
export enum PromptsEditorMode {
SIMPLE = 'simple',
ADVANCED = 'advanced',
@ -65,21 +50,21 @@ export type AudioChunk = {
export type AssistantListItem = {
id: string;
name: string;
metadata: Assistant['metadata'];
metadata: t.Assistant['metadata'];
model: string;
};
export type AgentListItem = {
id: string;
name: string;
avatar: Agent['avatar'];
avatar: t.Agent['avatar'];
};
export type TPluginMap = Record<string, TPlugin>;
export type TPluginMap = Record<string, t.TPlugin>;
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<EModelEndpoint, string>;
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
@ -145,11 +130,11 @@ export type FileSetter =
export type ActionAuthForm = {
/* General */
type: AuthTypeEnum;
type: t.AuthTypeEnum;
saved_auth_fields: boolean;
/* API key */
api_key: string; // not nested
authorization_type: AuthorizationTypeEnum;
authorization_type: t.AuthorizationTypeEnum;
custom_auth_header: string;
/* OAuth */
oauth_client_id: string; // not nested
@ -157,23 +142,23 @@ export type ActionAuthForm = {
authorization_url: string;
client_url: string;
scope: string;
token_exchange_method: TokenExchangeMethodEnum;
token_exchange_method: t.TokenExchangeMethodEnum;
};
export type ActionWithNullableMetadata = Omit<Action, 'metadata'> & {
metadata: ActionMetadata | null;
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
metadata: t.ActionMetadata | null;
};
export type AssistantPanelProps = {
index?: number;
action?: ActionWithNullableMetadata;
actions?: Action[];
actions?: t.Action[];
assistant_id?: string;
activePanel?: string;
endpoint: AssistantsEndpoint;
endpoint: t.AssistantsEndpoint;
version: number | string;
documentsMap: Map<string, AssistantDocument> | null;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
documentsMap: Map<string, t.AssistantDocument> | null;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
@ -182,11 +167,11 @@ export type AgentPanelProps = {
index?: number;
agent_id?: string;
activePanel?: string;
action?: Action;
actions?: Action[];
action?: t.Action;
actions?: t.Action[];
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
endpointsConfig?: TEndpointsConfig;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
};
@ -199,7 +184,7 @@ export type AgentModelPanelProps = {
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
export type TSetOption = SetOption;
export type TSetOption = t.TSetOption;
export type TSetExample = (
i: number,
@ -234,7 +219,7 @@ export type TShowToast = {
};
export type TBaseSettingsProps = {
conversation: TConversation | TPreset | null;
conversation: t.TConversation | t.TPreset | null;
className?: string;
isPreset?: boolean;
readonly?: boolean;
@ -255,7 +240,7 @@ export type TModelSelectProps = TSettingsProps & TModels;
export type TEditPresetProps = {
open: boolean;
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
preset: TPreset;
preset: t.TPreset;
title?: string;
};
@ -266,18 +251,18 @@ export type TSetOptionsPayload = {
addExample: () => void;
removeExample: () => void;
setAgentOption: TSetOption;
// getConversation: () => TConversation | TPreset | null;
// getConversation: () => t.TConversation | t.TPreset | null;
checkPluginSelection: (value: string) => boolean;
setTools: (newValue: string, remove?: boolean) => void;
setOptions?: TSetOptions;
};
export type TPresetItemProps = {
preset: TPreset;
value: TPreset;
onSelect: (preset: TPreset) => void;
onChangePreset: (preset: TPreset) => void;
onDeletePreset: (preset: TPreset) => void;
preset: t.TPreset;
value: t.TPreset;
onSelect: (preset: t.TPreset) => void;
onChangePreset: (preset: t.TPreset) => void;
onDeletePreset: (preset: t.TPreset) => void;
};
export type TOnClick = (e: React.MouseEvent<HTMLButtonElement>) => void;
@ -302,16 +287,16 @@ export type TOptions = {
isRegenerate?: boolean;
isContinued?: boolean;
isEdited?: boolean;
overrideMessages?: TMessage[];
overrideMessages?: t.TMessage[];
};
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
export type TMessageProps = {
conversation?: TConversation | null;
conversation?: t.TConversation | null;
messageId?: string | null;
message?: TMessage;
messagesTree?: TMessage[];
message?: t.TMessage;
messagesTree?: t.TMessage[];
currentEditId: string | number | null;
isSearchView?: boolean;
siblingIdx?: number;
@ -330,7 +315,7 @@ export type TInitialProps = {
};
export type TAdditionalProps = {
ask: TAskFunction;
message: TMessage;
message: t.TMessage;
isCreatedByUser: boolean;
siblingIdx: number;
enterEdit: (cancel: boolean) => void;
@ -354,7 +339,7 @@ export type TDisplayProps = TText &
export type TConfigProps = {
userKey: string;
setUserKey: React.Dispatch<React.SetStateAction<string>>;
endpoint: EModelEndpoint | string;
endpoint: t.EModelEndpoint | string;
};
export type TDangerButtonProps = {
@ -389,18 +374,18 @@ export type TResError = {
};
export type TAuthContext = {
user: TUser | undefined;
user: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
error: string | undefined;
login: (data: TLoginUser) => void;
login: (data: t.TLoginUser) => void;
logout: () => void;
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
roles?: Record<string, TRole | null | undefined>;
roles?: Record<string, t.TRole | null | undefined>;
};
export type TUserContext = {
user?: TUser | undefined;
user?: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
redirect?: string;
@ -411,16 +396,16 @@ export type TAuthConfig = {
test?: boolean;
};
export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
Pick<TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
export type IconProps = Pick<t.TMessage, 'isCreatedByUser' | 'model'> &
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
size?: number;
button?: boolean;
iconURL?: string;
message?: boolean;
className?: string;
iconClassName?: string;
endpoint?: EModelEndpoint | string | null;
endpointType?: EModelEndpoint | null;
endpoint?: t.EModelEndpoint | string | null;
endpointType?: t.EModelEndpoint | null;
assistantName?: string;
agentName?: string;
error?: boolean;
@ -440,7 +425,7 @@ export type VoiceOption = {
export type TMessageAudio = {
messageId?: string;
content?: TMessageContentParts[] | string;
content?: t.TMessageContentParts[] | string;
className?: string;
isLast: boolean;
index: number;
@ -482,12 +467,12 @@ export interface ExtendedFile {
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
export interface SwitcherProps {
endpoint?: EModelEndpoint | null;
endpoint?: t.EModelEndpoint | null;
endpointKeyProvided: boolean;
isCollapsed: boolean;
}
export type TLoginLayoutContext = {
startupConfig: TStartupConfig | null;
startupConfig: t.TStartupConfig | null;
startupConfigError: unknown;
isFetching: boolean;
error: string | null;
@ -497,34 +482,34 @@ export type TLoginLayoutContext = {
};
export type NewConversationParams = {
template?: Partial<TConversation>;
preset?: Partial<TPreset>;
modelsData?: TModelsConfig;
template?: Partial<t.TConversation>;
preset?: Partial<t.TPreset>;
modelsData?: t.TModelsConfig;
buildDefault?: boolean;
keepLatestMessage?: boolean;
keepAddedConvos?: boolean;
};
export type ConvoGenerator = (params: NewConversationParams) => void | TConversation;
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
export type TBaseResData = {
plugin?: TResPlugin;
plugin?: t.TResPlugin;
final?: boolean;
initial?: boolean;
previousMessages?: TMessage[];
conversation: TConversation;
previousMessages?: t.TMessage[];
conversation: t.TConversation;
conversationId?: string;
runMessages?: TMessage[];
runMessages?: t.TMessage[];
};
export type TResData = TBaseResData & {
requestMessage: TMessage;
responseMessage: TMessage;
requestMessage: t.TMessage;
responseMessage: t.TMessage;
};
export type TFinalResData = TBaseResData & {
requestMessage?: TMessage;
responseMessage?: TMessage;
requestMessage?: t.TMessage;
responseMessage?: t.TMessage;
};
export type TVectorStore = {

View file

@ -5,7 +5,6 @@ import { useChatContext, useAddedChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
import { mainTextareaId } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
function AddMultiConvo() {
const { conversation } = useChatContext();

View file

@ -0,0 +1,100 @@
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { EToolResources } from 'librechat-data-provider';
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
import { AttachmentIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface AttachFileProps {
isRTL: boolean;
disabled?: boolean | null;
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
setToolResource?: React.Dispatch<React.SetStateAction<string | undefined>>;
}
const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: AttachFileProps) => {
const localize = useLocalize();
const isUploadDisabled = disabled ?? false;
const inputRef = useRef<HTMLInputElement>(null);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const handleUploadClick = (isImage?: boolean) => {
if (!inputRef.current) {
return;
}
inputRef.current.value = '';
inputRef.current.accept = isImage === true ? 'image/*' : '';
inputRef.current.click();
inputRef.current.accept = '';
};
const dropdownItems = [
{
label: localize('com_ui_upload_image_input'),
onClick: () => {
setToolResource?.(undefined);
handleUploadClick(true);
},
icon: <ImageUpIcon className="icon-md" />,
},
{
label: localize('com_ui_upload_file_search'),
onClick: () => {
setToolResource?.(EToolResources.file_search);
handleUploadClick();
},
icon: <FileSearch className="icon-md" />,
},
{
label: localize('com_ui_upload_code_files'),
onClick: () => {
setToolResource?.(EToolResources.execute_code);
handleUploadClick();
},
icon: <TerminalSquareIcon className="icon-md" />,
},
];
const menuTrigger = (
<TooltipAnchor
render={
<Ariakit.MenuButton
disabled={isUploadDisabled}
id="attach-file-menu-button"
aria-label="Attach File Options"
className={cn(
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
isRTL ? 'bottom-2 right-2' : 'bottom-2 left-1 md:left-2',
)}
>
<div className="flex w-full items-center justify-center gap-2">
<AttachmentIcon />
</div>
</Ariakit.MenuButton>
}
id="attach-file-menu-button"
description={localize('com_sidepanel_attach_files')}
disabled={isUploadDisabled}
/>
);
return (
<FileUpload ref={inputRef} handleFileChange={handleFileChange}>
<div className="relative">
<DropdownPopup
menuId="attach-file-menu"
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
modal={true}
trigger={menuTrigger}
items={dropdownItems}
iconClassName="mr-0"
/>
</div>
</FileUpload>
);
};
export default React.memo(AttachFile);

View file

@ -1,12 +1,14 @@
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import {
supportsFiles,
mergeFileConfig,
isAgentsEndpoint,
EndpointFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useGetFileConfig } from '~/data-provider';
import AttachFileMenu from './AttachFileMenu';
import { useChatContext } from '~/Providers';
import { useFileHandling } from '~/hooks';
import AttachFile from './AttachFile';
@ -20,23 +22,46 @@ function FileFormWrapper({
disableInputs: boolean;
children?: React.ReactNode;
}) {
const { handleFileChange, abortUpload } = useFileHandling();
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
const { files, setFiles, conversation, setFilesLoading } = useChatContext();
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
const { handleFileChange, abortUpload, setToolResource } = useFileHandling();
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const isRTL = chatDirection === 'rtl';
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
| EndpointFileConfig
| undefined;
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
const renderAttachFile = () => {
if (isAgents) {
return (
<AttachFileMenu
isRTL={isRTL}
disabled={disableInputs}
setToolResource={setToolResource}
handleFileChange={handleFileChange}
/>
);
}
if (endpointSupportsFiles && !isUploadDisabled) {
return (
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
);
}
return null;
};
return (
<>
<FileRow
@ -50,9 +75,7 @@ function FileFormWrapper({
)}
/>
{children}
{endpointSupportsFiles && !isUploadDisabled && (
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
)}
{renderAttachFile()}
</>
);
}

View file

@ -26,8 +26,15 @@ export default function Mention({
}) {
const localize = useLocalize();
const assistantMap = useAssistantsMapContext();
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
useMentions({ assistantMap: assistantMap || {}, includeAssistants });
const {
options,
presets,
modelSpecs,
agentsList,
modelsConfig,
endpointsConfig,
assistantListMap,
} = useMentions({ assistantMap: assistantMap || {}, includeAssistants });
const { onSelectMention } = useSelectMention({
presets,
modelSpecs,
@ -62,18 +69,23 @@ export default function Mention({
}
};
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.agents) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.assistants]);
setInputOptions(agentsList ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.assistants] ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.azureAssistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants]);
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants] ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint') {
const models = (modelsConfig?.[mention.value ?? ''] ?? []).map((model) => ({
const models = (modelsConfig?.[mention.value || ''] ?? []).map((model) => ({
value: mention.value,
label: model,
type: 'model',

View file

@ -1,47 +1,57 @@
import type { FC } from 'react';
import { Close } from '@radix-ui/react-popover';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import {
EModelEndpoint,
alternateName,
PermissionTypes,
Permissions,
} from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import MenuSeparator from '../UI/MenuSeparator';
import { getEndpointField } from '~/utils';
import { useHasAccess } from '~/hooks';
import MenuItem from './MenuItem';
const EndpointItems: FC<{
endpoints: EModelEndpoint[];
endpoints: Array<EModelEndpoint | undefined>;
selected: EModelEndpoint | '';
}> = ({ endpoints, selected }) => {
}> = ({ endpoints = [], selected }) => {
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const { data: endpointsConfig } = useGetEndpointsQuery();
return (
<>
{endpoints &&
endpoints.map((endpoint, i) => {
if (!endpoint) {
return null;
} else if (!endpointsConfig?.[endpoint]) {
return null;
}
const userProvidesKey: boolean | null | undefined = getEndpointField(
endpointsConfig,
endpoint,
'userProvide',
);
return (
<Close asChild key={`endpoint-${endpoint}`}>
<div key={`endpoint-${endpoint}`}>
<MenuItem
key={`endpoint-item-${endpoint}`}
title={alternateName[endpoint] || endpoint}
value={endpoint}
selected={selected === endpoint}
data-testid={`endpoint-item-${endpoint}`}
userProvidesKey={!!userProvidesKey}
// description="With DALL·E, browsing and analysis"
/>
{i !== endpoints.length - 1 && <MenuSeparator />}
</div>
</Close>
);
})}
{endpoints.map((endpoint, i) => {
if (!endpoint) {
return null;
} else if (!endpointsConfig?.[endpoint]) {
return null;
}
if (endpoint === EModelEndpoint.agents && !hasAccessToAgents) {
return null;
}
const userProvidesKey: boolean | null | undefined =
getEndpointField(endpointsConfig, endpoint, 'userProvide') ?? false;
return (
<Close asChild key={`endpoint-${endpoint}`}>
<div key={`endpoint-${endpoint}`}>
<MenuItem
key={`endpoint-item-${endpoint}`}
title={alternateName[endpoint] || endpoint}
value={endpoint}
selected={selected === endpoint}
data-testid={`endpoint-item-${endpoint}`}
userProvidesKey={!!userProvidesKey}
// description="With DALL·E, browsing and analysis"
/>
{i !== endpoints.length - 1 && <MenuSeparator />}
</div>
</Close>
);
})}
</>
);
};

View file

@ -4,12 +4,14 @@ import { ContentTypes } from 'librechat-data-provider';
import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider';
import EditTextPart from './Parts/EditTextPart';
import { mapAttachments } from '~/utils/map';
import { MessageContext } from '~/Providers';
import store from '~/store';
import Part from './Part';
type ContentPartsProps = {
content: Array<TMessageContentParts | undefined> | undefined;
messageId: string;
conversationId?: string | null;
attachments?: TAttachment[];
isCreatedByUser: boolean;
isLast: boolean;
@ -27,6 +29,7 @@ const ContentParts = memo(
({
content,
messageId,
conversationId,
attachments,
isCreatedByUser,
isLast,
@ -79,15 +82,23 @@ const ContentParts = memo(
const attachments = attachmentMap[toolCallId];
return (
<Part
part={part}
isSubmitting={isSubmitting}
attachments={attachments}
key={`display-${messageId}-${idx}`}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
<MessageContext.Provider
key={`provider-${messageId}-${idx}`}
value={{
messageId,
conversationId,
partIndex: idx,
}}
>
<Part
part={part}
attachments={attachments}
isSubmitting={isSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>
);
})}
</>

View file

@ -1,4 +1,4 @@
import React, { memo, useMemo } from 'react';
import React, { memo, useMemo, useRef, useEffect } from 'react';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
@ -10,10 +10,10 @@ import remarkDirective from 'remark-directive';
import type { Pluggable } from 'unified';
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
import { useToastContext, CodeBlockProvider, useCodeBlockContext } from '~/Providers';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
import { useToastContext } from '~/Providers';
import store from '~/store';
type TCodeProps = {
@ -25,6 +25,32 @@ type TCodeProps = {
export const code: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMath = lang === 'math';
const isSingleLine = typeof children === 'string' && children.split('\n').length === 1;
const { getNextIndex, resetCounter } = useCodeBlockContext();
const blockIndex = useRef(getNextIndex(isMath || isSingleLine)).current;
useEffect(() => {
resetCounter();
}, [children, resetCounter]);
if (isMath) {
return children;
} else if (isSingleLine) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>
{children}
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} blockIndex={blockIndex} />;
}
});
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
if (lang === 'math') {
return children;
@ -35,7 +61,7 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} allowExecution={false} />;
}
});
@ -45,7 +71,11 @@ export const a: React.ElementType = memo(
const { showToast } = useToastContext();
const localize = useLocalize();
const { file_id, filename, filepath } = useMemo(() => {
const {
file_id = '',
filename = '',
filepath,
} = useMemo(() => {
const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
const match = href.match(pattern);
if (match && match[0]) {
@ -164,25 +194,27 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
: [supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]];
return (
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={remarkPlugins}
/* @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code,
a,
p,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
<CodeBlockProvider>
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={remarkPlugins}
/* @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code,
a,
p,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
}
}
}
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
</CodeBlockProvider>
);
});

View file

@ -6,40 +6,51 @@ import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import type { PluggableList } from 'unified';
import { code, codeNoExecution, a, p } from './Markdown';
import { CodeBlockProvider } from '~/Providers';
import { langSubset } from '~/utils';
import { code, a, p } from './Markdown';
const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
];
return (
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
const MarkdownLite = memo(
({ content = '', codeExecution = true }: { content?: string; codeExecution?: boolean }) => {
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
code,
a,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{content}
</ReactMarkdown>
);
});
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
];
return (
<CodeBlockProvider>
<ReactMarkdown
remarkPlugins={[
/** @ts-ignore */
supersub,
remarkGfm,
[remarkMath, { singleDollarTextMath: true }],
]}
/** @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code: codeExecution ? code : codeNoExecution,
a,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{content}
</ReactMarkdown>
</CodeBlockProvider>
);
},
);
export default MarkdownLite;

View file

@ -21,143 +21,130 @@ type PartProps = {
part?: TMessageContentParts;
isSubmitting: boolean;
showCursor: boolean;
messageId: string;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(
({ part, isSubmitting, attachments, showCursor, messageId, isCreatedByUser }: PartProps) => {
attachments && console.log(attachments);
if (!part) {
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
}
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
return null;
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
return null;
},
);
return null;
});
export default Part;

View file

@ -0,0 +1,19 @@
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
import Image from '~/components/Chat/Messages/Content/Image';
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
if (!attachment) {
return null;
}
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) && width != null && height != null && filepath != null;
if (isImage) {
return (
<Image altText={attachment.filename} imagePath={filepath} height={height} width={width} />
);
}
return null;
}

View file

@ -1,12 +1,11 @@
import React, { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CodeInProgress } from './CodeProgress';
import { imageExtRegex } from 'librechat-data-provider';
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Image from '~/components/Chat/Messages/Content/Image';
import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment';
import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store';
@ -86,7 +85,10 @@ export default function ExecuteCode({
</div>
{showCode && (
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
<MarkdownLite content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} />
<MarkdownLite
content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''}
codeExecution={false}
/>
{output.length > 0 && (
<div className="bg-gray-700 p-4 text-xs">
<div
@ -103,25 +105,9 @@ export default function ExecuteCode({
)}
</div>
)}
{attachments?.map((attachment, index) => {
const { width, height, filepath } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) &&
width != null &&
height != null &&
filepath != null;
if (isImage) {
return (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
}
})}
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</>
);
}

View file

@ -1,17 +1,26 @@
import { isAfter } from 'date-fns';
import React, { useMemo } from 'react';
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
import Image from '~/components/Chat/Messages/Content/Image';
import { useLocalize } from '~/hooks';
import LogLink from './LogLink';
interface LogContentProps {
output?: string;
renderImages?: boolean;
attachments?: TAttachment[];
}
const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) => {
type ImageAttachment = TFile &
TAttachmentMetadata & {
height: number;
width: number;
};
const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, attachments }) => {
const localize = useLocalize();
const processedContent = useMemo(() => {
if (!output) {
return '';
@ -21,8 +30,29 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
return parts[0].trim();
}, [output]);
const nonImageAttachments =
attachments?.filter((file) => !imageExtRegex.test(file.filename)) || [];
const { imageAttachments, nonImageAttachments } = useMemo(() => {
const imageAtts: ImageAttachment[] = [];
const nonImageAtts: TAttachment[] = [];
attachments?.forEach((attachment) => {
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) &&
width != null &&
height != null &&
filepath != null;
if (isImage) {
imageAtts.push(attachment as ImageAttachment);
} else {
nonImageAtts.push(attachment);
}
});
return {
imageAttachments: renderImages === true ? imageAtts : null,
nonImageAttachments: nonImageAtts,
};
}, [attachments, renderImages]);
const renderAttachment = (file: TAttachment) => {
const now = new Date();
@ -59,6 +89,18 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
))}
</div>
)}
{imageAttachments?.map((attachment, index) => {
const { width, height, filepath } = attachment;
return (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
})}
</>
);
};

View file

@ -2,15 +2,14 @@ import { memo, useMemo, ReactElement } from 'react';
import { useRecoilValue } from 'recoil';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Markdown from '~/components/Chat/Messages/Content/Markdown';
import { useChatContext } from '~/Providers';
import { useChatContext, useMessageContext } from '~/Providers';
import { cn } from '~/utils';
import store from '~/store';
type TextPartProps = {
text: string;
isCreatedByUser: boolean;
messageId: string;
showCursor: boolean;
isCreatedByUser: boolean;
};
type ContentType =
@ -18,7 +17,8 @@ type ContentType =
| ReactElement<React.ComponentProps<typeof MarkdownLite>>
| ReactElement;
const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => {
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
const { messageId } = useMessageContext();
const { isSubmitting, latestMessage } = useChatContext();
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);

View file

@ -1,9 +1,11 @@
import { useMemo } from 'react';
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
import * as Popover from '@radix-ui/react-popover';
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize';
import ProgressCircle from './ProgressCircle';
import InProgressCall from './InProgressCall';
import Attachment from './Parts/Attachment';
import CancelledIcon from './CancelledIcon';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
@ -18,12 +20,14 @@ export default function ToolCall({
name,
args: _args = '',
output,
attachments,
}: {
initialProgress: number;
isSubmitting: boolean;
name: string;
args: string | Record<string, unknown>;
output?: string | null;
attachments?: TAttachment[];
}) {
const localize = useLocalize();
const progress = useProgress(initialProgress);
@ -106,6 +110,9 @@ export default function ToolCall({
/>
)}
</div>
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</Popover.Root>
);
}

View file

@ -33,7 +33,7 @@ export default function ToolPopover({
<div tabIndex={-1}>
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
<div className="mb-2 text-sm font-medium dark:text-gray-100">
{domain
{domain != null && domain
? localize('com_assistants_domain_info', domain)
: localize('com_assistants_function_use', function_name)}
</div>
@ -42,7 +42,7 @@ export default function ToolPopover({
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
</div>
</div>
{output && (
{output != null && output && (
<>
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100">
{localize('com_ui_result')}

View file

@ -82,11 +82,12 @@ export default function Message(props: TMessageProps) {
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
<ContentParts
content={message.content as Array<TMessageContentParts | undefined>}
messageId={message.messageId}
isCreatedByUser={message.isCreatedByUser}
isLast={isLast}
isSubmitting={isSubmitting}
messageId={message.messageId}
isCreatedByUser={message.isCreatedByUser}
conversationId={conversation?.conversationId}
content={message.content as Array<TMessageContentParts | undefined>}
/>
</div>
</div>

View file

@ -9,6 +9,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers';
import { useMessageActions } from '~/hooks';
import { cn, logger } from '~/utils';
import store from '~/store';
@ -59,9 +60,10 @@ const MessageRender = memo(
const fontSize = useRecoilValue(store.fontSize);
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
const { isCreatedByUser, error, unfinished } = msg ?? {};
const hasNoChildren = !(msg?.children?.length ?? 0);
const isLast = useMemo(
() => !msg?.children?.length && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
[msg?.children, msg?.depth, latestMessage?.depth],
() => hasNoChildren && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
[hasNoChildren, msg?.depth, latestMessage?.depth],
);
if (!msg) {
@ -122,24 +124,31 @@ const MessageRender = memo(
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
isLast={isLast}
text={msg.text || ''}
message={msg}
enterEdit={enterEdit}
error={!!(error ?? false)}
isSubmitting={isSubmitting}
unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
<MessageContext.Provider
value={{
messageId: msg.messageId,
conversationId: conversation?.conversationId,
}}
>
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
isLast={isLast}
text={msg.text || ''}
message={msg}
enterEdit={enterEdit}
error={!!(error ?? false)}
isSubmitting={isSubmitting}
unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
</MessageContext.Provider>
</div>
</div>
{!msg.children?.length && (isSubmittingFamily === true || isSubmitting) ? (
{hasNoChildren && (isSubmittingFamily === true || isSubmitting) ? (
<PlaceholderRow isCard={isCard} />
) : (
<SubRow classes="text-xs">

View file

@ -28,7 +28,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
createPresetMutation.mutate(_preset, {
onSuccess: () => {
showToast({
message: `${toastTitle} ${localize('com_endpoint_preset_saved')}`,
message: `${toastTitle} ${localize('com_ui_saved')}`,
});
onOpenChange(false); // Close the dialog on success
},

View file

@ -1,81 +1,133 @@
import copy from 'copy-to-clipboard';
import { InfoIcon } from 'lucide-react';
import React, { useRef, useState, RefObject } from 'react';
import { Tools } from 'librechat-data-provider';
import React, { useRef, useState, useMemo, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
import LogContent from '~/components/Chat/Messages/Content/Parts/LogContent';
import ResultSwitcher from '~/components/Messages/Content/ResultSwitcher';
import { useToolCallsMapContext, useMessageContext } from '~/Providers';
import RunCode from '~/components/Messages/Content/RunCode';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
import useLocalize from '~/hooks/useLocalize';
import cn from '~/utils/cn';
type CodeBarProps = {
lang: string;
codeRef: RefObject<HTMLElement>;
plugin?: boolean;
error?: boolean;
};
type CodeBlockProps = Pick<CodeBarProps, 'lang' | 'plugin' | 'error'> & {
type CodeBlockProps = Pick<
CodeBarProps,
'lang' | 'plugin' | 'error' | 'allowExecution' | 'blockIndex'
> & {
codeChildren: React.ReactNode;
classProp?: string;
};
const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plugin = null }) => {
const localize = useLocalize();
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
{plugin === true ? (
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
) : (
<button
type="button"
className={cn(
'ml-auto flex gap-2',
error === true ? 'h-4 w-4 items-start text-white/50' : '',
)}
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
setIsCopied(true);
copy(codeString.trim(), { format: 'text/plain' });
const CodeBar: React.FC<CodeBarProps> = React.memo(
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => {
const localize = useLocalize();
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
{plugin === true ? (
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
) : (
<div className="flex items-center justify-center gap-4">
{allowExecution === true && (
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
)}
<button
type="button"
className={cn(
'ml-auto flex gap-2',
error === true ? 'h-4 w-4 items-start text-white/50' : '',
)}
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
setIsCopied(true);
copy(codeString.trim(), { format: 'text/plain' });
setTimeout(() => {
setIsCopied(false);
}, 3000);
}
}}
>
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{error === true ? '' : localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{error === true ? '' : localize('com_ui_copy_code')}
</>
)}
</button>
)}
</div>
);
});
setTimeout(() => {
setIsCopied(false);
}, 3000);
}
}}
>
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{error === true ? '' : localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{error === true ? '' : localize('com_ui_copy_code')}
</>
)}
</button>
</div>
)}
</div>
);
},
);
const CodeBlock: React.FC<CodeBlockProps> = ({
lang,
blockIndex,
codeChildren,
classProp = '',
allowExecution = true,
plugin = null,
error,
}) => {
const codeRef = useRef<HTMLElement>(null);
const toolCallsMap = useToolCallsMapContext();
const { messageId, partIndex } = useMessageContext();
const key = allowExecution
? `${messageId}_${partIndex ?? 0}_${blockIndex ?? 0}_${Tools.execute_code}`
: '';
const [currentIndex, setCurrentIndex] = useState(0);
const fetchedToolCalls = toolCallsMap?.[key];
const [toolCalls, setToolCalls] = useState(toolCallsMap?.[key] ?? null);
useEffect(() => {
if (fetchedToolCalls) {
setToolCalls(fetchedToolCalls);
setCurrentIndex(fetchedToolCalls.length - 1);
}
}, [fetchedToolCalls]);
const currentToolCall = useMemo(() => toolCalls?.[currentIndex], [toolCalls, currentIndex]);
const next = () => {
if (!toolCalls) {
return;
}
if (currentIndex < toolCalls.length - 1) {
setCurrentIndex(currentIndex + 1);
}
};
const previous = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
};
const isNonCode = !!(plugin === true || error === true);
const language = isNonCode ? 'json' : lang;
return (
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
<CodeBar lang={lang} codeRef={codeRef} plugin={plugin === true} error={error} />
<CodeBar
lang={lang}
error={error}
codeRef={codeRef}
blockIndex={blockIndex}
plugin={plugin === true}
allowExecution={allowExecution}
/>
<div className={cn(classProp, 'overflow-y-auto p-4')}>
<code
ref={codeRef}
@ -86,6 +138,34 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
{codeChildren}
</code>
</div>
{allowExecution === true && toolCalls && toolCalls.length > 0 && (
<>
<div className="bg-gray-700 p-4 text-xs">
<div
className="prose flex flex-col-reverse text-white"
style={{
color: 'white',
}}
>
<pre className="shrink-0">
<LogContent
output={(currentToolCall?.result as string | undefined) ?? ''}
attachments={currentToolCall?.attachments ?? []}
renderImages={true}
/>
</pre>
</div>
</div>
{toolCalls.length > 1 && (
<ResultSwitcher
currentIndex={currentIndex}
totalCount={toolCalls.length}
onPrevious={previous}
onNext={next}
/>
)}
</>
)}
</div>
);
};

View file

@ -0,0 +1,69 @@
interface ResultSwitcherProps {
currentIndex: number;
totalCount: number;
onPrevious: () => void;
onNext: () => void;
}
const ResultSwitcher: React.FC<ResultSwitcherProps> = ({
currentIndex,
totalCount,
onPrevious,
onNext,
}) => {
if (totalCount <= 1) {
return null;
}
return (
<div className="flex items-center justify-start gap-1 self-center bg-gray-700 pb-2 text-xs">
<button
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
type="button"
onClick={onPrevious}
disabled={currentIndex === 0}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<span className="flex-shrink-0 tabular-nums">
{currentIndex + 1} / {totalCount}
</span>
<button
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
type="button"
onClick={onNext}
disabled={currentIndex === totalCount - 1}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
</div>
);
};
export default ResultSwitcher;

View file

@ -0,0 +1,109 @@
import debounce from 'lodash/debounce';
import { Tools, AuthType } from 'librechat-data-provider';
import { TerminalSquareIcon, Loader } from 'lucide-react';
import React, { useMemo, useCallback, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
import { useVerifyAgentToolAuth, useToolCallMutation } from '~/data-provider';
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { useMessageContext } from '~/Providers';
import { cn, normalizeLanguage } from '~/utils';
import { useToastContext } from '~/Providers';
const RunCode: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, blockIndex }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const execute = useToolCallMutation(Tools.execute_code, {
onError: () => {
showToast({ message: localize('com_ui_run_code_error'), status: 'error' });
},
});
const { messageId, conversationId, partIndex } = useMessageContext();
const normalizedLang = useMemo(() => normalizeLanguage(lang), [lang]);
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
const authType = useMemo(() => data?.message ?? false, [data?.message]);
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
useCodeApiKeyForm({});
const handleExecute = useCallback(async () => {
if (!isAuthenticated) {
setIsDialogOpen(true);
return;
}
const codeString: string = codeRef.current?.textContent ?? '';
if (
typeof codeString !== 'string' ||
codeString.length === 0 ||
typeof normalizedLang !== 'string' ||
normalizedLang.length === 0
) {
return;
}
execute.mutate({
partIndex,
messageId,
blockIndex,
conversationId: conversationId ?? '',
lang: normalizedLang,
code: codeString,
});
}, [
codeRef,
execute,
partIndex,
messageId,
blockIndex,
conversationId,
normalizedLang,
setIsDialogOpen,
isAuthenticated,
]);
const debouncedExecute = useMemo(
() => debounce(handleExecute, 1000, { leading: true }),
[handleExecute],
);
useEffect(() => {
return () => {
debouncedExecute.cancel();
};
}, [debouncedExecute]);
if (typeof normalizedLang !== 'string' || normalizedLang.length === 0) {
return null;
}
return (
<>
<button
type="button"
className={cn('ml-auto flex gap-2')}
onClick={debouncedExecute}
disabled={execute.isLoading}
>
{execute.isLoading ? (
<Loader className="animate-spin" size={18} />
) : (
<TerminalSquareIcon size={18} />
)}
{localize('com_ui_run_code')}
</button>
<ApiKeyDialog
onSubmit={onSubmit}
isOpen={isDialogOpen}
register={methods.register}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
handleSubmit={methods.handleSubmit}
isToolAuthenticated={isAuthenticated}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
});
export default RunCode;

View file

@ -129,16 +129,17 @@ const ContentRender = memo(
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
<ContentParts
content={msg.content as Array<TMessageContentParts | undefined>}
messageId={msg.messageId}
isCreatedByUser={msg.isCreatedByUser}
isLast={isLast}
isSubmitting={isSubmitting}
edit={edit}
isLast={isLast}
enterEdit={enterEdit}
siblingIdx={siblingIdx}
messageId={msg.messageId}
isSubmitting={isSubmitting}
setSiblingIdx={setSiblingIdx}
attachments={msg.attachments}
isCreatedByUser={msg.isCreatedByUser}
conversationId={conversation?.conversationId}
content={msg.content as Array<TMessageContentParts | undefined>}
/>
</div>
</div>

View file

@ -29,17 +29,19 @@ const LabelController: React.FC<LabelControllerProps> = ({
setValue,
}) => (
<div className="mb-4 flex items-center justify-between gap-2">
<label
<button
className="cursor-pointer select-none"
htmlFor={promptPerm}
type="button"
// htmlFor={promptPerm}
onClick={() =>
setValue(promptPerm, !getValues(promptPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
{label}
</label>
</button>
<Controller
name={promptPerm}
control={control}
@ -48,7 +50,7 @@ const LabelController: React.FC<LabelControllerProps> = ({
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>
@ -61,7 +63,7 @@ const AdminSettings = () => {
const { showToast } = useToastContext();
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_endpoint_preset_saved') });
showToast({ status: 'success', message: localize('com_ui_saved') });
},
onError: () => {
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });

View file

@ -14,9 +14,9 @@ import {
replaceSpecialVars,
extractVariableInfo,
} from '~/utils';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
import { TextareaAutosize, InputCombobox } from '~/components/ui';
import { code } from '~/components/Chat/Messages/Content/Markdown';
type FieldType = 'text' | 'select';
@ -143,12 +143,16 @@ export default function VariableForm({
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-gray-100 p-4 text-text-secondary dark:bg-gray-700/50 sm:max-w-full md:max-h-80">
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}
components={{ code }}
/** @ts-ignore */
components={{ code: codeNoExecution }}
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
>
{generateHighlightedMarkdown()}

View file

@ -6,7 +6,7 @@ import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import rehypeHighlight from 'rehype-highlight';
import type { TPromptGroup } from 'librechat-data-provider';
import { code } from '~/components/Chat/Messages/Content/Markdown';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import { useLocalize, useAuthContext } from '~/hooks';
import CategoryIcon from './Groups/CategoryIcon';
import PromptVariables from './PromptVariables';
@ -50,12 +50,20 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
</h2>
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600 sm:max-w-full">
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
remarkPlugins={[
/** @ts-ignore */
supersub,
remarkGfm,
[remarkMath, { singleDollarTextMath: true }],
]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}
components={{ p: PromptVariableGfm, code }}
/** @ts-ignore */
components={{ p: PromptVariableGfm, code: codeNoExecution }}
className="prose dark:prose-invert light dark:text-gray-70 my-1"
>
{mainText}

View file

@ -9,8 +9,8 @@ import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
import { code } from '~/components/Chat/Messages/Content/Markdown';
import { SaveIcon, CrossIcon } from '~/components/svg';
import { TextareaAutosize } from '~/components/ui';
import { PromptVariableGfm } from './Markdown';
@ -75,7 +75,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
role="button"
className={cn(
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
{ 'bg-surface-secondary-alt cursor-pointer hover:bg-surface-tertiary': !isEditing },
{ 'cursor-pointer bg-surface-secondary-alt hover:bg-surface-tertiary': !isEditing },
)}
onClick={() => !isEditing && setIsEditing(true)}
onKeyDown={(e) => {
@ -107,9 +107,12 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
/>
) : (
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
/** @ts-ignore */
rehypePlugins={rehypePlugins}
components={{ p: PromptVariableGfm, code }}
/** @ts-ignore */
components={{ p: PromptVariableGfm, code: codeNoExecution }}
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
>
{field.value}

View file

@ -53,6 +53,7 @@ const PromptVariables = ({
) : (
<div className="flex h-7 items-center">
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_variables_info')}
</ReactMarkdown>
@ -68,6 +69,7 @@ const PromptVariables = ({
</span>
{'\u00A0'}
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_special_variables_info')}
</ReactMarkdown>
@ -79,6 +81,7 @@ const PromptVariables = ({
</span>
{'\u00A0'}
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_dropdown_variables_info')}
</ReactMarkdown>

View file

@ -6,6 +6,7 @@ import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers';
// eslint-disable-next-line import/no-cycle
import MultiMessage from './MultiMessage';
import { cn } from '~/utils';
@ -31,10 +32,10 @@ export default function Message(props: TMessageProps) {
const {
text = '',
children,
messageId = null,
isCreatedByUser = true,
error = false,
messageId = '',
unfinished = false,
isCreatedByUser = true,
} = message;
let messageLabel = '';
@ -64,26 +65,33 @@ export default function Message(props: TMessageProps) {
<div className={cn('select-none font-semibold', fontSize)}>{messageLabel}</div>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{/* Legacy Plugins */}
{message.plugin && <Plugin plugin={message.plugin} />}
{message.content ? (
<SearchContent message={message} />
) : (
<MessageContent
edit={false}
error={error}
isLast={false}
ask={() => ({})}
text={text}
message={message}
isSubmitting={false}
enterEdit={() => ({})}
unfinished={!!unfinished}
isCreatedByUser={isCreatedByUser}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
)}
<MessageContext.Provider
value={{
messageId,
conversationId: conversation?.conversationId,
}}
>
{/* Legacy Plugins */}
{message.plugin && <Plugin plugin={message.plugin} />}
{message.content ? (
<SearchContent message={message} />
) : (
<MessageContent
edit={false}
error={error}
isLast={false}
ask={() => ({})}
text={text || ''}
message={message}
isSubmitting={false}
enterEdit={() => ({})}
unfinished={unfinished}
siblingIdx={siblingIdx ?? 0}
isCreatedByUser={isCreatedByUser}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
)}
</MessageContext.Provider>
</div>
</div>
<SubRow classes="text-xs">

View file

@ -0,0 +1,163 @@
import { useMemo, useEffect } from 'react';
import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui';
import { useUpdateAgentPermissionsMutation } from '~/data-provider';
import { useLocalize, useAuthContext } from '~/hooks';
import { Button, Switch } from '~/components/ui';
import { useToastContext } from '~/Providers';
type FormValues = Record<Permissions, boolean>;
type LabelControllerProps = {
label: string;
agentPerm: Permissions;
control: Control<FormValues, unknown, FormValues>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
};
const defaultValues = roleDefaults[SystemRoles.USER];
const LabelController: React.FC<LabelControllerProps> = ({
control,
agentPerm,
label,
getValues,
setValue,
}) => (
<div className="mb-4 flex items-center justify-between gap-2">
<button
className="cursor-pointer select-none"
type="button"
onClick={() =>
setValue(agentPerm, !getValues(agentPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
{label}
</button>
<Controller
name={agentPerm}
control={control}
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
/>
)}
/>
</div>
);
const AdminSettings = () => {
const localize = useLocalize();
const { user, roles } = useAuthContext();
const { showToast } = useToastContext();
const { mutate, isLoading } = useUpdateAgentPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') });
},
onError: () => {
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });
},
});
const {
reset,
control,
setValue,
getValues,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: useMemo(() => {
if (roles?.[SystemRoles.USER]) {
return roles[SystemRoles.USER][PermissionTypes.AGENTS];
}
return defaultValues[PermissionTypes.AGENTS];
}, [roles]),
});
useEffect(() => {
if (roles?.[SystemRoles.USER]?.[PermissionTypes.AGENTS]) {
reset(roles[SystemRoles.USER][PermissionTypes.AGENTS]);
}
}, [roles, reset]);
if (user?.role !== SystemRoles.ADMIN) {
return null;
}
const labelControllerData = [
{
agentPerm: Permissions.SHARED_GLOBAL,
label: localize('com_ui_agents_allow_share_global'),
},
{
agentPerm: Permissions.USE,
label: localize('com_ui_agents_allow_use'),
},
{
agentPerm: Permissions.CREATE,
label: localize('com_ui_agents_allow_create'),
},
];
const onSubmit = (data: FormValues) => {
mutate({ roleName: SystemRoles.USER, updates: data });
};
return (
<OGDialog>
<OGDialogTrigger asChild>
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative my-1 h-9 w-full rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" />
{localize('com_ui_admin_settings')}
</Button>
</OGDialogTrigger>
<OGDialogContent className="w-1/4 bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300">
<OGDialogTitle>{`${localize('com_ui_admin_settings')} - ${localize(
'com_ui_agents',
)}`}</OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)}>
<div className="py-5">
{labelControllerData.map(({ agentPerm, label }) => (
<LabelController
key={agentPerm}
control={control}
agentPerm={agentPerm}
label={label}
getValues={getValues}
setValue={setValue}
/>
))}
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={isSubmitting || isLoading}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>
{localize('com_ui_save')}
</button>
</div>
</form>
</OGDialogContent>
</OGDialog>
);
};
export default AdminSettings;

View file

@ -1,24 +1,32 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } from 'librechat-data-provider';
import {
QueryKeys,
SystemRoles,
Permissions,
EModelEndpoint,
PermissionTypes,
AgentCapabilities,
} from 'librechat-data-provider';
import type { TConfig, TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import { useLocalize, useAuthContext } from '~/hooks';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import { Spinner } from '~/components/svg';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import AgentTool from './AgentTool';
// import CodeForm from './Code/Form';
import CodeForm from './Code/Form';
import { Panel } from '~/common';
const labelClass = 'mb-2 text-token-text-primary block font-medium';
@ -55,6 +63,11 @@ export default function AgentConfig({
const tools = useWatch({ control, name: 'tools' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const toolsEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(AgentCapabilities.tools),
[agentsConfig],
@ -263,7 +276,7 @@ export default function AgentConfig({
/>
</div>
{/* Instructions */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass} htmlFor="instructions">
{localize('com_ui_instructions')}
</label>
@ -275,7 +288,7 @@ export default function AgentConfig({
<textarea
{...field}
value={field.value ?? ''}
maxLength={32768}
// maxLength={32768}
className={cn(inputClass, 'min-h-[100px] resize-y')}
id="instructions"
placeholder={localize('com_agents_instructions_placeholder')}
@ -297,7 +310,7 @@ export default function AgentConfig({
/>
</div>
{/* Model and Provider */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass} htmlFor="provider">
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
@ -319,16 +332,23 @@ export default function AgentConfig({
/>
</div>
)}
<span>{model != null ? model : localize('com_ui_select_model')}</span>
<span>{model != null && model ? model : localize('com_ui_select_model')}</span>
</div>
</button>
</div>
{/* Code Execution */}
{/* {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} */}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{(codeEnabled || fileSearchEnabled) && (
<div className="mb-4 flex w-full flex-col items-start gap-3">
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
{/* Code Execution */}
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
</div>
)}
{/* Agent Tools & Actions */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
@ -360,7 +380,7 @@ export default function AgentConfig({
<button
type="button"
onClick={() => setShowToolDialog(true)}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
aria-haspopup="dialog"
>
<div className="flex w-full items-center justify-center gap-2">
@ -373,7 +393,7 @@ export default function AgentConfig({
type="button"
disabled={!agent_id}
onClick={handleAddActions}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
aria-haspopup="dialog"
>
<div className="flex w-full items-center justify-center gap-2">
@ -384,6 +404,7 @@ export default function AgentConfig({
</div>
</div>
</div>
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
@ -391,7 +412,8 @@ export default function AgentConfig({
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
@ -401,7 +423,7 @@ export default function AgentConfig({
)}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={create.isLoading || update.isLoading}
aria-busy={create.isLoading || update.isLoading}

View file

@ -126,6 +126,9 @@ export default function AgentPanel({
model: _model,
model_parameters,
provider: _provider,
agent_ids,
end_after_tools,
hide_sequential_outputs,
} = data;
const model = _model ?? '';
@ -143,6 +146,9 @@ export default function AgentPanel({
tools,
provider,
model_parameters,
agent_ids,
end_after_tools,
hide_sequential_outputs,
},
});
return;
@ -163,6 +169,9 @@ export default function AgentPanel({
tools,
provider,
model_parameters,
agent_ids,
end_after_tools,
hide_sequential_outputs,
});
},
[agent_id, create, update, showToast, localize],

View file

@ -58,6 +58,8 @@ export default function AgentSelect({
const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
[AgentCapabilities.end_after_tools]: false,
[AgentCapabilities.hide_sequential_outputs]: false,
};
const agentTools: string[] = [];

View file

@ -1,38 +1,39 @@
import { useState } from 'react';
import { KeyRoundIcon } from 'lucide-react';
import { AuthType, AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller, useForm, useWatch } from 'react-hook-form';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Input,
OGDialog,
Checkbox,
HoverCard,
HoverCardContent,
HoverCardPortal,
HoverCardTrigger,
Button,
} from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useAuthCodeTool } from '~/hooks';
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { CircleHelpIcon } from '~/components/svg';
import ApiKeyDialog from './ApiKeyDialog';
import { ESide } from '~/common';
type ApiKeyFormData = {
apiKey: string;
authType?: string | AuthType;
};
export default function Action({ authType = '', isToolAuthenticated = false }) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
const [isDialogOpen, setIsDialogOpen] = useState(false);
const {
onSubmit,
isDialogOpen,
setIsDialogOpen,
handleRevokeApiKey,
methods: keyFormMethods,
} = useCodeApiKeyForm({
onSubmit: () => {
setValue(AgentCapabilities.execute_code, true, { shouldDirty: true });
},
onRevoke: () => {
setValue(AgentCapabilities.execute_code, false, { shouldDirty: true });
},
});
const runCodeIsEnabled = useWatch({ control, name: AgentCapabilities.execute_code });
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
const { reset, register, handleSubmit } = useForm<ApiKeyFormData>();
const isUserProvided = authType === AuthType.USER_PROVIDED;
const handleCheckboxChange = (checked: boolean) => {
@ -45,18 +46,6 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
}
};
const onSubmit = (data: { apiKey: string }) => {
reset();
installTool(data.apiKey);
setIsDialogOpen(false);
};
const handleRevokeApiKey = () => {
reset();
removeTool();
setIsDialogOpen(false);
};
return (
<>
<HoverCard openDelay={50}>
@ -87,7 +76,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.execute_code}
>
{localize('com_agents_execute_code')}
{localize('com_ui_run_code')}
</label>
</button>
<div className="ml-2 flex gap-2">
@ -104,48 +93,23 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{/* // TODO: add a Code Interpreter description */}
{localize('com_agents_code_interpreter')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
<OGDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<OGDialogTemplate
className="w-11/12 sm:w-1/4"
title={localize('com_agents_tool_not_authenticated')}
main={
<form onSubmit={handleSubmit(onSubmit)}>
<Input
type="password"
placeholder="Enter API Key"
autoComplete="one-time-code"
readOnly={true}
onFocus={(e) => (e.target.readOnly = false)}
{...register('apiKey', { required: true })}
/>
</form>
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isUserProvided &&
isToolAuthenticated && (
<Button
onClick={handleRevokeApiKey}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{localize('com_ui_revoke')}
</Button>
)
}
showCancelButton={true}
/>
</OGDialog>
<ApiKeyDialog
isOpen={isDialogOpen}
onSubmit={onSubmit}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
register={keyFormMethods.register}
isToolAuthenticated={isToolAuthenticated}
handleSubmit={keyFormMethods.handleSubmit}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
}

View file

@ -0,0 +1,106 @@
import type { UseFormRegister, UseFormHandleSubmit } from 'react-hook-form';
import type { ApiKeyFormData } from '~/common';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { Input, Button, OGDialog } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function ApiKeyDialog({
isOpen,
onSubmit,
onRevoke,
onOpenChange,
isUserProvided,
isToolAuthenticated,
register,
handleSubmit,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: { apiKey: string }) => void;
onRevoke: () => void;
isUserProvided: boolean;
isToolAuthenticated: boolean;
register: UseFormRegister<ApiKeyFormData>;
handleSubmit: UseFormHandleSubmit<ApiKeyFormData>;
}) {
const localize = useLocalize();
const languageIcons = [
'python.svg',
'nodedotjs.svg',
'tsnode.svg',
'rust.svg',
'go.svg',
'c.svg',
'cplusplus.svg',
'php.svg',
'fortran.svg',
];
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogTemplate
className="w-11/12 sm:w-[450px]"
title=""
main={
<>
<div className="mb-4 text-center font-medium">
{localize('com_ui_librechat_code_api_title')}
</div>
<div className="mb-4 text-center text-sm">
{localize('com_ui_librechat_code_api_subtitle')}
</div>
{/* Language Icons Stack */}
<div className="mb-6">
<div className="mx-auto mb-4 flex max-w-[400px] flex-wrap justify-center gap-3">
{languageIcons.map((icon) => (
<div key={icon} className="h-6 w-6">
<img
src={`/assets/${icon}`}
alt=""
className="h-full w-full object-contain opacity-[0.85] dark:invert"
/>
</div>
))}
</div>
<a
href="https://code.librechat.ai/pricing"
target="_blank"
rel="noopener noreferrer"
className="block text-center text-[15px] font-medium text-blue-500 underline decoration-1 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
>
{localize('com_ui_librechat_code_api_key')}
</a>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<Input
type="password"
placeholder={localize('com_ui_enter_api_key')}
autoComplete="one-time-code"
readOnly={true}
onFocus={(e) => (e.target.readOnly = false)}
{...register('apiKey', { required: true })}
/>
</form>
</>
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isUserProvided &&
isToolAuthenticated && (
<Button
onClick={onRevoke}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{localize('com_ui_revoke')}
</Button>
)
}
showCancelButton={true}
/>
</OGDialog>
);
}

View file

@ -12,6 +12,7 @@ import type { ExtendedFile, AgentForm } from '~/common';
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
const tool_resource = EToolResources.execute_code;
@ -68,8 +69,8 @@ export default function Files({
return (
<div className="mb-2 w-full">
<div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs">
<div className="flex flex-col gap-3">
<div className="rounded-lg text-xs text-text-secondary">
{localize('com_assistants_code_interpreter_files')}
</div>
<FileRow
@ -85,10 +86,10 @@ export default function Files({
<button
type="button"
disabled={!agent_id || codeChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">
<div className="flex w-full items-center justify-center gap-1">
<input
multiple={true}
type="file"
@ -98,7 +99,8 @@ export default function Files({
disabled={!agent_id || codeChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
{localize('com_ui_upload_code_files')}
</div>
</button>
</div>

View file

@ -16,13 +16,18 @@ export default function CodeForm({
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
return (
<div className="mb-4">
<div className="mb-1.5 flex items-center">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
</span>
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<div className="flex flex-row items-center gap-1">
<div className="flex items-center gap-1">
<span className="text-token-text-primary block font-medium">
{localize('com_agents_code_interpreter_title')}
</span>
<span className="text-xs text-text-secondary">
{localize('com_agents_by_librechat')}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-start gap-2">
<Action authType={data?.message} isToolAuthenticated={data?.authenticated} />

View file

@ -67,7 +67,7 @@ export default function FileSearch({
};
return (
<div className="mb-6">
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
@ -76,12 +76,12 @@ export default function FileSearch({
</span>
</div>
<FileSearchCheckbox />
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
<div>
<button
type="button"
disabled={!agent_id || fileSearchChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-1">
@ -95,13 +95,13 @@ export default function FileSearch({
disabled={!agent_id || fileSearchChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
{localize('com_ui_upload_file_search')}
</div>
</button>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-sm text-text-secondary">
<div className="text-xs text-text-secondary">
{localize('com_agents_file_search_disabled')}
</div>
)}

View file

@ -31,14 +31,17 @@ export default function Parameters({
: (providerOption as StringOption | undefined)?.value;
return value ?? '';
}, [providerOption]);
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
const models = useMemo(
() => (provider ? modelsData[provider] ?? [] : []),
[modelsData, provider],
);
useEffect(() => {
const _model = model ?? '';
if (provider && _model) {
const modelExists = models.includes(_model);
if (!modelExists) {
const newModels = modelsData[provider];
const newModels = modelsData[provider] ?? [];
setValue('model', newModels[0] ?? '');
}
}
@ -105,14 +108,16 @@ export default function Parameters({
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
title={localize('com_ui_provider')}
placeholder={localize('com_ui_select_provider')}
searchPlaceholder={localize('com_ui_select_search_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
'flex h-9 w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
(field.value === undefined || field.value === '') &&
'border-2 border-yellow-400',
)}

View file

@ -0,0 +1,74 @@
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Checkbox,
HoverCard,
// HoverCardContent,
// HoverCardPortal,
// HoverCardTrigger,
} from '~/components/ui';
// import { CircleHelpIcon } from '~/components/svg';
// import { useLocalize } from '~/hooks';
// import { ESide } from '~/common';
export default function HideSequential() {
// const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
return (
<>
<HoverCard openDelay={50}>
<div className="my-2 flex items-center">
<Controller
name={AgentCapabilities.hide_sequential_outputs}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value?.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),
{
shouldDirty: true,
},
)
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.hide_sequential_outputs}
>
Hide Sequential Agent Outputs except the last agent&apos;s
</label>
{/* <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger> */}
</button>
{/* <HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_ttg_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal> */}
</div>
</HoverCard>
</>
);
}

View file

@ -0,0 +1,153 @@
import { Plus, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import HideSequential from './HideSequential';
interface SequentialAgentsProps {
field: {
value: string[];
onChange: (value: string[]) => void;
};
}
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
removeFocusOutlines,
);
const maxAgents = 5;
const SequentialAgents: React.FC<SequentialAgentsProps> = ({ field }) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const nodeRef = useRef(null);
const [newAgentId, setNewAgentId] = useState('');
const handleAddAgentId = () => {
if (newAgentId.trim() && field.value.length < maxAgents) {
const newValues = [...field.value, newAgentId];
field.onChange(newValues);
setNewAgentId('');
}
};
const handleDeleteAgentId = (index: number) => {
const newValues = field.value.filter((_, i) => i !== index);
field.onChange(newValues);
};
const defaultStyle = {
transition: 'opacity 200ms ease-in-out',
opacity: 0,
};
const triggerShake = (element: HTMLElement) => {
element.classList.remove('shake');
void element.offsetWidth;
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 200);
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
return (
<div className="relative">
<label className={labelClass} htmlFor="agent_ids">
Sequential Agents
</label>
<div className="mt-4 space-y-2">
<HideSequential />
{/* Display existing agents first */}
{field.value.map((agentId, index) => (
<div key={index} className="relative">
<input
ref={(el) => (inputRefs.current[index] = el)}
value={agentId}
onChange={(e) => {
const newValue = [...field.value];
newValue[index] = e.target.value;
field.onChange(newValue);
}}
className={`${inputClass} pr-10`}
type="text"
maxLength={64}
/>
<TooltipAnchor
side="top"
description={'Remove agent ID'}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteAgentId(index)}
>
<X className="size-4" />
</TooltipAnchor>
</div>
))}
{/* Input for new agent at the bottom */}
<div className="relative">
<input
ref={(el) => (inputRefs.current[field.value.length] = el)}
value={newAgentId}
maxLength={64}
className={`${inputClass} pr-10`}
type="text"
placeholder={hasReachedMax ? 'Max agents reached' : 'Enter agent ID (e.g. agent_1234)'}
onChange={(e) => setNewAgentId(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (hasReachedMax) {
triggerShake(e.currentTarget);
} else {
handleAddAgentId();
}
}
}}
/>
<Transition
nodeRef={nodeRef}
in={field.value.length < Constants.MAX_CONVO_STARTERS}
timeout={200}
unmountOnExit
>
{(state: string) => (
<div
ref={nodeRef}
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
transition: state === 'entering' ? 'none' : defaultStyle.transition,
}}
className="absolute right-1 top-1"
>
<TooltipAnchor
side="top"
description={hasReachedMax ? 'Max agents reached' : 'Add agent ID'}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddAgentId}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</TooltipAnchor>
</div>
)}
</Transition>
</div>
</div>
</div>
);
};
export default SequentialAgents;

View file

@ -76,7 +76,7 @@ export default function CodeFiles({
<button
type="button"
disabled={!assistant_id}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">

View file

@ -32,6 +32,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
<TooltipAnchor
description={localize(link.title)}
side="left"
key={`nav-link-${index}`}
render={
<Button
variant="ghost"

View file

@ -6,8 +6,8 @@ import {
useGetStartupConfig,
useUserKeyQuery,
} from 'librechat-data-provider/react-query';
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import type { TEndpointsConfig } from 'librechat-data-provider';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
@ -65,7 +65,7 @@ const SidePanel = ({
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
[startupConfig],
);
@ -117,17 +117,17 @@ const SidePanel = ({
});
const calculateLayout = useCallback(() => {
if (!artifacts) {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = Math.max(minSize, navCollapsedSize);
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout, minSize, navCollapsedSize]);
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
@ -261,7 +261,7 @@ const SidePanel = ({
: 'opacity-100',
)}
>
{interfaceConfig.modelSelect && (
{interfaceConfig.modelSelect === true && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',

View file

@ -1,5 +1,6 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import { cn } from '~/utils';
interface DropdownProps {
trigger: React.ReactNode;
@ -15,11 +16,21 @@ interface DropdownProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
className?: string;
iconClassName?: string;
anchor?: { x: string; y: string };
modal?: boolean;
menuId: string;
}
const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsOpen, menuId }) => {
const DropdownPopup: React.FC<DropdownProps> = ({
trigger,
items,
isOpen,
setIsOpen,
menuId,
modal,
iconClassName,
}) => {
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen });
return (
@ -27,8 +38,9 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
{trigger}
<Ariakit.Menu
id={menuId}
className="z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="absolute z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
gutter={8}
modal={modal}
>
{items
.filter((item) => item.show !== false)
@ -49,7 +61,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
}}
>
{item.icon != null && (
<span className="mr-2 h-5 w-5" aria-hidden="true">
<span className={cn('mr-2 h-5 w-5', iconClassName)} aria-hidden="true">
{item.icon}
</span>
)}

View file

@ -81,13 +81,13 @@ function defaultGetStringKey(node: unknown): string {
* @returns
*/
export function useMultiSearch<OptionsType extends unknown[]>({
availableOptions,
availableOptions = [] as unknown as OptionsType,
placeholder,
getTextKeyOverride,
className,
disabled = false,
}: {
availableOptions: OptionsType;
availableOptions?: OptionsType;
placeholder?: string;
getTextKeyOverride?: (node: OptionsType[0]) => string;
className?: string;

View file

@ -20,7 +20,7 @@ type SelectDropDownProps = {
value: string | null | Option | OptionWithIcon;
setValue: DropdownValueSetter | ((value: string) => void);
tabIndex?: number;
availableValues: string[] | Option[] | OptionWithIcon[];
availableValues?: string[] | Option[] | OptionWithIcon[];
emptyTitle?: boolean;
showAbove?: boolean;
showLabel?: boolean;
@ -89,18 +89,20 @@ function SelectDropDown({
title = localize('com_ui_model');
}
const values = availableValues ?? [];
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
// reset once the component is unmounted (as per a normal search)
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: availableValues,
availableOptions: values,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => getOptionText(option).toUpperCase(),
className: searchClassName,
disabled,
});
const hasSearchRender = searchRender != null;
const options = hasSearchRender ? filteredValues : availableValues;
const options = hasSearchRender ? filteredValues : values;
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;

View file

@ -7,6 +7,7 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
className?: string;
focusable?: boolean;
role?: string;
}

View file

@ -0,0 +1 @@
export * from './queries';

View file

@ -0,0 +1,76 @@
import { QueryKeys, dataService, EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
/**
* AGENTS
*/
/**
* Hook for getting all available tools for A
*/
export const useAvailableAgentToolsQuery = (): QueryObserverResult<t.TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<t.TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
});
};
/**
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
*/
export const useListAgentsQuery = <TData = t.AgentListResponse>(
params: t.AgentListParams = defaultOrderQuery,
config?: UseQueryOptions<t.AgentListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<t.AgentListResponse, unknown, TData>(
[QueryKeys.agents, params],
() => dataService.listAgents(params),
{
// Example selector to sort them by created_at
// select: (res) => {
// return res.data.sort((a, b) => a.created_at - b.created_at);
// },
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving details about a single agent
*/
export const useGetAgentByIdQuery = (
agent_id: string,
config?: UseQueryOptions<t.Agent>,
): QueryObserverResult<t.Agent> => {
return useQuery<t.Agent>(
[QueryKeys.agent, agent_id],
() =>
dataService.getAgentById({
agent_id,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
};

View file

@ -1,2 +1,2 @@
export * from './queries';
// export * from './mutations';
export * from './mutations';

View file

@ -0,0 +1,42 @@
import { dataService, QueryKeys, Tools } from 'librechat-data-provider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { UseMutationResult } from '@tanstack/react-query';
import type * as t from 'librechat-data-provider';
export const useToolCallMutation = <T extends t.ToolId>(
toolId: T,
options?: t.ToolCallMutationOptions<T>,
): UseMutationResult<t.ToolCallResponse, Error, t.ToolParams<T>> => {
const queryClient = useQueryClient();
return useMutation(
(toolParams: t.ToolParams<T>) => {
return dataService.callTool({
toolId,
toolParams,
});
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (response, variables, context) => {
queryClient.setQueryData<t.ToolCallResults>(
[QueryKeys.toolCalls, variables.conversationId],
(prev) => [
...(prev ?? []),
{
user: '',
toolId: Tools.execute_code,
partIndex: variables.partIndex,
messageId: variables.messageId,
blockIndex: variables.blockIndex,
conversationId: variables.conversationId,
result: response.result,
attachments: response.attachments,
},
],
);
return options?.onSuccess?.(response, variables, context);
},
},
);
};

View file

@ -1,5 +1,5 @@
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery } from '@tanstack/react-query';
import { Constants, QueryKeys, dataService } from 'librechat-data-provider';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
@ -18,3 +18,24 @@ export const useVerifyAgentToolAuth = (
},
);
};
export const useGetToolCalls = <TData = t.ToolCallResults>(
params: t.GetToolCallParams,
config?: UseQueryOptions<t.ToolCallResults, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
const { conversationId = '' } = params;
return useQuery<t.ToolCallResults, unknown, TData>(
[QueryKeys.toolCalls, conversationId],
() => dataService.getToolCalls(params),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled:
conversationId.length > 0 &&
conversationId !== Constants.NEW_CONVO &&
conversationId !== Constants.SEARCH,
...config,
},
);
};

View file

@ -1,3 +1,4 @@
export * from './Agents';
export * from './Files';
export * from './Tools';
export * from './connection';

View file

@ -22,9 +22,6 @@ import type {
AssistantListParams,
AssistantListResponse,
AssistantDocument,
Agent,
AgentListParams,
AgentListResponse,
TEndpointsConfig,
TCheckUserKeyResponse,
SharedLinkListParams,
@ -370,78 +367,6 @@ export const useGetAssistantDocsQuery = <TData = AssistantDocument[]>(
);
};
/**
* AGENTS
*/
/**
* Hook for getting all available tools for A
*/
export const useAvailableAgentToolsQuery = (): QueryObserverResult<TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
});
};
/**
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
*/
export const useListAgentsQuery = <TData = AgentListResponse>(
params: AgentListParams = defaultOrderQuery,
config?: UseQueryOptions<AgentListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<AgentListResponse, unknown, TData>(
[QueryKeys.agents, params],
() => dataService.listAgents(params),
{
// Example selector to sort them by created_at
// select: (res) => {
// return res.data.sort((a, b) => a.created_at - b.created_at);
// },
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving details about a single agent
*/
export const useGetAgentByIdQuery = (
agent_id: string,
config?: UseQueryOptions<Agent>,
): QueryObserverResult<Agent> => {
return useQuery<Agent>(
[QueryKeys.agent, agent_id],
() =>
dataService.getAgentById({
agent_id,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
};
/** STT/TTS */
/* Text to speech voices */

View file

@ -22,7 +22,12 @@ export const useGetRole = (
export const useUpdatePromptPermissionsMutation = (
options?: t.UpdatePromptPermOptions,
): UseMutationResult<t.UpdatePromptPermResponse, t.TError, t.UpdatePromptPermVars, unknown> => {
): UseMutationResult<
t.UpdatePermResponse,
t.TError | undefined,
t.UpdatePromptPermVars,
unknown
> => {
const queryClient = useQueryClient();
const { onMutate, onSuccess, onError } = options ?? {};
return useMutation(
@ -38,7 +43,10 @@ export const useUpdatePromptPermissionsMutation = (
}
},
onError: (...args) => {
args[0] && console.error('Failed to update prompt permissions:', args[0]);
const error = args[0];
if (error != null) {
console.error('Failed to update prompt permissions:', error);
}
if (onError) {
onError(...args);
}
@ -47,3 +55,39 @@ export const useUpdatePromptPermissionsMutation = (
},
);
};
export const useUpdateAgentPermissionsMutation = (
options?: t.UpdateAgentPermOptions,
): UseMutationResult<
t.UpdatePermResponse,
t.TError | undefined,
t.UpdateAgentPermVars,
unknown
> => {
const queryClient = useQueryClient();
const { onMutate, onSuccess, onError } = options ?? {};
return useMutation(
(variables) => {
promptPermissionsSchema.partial().parse(variables.updates);
return dataService.updateAgentPermissions(variables);
},
{
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries([QueryKeys.roles, variables.roleName]);
if (onSuccess != null) {
onSuccess(data, variables, context);
}
},
onError: (...args) => {
const error = args[0];
if (error != null) {
console.error('Failed to update prompt permissions:', error);
}
if (onError != null) {
onError(...args);
}
},
onMutate,
},
);
};

View file

@ -16,7 +16,7 @@ export default function useAssistantListMap<T = AssistantListItem[] | null>(
selector: (res: AssistantListResponse) => T = selectAssistantsResponse as (
res: AssistantListResponse,
) => T,
): Record<AssistantsEndpoint, T> {
): Record<AssistantsEndpoint, T | null> {
const { data: assistantsList = null } = useListAssistantsQuery(
EModelEndpoint.assistants,
undefined,

View file

@ -46,7 +46,7 @@ export default function usePresets() {
return;
}
if (presets && presets.length > 0 && user && presets[0].user !== user?.id) {
if (presets && presets.length > 0 && user && presets[0].user !== user.id) {
presetsQuery.refetch();
return;
}
@ -80,7 +80,7 @@ export default function usePresets() {
}
const previousPresets = presetsQuery.data ?? [];
if (previousPresets) {
setPresets(previousPresets.filter((p) => p.presetId !== preset?.presetId));
setPresets(previousPresets.filter((p) => p.presetId !== preset.presetId));
}
},
onSuccess: () => {
@ -99,12 +99,12 @@ export default function usePresets() {
const updatePreset = useUpdatePresetMutation({
onSuccess: (data, preset) => {
const toastTitle = data.title ? `"${data.title}"` : localize('com_endpoint_preset_title');
let message = `${toastTitle} ${localize('com_endpoint_preset_saved')}`;
let message = `${toastTitle} ${localize('com_ui_saved')}`;
if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) {
message = `${toastTitle} ${localize('com_endpoint_preset_default')}`;
setDefaultPreset(data);
newConversation({ preset: data });
} else if (preset?.defaultPreset === false) {
} else if (preset.defaultPreset === false) {
setDefaultPreset(null);
message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`;
}
@ -233,7 +233,7 @@ export default function usePresets() {
if (!preset) {
return;
}
const fileName = filenamify(preset?.title || 'preset');
const fileName = filenamify(preset.title || 'preset');
exportFromJSON({
data: cleanupPreset({ preset }),
fileName,

View file

@ -25,7 +25,7 @@ export const useDelayedUploadToast = () => {
showToast({
message,
status: 'warning',
duration: 7000,
duration: 10000,
});
}, delay);

View file

@ -8,6 +8,7 @@ import {
EModelEndpoint,
codeTypeMapping,
mergeFileConfig,
isAgentsEndpoint,
isAssistantsEndpoint,
defaultAssistantsVersion,
fileConfig as defaultFileConfig,
@ -38,6 +39,7 @@ const useFileHandling = (params?: UseFileHandling) => {
const [errors, setErrors] = useState<string[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
const [toolResource, setToolResource] = useState<string | undefined>();
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
const { addFile, replaceFile, updateFileById, deleteFileById } = useUpdateFiles(
@ -147,6 +149,9 @@ const useFileHandling = (params?: UseFileHandling) => {
: error?.response?.data?.message ?? 'com_error_files_upload';
setError(errorMessage);
},
onMutate: () => {
setToolResource(undefined);
},
},
abortControllerRef.current?.signal,
);
@ -178,6 +183,18 @@ const useFileHandling = (params?: UseFileHandling) => {
}
}
if (isAgentsEndpoint(endpoint)) {
if (!agent_id) {
formData.append('message_file', 'true');
}
if (toolResource != null) {
formData.append('tool_resource', toolResource);
}
if (conversation?.agent_id != null && formData.get('agent_id') == null) {
formData.append('agent_id', conversation.agent_id);
}
}
if (!isAssistantsEndpoint(endpoint)) {
uploadFile.mutate(formData);
return;
@ -377,6 +394,7 @@ const useFileHandling = (params?: UseFileHandling) => {
return {
handleFileChange,
setToolResource,
handleFiles,
abortUpload,
setFiles,

View file

@ -5,17 +5,17 @@ import {
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import {
getConfigDefaults,
EModelEndpoint,
alternateName,
EModelEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { useGetPresetsQuery, useListAgentsQuery } from '~/data-provider';
import { mapEndpoints, getPresetTitle } from '~/utils';
import { EndpointIcon } from '~/components/Endpoints';
import { useGetPresetsQuery } from '~/data-provider';
const defaultInterface = getConfigDefaults().interface;
@ -25,7 +25,7 @@ const assistantMapFn =
assistantMap,
endpointsConfig,
}: {
endpoint: AssistantsEndpoint;
endpoint: EModelEndpoint | string;
assistantMap: TAssistantsMap;
endpointsConfig: TEndpointsConfig;
}) =>
@ -65,6 +65,27 @@ export default function useMentions({
description,
})),
);
const { data: agentsList = null } = useListAgentsQuery(undefined, {
select: (res) => {
const { data } = res;
return data.map(({ id, name, avatar }) => ({
value: id,
label: name ?? '',
type: EModelEndpoint.agents,
icon: EndpointIcon({
conversation: {
agent_id: id,
endpoint: EModelEndpoint.agents,
iconURL: avatar?.filepath,
},
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
endpointsConfig: endpointsConfig,
context: 'menu-item',
size: 20,
}),
}));
},
});
const assistantListMap = useMemo(
() => ({
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
@ -101,7 +122,7 @@ export default function useMentions({
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
}
const mentions = [
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
...(modelSpecs.length > 0 ? modelSpecs : []).map((modelSpec) => ({
value: modelSpec.name,
label: modelSpec.label,
description: modelSpec.description,
@ -116,9 +137,9 @@ export default function useMentions({
}),
type: 'modelSpec' as const,
})),
...(interfaceConfig.endpointsMenu ? validEndpoints : []).map((endpoint) => ({
...(interfaceConfig.endpointsMenu === true ? validEndpoints : []).map((endpoint) => ({
value: endpoint,
label: alternateName[endpoint] ?? endpoint ?? '',
label: alternateName[endpoint as string] ?? endpoint ?? '',
type: 'endpoint' as const,
icon: EndpointIcon({
conversation: { endpoint },
@ -127,13 +148,14 @@ export default function useMentions({
size: 20,
}),
})),
...(agentsList ?? []),
...(endpointsConfig?.[EModelEndpoint.assistants] && includeAssistants
? assistantListMap[EModelEndpoint.assistants] || []
: []),
...(endpointsConfig?.[EModelEndpoint.azureAssistants] && includeAssistants
? assistantListMap[EModelEndpoint.azureAssistants] || []
: []),
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
...((interfaceConfig.presets === true ? presets : [])?.map((preset, index) => ({
value: preset.presetId ?? `preset-${index}`,
label: preset.title ?? preset.modelLabel ?? preset.chatGptLabel ?? '',
description: getPresetTitle(preset, true),
@ -154,6 +176,7 @@ export default function useMentions({
presets,
endpoints,
modelSpecs,
agentsList,
assistantMap,
endpointsConfig,
assistantListMap,
@ -166,6 +189,7 @@ export default function useMentions({
options,
presets,
modelSpecs,
agentsList,
modelsConfig,
endpointsConfig,
assistantListMap,

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TPreset,
TModelSpec,
@ -64,7 +64,11 @@ export default function useSelectMention({
preset.endpointType = newEndpointType;
}
if (isAssistantsEndpoint(newEndpoint) && preset.assistant_id != null && !(preset.model ?? '')) {
if (
isAssistantsEndpoint(newEndpoint) &&
preset.assistant_id != null &&
!(preset.model ?? '')
) {
preset.model = assistantMap?.[newEndpoint]?.[preset.assistant_id]?.model;
}
@ -94,11 +98,19 @@ export default function useSelectMention({
keepAddedConvos: isModular,
});
},
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig, assistantMap],
[
conversation,
getDefaultConversation,
modularChat,
newConversation,
endpointsConfig,
assistantMap,
],
);
type Kwargs = {
model?: string;
agent_id?: string;
assistant_id?: string;
};
@ -228,6 +240,10 @@ export default function useSelectMention({
assistant_id: key,
model: assistantMap?.[option.type]?.[key]?.model ?? '',
});
} else if (isAgentsEndpoint(option.type)) {
onSelectEndpoint(option.type, {
agent_id: key,
});
}
},
[modelSpecs, onSelectEndpoint, onSelectPreset, onSelectSpec, presets, assistantMap],

View file

@ -1,11 +1,12 @@
import debounce from 'lodash/debounce';
import { useEffect, useRef, useCallback } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { Constants } from 'librechat-data-provider';
import type { TEndpointOption } from 'librechat-data-provider';
import type { KeyboardEvent } from 'react';
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
import { forceResize, insertTextAtCursor, getEntityName, getEntity } from '~/utils';
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
import useGetSender from '~/hooks/Conversations/useGetSender';
import useFileHandling from '~/hooks/Files/useFileHandling';
import { useInteractionHealthCheck } from '~/data-provider';
@ -28,6 +29,7 @@ export default function useTextarea({
const localize = useLocalize();
const getSender = useGetSender();
const isComposing = useRef(false);
const agentsMap = useAgentsMapContext();
const { handleFiles } = useFileHandling();
const assistantMap = useAssistantsMapContext();
const checkHealth = useInteractionHealthCheck();
@ -44,19 +46,25 @@ export default function useTextarea({
} = useChatContext();
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
const { conversationId, jailbreak = false, endpoint = '' } = conversation || {};
const { entity, isAgent, isAssistant } = getEntity({
endpoint,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
});
const entityName = entity?.name ?? '';
const isNotAppendable =
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
!isAssistantsEndpoint(endpoint);
(((latestMessage?.unfinished ?? false) && !isSubmitting) || (latestMessage?.error ?? false)) &&
!isAssistant;
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant.name) || '';
useEffect(() => {
if (activePrompt && textAreaRef.current) {
insertTextAtCursor(textAreaRef.current, activePrompt);
const prompt = activePrompt ?? '';
if (prompt && textAreaRef.current) {
insertTextAtCursor(textAreaRef.current, prompt);
forceResize(textAreaRef.current);
setActivePrompt(undefined);
}
@ -64,16 +72,17 @@ export default function useTextarea({
// auto focus to input, when enter a conversation.
useEffect(() => {
if (!conversationId) {
const convoId = conversationId ?? '';
if (!convoId) {
return;
}
// Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak
if (conversationId === 'new' || !jailbreak) {
if (convoId === Constants.NEW_CONVO || !jailbreak) {
setShowBingToneSetting(false);
}
if (conversationId !== 'search') {
if (convoId !== Constants.SEARCH) {
textAreaRef.current?.focus();
}
// setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array
@ -89,7 +98,8 @@ export default function useTextarea({
}, [isSubmitting, textAreaRef]);
useEffect(() => {
if (textAreaRef.current?.value) {
const currentValue = textAreaRef.current?.value ?? '';
if (currentValue) {
return;
}
@ -98,10 +108,13 @@ export default function useTextarea({
return localize('com_endpoint_config_placeholder');
}
const currentEndpoint = conversation?.endpoint ?? '';
const currentAgentId = conversation?.agent_id ?? '';
const currentAssistantId = conversation?.assistant_id ?? '';
if (
isAssistantsEndpoint(currentEndpoint) &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId ?? ''])
if (isAgent && (!currentAgentId || !agentsMap?.[currentAgentId])) {
return localize('com_endpoint_agent_placeholder');
} else if (
isAssistant &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId])
) {
return localize('com_endpoint_assistant_placeholder');
}
@ -110,9 +123,10 @@ export default function useTextarea({
return localize('com_endpoint_message_not_appendable');
}
const sender = isAssistantsEndpoint(currentEndpoint)
? getAssistantName({ name: assistantName, localize })
: getSender(conversation as TEndpointOption);
const sender =
isAssistant || isAgent
? getEntityName({ name: entityName, isAgent, localize })
: getSender(conversation as TEndpointOption);
return `${localize('com_endpoint_message')} ${sender ? sender : 'AI'}`;
};
@ -137,15 +151,18 @@ export default function useTextarea({
return () => debouncedSetPlaceholder.cancel();
}, [
conversation,
isAgent,
localize,
disabled,
getSender,
agentsMap,
entityName,
textAreaRef,
isAssistant,
assistantMap,
conversation,
latestMessage,
isNotAppendable,
localize,
getSender,
assistantName,
textAreaRef,
assistantMap,
]);
const handleKeyDown = useCallback(
@ -181,7 +198,7 @@ export default function useTextarea({
}
if ((isNonShiftEnter || isCtrlEnter) && !isComposing.current) {
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement;
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement | undefined;
if (globalAudio) {
console.log('Unmuting global audio');
globalAudio.muted = false;
@ -207,14 +224,15 @@ export default function useTextarea({
return;
}
if (!e.clipboardData) {
const clipboardData = e.clipboardData as DataTransfer | undefined;
if (!clipboardData) {
return;
}
if (e.clipboardData.files.length > 0) {
if (clipboardData.files.length > 0) {
setFilesLoading(true);
const timestampedFiles: File[] = [];
for (const file of e.clipboardData.files) {
for (const file of clipboardData.files) {
const newFile = new File([file], `clipboard_${+new Date()}_${file.name}`, {
type: file.type,
});

View file

@ -41,7 +41,7 @@ export default function useMessageActions(props: TMessageActions) {
[isMultiMessage, addedConvo, rootConvo],
);
const agentMap = useAgentsMapContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { text, content, messageId = null, isCreatedByUser } = message ?? {};
@ -68,20 +68,20 @@ export default function useMessageActions(props: TMessageActions) {
return undefined;
}
if (!agentMap) {
if (!agentsMap) {
return undefined;
}
const modelKey = message?.model ?? '';
if (modelKey) {
return agentMap[modelKey];
return agentsMap[modelKey];
}
const agentId = conversation?.agent_id ?? '';
if (agentId) {
return agentMap[agentId];
return agentsMap[agentId];
}
}, [agentMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
}, [agentsMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
const isSubmitting = useMemo(
() => (isMultiMessage === true ? isSubmittingAdditional : isSubmittingRoot),

View file

@ -22,7 +22,7 @@ export default function useMessageHelpers(props: TMessageProps) {
setLatestMessage,
} = useChatContext();
const assistantMap = useAssistantsMapContext();
const agentMap = useAgentsMapContext();
const agentsMap = useAgentsMapContext();
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
const edit = messageId === currentEditId;
@ -102,8 +102,8 @@ export default function useMessageHelpers(props: TMessageProps) {
const modelKey = message?.model ?? '';
return agentMap ? agentMap[modelKey] : undefined;
}, [agentMap, conversation?.endpoint, message?.model]);
return agentsMap ? agentsMap[modelKey] : undefined;
}, [agentsMap, conversation?.endpoint, message?.model]);
const regenerateMessage = () => {
if ((isSubmitting && isCreatedByUser === true) || !message) {

View file

@ -10,9 +10,9 @@ import {
} from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
import Parameters from '~/components/SidePanel/Parameters/Panel';
import FilesPanel from '~/components/SidePanel/Files/Panel';
@ -44,6 +44,14 @@ export default function useSideNavLinks({
permissionType: PermissionTypes.BOOKMARKS,
permission: Permissions.USE,
});
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToCreateAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.CREATE,
});
const Links = useMemo(() => {
const links: NavLink[] = [];
@ -64,6 +72,8 @@ export default function useSideNavLinks({
}
if (
hasAccessToAgents &&
hasAccessToCreateAgents &&
isAgentsEndpoint(endpoint) &&
agents &&
// agents.disableBuilder !== true &&
@ -137,8 +147,10 @@ export default function useSideNavLinks({
endpointType,
endpoint,
agents,
hasAccessToAgents,
hasAccessToPrompts,
hasAccessToBookmarks,
hasAccessToCreateAgents,
hidePanel,
]);

View file

@ -1,3 +1,4 @@
export { default as useAuthCodeTool } from './useAuthCodeTool';
export { default as usePluginInstall } from './usePluginInstall';
export { default as useCodeApiKeyForm } from './useCodeApiKeyForm';
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';

View file

@ -0,0 +1,43 @@
// client/src/hooks/Plugins/useCodeApiKeyForm.ts
import { useState, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import type { ApiKeyFormData } from '~/common';
import useAuthCodeTool from '~/hooks/Plugins/useAuthCodeTool';
export default function useCodeApiKeyForm({
onSubmit,
onRevoke,
}: {
onSubmit?: () => void;
onRevoke?: () => void;
}) {
const methods = useForm<ApiKeyFormData>();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
const { reset } = methods;
const onSubmitHandler = useCallback(
(data: { apiKey: string }) => {
reset();
installTool(data.apiKey);
setIsDialogOpen(false);
onSubmit?.();
},
[onSubmit, reset, installTool],
);
const handleRevokeApiKey = useCallback(() => {
reset();
removeTool();
setIsDialogOpen(false);
onRevoke?.();
}, [reset, onRevoke, removeTool]);
return {
methods,
isDialogOpen,
setIsDialogOpen,
handleRevokeApiKey,
onSubmit: onSubmitHandler,
};
}

View file

@ -0,0 +1,28 @@
import { ToolCallResult } from 'librechat-data-provider';
import { useMemo } from 'react';
import { useGetToolCalls } from '~/data-provider';
import { mapToolCalls, logger } from '~/utils';
type ToolCallsMap = {
[x: string]: ToolCallResult[] | undefined;
};
export default function useToolCallsMap({
conversationId,
}: {
conversationId: string;
}): ToolCallsMap | undefined {
const { data: toolCallsMap = null } = useGetToolCalls(
{ conversationId },
{
select: (res) => mapToolCalls(res),
},
);
const result = useMemo<ToolCallsMap | undefined>(() => {
return toolCallsMap !== null ? toolCallsMap : undefined;
}, [toolCallsMap]);
logger.log('tools', 'tool calls map:', result);
return result;
}

View file

@ -21,7 +21,14 @@ type TUseStepHandler = {
type TStepEvent = {
event: string;
data: Agents.MessageDeltaEvent | Agents.RunStep | Agents.ToolEndEvent;
data:
| Agents.MessageDeltaEvent
| Agents.RunStep
| Agents.ToolEndEvent
| {
runId?: string;
message: string;
};
};
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
@ -166,6 +173,30 @@ export default function useStepHandler({
}
});
}
} else if (event === 'on_agent_update') {
const { runId, message } = data as { runId?: string; message: string };
const responseMessageId = runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in agent update event');
return;
}
const responseMessage = messages[messages.length - 1] as TMessage;
const response = {
...responseMessage,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [
{
type: ContentTypes.TEXT,
text: message,
},
],
} as TMessage;
setMessages([...messages.slice(0, -1), response]);
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);

View file

@ -528,7 +528,7 @@ export default {
com_endpoint_preset_default_item: 'الافتراضي:',
com_endpoint_preset_default_none: 'لا يوجد إعداد مسبق افتراضي نشط.',
com_endpoint_preset_title: 'إعداد مسبق',
com_endpoint_preset_saved: 'تم الحفظ!',
com_ui_saved: 'تم الحفظ!',
com_endpoint_preset_default: 'أصبح الإعداد المسبق الافتراضي الآن.',
com_endpoint_preset_selected: 'الإعداد المسبق نشط!',
com_endpoint_preset_selected_title: 'مُحدَّد!',
@ -644,7 +644,7 @@ export default {
com_agents_file_search_info:
'عند التمكين، سيتم إعلام الوكيل بأسماء الملفات المدرجة أدناه بالضبط، مما يتيح له استرجاع السياق ذي الصلة من هذه الملفات.',
com_ui_agent_already_shared_to_all: 'هذا المساعد مشارك بالفعل مع جميع المستخدمين',
com_agents_execute_code: 'تنفيذ الشفرة',
com_ui_run_code: 'تنفيذ الشفرة',
com_ui_no_changes: 'لا توجد تغييرات للتحديث',
com_ui_agent_editing_allowed: 'يمكن للمستخدمين الآخرين تعديل هذا الوكيل بالفعل',
com_ui_error_connection: 'خطأ في الاتصال بالخادم، حاول تحديث الصفحة.',

View file

@ -548,7 +548,7 @@ export default {
com_endpoint_preset_default_item: 'Padrão:',
com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Salvo!',
com_ui_saved: 'Salvo!',
com_endpoint_preset_default: 'é agora o preset padrão.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -509,7 +509,7 @@ export default {
com_endpoint_preset_default_item: 'Standard:',
com_endpoint_preset_default_none: 'Keine Standardvoreinstellung aktiv.',
com_endpoint_preset_title: 'Voreinstellung',
com_endpoint_preset_saved: 'Gespeichert!',
com_ui_saved: 'Gespeichert!',
com_endpoint_preset_default: 'ist jetzt die Standardvoreinstellung.',
com_endpoint_preset: 'Voreinstellung',
com_endpoint_presets: 'Voreinstellungen',
@ -801,7 +801,7 @@ export default {
com_ui_endpoint: 'Endpunkt',
com_ui_region: 'Region',
com_ui_model_parameters: 'Modell-Parameter',
com_agents_execute_code: 'Code ausführen',
com_ui_run_code: 'Code ausführen',
com_ui_provider: 'Anbieter',
com_ui_model_save_success: 'Modellparameter erfolgreich gespeichert',
com_ui_select_region: 'Wähle eine Region',

View file

@ -3,6 +3,10 @@
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
export default {
com_ui_enter_api_key: 'Enter API Key',
com_ui_librechat_code_api_title: 'Run AI Code',
com_ui_librechat_code_api_subtitle: 'Secure. Multi-language. Input/Output Files.',
com_ui_librechat_code_api_key: 'Get your LibreChat Code Interpreter API key',
com_nav_convo_menu_options: 'Conversation Menu Options',
com_ui_artifacts: 'Artifacts',
com_ui_artifacts_toggle: 'Toggle Artifacts UI',
@ -102,6 +106,7 @@ export default {
com_agents_description_placeholder: 'Optional: Describe your Agent here',
com_agents_instructions_placeholder: 'The system instructions that the agent uses',
com_agents_search_name: 'Search agents by name',
com_sidepanel_select_agent: 'Select an Agent',
com_agents_update_error: 'There was an error updating your agent.',
com_agents_create_error: 'There was an error creating your agent.',
com_agents_missing_provider_model: 'Please select a provider and model before creating an agent.',
@ -111,8 +116,11 @@ export default {
com_agents_enable_file_search: 'Enable File Search',
com_agents_file_search_info:
'When enabled, the agent will be informed of the exact filenames listed below, allowing it to retrieve relevant context from these files.',
com_agents_code_interpreter_title: 'Code Interpreter',
com_agents_by_librechat: 'by LibreChat',
com_agents_code_interpreter:
'When enabled, allows your agent to leverage the LibreChat Code Interpreter API to run generated code, including file processing, securely. Requires a valid API key.',
com_agents_file_search_disabled: 'Agent must be created before uploading files for File Search.',
com_agents_execute_code: 'Run Code',
com_ui_agent_already_shared_to_all: 'This agent is already shared to all users',
com_ui_agent_editing_allowed: 'Other users can already edit this agent',
com_ui_no_changes: 'No changes to update',
@ -177,6 +185,7 @@ export default {
com_ui_select_provider: 'Select a provider',
com_ui_select_provider_first: 'Select a provider first',
com_ui_select_search_model: 'Search model by name',
com_ui_select_search_provider: 'Search provider by name',
com_ui_select_search_region: 'Search region by name',
com_ui_select_search_plugin: 'Search plugin by name',
com_ui_use_prompt: 'Use prompt',
@ -184,6 +193,9 @@ export default {
com_ui_next: 'Next',
com_ui_stop: 'Stop',
com_ui_upload_files: 'Upload files',
com_ui_upload_image_input: 'Upload Image',
com_ui_upload_file_search: 'Upload for File Search',
com_ui_upload_code_files: 'Upload for Code Interpreter',
com_ui_prompt: 'Prompt',
com_ui_prompts: 'Prompts',
com_ui_prompt_name: 'Prompt Name',
@ -236,6 +248,8 @@ export default {
com_ui_read_aloud: 'Read aloud',
com_ui_copied: 'Copied!',
com_ui_copy_code: 'Copy code',
com_ui_run_code: 'Run Code',
com_ui_run_code_error: 'There was an error running the code',
com_ui_copy_to_clipboard: 'Copy to clipboard',
com_ui_copied_to_clipboard: 'Copied to clipboard',
com_ui_fork: 'Fork',
@ -331,6 +345,9 @@ export default {
com_ui_prompts_allow_share_global: 'Allow sharing Prompts to all users',
com_ui_prompt_shared_to_all: 'This prompt is shared to all users',
com_ui_prompt_update_error: 'There was an error updating the prompt',
com_ui_agents_allow_share_global: 'Allow sharing Agents to all users',
com_ui_agents_allow_use: 'Allow using Agents',
com_ui_agents_allow_create: 'Allow creating Agents',
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
@ -574,7 +591,7 @@ export default {
com_endpoint_preset_default_item: 'Default:',
com_endpoint_preset_default_none: 'No default preset active.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Saved!',
com_ui_saved: 'Saved!',
com_endpoint_preset_default: 'is now the default preset.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',
@ -610,6 +627,7 @@ export default {
com_endpoint_skip_hover:
'Enable skipping the completion step, which reviews the final answer and generated steps',
com_endpoint_config_key: 'Set API Key',
com_endpoint_agent_placeholder: 'Please select an Agent',
com_endpoint_assistant_placeholder: 'Please select an Assistant from the right-hand Side Panel',
com_endpoint_config_placeholder: 'Set your Key in the Header menu to chat.',
com_endpoint_config_key_for: 'Set API Key for',
@ -651,6 +669,7 @@ export default {
com_nav_font_size_lg: 'Large',
com_nav_font_size_xl: 'Extra Large',
com_nav_welcome_assistant: 'Please Select an Assistant',
com_nav_welcome_agent: 'Please Select an Agent',
com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open',
com_nav_user_msg_markdown: 'Render user messages as markdown',

View file

@ -350,7 +350,7 @@ export default {
com_endpoint_preset_default_item: 'Predeterminado:',
com_endpoint_preset_default_none: 'No hay configuración preestablecida predeterminada activa.',
com_endpoint_preset_title: 'Configuración preestablecida',
com_endpoint_preset_saved: '¡Guardado!',
com_ui_saved: '¡Guardado!',
com_endpoint_preset_default: 'es ahora la configuración preestablecida predeterminada.',
com_endpoint_preset: 'configuración preestablecida',
com_endpoint_presets: 'configuraciones preestablecidas',
@ -716,7 +716,7 @@ export default {
com_agents_file_search_disabled:
'Es necesario crear el Agente antes de subir archivos para la Búsqueda de Archivos.',
com_agents_execute_code: 'Ejecutar código',
com_ui_run_code: 'Ejecutar código',
com_ui_agent_already_shared_to_all: 'Este asistente ya está compartido con todos los usuarios',

View file

@ -489,7 +489,7 @@ export default {
com_endpoint_preset_default_item: 'Oletus:',
com_endpoint_preset_default_none: 'Oletus-esiasetusta ei ole käytössä',
com_endpoint_preset_title: 'Esiasetus',
com_endpoint_preset_saved: 'Tallennettu!',
com_ui_saved: 'Tallennettu!',
com_endpoint_preset_default: 'on nyt oletus-esiasetus.',
com_endpoint_preset: 'esiasetus',
com_endpoint_presets: 'esiasetukset',

View file

@ -257,7 +257,7 @@ export default {
com_endpoint_preset_default_item: 'Par défaut :',
com_endpoint_preset_default_none: 'Aucun préréglage par défaut actif.',
com_endpoint_preset_title: 'Préréglage',
com_endpoint_preset_saved: 'Enregistré!',
com_ui_saved: 'Enregistré!',
com_endpoint_preset_default: 'est maintenant le préréglage par défaut.',
com_endpoint_preset: 'préréglage',
com_endpoint_presets: 'préréglages',
@ -794,7 +794,7 @@ export default {
com_agents_enable_file_search: 'Activer la recherche de fichiers',
com_agents_file_search_info:
'Lorsque cette option est activée, l\'agent sera informé des noms exacts des fichiers listés ci-dessous, lui permettant d\'extraire le contexte pertinent de ces fichiers.',
com_agents_execute_code: 'Exécuter le code',
com_ui_run_code: 'Exécuter le code',
com_agents_file_search_disabled:
'L\'agent doit être créé avant de pouvoir télécharger des fichiers pour la Recherche de Fichiers.',
com_ui_agent_already_shared_to_all: 'Cet agent est déjà partagé avec tous les utilisateurs',

View file

@ -283,7 +283,7 @@ export default {
com_endpoint_preset_default_item: 'ברירת מחדל:',
com_endpoint_preset_default_none: 'אין ברירת מחדל פעילה.',
com_endpoint_preset_title: 'הגדרה מראש',
com_endpoint_preset_saved: 'שמור!',
com_ui_saved: 'שמור!',
com_endpoint_preset_default: 'הוא כעת ברירת המחדל המוגדרת מראש.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -248,7 +248,7 @@ export default {
com_endpoint_preset_default_item: 'Default:',
com_endpoint_preset_default_none: 'Tidak ada preset default yang aktif.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Tersimpan!',
com_ui_saved: 'Tersimpan!',
com_endpoint_preset_default: 'sekarang menjadi preset default.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -402,7 +402,7 @@ export default {
com_endpoint_preset_default_item: 'Predefinita:',
com_endpoint_preset_default_none: 'Nessuna preimpostazione predefinita attiva.',
com_endpoint_preset_title: 'Preimpostazione',
com_endpoint_preset_saved: 'Salvata!',
com_ui_saved: 'Salvata!',
com_endpoint_preset_default: 'è ora la preimpostazione predefinita.',
com_endpoint_preset: 'preimpostazione',
com_endpoint_presets: 'preimpostazioni',
@ -676,7 +676,7 @@ export default {
com_agents_enable_file_search: 'Abilita Ricerca File',
com_agents_file_search_info:
'Quando abilitato, l\'agente verrà informato dei nomi esatti dei file elencati di seguito, permettendogli di recuperare il contesto pertinente da questi file.',
com_agents_execute_code: 'Esegui Codice',
com_ui_run_code: 'Esegui Codice',
com_agents_file_search_disabled:
'L\'Agente deve essere creato prima di caricare file per la Ricerca File.',
com_ui_agent_already_shared_to_all: 'Questo assistente è già condiviso con tutti gli utenti',

View file

@ -541,7 +541,7 @@ export default {
com_endpoint_preset_default_item: 'デフォルト:',
com_endpoint_preset_default_none: '現在有効なプリセットはありません。',
com_endpoint_preset_title: 'プリセット',
com_endpoint_preset_saved: '保存しました!',
com_ui_saved: '保存しました!',
com_endpoint_preset_default: 'が有効化されました。',
com_endpoint_preset: 'プリセット',
com_endpoint_presets: 'プリセット',
@ -834,7 +834,7 @@ export default {
'ファイル検索用のファイルをアップロードする前に、エージェントを作成する必要があります。',
com_agents_file_search_info:
'有効にすると、エージェントは以下に表示されているファイル名を正確に認識し、それらのファイルから関連する情報を取得することができます。',
com_agents_execute_code: 'コードを実行',
com_ui_run_code: 'コードを実行',
com_ui_agent_editing_allowed: 'このエージェントは他のユーザーが既に編集可能です',
com_ui_agent_already_shared_to_all: 'このアシスタントは既に全ユーザーに共有されています',
com_ui_no_changes: '更新する変更はありません',

View file

@ -859,7 +859,7 @@ export default {
com_endpoint_preset_default_item: '기본값:',
com_endpoint_preset_default_none: '기본 프리셋이 설정되지 않았습니다.',
com_endpoint_preset_title: '프리셋',
com_endpoint_preset_saved: '저장되었습니다!',
com_ui_saved: '저장되었습니다!',
com_endpoint_preset_default: '이제 기본 프리셋입니다.',
com_endpoint_preset_selected: '프리셋 활성화됨',
com_endpoint_preset_selected_title: '활성화됨',
@ -1020,7 +1020,7 @@ export default {
com_agents_file_search_disabled:
'파일 검색을 위해 파일을 업로드하기 전에 에이전트를 먼저 생성해야 합니다',
com_agents_execute_code: '코드 실행',
com_ui_run_code: '코드 실행',
com_ui_agent_already_shared_to_all: '이 에이전트는 이미 모든 사용자와 공유되어 있습니다',

View file

@ -251,7 +251,7 @@ export default {
com_endpoint_preset_default_item: 'По умолчанию:',
com_endpoint_preset_default_none: 'Активных пресетов по умолчанию нет.',
com_endpoint_preset_title: 'Пресет',
com_endpoint_preset_saved: 'Сохранено!',
com_ui_saved: 'Сохранено!',
com_endpoint_preset_default: 'теперь пресет "По умолчаанию".',
com_endpoint_preset: 'пресет',
com_endpoint_presets: 'пресеты',
@ -699,7 +699,7 @@ export default {
com_agents_file_search_disabled: 'Для загрузки файлов в Поиск необходимо сначала создать агента',
com_agents_execute_code: 'Выполнить код',
com_ui_run_code: 'Выполнить код',
com_ui_agent_editing_allowed: 'Другие пользователи уже могут редактировать этого ассистента',

View file

@ -450,7 +450,7 @@ export default {
com_endpoint_preset_default_item: 'Varsayılan:',
com_endpoint_preset_default_none: 'Aktif varsayılan hazır ayar yok.',
com_endpoint_preset_title: 'Hazır Ayar',
com_endpoint_preset_saved: 'Kaydedildi!',
com_ui_saved: 'Kaydedildi!',
com_endpoint_preset_default: 'şu anda varsayılan hazır ayar.',
com_endpoint_preset: 'hazır ayar',
com_endpoint_presets: 'hazır ayarlar',

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