From 206fa345855275741fbd7a9402423e1f729ccfc9 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Thu, 16 May 2024 15:27:28 +0200 Subject: [PATCH] fix(chat): fix links sharing and playback with folders (Issue #1372, #967) (#1379) --- .../src/components/Chat/MessageAttachment.tsx | 8 ++-- .../Chat/Playback/PlaybackAttachments.tsx | 4 +- .../__tests__/PlaybackAttachments.test.tsx | 1 + .../src/components/Files/AttachButton.tsx | 3 +- apps/chat/src/constants/chat.ts | 2 +- apps/chat/src/store/share/share.epics.ts | 16 +++++-- apps/chat/src/utils/app/file.ts | 47 +++++++++++++++---- apps/chat/src/utils/app/import-export.ts | 8 ++-- 8 files changed, 66 insertions(+), 23 deletions(-) diff --git a/apps/chat/src/components/Chat/MessageAttachment.tsx b/apps/chat/src/components/Chat/MessageAttachment.tsx index e891cc9f4e..1ff0a81696 100644 --- a/apps/chat/src/components/Chat/MessageAttachment.tsx +++ b/apps/chat/src/components/Chat/MessageAttachment.tsx @@ -20,7 +20,7 @@ import { import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; -import { chartType, stopBubbling } from '@/src/constants/chat'; +import { PLOTLY_CONTENT_TYPE, stopBubbling } from '@/src/constants/chat'; import { FOLDER_ATTACHMENT_CONTENT_TYPE } from '@/src/constants/folders'; import { Spinner } from '@/src/components/Common/Spinner'; @@ -100,7 +100,7 @@ const AttachmentDataRenderer = ({ /> ); } - if (attachment.type === chartType) { + if (attachment.type === PLOTLY_CONTENT_TYPE) { return ( ); @@ -214,7 +214,7 @@ const AttachmentUrlRendererComponent = ({ ); } - if (attachmentType === chartType) { + if (attachmentType === PLOTLY_CONTENT_TYPE) { return ; } @@ -268,7 +268,7 @@ export const MessageAttachment = ({ attachment, isInner }: Props) => { const isOpenable = attachment.data || (attachment.url && imageTypes.has(attachment.type)) || - attachment.type === chartType || + attachment.type === PLOTLY_CONTENT_TYPE || isCustomAttachmentType; const mappedAttachmentUrl = useMemo( () => getMappedAttachmentUrl(attachment.url), diff --git a/apps/chat/src/components/Chat/Playback/PlaybackAttachments.tsx b/apps/chat/src/components/Chat/Playback/PlaybackAttachments.tsx index 8d1144b00f..ebabe8763a 100644 --- a/apps/chat/src/components/Chat/Playback/PlaybackAttachments.tsx +++ b/apps/chat/src/components/Chat/Playback/PlaybackAttachments.tsx @@ -1,5 +1,6 @@ import { getDialFilesFromAttachments, + getDialFoldersFromAttachments, getDialLinksFromAttachments, } from '@/src/utils/app/file'; @@ -13,12 +14,13 @@ interface PlaybackAttachmentsProps { export function PlaybackAttachments({ attachments }: PlaybackAttachmentsProps) { const files = getDialFilesFromAttachments(attachments); + const folders = getDialFoldersFromAttachments(attachments); const links = getDialLinksFromAttachments(attachments); return (
- +
); diff --git a/apps/chat/src/components/Chat/__tests__/PlaybackAttachments.test.tsx b/apps/chat/src/components/Chat/__tests__/PlaybackAttachments.test.tsx index 43cf7948b7..debdbbdedb 100644 --- a/apps/chat/src/components/Chat/__tests__/PlaybackAttachments.test.tsx +++ b/apps/chat/src/components/Chat/__tests__/PlaybackAttachments.test.tsx @@ -21,6 +21,7 @@ vi.mock('@/src/utils/app/file', () => { }); return files; }), + getDialFoldersFromAttachments: vi.fn().mockReturnValue([]), getDialLinksFromAttachments: vi.fn().mockReturnValue([]), }; }); diff --git a/apps/chat/src/components/Files/AttachButton.tsx b/apps/chat/src/components/Files/AttachButton.tsx index 2f75ce0533..6986604d58 100644 --- a/apps/chat/src/components/Files/AttachButton.tsx +++ b/apps/chat/src/components/Files/AttachButton.tsx @@ -1,6 +1,7 @@ import { Placement } from '@floating-ui/react'; import { IconFileDescription, + IconFolder, IconLink, IconPaperclip, IconUpload, @@ -88,7 +89,7 @@ export const AttachButton = ({ ), dataQa: 'attach_uploaded', display: canAttachFiles || canAttachFolders, - Icon: IconFileDescription, + Icon: !canAttachFiles ? IconFolder : IconFileDescription, onClick: handleOpenAttachmentsModal, }, { diff --git a/apps/chat/src/constants/chat.ts b/apps/chat/src/constants/chat.ts index 23127f1f08..bff7e37c19 100644 --- a/apps/chat/src/constants/chat.ts +++ b/apps/chat/src/constants/chat.ts @@ -17,6 +17,6 @@ export const resetShareEntity: ShareInterface = { sharedWithMe: false, }; -export const chartType = 'application/vnd.plotly.v1+json'; +export const PLOTLY_CONTENT_TYPE = 'application/vnd.plotly.v1+json'; export const ISOLATED_MODEL_QUERY_PARAM = 'isolated-model-id'; diff --git a/apps/chat/src/store/share/share.epics.ts b/apps/chat/src/store/share/share.epics.ts index 5387de5ba6..9a2fd951e5 100644 --- a/apps/chat/src/store/share/share.epics.ts +++ b/apps/chat/src/store/share/share.epics.ts @@ -17,7 +17,7 @@ import { combineEpics } from 'redux-observable'; import { ConversationService } from '@/src/utils/app/data/conversation-service'; import { ShareService } from '@/src/utils/app/data/share-service'; -import { constructPath } from '@/src/utils/app/file'; +import { constructPath, isAttachmentLink } from '@/src/utils/app/file'; import { splitEntityId } from '@/src/utils/app/folders'; import { isConversationId, isFolderId, isPromptId } from '@/src/utils/app/id'; import { EnumMapper } from '@/src/utils/app/mappers'; @@ -36,6 +36,7 @@ import { } from '@/src/types/share'; import { AppEpic } from '@/src/types/store'; +import { PLOTLY_CONTENT_TYPE } from '@/src/constants/chat'; import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-ui-settings'; import { errorsMessages } from '@/src/constants/errors'; @@ -55,8 +56,13 @@ const getInternalResourcesUrls = ( return (messages ?.map((message) => message.custom_content?.attachments - ?.map((attachment) => attachment.url) - .filter(Boolean), + ?.map((attachment) => + attachment.url && attachment.type === PLOTLY_CONTENT_TYPE + ? ApiUtils.encodeApiUrl(attachment.url) + : attachment.url, + ) + .filter(Boolean) + .filter((url) => url && !isAttachmentLink(url)), ) .filter(Boolean) .flat() || []) as string[]; @@ -116,7 +122,9 @@ const shareConversationEpic: AppEpic = (action$) => { url: ApiUtils.encodeApiUrl(payload.resourceId), }, - ...internalResources.map((res) => ({ url: res })), + ...internalResources.map((res) => ({ + url: res, + })), ], }).pipe( map((response: ShareByLinkResponseModel) => { diff --git a/apps/chat/src/utils/app/file.ts b/apps/chat/src/utils/app/file.ts index 3c373514ea..9157c11d6d 100644 --- a/apps/chat/src/utils/app/file.ts +++ b/apps/chat/src/utils/app/file.ts @@ -2,14 +2,15 @@ import { TFunction } from 'next-i18next'; import { Attachment, Conversation } from '@/src/types/chat'; import { UploadStatus } from '@/src/types/common'; -import { DialFile, DialLink } from '@/src/types/files'; -import { FolderInterface } from '@/src/types/folder'; +import { DialFile, DialLink, FileFolderInterface } from '@/src/types/files'; +import { FolderInterface, FolderType } from '@/src/types/folder'; import { FOLDER_ATTACHMENT_CONTENT_TYPE } from '@/src/constants/folders'; import { ApiUtils } from '../server/api'; import { doesHaveDotsInTheEnd } from './common'; import { getPathToFolderById } from './folders'; +import { isFolderId } from './id'; import escapeRegExp from 'lodash-es/escapeRegExp'; import { extensions } from 'mime-types'; @@ -171,6 +172,9 @@ const parseAttachmentUrl = (url: string) => { }; }; +export const isAttachmentLink = (url: string): boolean => + url.startsWith('http') || url.startsWith('//'); + export const getDialFilesFromAttachments = ( attachments: Attachment[] | undefined, ): Omit[] => { @@ -182,8 +186,8 @@ export const getDialFilesFromAttachments = ( .map((attachment): Omit | null => { if ( !attachment.url || - attachment.url.startsWith('http') || - attachment.url.startsWith('//') + isAttachmentLink(attachment.url) || + isFolderId(attachment.url) ) { return null; } @@ -201,22 +205,49 @@ export const getDialFilesFromAttachments = ( .filter(Boolean) as Omit[]; }; -export const getDialLinksFromAttachments = ( +export const getDialFoldersFromAttachments = ( attachments: Attachment[] | undefined, -): DialLink[] => { +): FileFolderInterface[] => { if (!attachments) { return []; } return attachments - .map((attachment): DialLink | null => { + .map((attachment): FileFolderInterface | null => { if ( !attachment.url || - (!attachment.url.startsWith('http') && !attachment.url.startsWith('//')) + isAttachmentLink(attachment.url) || + !isFolderId(attachment.url) ) { return null; } + const { absolutePath, name } = parseAttachmentUrl(attachment.url); + + return { + id: attachment.url, + type: FolderType.File, + name, + folderId: absolutePath, + absolutePath, + }; + }) + .filter(Boolean) as FileFolderInterface[]; +}; + +export const getDialLinksFromAttachments = ( + attachments: Attachment[] | undefined, +): DialLink[] => { + if (!attachments) { + return []; + } + + return attachments + .map((attachment): DialLink | null => { + if (!attachment.url || !isAttachmentLink(attachment.url)) { + return null; + } + return { href: attachment.url, title: attachment.title, diff --git a/apps/chat/src/utils/app/import-export.ts b/apps/chat/src/utils/app/import-export.ts index 4ef377cfaf..fa14d56000 100644 --- a/apps/chat/src/utils/app/import-export.ts +++ b/apps/chat/src/utils/app/import-export.ts @@ -16,7 +16,7 @@ import { Prompt } from '@/src/types/prompt'; import { UploadedAttachment } from '@/src/store/import-export/importExport.reducers'; -import { chartType } from '@/src/constants/chat'; +import { PLOTLY_CONTENT_TYPE } from '@/src/constants/chat'; import { ApiUtils } from '../server/api'; import { cleanConversationHistory } from './clean'; @@ -425,19 +425,19 @@ export const updateAttachment = ({ // TODO: remove ApiUtils.encodeApiUrl from updateAttachment() const newAttachmentUrl = oldAttachment.url && - (oldAttachment.type === chartType + (oldAttachment.type === PLOTLY_CONTENT_TYPE ? constructPath(newAttachmentFile.absolutePath, newAttachmentFile.name) : ApiUtils.encodeApiUrl( constructPath(newAttachmentFile.absolutePath, newAttachmentFile.name), )); const newType = - oldAttachment.type === chartType + oldAttachment.type === PLOTLY_CONTENT_TYPE ? oldAttachment.type ?? newAttachmentFile.contentType : newAttachmentFile.contentType ?? oldAttachment.type; const newTitle = - oldAttachment.type === chartType + oldAttachment.type === PLOTLY_CONTENT_TYPE ? oldAttachment.title ?? newAttachmentFile.name : newAttachmentFile.name ?? oldAttachment.title;