From 98cdc53508ff79af0e3f47e8bda73df3d56bab65 Mon Sep 17 00:00:00 2001 From: gray Date: Fri, 6 Dec 2024 11:40:33 +0800 Subject: [PATCH 01/10] optimize useHooks --- src/FE/components/Chatbar/Conversation.tsx | 9 +-- src/FE/hooks/useFetch.ts | 85 ++++++++++++---------- src/FE/utils/user.ts | 12 +++ 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/FE/components/Chatbar/Conversation.tsx b/src/FE/components/Chatbar/Conversation.tsx index 43dfb4f8..f2d11585 100644 --- a/src/FE/components/Chatbar/Conversation.tsx +++ b/src/FE/components/Chatbar/Conversation.tsx @@ -12,8 +12,6 @@ import useTranslation from '@/hooks/useTranslation'; import { ChatResult } from '@/types/clientApis'; import { DBModelProvider } from '@/types/model'; -import { HomeContext } from '@/contexts/Home.context'; - import SidebarActionButton from '@/components/Button/SidebarActionButton'; import { SharedMessageModal } from '@/components/Chat/SharedMessageModal'; import ChatIcon from '@/components/ChatIcon/ChatIcon'; @@ -35,6 +33,7 @@ import { } from '@/components/ui/dropdown-menu'; import { deleteChats, putChats } from '@/apis/clientApis'; +import { HomeContext } from '@/contexts/Home.context'; interface Props { chat: ChatResult; @@ -172,11 +171,9 @@ export const ConversationComponent = ({ chat }: Props) => { - + { } }; +const handleErrorResponse = async (err: Response) => { + const { t } = useTranslation(); + const error = await readResponse(err); + let message = error?.message || error?.errMessage || error; + + switch (err.status) { + case 500: + message = 'Internal server error, Please try again later'; + break; + case 403: + message = 'Resource denial of authorized access'; + redirectToHome(1000); + break; + case 401: + redirectToLogin(); + return; + default: + message = + typeof message === 'string' && message !== '' + ? message + : 'Operation failed, Please try again later, or contact technical personnel'; + } + + toast.error(t(message)); + throw error; +}; + export const useFetch = () => { const handleFetch = async ( url: string, @@ -45,62 +77,39 @@ export const useFetch = () => { signal?: AbortSignal, ) => { const apiPrefix = getApiUrl(); - const apiUrl = `${apiPrefix}${url}`; - const requestUrl = request?.params ? `${apiUrl}${request.params}` : apiUrl; + const requestUrl = `${apiPrefix}${url}${ + request?.params ? request.params : '' + }`; - const requestBody = request?.body + const body = request?.body ? request.body instanceof FormData ? { ...request, body: request.body } : { ...request, body: JSON.stringify(request.body) } : request; const headers = { - ...(request?.headers - ? request.headers - : request?.body && request.body instanceof FormData - ? {} - : { 'Content-type': 'application/json' }), + ...request?.headers, + ...(!request?.body || !(request.body instanceof FormData) + ? { 'Content-type': 'application/json' } + : {}), + Authorization: `Bearer ${getUserSession()}`, }; return fetch(requestUrl, { - ...requestBody, - headers: { ...headers, Authorization: `Bearer ${getUserSession()}` }, + ...body, + headers, signal, }) - .then((response) => { + .then(async (response) => { if (!response.ok) { - if (response.status === 401) { - location.href = getLoginUrl(); - return; - } - throw response; + await handleErrorResponse(response); } const result = readResponse(response); return result; }) .catch(async (err: Response) => { - const { t } = useTranslation(); - const error = await readResponse(err); - let message = error?.message || error?.errMessage || error; - - if (err.status === 500) { - message = 'Internal server error, Please try again later'; - } else if (err.status === 403) { - message = 'Resource denial of authorized access'; - setTimeout(() => (location.href = '/'), 1000); - } else if (err.status === 401) { - location.href = getLoginUrl(); - return; - } - - message = - typeof message === 'string' && message !== '' - ? message - : 'Operation failed, Please try again later, or contact technical personnel'; - - toast.error(t(message)); - throw error; + await handleErrorResponse(err); }); }; diff --git a/src/FE/utils/user.ts b/src/FE/utils/user.ts index 21b078c1..a15b7362 100644 --- a/src/FE/utils/user.ts +++ b/src/FE/utils/user.ts @@ -36,6 +36,18 @@ export const getLoginUrl = () => { return '/login'; }; +export const redirectToLogin = () => { + location.href = getLoginUrl(); +}; + +export const redirectToHome = (ms?: number) => { + const toHome = () => { + location.href = '/'; + }; + + ms ? setTimeout(toHome, ms) : toHome(); +}; + export const setUserSession = (sessionId: string) => { let expires = new Date(); expires.setMinutes(expires.getMinutes() + 10080); From c88fc414b96c8bab5de4bd018bb106e2d80e6c5f Mon Sep 17 00:00:00 2001 From: sdcb Date: Fri, 6 Dec 2024 18:44:36 +0800 Subject: [PATCH 02/10] possible models --- .../Controllers/Admin/ModelKeys/Dtos/PossibleModelDto.cs | 3 +++ .../Controllers/Admin/ModelKeys/ModelKeysController.cs | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/BE/Controllers/Admin/ModelKeys/Dtos/PossibleModelDto.cs b/src/BE/Controllers/Admin/ModelKeys/Dtos/PossibleModelDto.cs index 9fe97ea1..4413ad59 100644 --- a/src/BE/Controllers/Admin/ModelKeys/Dtos/PossibleModelDto.cs +++ b/src/BE/Controllers/Admin/ModelKeys/Dtos/PossibleModelDto.cs @@ -13,6 +13,9 @@ public record PossibleModelDto [JsonPropertyName("isExists")] public required bool IsExists { get; init; } + [JsonPropertyName("deploymentName")] + public required string? DeploymentName { get; init; } + [JsonPropertyName("isLegacy")] public required bool IsLegacy { get; init; } } diff --git a/src/BE/Controllers/Admin/ModelKeys/ModelKeysController.cs b/src/BE/Controllers/Admin/ModelKeys/ModelKeysController.cs index 2b2ab0b8..7671d926 100644 --- a/src/BE/Controllers/Admin/ModelKeys/ModelKeysController.cs +++ b/src/BE/Controllers/Admin/ModelKeys/ModelKeysController.cs @@ -195,20 +195,19 @@ public async Task> ListModelKeyPossibleModels(s return NotFound(); } - HashSet existingModelRefIds = modelKey.Models - .Select(x => x.ModelReferenceId) - .ToHashSet(); - PossibleModelDto[] readyRefs = await db.ModelReferences .Where(x => x.ProviderId == modelKey.ModelProviderId) .OrderBy(x => x.Name) .Select(x => new PossibleModelDto() { - IsExists = x.Models.Any(m => m.ModelKeyId == modelKeyId), + DeploymentName = x.Models.FirstOrDefault(m => m.ModelKeyId == modelKeyId)!.DeploymentName, ReferenceId = x.Id, ReferenceName = x.Name, IsLegacy = x.IsLegacy, + IsExists = x.Models.Any(m => m.ModelKeyId == modelKeyId), }) + .OrderBy(x => (x.IsLegacy ? 1 : 0) + (x.IsExists ? 2 : 0)) + .ThenByDescending(x => x.ReferenceId) .ToArrayAsync(cancellationToken); return Ok(readyRefs); From 415b2f615804f06bf6fe6ffb337c0c61063cfb1a Mon Sep 17 00:00:00 2001 From: ZHOU Jie Date: Fri, 6 Dec 2024 15:58:56 +0800 Subject: [PATCH 03/10] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 574653d48675ba81cd9ab24cdabec5ebdaeef491 Mon Sep 17 00:00:00 2001 From: gray Date: Fri, 6 Dec 2024 16:17:15 +0800 Subject: [PATCH 04/10] add deploymentname/fixed button warning --- .../Admin/ModelKeys/ConfigModelModal.tsx | 93 ++- .../Admin/ModelKeys/ModelKeysModal.tsx | 2 +- src/FE/components/Chat/ChangeModel.tsx | 18 +- src/FE/components/Chat/Chat.tsx | 13 +- src/FE/components/Chat/ChatInput.tsx | 694 +++++++++--------- src/FE/components/Chat/PromptList.tsx | 68 +- src/FE/components/Chat/VariableModal.tsx | 1 - src/FE/components/Chatbar/ChatbarSettings.tsx | 2 +- src/FE/components/HomeContent/HomeContent.tsx | 3 +- src/FE/components/Sidebar/OpenCloseButton.tsx | 2 +- src/FE/components/Sidebar/Sidebar.tsx | 19 +- src/FE/components/Sidebar/SidebarButton.tsx | 9 +- src/FE/locales/zh-CN.json | 3 +- src/FE/pages/admin/model-keys/index.tsx | 2 +- src/FE/types/adminApis.ts | 2 + 15 files changed, 474 insertions(+), 457 deletions(-) diff --git a/src/FE/components/Admin/ModelKeys/ConfigModelModal.tsx b/src/FE/components/Admin/ModelKeys/ConfigModelModal.tsx index 054e2b28..25c810a8 100644 --- a/src/FE/components/Admin/ModelKeys/ConfigModelModal.tsx +++ b/src/FE/components/Admin/ModelKeys/ConfigModelModal.tsx @@ -3,6 +3,8 @@ import toast from 'react-hot-toast'; import useTranslation from '@/hooks/useTranslation'; +import { DBModelProvider } from '@/types/model'; + import { IconInfo } from '@/components/Icons'; import Spinner from '@/components/Spinner'; import Tips from '@/components/Tips/Tips'; @@ -14,7 +16,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { Switch } from '@/components/ui/switch'; +import { Input } from '@/components/ui/input'; import { Table, TableBody, @@ -32,6 +34,7 @@ import { interface IProps { modelKeyId: number; + modelProverId: number; isOpen: boolean; onClose: () => void; onSuccessful: () => void; @@ -43,17 +46,16 @@ export interface PossibleModel { isExists: boolean; isLegacy: boolean; validating: boolean; - validated: boolean; validateMessage: string | null; adding: boolean; + deploymentName: string | null; } export const ConfigModelModal = (props: IProps) => { const { t } = useTranslation(); - const { modelKeyId, isOpen, onClose } = props; + const { modelKeyId, modelProverId, isOpen, onClose } = props; const [models, setModels] = useState([]); const [loading, setLoading] = React.useState(false); - const onSave = async (index: number) => { const model = models[index]; postModelFastCreate({ @@ -68,16 +70,12 @@ export const ConfigModelModal = (props: IProps) => { setLoading(true); getModelKeyPossibleModels(modelKeyId).then((data) => { setModels( - data - .sort((x) => (x.isLegacy ? -1 : 1)) - .sort((x) => (x.isExists ? 1 : -1)) - .map((x) => ({ - ...x, - validating: false, - validated: false, - adding: false, - validateMessage: null, - })), + data.map((x) => ({ + ...x, + validating: false, + adding: false, + validateMessage: null, + })), ); setLoading(false); }); @@ -92,7 +90,7 @@ export const ConfigModelModal = (props: IProps) => { postModelFastCreate({ modelKeyId: modelKeyId, modelReferenceId: model.modelReferenceId, - deploymentName: model.referenceName, + deploymentName: model.deploymentName || null, }).then(() => { toast.success(t('Added successfully')); model.isExists = true; @@ -109,11 +107,11 @@ export const ConfigModelModal = (props: IProps) => { postModelValidate({ modelKeyId: modelKeyId, modelReferenceId: model.modelReferenceId, - deploymentName: model.referenceName, + deploymentName: model.deploymentName || null, }).then((data) => { if (data.isSuccess) { + model.validateMessage = null; toast.success(t('Verified Successfully')); - model.validated = true; } else { toast.error(t('Verified Failed')); model.validateMessage = data.errorMessage; @@ -123,9 +121,16 @@ export const ConfigModelModal = (props: IProps) => { }); } + function handleChangeDeploymentName(index: number, value: string) { + const model = models[index]; + const modelList = [...models]; + model.deploymentName = value; + setModels(modelList); + } + return ( - + {t('Add Model')} @@ -134,6 +139,9 @@ export const ConfigModelModal = (props: IProps) => { {t('Model Display Name')} + {modelProverId == DBModelProvider.Azure && ( + {t('Deployment Name')} + )} {t('Actions')} @@ -142,13 +150,30 @@ export const ConfigModelModal = (props: IProps) => { {model.referenceName} + {model.isExists && ( + + {t('Existed')} + + )} {model.isLegacy && ( - {t('Is Legacy')} + {t('Legacy')} )} - + {modelProverId == DBModelProvider.Azure && ( + + { + handleChangeDeploymentName(index, e.target.value); + }} + /> + + )} + {!model.isExists && ( )} - {model.validated ? ( - {t('Validated')} - ) : ( -
- -
- )} +
+ +
{model.validateMessage && ( { return ( - + {selected ? t('Edit Model Keys') : t('Add Model Keys')} diff --git a/src/FE/components/Chat/ChangeModel.tsx b/src/FE/components/Chat/ChangeModel.tsx index 3e0e7bec..1e142c7e 100644 --- a/src/FE/components/Chat/ChangeModel.tsx +++ b/src/FE/components/Chat/ChangeModel.tsx @@ -5,8 +5,6 @@ import useTranslation from '@/hooks/useTranslation'; import { AdminModelDto } from '@/types/adminApis'; import { feModelProviders } from '@/types/model'; -import { HomeContext } from '@/contexts/Home.context'; - import ChatIcon from '@/components/ChatIcon/ChatIcon'; import { IconChevronDown } from '@/components/Icons'; import Search from '@/components/Search'; @@ -23,6 +21,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { HomeContext } from '@/contexts/Home.context'; import { cn } from '@/lib/utils'; const ChangeModel = ({ @@ -72,15 +71,16 @@ const ChangeModel = ({ return ( - - + {!readonly && typeof content === 'string' && } + ; -} - -export const Chat = memo(({ stopConversationRef }: Props) => { +export const Chat = memo(() => { const { t } = useTranslation(); const { @@ -66,7 +61,7 @@ export const Chat = memo(({ stopConversationRef }: Props) => { const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); - const textareaRef = useRef(null); + const stopConversationRef = useRef(false); const getSelectMessagesLast = () => { const selectMessageLength = selectMessages.length - 1; @@ -323,7 +318,6 @@ export const Chat = memo(({ stopConversationRef }: Props) => { useCallback(() => { if (autoScrollEnabled) { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - textareaRef.current?.focus(); } }, [autoScrollEnabled]); @@ -532,8 +526,7 @@ export const Chat = memo(({ stopConversationRef }: Props) => { {hasModel() && ( { const { lastMessage } = getSelectMessagesLast(); handleSend(message, lastMessage?.id, false); diff --git a/src/FE/components/Chat/ChatInput.tsx b/src/FE/components/Chat/ChatInput.tsx index c6c3aaa7..35986c1e 100644 --- a/src/FE/components/Chat/ChatInput.tsx +++ b/src/FE/components/Chat/ChatInput.tsx @@ -1,6 +1,6 @@ import { KeyboardEvent, - MutableRefObject, + forwardRef, useCallback, useContext, useEffect, @@ -42,370 +42,378 @@ interface Props { onScrollDownClick: () => void; onChangePrompt: (prompt: Prompt) => void; model: AdminModelDto; - stopConversationRef: MutableRefObject; - textareaRef: MutableRefObject; showScrollDownButton: boolean; } -export const ChatInput = ({ - onSend, - onScrollDownClick, - onChangePrompt, - stopConversationRef, - textareaRef, - showScrollDownButton, -}: Props) => { - const { t } = useTranslation(); - - const { - state: { selectModel, messageIsStreaming, prompts, selectChat, chatError }, - } = useContext(HomeContext); - - const [content, setContent] = useState({ - text: '', - fileIds: [], - }); - const [isTyping, setIsTyping] = useState(false); - const [uploading, setUploading] = useState(false); - const [showPromptList, setShowPromptList] = useState(false); - const [activePromptIndex, setActivePromptIndex] = useState(0); - const [promptInputValue, setPromptInputValue] = useState(''); - const [variables, setVariables] = useState([]); - const [isModalVisible, setIsModalVisible] = useState(false); - const promptListRef = useRef(null); - const filteredPrompts = prompts.filter((prompt) => - prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), - ); - const updatePromptListVisibility = useCallback((text: string) => { - const match = text.match(/\/\w*$/); - - if (match) { - setShowPromptList(true); - setPromptInputValue(match[0].slice(1)); - } else { - setShowPromptList(false); - setPromptInputValue(''); - } - }, []); - - const handleChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (selectModel && value.length > selectModel.contextWindow * 2) { - toast.error( - t( - `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, - { - maxLength: selectModel.contextWindow * 2, - valueLength: value.length, - }, - ), - ); - return; - } - - setContent({ ...content, text: value }); - updatePromptListVisibility(value); - }; - - const handleSend = () => { - if (messageIsStreaming) { - return; - } - - if (!content.text) { - toast.error(t('Please enter a message')); - return; - } - onSend({ role: 'user', content }); - setContent({ text: '', fileIds: [] }); - - if (window.innerWidth < 640 && textareaRef && textareaRef.current) { - textareaRef.current.blur(); - } - }; - - const handleStopChat = () => { - stopConversationRef.current = true; - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (showPromptList) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex, - ); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex > 0 ? prevIndex - 1 : prevIndex, - ); - } else if (e.key === 'Tab') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex < prompts.length - 1 ? prevIndex + 1 : 0, +export const ChatInput = forwardRef( + ( + { onSend, onScrollDownClick, onChangePrompt, showScrollDownButton }: Props, + stopConversationRef: any, + ) => { + const { t } = useTranslation(); + + const { + state: { + selectModel, + messageIsStreaming, + prompts, + selectChat, + chatError, + }, + } = useContext(HomeContext); + + const textareaRef = useRef(null); + const promptListRef = useRef(null); + + const [content, setContent] = useState({ + text: '', + fileIds: [], + }); + const [isTyping, setIsTyping] = useState(false); + const [uploading, setUploading] = useState(false); + const [showPromptList, setShowPromptList] = useState(false); + const [activePromptIndex, setActivePromptIndex] = useState(0); + const [promptInputValue, setPromptInputValue] = useState(''); + const [variables, setVariables] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const filteredPrompts = prompts.filter((prompt) => + prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), + ); + const updatePromptListVisibility = useCallback((text: string) => { + const match = text.match(/\/\w*$/); + + if (match) { + setShowPromptList(true); + setPromptInputValue(match[0].slice(1)); + } else { + setShowPromptList(false); + setPromptInputValue(''); + } + }, []); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (selectModel && value.length > selectModel.contextWindow * 2) { + toast.error( + t( + `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, + { + maxLength: selectModel.contextWindow * 2, + valueLength: value.length, + }, + ), ); - } else if (e.key === 'Enter') { - e.preventDefault(); - handleInitModal(); - } else if (e.key === 'Escape') { + return; + } + + setContent({ ...content, text: value }); + updatePromptListVisibility(value); + }; + + const handleSend = () => { + if (messageIsStreaming) { + return; + } + + if (!content.text) { + toast.error(t('Please enter a message')); + return; + } + onSend({ role: 'user', content }); + setContent({ text: '', fileIds: [] }); + + if (window.innerWidth < 640 && textareaRef && textareaRef.current) { + textareaRef.current.blur(); + } + }; + + const handleStopChat = () => { + stopConversationRef.current = true; + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (showPromptList) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActivePromptIndex((prevIndex) => + prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex, + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActivePromptIndex((prevIndex) => + prevIndex > 0 ? prevIndex - 1 : prevIndex, + ); + } else if (e.key === 'Tab') { + e.preventDefault(); + setActivePromptIndex((prevIndex) => + prevIndex < prompts.length - 1 ? prevIndex + 1 : 0, + ); + } else if (e.key === 'Enter') { + e.preventDefault(); + handleInitModal(); + } else if (e.key === 'Escape') { + e.preventDefault(); + setShowPromptList(false); + } else { + setActivePromptIndex(0); + } + } else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) { e.preventDefault(); - setShowPromptList(false); - } else { - setActivePromptIndex(0); + handleSend(); } - } else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; - - const parseVariables = (content: string) => { - const regex = /{{(.*?)}}/g; - const foundVariables = []; - let match; - - while ((match = regex.exec(content)) !== null) { - foundVariables.push(match[1]); - } - - return foundVariables; - }; - - const handlePromptSelect = (prompt: Prompt) => { - const formatted = formatPrompt(prompt.content, { model: selectModel! }); - const parsedVariables = parseVariables(formatted); - onChangePrompt(prompt); - setVariables(parsedVariables); - - if (parsedVariables.length > 0) { - setIsModalVisible(true); - } else { - const text = content.text?.replace(/\/\w*$/, formatted); - - setContent({ - ...content, - text, - }); + }; + + const parseVariables = (content: string) => { + const regex = /{{(.*?)}}/g; + const foundVariables = []; + let match; - updatePromptListVisibility(formatted); - } - }; - - const handleInitModal = () => { - const selectedPrompt = filteredPrompts[activePromptIndex]; - selectedPrompt && - getUserPromptDetail(selectedPrompt.id).then((data) => { - setContent((prevContent) => { - const newContent = prevContent.text?.replace(/\/\w*$/, data.content); - return { ...prevContent, text: newContent }; + while ((match = regex.exec(content)) !== null) { + foundVariables.push(match[1]); + } + + return foundVariables; + }; + + const handlePromptSelect = (prompt: Prompt) => { + const formatted = formatPrompt(prompt.content, { model: selectModel! }); + const parsedVariables = parseVariables(formatted); + onChangePrompt(prompt); + setVariables(parsedVariables); + + if (parsedVariables.length > 0) { + setIsModalVisible(true); + } else { + const text = content.text?.replace(/\/\w*$/, formatted); + + setContent({ + ...content, + text, }); - handlePromptSelect(data); - setShowPromptList(false); + + updatePromptListVisibility(formatted); + } + }; + + const handleInitModal = () => { + const selectedPrompt = filteredPrompts[activePromptIndex]; + selectedPrompt && + getUserPromptDetail(selectedPrompt.id).then((data) => { + setContent((prevContent) => { + const newContent = prevContent.text?.replace( + /\/\w*$/, + data.content, + ); + return { ...prevContent, text: newContent }; + }); + handlePromptSelect(data); + setShowPromptList(false); + }); + }; + + const handleSubmit = (updatedVariables: string[]) => { + const newContent = content.text?.replace(/{{(.*?)}}/g, (_, variable) => { + const index = variables.indexOf(variable); + return updatedVariables[index]; }); - }; - const handleSubmit = (updatedVariables: string[]) => { - const newContent = content.text?.replace(/{{(.*?)}}/g, (_, variable) => { - const index = variables.indexOf(variable); - return updatedVariables[index]; - }); + setContent({ ...content, text: newContent }); - setContent({ ...content, text: newContent }); + if (textareaRef && textareaRef.current) { + textareaRef.current.focus(); + } + }; + + const canUploadFile = () => { + return ( + selectModel && + selectModel.allowVision && + selectModel.fileServiceId && + !uploading && + (content?.fileIds?.length ?? 0) <= defaultFileConfig.count + ); + }; - if (textareaRef && textareaRef.current) { - textareaRef.current.focus(); - } - }; + const handleUploadFailed = (reason: string | null) => { + setUploading(false); + if (reason) { + toast.error(t(reason)); + } else { + toast.error(t('File upload failed')); + } + }; + + const handleUploadSuccessful = (def: ImageDef) => { + setContent((old) => { + return { + ...old, + fileIds: old.fileIds!.concat(def), + }; + }); + setUploading(false); + }; + + const handleUploading = () => { + setUploading(true); + }; + + useEffect(() => { + if (textareaRef && textareaRef.current) { + textareaRef.current.style.height = 'inherit'; + textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; + textareaRef.current.style.overflow = `${ + textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' + }`; + } + }, [content]); + + useEffect(() => { + setContent({ ...content, fileIds: [] }); + }, [selectModel, selectChat]); - const canUploadFile = () => { return ( - selectModel && - selectModel.allowVision && - selectModel.fileServiceId && - !uploading && - (content?.fileIds?.length ?? 0) <= defaultFileConfig.count - ); - }; - - const handleUploadFailed = (reason: string | null) => { - setUploading(false); - if (reason) { - toast.error(t(reason)); - } else { - toast.error(t('File upload failed')); - } - }; - - const handleUploadSuccessful = (def: ImageDef) => { - setContent((old) => { - return { - ...old, - fileIds: old.fileIds!.concat(def), - }; - }); - setUploading(false); - }; - - const handleUploading = () => { - setUploading(true); - }; - - useEffect(() => { - if (textareaRef && textareaRef.current) { - textareaRef.current.style.height = 'inherit'; - textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; - textareaRef.current.style.overflow = `${ - textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' - }`; - } - }, [content]); - - useEffect(() => { - setContent({ ...content, fileIds: [] }); - }, [selectModel, selectChat]); - - return ( -
-
- {!chatError ? ( -
-
- {content?.fileIds && - content.fileIds.map((img, index) => ( -
-
- - + +
-
- ))} -
+ ))} +
+ +