From 0c313f9b0bb596bb7c628ed80eb8dd5007af08f9 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 12 Feb 2024 12:47:06 +0100 Subject: [PATCH] feat(chat): integrate with DIAL stateful API (Issue #165, #265) (#611) --- .eslintrc.json | 2 +- apps/chat/README.md | 2 +- apps/chat/project.json | 9 +- .../src/components/Chat/ChangePathDialog.tsx | 2 + apps/chat/src/components/Chat/Chat.tsx | 57 +- .../src/components/Chat/ChatCompareSelect.tsx | 99 +- .../Chat/ChatInput/ChatInputAttachment.tsx | 9 +- .../Chat/ChatInput/ChatInputMessage.tsx | 7 +- .../Chat/ChatInput/SendMessageButton.tsx | 6 +- apps/chat/src/components/Chat/ChatMessage.tsx | 3 +- .../chat/src/components/Chat/MessageStage.tsx | 8 +- .../Chat/Publish/PublishAttachment.tsx | 6 +- .../chat/src/components/Chat/SystemPrompt.tsx | 38 +- .../src/components/Chatbar/ChatFolders.tsx | 27 +- apps/chat/src/components/Chatbar/Chatbar.tsx | 8 +- .../src/components/Chatbar/Conversation.tsx | 98 +- .../src/components/Chatbar/Conversations.tsx | 66 +- apps/chat/src/components/Common/Combobox.tsx | 2 +- .../src/components/Common/ContextMenu.tsx | 8 +- .../components/Common/FolderContextMenu.tsx | 2 +- .../src/components/Common/ItemContextMenu.tsx | 1 + .../ChatLoader.tsx => Common/Loader.tsx} | 4 +- .../Common/MoveToFolderMobileModal.tsx | 14 +- .../src/components/Common/NotFoundEntity.tsx | 39 + apps/chat/src/components/Common/ShareIcon.tsx | 2 +- .../src/components/Files/AttachButton.tsx | 2 + apps/chat/src/components/Files/Download.tsx | 2 +- apps/chat/src/components/Files/FileItem.tsx | 60 +- .../components/Files/FileItemContextMenu.tsx | 6 +- .../src/components/Files/FileManagerModal.tsx | 17 +- .../components/Files/SelectFolderModal.tsx | 18 +- apps/chat/src/components/Folder/Folder.tsx | 107 +- .../src/components/Promptbar/Promptbar.tsx | 11 +- .../Promptbar/components/Prompt.tsx | 45 +- .../Promptbar/components/PromptFolders.tsx | 32 +- .../Promptbar/components/PromptModal.tsx | 208 +-- .../Promptbar/components/Prompts.tsx | 4 +- .../components/Sidebar/BetweenFoldersLine.tsx | 2 +- apps/chat/src/components/Sidebar/Sidebar.tsx | 85 +- apps/chat/src/constants/default-settings.ts | 2 + apps/chat/src/constants/errors.ts | 6 +- apps/chat/src/hooks/usePromptSelection.ts | 79 +- .../{files/file => [entitytype]}/[...slug].ts | 46 +- .../src/pages/api/[entitytype]/listing.ts | 92 ++ apps/chat/src/pages/api/{files => }/bucket.ts | 4 +- apps/chat/src/pages/api/chat.ts | 3 +- apps/chat/src/pages/api/files/listing.ts | 68 - apps/chat/src/pages/api/rate.ts | 3 +- apps/chat/src/pages/index.tsx | 7 +- .../conversations/conversations.epics.ts | 1371 +++++++++++------ .../conversations/conversations.reducers.ts | 520 ++++--- .../conversations/conversations.selectors.ts | 119 +- .../conversations/conversations.types.ts | 11 +- apps/chat/src/store/files/files.epics.ts | 108 +- apps/chat/src/store/files/files.reducers.ts | 72 +- .../store/import-export/importExport.epics.ts | 20 +- apps/chat/src/store/models/models.reducers.ts | 18 +- apps/chat/src/store/prompts/prompts.epics.ts | 442 ++++-- .../src/store/prompts/prompts.reducers.ts | 165 +- .../src/store/prompts/prompts.selectors.ts | 47 +- apps/chat/src/store/prompts/prompts.types.ts | 6 +- apps/chat/src/store/settings/settings.epic.ts | 35 +- .../src/store/settings/settings.reducers.ts | 8 +- apps/chat/src/store/ui/ui.reducers.ts | 75 +- apps/chat/src/styles/globals.css | 2 +- apps/chat/src/types/chat.ts | 13 +- apps/chat/src/types/common.ts | 69 + apps/chat/src/types/files.ts | 50 +- apps/chat/src/types/folder.ts | 10 + apps/chat/src/types/menu.ts | 5 +- apps/chat/src/types/models.ts | 6 - apps/chat/src/types/prompt.ts | 6 +- apps/chat/src/types/storage.ts | 82 +- .../src/utils/app/__tests__/folders.test.ts | 55 +- .../utils/app/__tests__/importExports.test.ts | 2 +- apps/chat/src/utils/app/attachments.ts | 4 +- apps/chat/src/utils/app/clean.ts | 78 +- apps/chat/src/utils/app/common.ts | 54 + apps/chat/src/utils/app/conversation.ts | 112 +- .../chat/src/utils/app/data/bucket-service.ts | 23 + .../utils/app/data/conversation-service.ts | 76 + apps/chat/src/utils/app/data/data-service.ts | 239 +-- apps/chat/src/utils/app/data/file-service.ts | 153 ++ .../chat/src/utils/app/data/prompt-service.ts | 43 + apps/chat/src/utils/app/data/storage.ts | 24 - .../app/data/storages/api-mock-storage.ts | 105 -- .../utils/app/data/storages/api-storage.ts | 158 +- .../data/storages/api/api-entity-storage.ts | 186 +++ .../storages/api/conversation-api-storage.ts | 36 + .../data/storages/api/prompt-api-storage.ts | 34 + .../app/data/storages/browser-storage.ts | 144 +- apps/chat/src/utils/app/file.ts | 15 +- apps/chat/src/utils/app/folders.ts | 175 ++- apps/chat/src/utils/app/import-export.ts | 49 +- apps/chat/src/utils/app/move.ts | 12 +- apps/chat/src/utils/app/prompts.ts | 9 + apps/chat/src/utils/app/search.ts | 39 +- apps/chat/src/utils/app/share.ts | 6 +- .../src/utils/server/__tests__/api.test.ts | 13 + apps/chat/src/utils/server/api.ts | 193 +++ apps/chat/src/utils/server/error.ts | 13 + apps/chat/src/utils/server/index.ts | 15 +- libs/shared/project.json | 5 + nx.json | 7 + package.json | 4 +- 105 files changed, 4540 insertions(+), 2229 deletions(-) rename apps/chat/src/components/{Chat/ChatLoader.tsx => Common/Loader.tsx} (86%) create mode 100644 apps/chat/src/components/Common/NotFoundEntity.tsx rename apps/chat/src/pages/api/{files/file => [entitytype]}/[...slug].ts (75%) create mode 100644 apps/chat/src/pages/api/[entitytype]/listing.ts rename apps/chat/src/pages/api/{files => }/bucket.ts (90%) delete mode 100644 apps/chat/src/pages/api/files/listing.ts create mode 100644 apps/chat/src/utils/app/common.ts create mode 100644 apps/chat/src/utils/app/data/bucket-service.ts create mode 100644 apps/chat/src/utils/app/data/conversation-service.ts create mode 100644 apps/chat/src/utils/app/data/file-service.ts create mode 100644 apps/chat/src/utils/app/data/prompt-service.ts delete mode 100644 apps/chat/src/utils/app/data/storage.ts delete mode 100644 apps/chat/src/utils/app/data/storages/api-mock-storage.ts create mode 100644 apps/chat/src/utils/app/data/storages/api/api-entity-storage.ts create mode 100644 apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts create mode 100644 apps/chat/src/utils/app/data/storages/api/prompt-api-storage.ts create mode 100644 apps/chat/src/utils/app/prompts.ts create mode 100644 apps/chat/src/utils/server/__tests__/api.test.ts create mode 100644 apps/chat/src/utils/server/api.ts create mode 100644 apps/chat/src/utils/server/error.ts diff --git a/.eslintrc.json b/.eslintrc.json index 8335a71e12..02d1f799d2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,7 +42,7 @@ "rules": { "@typescript-eslint/no-unused-vars": [ "error", - { "argsIgnorePattern": "^_" } + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^__" } ], "@typescript-eslint/no-explicit-any": "warn" } diff --git a/apps/chat/README.md b/apps/chat/README.md index 2f9fc37e0c..c78ad36d6a 100644 --- a/apps/chat/README.md +++ b/apps/chat/README.md @@ -125,7 +125,7 @@ This project leverages environment variables for configuration. | `REQUEST_API_KEY_CODE` | No | Request API Key Code used when sending request api key info to Azure Functions API Host | Any string | | | `CODE_GENERATION_WARNING` | No | Warning text regarding code generation | Any string | | | `SHOW_TOKEN_SUB` | No | Show token sub in refresh login error logs | `true`, `false` | false | -| `STORAGE_TYPE` | No | Type of storage used for getting and saving information generated by user. Now supported only `browserStorage` | `browserStorage`, `api`,`apiMock` | `browserStorage` | +| `STORAGE_TYPE` | No | Type of storage used for getting and saving information generated by user. Now supported only `api` | `browserStorage`, `api` | `api` | | `KEEP_ALIVE_TIMEOUT` | No | Determines the maximum time in milliseconds in seconds that a connection may be idle before it is closed by the server. This is needed because infrastructure usually have default keep alive timeout 60 seconds and next server should have bigger value. Used only when running dockerfile. | Any number string | 61000 | | `TRACES_URL` | No | Traces URL | Any string | | diff --git a/apps/chat/project.json b/apps/chat/project.json index c86501b4ab..2ae707cbc5 100644 --- a/apps/chat/project.json +++ b/apps/chat/project.json @@ -42,12 +42,17 @@ "lint:fix": {}, "test": { "options": { - "reportsDirectory": "../../coverage/libs/shared" + "reportsDirectory": "../../coverage/apps/chat" + } + }, + "test:watch": { + "options": { + "reportsDirectory": "../../coverage/apps/chat" } }, "test:coverage": { "options": { - "reportsDirectory": "../../coverage/libs/shared" + "reportsDirectory": "../../coverage/apps/chat" } }, "format": {}, diff --git a/apps/chat/src/components/Chat/ChangePathDialog.tsx b/apps/chat/src/components/Chat/ChangePathDialog.tsx index ae650493ea..a1158fdc10 100644 --- a/apps/chat/src/components/Chat/ChangePathDialog.tsx +++ b/apps/chat/src/components/Chat/ChangePathDialog.tsx @@ -8,6 +8,7 @@ import { validateFolderRenaming, } from '@/src/utils/app/folders'; +import { FeatureType } from '@/src/types/common'; import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; @@ -194,6 +195,7 @@ export const ChangePathDialog = ({ onDeleteFolder: handleDeleteFolder, onAddFolder: handleAddFolder, newAddedFolderId: newFolderId, + featureType: FeatureType.File, }} handleToggleFolder={handleToggleFolder} isAllEntitiesOpened={isAllFoldersOpened} diff --git a/apps/chat/src/components/Chat/Chat.tsx b/apps/chat/src/components/Chat/Chat.tsx index 99916d244f..95843856ac 100644 --- a/apps/chat/src/components/Chat/Chat.tsx +++ b/apps/chat/src/components/Chat/Chat.tsx @@ -1,5 +1,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'next-i18next'; + import { clearStateForMessages } from '@/src/utils/app/clear-messages-state'; import { throttle } from '@/src/utils/data/throttle'; @@ -12,7 +14,8 @@ import { Replay, Role, } from '@/src/types/chat'; -import { EntityType } from '@/src/types/common'; +import { EntityType, UploadStatus } from '@/src/types/common'; +import { Translation } from '@/src/types/translation'; import { AddonsActions, @@ -33,6 +36,8 @@ import { UISelectors } from '@/src/store/ui/ui.reducers'; import { DEFAULT_ASSISTANT_SUBMODEL } from '@/src/constants/default-settings'; +import Loader from '../Common/Loader'; +import { NotFoundEntity } from '../Common/NotFoundEntity'; import { ChatCompareRotate } from './ChatCompareRotate'; import { ChatCompareSelect } from './ChatCompareSelect'; import ChatExternalControls from './ChatExternalControls'; @@ -51,7 +56,7 @@ import { Feature } from '@epam/ai-dial-shared'; const scrollThrottlingTimeout = 250; -export const Chat = memo(() => { +export const ChatView = memo(() => { const dispatch = useAppDispatch(); const appName = useAppSelector(SettingsSelectors.selectAppName); const models = useAppSelector(ModelsSelectors.selectModels); @@ -492,9 +497,6 @@ export const Chat = memo(() => { values: { messages: clearStateForMessages(conversation.messages) }, }), ); - if (temporarySettings.modelId) { - handleSelectModel(conversation, temporarySettings.modelId); - } handleChangePrompt(conversation, temporarySettings.prompt); handleChangeTemperature(conversation, temporarySettings.temperature); if (temporarySettings.currentAssistentModelId) { @@ -506,6 +508,9 @@ export const Chat = memo(() => { if (temporarySettings.addonsIds) { handleOnApplyAddons(conversation, temporarySettings.addonsIds); } + if (temporarySettings.modelId) { + handleSelectModel(conversation, temporarySettings.modelId); + } } }); }, [ @@ -808,12 +813,7 @@ export const Chat = memo(() => { selectedConversations={selectedConversations} onConversationSelect={(conversation) => { dispatch( - ConversationsActions.selectConversations({ - conversationIds: [ - selectedConversations[0].id, - conversation.id, - ], - }), + ConversationsActions.selectForCompare(conversation), ); }} /> @@ -871,4 +871,37 @@ export const Chat = memo(() => { ); }); -Chat.displayName = 'Chat'; +ChatView.displayName = 'ChatView'; + +export function Chat() { + const { t } = useTranslation(Translation.Chat); + + const areSelectedConversationsLoaded = useAppSelector( + ConversationsSelectors.selectAreSelectedConversationsLoaded, + ); + const selectedConversationsIds = useAppSelector( + ConversationsSelectors.selectSelectedConversationsIds, + ); + const selectedConversations = useAppSelector( + ConversationsSelectors.selectSelectedConversations, + ); + if ( + !areSelectedConversationsLoaded && + (!selectedConversations.length || + selectedConversations.some((conv) => conv.status !== UploadStatus.LOADED)) + ) { + return ; + } + if ( + selectedConversations.length !== selectedConversationsIds.length || + selectedConversations.some((conv) => conv.status !== UploadStatus.LOADED) + ) { + return ( + + ); + } + return ; +} diff --git a/apps/chat/src/components/Chat/ChatCompareSelect.tsx b/apps/chat/src/components/Chat/ChatCompareSelect.tsx index 3442760b1e..4f5d2946ce 100644 --- a/apps/chat/src/components/Chat/ChatCompareSelect.tsx +++ b/apps/chat/src/components/Chat/ChatCompareSelect.tsx @@ -1,23 +1,28 @@ -import { useEffect, useMemo, useState } from 'react'; +import { IconCheck } from '@tabler/icons-react'; +import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { isValidConversationForCompare } from '@/src/utils/app/conversation'; +import { compareEntitiesByName } from '@/src/utils/app/folders'; import { isMobile } from '@/src/utils/app/mobile'; -import { Conversation, Role } from '@/src/types/chat'; +import { Conversation, ConversationInfo } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; import { Translation } from '@/src/types/translation'; +import { ConversationsSelectors } from '@/src/store/conversations/conversations.reducers'; import { useAppSelector } from '@/src/store/hooks'; import { ModelsSelectors } from '@/src/store/models/models.reducers'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { ModelIcon } from '../Chatbar/ModelIcon'; import { Combobox } from '../Common/Combobox'; +import Loader from '../Common/Loader'; import ShareIcon from '../Common/ShareIcon'; interface OptionProps { - item: Conversation; + item: ConversationInfo; } const Option = ({ item }: OptionProps) => { @@ -46,9 +51,9 @@ const Option = ({ item }: OptionProps) => { }; interface Props { - conversations: Conversation[]; + conversations: ConversationInfo[]; selectedConversations: Conversation[]; - onConversationSelect: (conversation: Conversation) => void; + onConversationSelect: (conversation: ConversationInfo) => void; } export const ChatCompareSelect = ({ @@ -57,47 +62,44 @@ export const ChatCompareSelect = ({ onConversationSelect, }: Props) => { const { t } = useTranslation(Translation.Chat); + const [showAll, setShowAll] = useState(false); + + const handleChangeShowAll = useCallback( + (e: ChangeEvent) => { + setShowAll(e.target.checked); + }, + [], + ); + + const isLoading = !!useAppSelector( + ConversationsSelectors.selectIsCompareLoading, + ); const [comparableConversations, setComparableConversations] = useState< - Conversation[] + ConversationInfo[] >([]); useEffect(() => { if (selectedConversations.length === 1) { const selectedConversation = selectedConversations[0]; - const comparableConversations = conversations - .filter((conv) => !conv.replay.isReplay) - .filter((conv) => { - if (conv.id === selectedConversation.id) { - return false; - } - const convUserMessages = conv.messages.filter( - (message) => message.role === Role.User, - ); - const selectedConvUserMessages = selectedConversation.messages.filter( - (message) => message.role === Role.User, - ); - - if (convUserMessages.length !== selectedConvUserMessages.length) { - return false; - } - - return selectedConvUserMessages.every( - (message, index) => - message.content === convUserMessages[index].content, - ); - }); - setComparableConversations(comparableConversations); + const comparableConversations = conversations.filter((conv) => + showAll + ? conv.id !== selectedConversation.id + : isValidConversationForCompare(selectedConversation, conv), + ); + setComparableConversations( + comparableConversations.sort(compareEntitiesByName), + ); } - }, [conversations, selectedConversations]); + }, [conversations, selectedConversations, showAll]); return (
-
+
{t('Select conversation to compare with')} @@ -105,16 +107,33 @@ export const ChatCompareSelect = ({ ( {t( - 'Note: only conversations with same user messages can be compared', + 'Only conversations containing the same number of messages can be compared.', )} )
+
+ + + +
{comparableConversations && ( conversation.name} - getItemValue={(conversation: Conversation) => conversation.id} + getItemLabel={(conversation: ConversationInfo) => conversation.name} + getItemValue={(conversation: ConversationInfo) => conversation.id} itemRow={Option} placeholder={ (comparableConversations?.length > 0 @@ -124,9 +143,9 @@ export const ChatCompareSelect = ({ disabled={!comparableConversations?.length || isMobile()} notFoundPlaceholder={t('No conversations available') || ''} onSelectItem={(itemID: string) => { - const selectedConversation = comparableConversations.filter( + const selectedConversation = comparableConversations.find( (conv) => conv.id === itemID, - )[0]; + ); if (selectedConversation) { onConversationSelect(selectedConversation); } @@ -134,6 +153,12 @@ export const ChatCompareSelect = ({ /> )}
+ {isLoading && ( + + )}
); }; diff --git a/apps/chat/src/components/Chat/ChatInput/ChatInputAttachment.tsx b/apps/chat/src/components/Chat/ChatInput/ChatInputAttachment.tsx index 04a5568509..ffe38949f2 100644 --- a/apps/chat/src/components/Chat/ChatInput/ChatInputAttachment.tsx +++ b/apps/chat/src/components/Chat/ChatInput/ChatInputAttachment.tsx @@ -7,6 +7,7 @@ import { import classNames from 'classnames'; +import { UploadStatus } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; interface Props { @@ -26,7 +27,7 @@ export const ChatInputAttachment = ({ key={file.id} className="flex gap-3 rounded border border-primary bg-layer-1 p-3" > - {file.status !== 'FAILED' ? ( + {file.status !== UploadStatus.FAILED ? ( ) : ( @@ -37,12 +38,12 @@ export const ChatInputAttachment = ({ {file.name} - {file.status === 'UPLOADING' && ( + {file.status === UploadStatus.LOADING && (
- {onRetryFile && file.status === 'FAILED' && ( + {onRetryFile && file.status === UploadStatus.FAILED && (