From 2a24cfdf8fa5bd8e284feb4fbdf4abadae4145af Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 12 Dec 2023 16:50:09 +0100 Subject: [PATCH 01/22] feat: add folder sharing, fix "shared with me" displaying (#291) (#299) --- package-lock.json | 10 +- public/locales/en/sidebar.json | 4 +- src/components/Chat/ShareModal.tsx | 6 +- src/components/Chatbar/Chatbar.tsx | 6 +- .../Chatbar/components/ChatFolders.tsx | 41 +++--- .../Chatbar/components/Conversation.tsx | 14 +- src/components/Common/FolderContextMenu.tsx | 12 +- src/components/Common/ShareIcon.tsx | 2 +- src/components/Folder/Folder.tsx | 86 +++++++++++- src/components/Promptbar/Promptbar.tsx | 4 +- .../Promptbar/components/Prompt.tsx | 14 +- .../Promptbar/components/PromptFolders.tsx | 51 ++++--- src/components/Search/SearchFiltersView.tsx | 12 +- src/store/addons/addons.reducers.ts | 8 +- .../conversations/conversations.epics.ts | 124 ++++++++++++++++-- .../conversations/conversations.reducers.ts | 48 ++++++- .../conversations/conversations.selectors.ts | 49 ++++--- src/store/files/files.epics.ts | 5 +- src/store/models/models.reducers.ts | 8 +- src/store/prompts/prompts.epics.ts | 102 +++++++++++++- src/store/prompts/prompts.reducers.ts | 41 ++++++ src/store/prompts/prompts.selectors.ts | 52 ++++---- src/store/settings/settings.reducers.ts | 30 +++-- src/types/chat.ts | 5 +- src/types/common.ts | 6 +- src/types/folder.ts | 9 +- src/types/prompt.ts | 5 +- src/types/search.ts | 8 ++ src/types/share.ts | 2 + src/utils/app/clean.ts | 2 + src/utils/app/search.ts | 31 +++-- 31 files changed, 619 insertions(+), 178 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10f1f8a9de..e9d86cbfe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2576,9 +2576,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -6964,9 +6965,10 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, diff --git a/public/locales/en/sidebar.json b/public/locales/en/sidebar.json index e4eb6f0634..b5c3e7dc05 100644 --- a/public/locales/en/sidebar.json +++ b/public/locales/en/sidebar.json @@ -1,5 +1,7 @@ { "share.modal.link.description": "This link is temporary and will be active for 3 days.", "share.modal.link_conversation": "This conversation and future changes to it will be visible to users who follow the link. Only owner will be able to make changes.", - "share.modal.link_prompt": "This prompt and future changes to it will be visible to users who follow the link. Only owner will be able to make changes." + "share.modal.link_prompt": "This prompt and future changes to it will be visible to users who follow the link. Only owner will be able to make changes.", + "share.modal.link_conversations_folder": "This conversation folder and future changes to it will be visible to users who follow the link. Only owner will be able to make changes.", + "share.modal.link_prompts_folder": "This prompt folder and future changes to it will be visible to users who follow the link. Only owner will be able to make changes." } diff --git a/src/components/Chat/ShareModal.tsx b/src/components/Chat/ShareModal.tsx index cfced68bf3..402c8fcbf5 100644 --- a/src/components/Chat/ShareModal.tsx +++ b/src/components/Chat/ShareModal.tsx @@ -18,6 +18,7 @@ import { import { useTranslation } from 'next-i18next'; +import { Entity } from '@/src/types/common'; import { Translation } from '@/src/types/translation'; import Tooltip from '../Common/Tooltip'; @@ -31,11 +32,6 @@ export enum SharingType { PromptFolder = 'prompts_folder', } -interface Entity { - id: string; - name: string; -} - interface Props { entity: Entity; type: SharingType; diff --git a/src/components/Chatbar/Chatbar.tsx b/src/components/Chatbar/Chatbar.tsx index e0e8cfde61..8c06b32e16 100644 --- a/src/components/Chatbar/Chatbar.tsx +++ b/src/components/Chatbar/Chatbar.tsx @@ -60,12 +60,14 @@ export const Chatbar = () => { const searchFilters = useAppSelector( ConversationsSelectors.selectSearchFilters, ); - const itemFilter = useAppSelector(ConversationsSelectors.selectItemFilter); + const myItemsFilters = useAppSelector( + ConversationsSelectors.selectMyItemsFilters, + ); const filteredConversations = useAppSelector((state) => ConversationsSelectors.selectFilteredConversations( state, - itemFilter, + myItemsFilters, searchTerm, ), ); diff --git a/src/components/Chatbar/components/ChatFolders.tsx b/src/components/Chatbar/components/ChatFolders.tsx index b04b1a2544..e75f1fe35a 100644 --- a/src/components/Chatbar/components/ChatFolders.tsx +++ b/src/components/Chatbar/components/ChatFolders.tsx @@ -5,9 +5,9 @@ import { useTranslation } from 'next-i18next'; import { SharedWithMeFilter } from '@/src/utils/app/search'; import { Conversation } from '@/src/types/chat'; -import { EntityFilter, HighlightColor } from '@/src/types/common'; -import { Feature } from '@/src/types/features'; +import { FeatureType, HighlightColor } from '@/src/types/common'; import { FolderInterface, FolderSectionProps } from '@/src/types/folder'; +import { EntityFilters } from '@/src/types/search'; import { Translation } from '@/src/types/translation'; import { @@ -29,7 +29,7 @@ interface ChatFolderProps { index: number; isLast: boolean; readonly?: boolean; - itemFilter: EntityFilter; + filters: EntityFilters; includeEmpty: boolean; } @@ -38,7 +38,7 @@ const ChatFolderTemplate = ({ index, isLast, readonly, - itemFilter, + filters, includeEmpty = false, }: ChatFolderProps) => { const dispatch = useAppDispatch(); @@ -47,14 +47,14 @@ const ChatFolderTemplate = ({ const conversations = useAppSelector((state) => ConversationsSelectors.selectFilteredConversations( state, - itemFilter, + filters, searchTerm, ), ); const conversationFolders = useAppSelector((state) => ConversationsSelectors.selectFilteredFolders( state, - itemFilter, + filters, searchTerm, includeEmpty, ), @@ -157,6 +157,7 @@ const ChatFolderTemplate = ({ } onDropBetweenFolders={onDropBetweenFolders} onClickFolder={handleFolderClick} + featureType={FeatureType.Chat} /> {isLast && ( ) => { +}: FolderSectionProps) => { const { t } = useTranslation(Translation.SideBar); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); const [isSectionHighlighted, setIsSectionHighlighted] = useState(false); const folders = useAppSelector((state) => ConversationsSelectors.selectFilteredFolders( state, - itemFilter, + filters, searchTerm, showEmptyFolders, ), @@ -194,7 +195,7 @@ export const ChatSection = ({ const conversations = useAppSelector((state) => ConversationsSelectors.selectFilteredConversations( state, - itemFilter, + filters, searchTerm, ), ); @@ -259,7 +260,7 @@ export const ChatSection = ({ folder={folder} index={index} isLast={index === arr.length - 1} - itemFilter={itemFilter} + filters={filters} includeEmpty={showEmptyFolders} /> ); @@ -280,33 +281,35 @@ export function ChatFolders() { const isFilterEmpty = useAppSelector( ConversationsSelectors.selectIsEmptySearchFilter, ); - const commonItemFilter = useAppSelector( - ConversationsSelectors.selectItemFilter, + const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); + const commonSearchFilter = useAppSelector( + ConversationsSelectors.selectMyItemsFilters, ); const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.ConversationsSharing), + SettingsSelectors.isSharingEnabled(state, FeatureType.Chat), ); - const folderItems: FolderSectionProps[] = useMemo( + const folderItems: FolderSectionProps[] = useMemo( () => [ { hidden: !isSharingEnabled || !isFilterEmpty, name: t('Shared with me'), - itemFilter: SharedWithMeFilter, + filters: SharedWithMeFilter, displayRootFiles: true, - dataQa: 'share-with-me', + dataQa: 'shared-with-me', + openByDefault: !!searchTerm.length, }, { name: t('Pinned chats'), - itemFilter: commonItemFilter, + filters: commonSearchFilter, showEmptyFolders: isFilterEmpty, openByDefault: true, dataQa: 'pinned-chats', }, ].filter(({ hidden }) => !hidden), - [commonItemFilter, isFilterEmpty, isSharingEnabled, t], + [commonSearchFilter, isFilterEmpty, isSharingEnabled, searchTerm.length, t], ); return ( diff --git a/src/components/Chatbar/components/Conversation.tsx b/src/components/Chatbar/components/Conversation.tsx index 66f35647ca..03d4617f24 100644 --- a/src/components/Chatbar/components/Conversation.tsx +++ b/src/components/Chatbar/components/Conversation.tsx @@ -15,7 +15,6 @@ import classNames from 'classnames'; import { Conversation } from '@/src/types/chat'; import { FeatureType, HighlightColor } from '@/src/types/common'; -import { Feature } from '@/src/types/features'; import { ConversationsActions, @@ -114,7 +113,7 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { ); const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.ConversationsSharing), + SettingsSelectors.isSharingEnabled(state, FeatureType.Chat), ); const [isDeleting, setIsDeleting] = useState(false); @@ -264,16 +263,11 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { }, []); const handleShared = useCallback( - (_newShareId: string) => { - //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + (shareUniqueId: string) => { dispatch( - ConversationsActions.updateConversation({ + ConversationsActions.shareConversation({ id: conversationId, - values: { - isShared: true, - //TODO: added for development purpose - emulate immediate sharing with yourself - sharedWithMe: true, - }, + shareUniqueId, }), ); }, diff --git a/src/components/Common/FolderContextMenu.tsx b/src/components/Common/FolderContextMenu.tsx index 0f06ed404c..5cc13127d2 100644 --- a/src/components/Common/FolderContextMenu.tsx +++ b/src/components/Common/FolderContextMenu.tsx @@ -3,6 +3,7 @@ import { IconFolderPlus, IconPencilMinus, IconTrashX, + IconUserShare, } from '@tabler/icons-react'; import { MouseEventHandler, useMemo } from 'react'; @@ -21,12 +22,14 @@ interface FolderContextMenuProps { onOpenChange?: (isOpen: boolean) => void; highlightColor: HighlightColor; isOpen?: boolean; + onOpenShareModal?: MouseEventHandler; } export const FolderContextMenu = ({ onDelete, onRename, onAddFolder, onOpenChange, + onOpenShareModal, highlightColor, isOpen, }: FolderContextMenuProps) => { @@ -40,6 +43,13 @@ export const FolderContextMenu = ({ Icon: IconPencilMinus, onClick: onRename, }, + { + name: t('Share'), + display: !!onOpenShareModal, + dataQa: 'share', + Icon: IconUserShare, + onClick: onOpenShareModal, + }, { name: t('Delete'), display: !!onDelete, @@ -55,7 +65,7 @@ export const FolderContextMenu = ({ onClick: onAddFolder, }, ], - [t, onRename, onDelete, onAddFolder], + [t, onRename, onOpenShareModal, onDelete, onAddFolder], ); if (!onDelete && !onRename && !onAddFolder) { diff --git a/src/components/Common/ShareIcon.tsx b/src/components/Common/ShareIcon.tsx index ec15e4806a..13c8c5f37d 100644 --- a/src/components/Common/ShareIcon.tsx +++ b/src/components/Common/ShareIcon.tsx @@ -18,7 +18,7 @@ interface ShareIsonProps extends ShareInterface { highlightColor: HighlightColor; size?: number; children: ReactNode | ReactNode[]; - featureType: FeatureType; + featureType?: FeatureType; } export default function ShareIcon({ diff --git a/src/components/Folder/Folder.tsx b/src/components/Folder/Folder.tsx index 6850ec6342..0a53b4664d 100644 --- a/src/components/Folder/Folder.tsx +++ b/src/components/Folder/Folder.tsx @@ -25,13 +25,16 @@ import { getByHighlightColor, getFoldersDepth } from '@/src/utils/app/folders'; import { doesEntityContainSearchItem } from '@/src/utils/app/search'; import { Conversation } from '@/src/types/chat'; -import { HighlightColor } from '@/src/types/common'; +import { FeatureType, HighlightColor } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; import { Translation } from '@/src/types/translation'; -import { useAppDispatch } from '@/src/store/hooks'; +import { ConversationsActions } from '@/src/store/conversations/conversations.reducers'; +import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { PromptsActions } from '@/src/store/prompts/prompts.reducers'; +import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions } from '@/src/store/ui/ui.reducers'; import { emptyImage } from '@/src/constants/drag-and-drop'; @@ -41,8 +44,10 @@ import CaretIconComponent from '@/src/components/Common/CaretIconComponent'; import CheckIcon from '../../../public/images/icons/check.svg'; import XmarkIcon from '../../../public/images/icons/xmark.svg'; +import ShareModal, { SharingType } from '../Chat/ShareModal'; import { ConfirmDialog } from '../Common/ConfirmDialog'; import { FolderContextMenu } from '../Common/FolderContextMenu'; +import ShareIcon from '../Common/ShareIcon'; import { Spinner } from '../Common/Spinner'; import { BetweenFoldersLine } from '../Sidebar/BetweenFoldersLine'; @@ -77,7 +82,7 @@ interface Props { onDeleteFolder?: (folderId: string) => void; onAddFolder?: (parentFolderId: string) => void; onClickFolder: (folderId: string) => void; - + featureType?: FeatureType; onItemEvent?: (eventId: string, data: unknown) => void; readonly?: boolean; } @@ -104,7 +109,7 @@ const Folder = ({ onClickFolder, onAddFolder, onItemEvent, - + featureType, readonly = false, }: Props) => { const { t } = useTranslation(Translation.Chat); @@ -123,6 +128,36 @@ const Folder = ({ const [isContextMenu, setIsContextMenu] = useState(false); const dragDropElement = useRef(null); + const [isSharing, setIsSharing] = useState(false); + const isSharingEnabled = useAppSelector((state) => + SettingsSelectors.isSharingEnabled(state, featureType), + ); + + const handleOpenSharing: MouseEventHandler = useCallback((e) => { + e.stopPropagation(); + setIsSharing(true); + }, []); + + const handleCloseShareModal = useCallback(() => { + setIsSharing(false); + }, []); + + const handleShared = useCallback( + (shareUniqueId: string) => { + const shareFolder = + featureType === FeatureType.Chat + ? ConversationsActions.shareFolder + : PromptsActions.shareFolder; + dispatch( + shareFolder({ + id: currentFolder.id, + shareUniqueId, + }), + ); + }, + [currentFolder.id, dispatch, featureType], + ); + const isFolderOpened = useMemo(() => { return openedFoldersIds.includes(currentFolder.id); }, [currentFolder.id, openedFoldersIds]); @@ -408,7 +443,14 @@ const Folder = ({ {loadingFolderId === currentFolder.id ? ( ) : ( - + + + )} ({ {loadingFolderId === currentFolder.id ? ( ) : ( - + + + )}
({ } onDelete={onDeleteFolder && onDelete} onAddFolder={onAddFolder && onAdd} + onOpenShareModal={ + isSharingEnabled ? handleOpenSharing : undefined + } highlightColor={highlightColor} onOpenChange={setIsContextMenu} isOpen={isContextMenu} @@ -564,6 +624,7 @@ const Folder = ({ onAddFolder={onAddFolder} onClickFolder={onClickFolder} onItemEvent={onItemEvent} + featureType={featureType} /> {onDropBetweenFolders && index === arr.length - 1 && ( ({ }} /> )} + {isSharing && isSharingEnabled && ( + + )}
); }; diff --git a/src/components/Promptbar/Promptbar.tsx b/src/components/Promptbar/Promptbar.tsx index 684c20f319..ec277d1940 100644 --- a/src/components/Promptbar/Promptbar.tsx +++ b/src/components/Promptbar/Promptbar.tsx @@ -47,10 +47,10 @@ const Promptbar = () => { const dispatch = useAppDispatch(); const showPromptbar = useAppSelector(UISelectors.selectShowPromptbar); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); - const itemFilter = useAppSelector(PromptsSelectors.selectItemFilter); + const myItemsFilters = useAppSelector(PromptsSelectors.selectMyItemsFilters); const filteredPrompts = useAppSelector((state) => - PromptsSelectors.selectFilteredPrompts(state, itemFilter, searchTerm), + PromptsSelectors.selectFilteredPrompts(state, myItemsFilters, searchTerm), ); const searchFilters = useAppSelector(PromptsSelectors.selectSearchFilters); diff --git a/src/components/Promptbar/components/Prompt.tsx b/src/components/Promptbar/components/Prompt.tsx index fa954cb8d1..b2fba62132 100644 --- a/src/components/Promptbar/components/Prompt.tsx +++ b/src/components/Promptbar/components/Prompt.tsx @@ -11,7 +11,6 @@ import { import classNames from 'classnames'; import { FeatureType, HighlightColor } from '@/src/types/common'; -import { Feature } from '@/src/types/features'; import { Prompt } from '@/src/types/prompt'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; @@ -54,7 +53,7 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { const showModal = useAppSelector(PromptsSelectors.selectIsEditModalOpen); const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.PromptsSharing), + SettingsSelectors.isSharingEnabled(state, FeatureType.Prompt), ); const [isDeleting, setIsDeleting] = useState(false); @@ -78,16 +77,11 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { }, []); const handleShared = useCallback( - (_newShareId: string) => { - //TODO: send newShareId to API to store {id, createdDate} + (shareUniqueId: string) => { dispatch( - PromptsActions.updatePrompt({ + PromptsActions.sharePrompt({ promptId, - values: { - isShared: true, - //TODO: for development purpose - emulate immediate sharing with yourself - sharedWithMe: true, - }, + shareUniqueId, }), ); }, diff --git a/src/components/Promptbar/components/PromptFolders.tsx b/src/components/Promptbar/components/PromptFolders.tsx index cbcfdeb08a..e33ed33ffc 100644 --- a/src/components/Promptbar/components/PromptFolders.tsx +++ b/src/components/Promptbar/components/PromptFolders.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'next-i18next'; import { SharedWithMeFilter } from '@/src/utils/app/search'; -import { EntityFilter, HighlightColor } from '@/src/types/common'; -import { Feature } from '@/src/types/features'; +import { FeatureType, HighlightColor } from '@/src/types/common'; import { FolderInterface, FolderSectionProps } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; +import { EntityFilters } from '@/src/types/search'; import { Translation } from '@/src/types/translation'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; @@ -28,14 +28,16 @@ interface promptFolderProps { folder: FolderInterface; index: number; isLast: boolean; - itemFilter: EntityFilter; + filters: EntityFilters; + includeEmpty: boolean; } const PromptFolderTemplate = ({ folder, index, isLast, - itemFilter, + filters, + includeEmpty = false, }: promptFolderProps) => { const dispatch = useAppDispatch(); @@ -44,9 +46,16 @@ const PromptFolderTemplate = ({ PromptsSelectors.selectSelectedPromptFoldersIds, ); const prompts = useAppSelector((state) => - PromptsSelectors.selectFilteredPrompts(state, itemFilter, searchTerm), + PromptsSelectors.selectFilteredPrompts(state, filters, searchTerm), + ); + const promptFolders = useAppSelector((state) => + PromptsSelectors.selectFilteredFolders( + state, + filters, + searchTerm, + includeEmpty, + ), ); - const conversationFolders = useAppSelector(PromptsSelectors.selectFolders); const openedFoldersIds = useAppSelector(UISelectors.selectOpenedFoldersIds); const handleDrop = useCallback( @@ -123,7 +132,7 @@ const PromptFolderTemplate = ({ currentFolder={folder} itemComponent={PromptComponent} allItems={prompts} - allFolders={conversationFolders} + allFolders={promptFolders} highlightColor={HighlightColor.Violet} highlightedFolders={highlightedFolders} openedFoldersIds={openedFoldersIds} @@ -141,6 +150,7 @@ const PromptFolderTemplate = ({ } onDropBetweenFolders={onDropBetweenFolders} onClickFolder={handleFolderClick} + featureType={FeatureType.Prompt} /> {isLast && ( ) => { +}: FolderSectionProps) => { const { t } = useTranslation(Translation.PromptBar); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); const [isSectionHighlighted, setIsSectionHighlighted] = useState(false); const folders = useAppSelector((state) => PromptsSelectors.selectFilteredFolders( state, - itemFilter, + filters, searchTerm, showEmptyFolders, ), ); const prompts = useAppSelector((state) => - PromptsSelectors.selectFilteredPrompts(state, itemFilter, searchTerm), + PromptsSelectors.selectFilteredPrompts(state, filters, searchTerm), ); const rootfolders = useMemo( @@ -236,7 +246,8 @@ export const PromptSection = ({ folder={folder} index={index} isLast={index === arr.length - 1} - itemFilter={itemFilter} + filters={filters} + includeEmpty={showEmptyFolders} /> ))} @@ -255,30 +266,34 @@ export function PromptFolders() { const isFilterEmpty = useAppSelector( PromptsSelectors.selectIsEmptySearchFilter, ); - const commonItemFilter = useAppSelector(PromptsSelectors.selectItemFilter); + const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); + const commonSearchFilter = useAppSelector( + PromptsSelectors.selectMyItemsFilters, + ); const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.PromptsSharing), + SettingsSelectors.isSharingEnabled(state, FeatureType.Prompt), ); - const folderItems: FolderSectionProps[] = useMemo( + const folderItems: FolderSectionProps[] = useMemo( () => [ { hidden: !isSharingEnabled || !isFilterEmpty, name: t('Shared with me'), - itemFilter: SharedWithMeFilter, + filters: SharedWithMeFilter, displayRootFiles: true, dataQa: 'share-with-me', + openByDefault: !!searchTerm.length, }, { name: t('Pinned prompts'), - itemFilter: commonItemFilter, + filters: commonSearchFilter, showEmptyFolders: isFilterEmpty, openByDefault: true, dataQa: 'pinned-prompts', }, ].filter(({ hidden }) => !hidden), - [commonItemFilter, isFilterEmpty, isSharingEnabled, t], + [commonSearchFilter, isFilterEmpty, isSharingEnabled, searchTerm.length, t], ); return ( diff --git a/src/components/Search/SearchFiltersView.tsx b/src/components/Search/SearchFiltersView.tsx index bdc93700fd..73803153b2 100644 --- a/src/components/Search/SearchFiltersView.tsx +++ b/src/components/Search/SearchFiltersView.tsx @@ -94,7 +94,17 @@ export default function SearchFiltersView({ triggerIconClassName="absolute right-4 cursor-pointer max-h-[18px]" TriggerCustomRenderer={ <> - + {searchFilters !== SearchFilters.None && ( ) => { state.isLoading = false; state.error = { - title: i18n?.t('Error fetching addons.'), + title: translate('Error fetching addons.'), code: payload.error.status || 'unknown', messageLines: payload.error.statusText ? [payload.error.statusText] - : [i18n?.t(errorsMessages.generalServer, { ns: 'common' })], + : [translate(errorsMessages.generalServer, { ns: 'common' })], } as ErrorMessage; }, initRecentAddons: ( diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index bc15f706f3..32547141c6 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -47,6 +47,7 @@ import { parseStreamMessages, } from '@/src/utils/app/merge-streams'; import { filterUnfinishedStages } from '@/src/utils/app/stages'; +import { translate } from '@/src/utils/app/translation'; import { ChatBody, @@ -71,6 +72,8 @@ import { ConversationsSelectors, } from './conversations.reducers'; +import { v4 as uuidv4 } from 'uuid'; + const createNewConversationEpic: AppEpic = (action$, state$) => action$.pipe( filter(ConversationsActions.createNewConversations.match), @@ -220,7 +223,7 @@ const clearConversationsEpic: AppEpic = (action$) => switchMap(() => { return of( ConversationsActions.createNewConversations({ - names: [(i18n as any).t(DEFAULT_CONVERSATION_NAME)], + names: [translate(DEFAULT_CONVERSATION_NAME)], }), ); }), @@ -238,7 +241,7 @@ const deleteConversationsEpic: AppEpic = (action$, state$) => if (conversations.length === 0) { return of( ConversationsActions.createNewConversations({ - names: [(i18n as any).t(DEFAULT_CONVERSATION_NAME)], + names: [translate(DEFAULT_CONVERSATION_NAME)], }), ); } else if (selectedConversationsIds.length === 0) { @@ -296,7 +299,7 @@ const initConversationsEpic: AppEpic = (action$) => actions.push( of( ConversationsActions.createNewConversations({ - names: [(i18n as any).t(DEFAULT_CONVERSATION_NAME)], + names: [translate(DEFAULT_CONVERSATION_NAME)], }), ), ); @@ -713,7 +716,7 @@ const streamMessageEpic: AppEpic = (action$, state$) => return of( ConversationsActions.streamMessageFail({ conversation: payload.conversation, - message: (i18n as any).t(errorsMessages.timeoutError), + message: translate(errorsMessages.timeoutError), }), ); } @@ -724,7 +727,7 @@ const streamMessageEpic: AppEpic = (action$, state$) => conversation: payload.conversation, message: (error.cause as any).message || - (i18n as any).t(errorsMessages.generalServer), + translate(errorsMessages.generalServer), response: error.cause instanceof Response ? error.cause : undefined, }), @@ -734,7 +737,7 @@ const streamMessageEpic: AppEpic = (action$, state$) => return of( ConversationsActions.streamMessageFail({ conversation: payload.conversation, - message: (i18n as any).t(errorsMessages.generalClient), + message: translate(errorsMessages.generalClient), }), ); }), @@ -1082,7 +1085,8 @@ const saveFoldersEpic: AppEpic = (action$, state$) => ConversationsActions.renameFolder.match(action) || ConversationsActions.moveFolder.match(action) || ConversationsActions.clearConversations.match(action) || - ConversationsActions.importConversationsSuccess.match(action), + ConversationsActions.importConversationsSuccess.match(action) || + ConversationsActions.addFolders.match(action), ), map(() => ({ conversationsFolders: ConversationsSelectors.selectFolders(state$.value), @@ -1103,7 +1107,8 @@ const selectConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.createNewReplayConversation.match(action) || ConversationsActions.importConversationsSuccess.match(action) || ConversationsActions.createNewPlaybackConversation.match(action) || - ConversationsActions.deleteConversations.match(action), + ConversationsActions.deleteConversations.match(action) || + ConversationsActions.addConversations.match(action), ), map(() => ConversationsSelectors.selectSelectedConversationsIds(state$.value), @@ -1133,7 +1138,8 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.updateConversations.match(action) || ConversationsActions.importConversationsSuccess.match(action) || ConversationsActions.deleteConversations.match(action) || - ConversationsActions.createNewPlaybackConversation.match(action), + ConversationsActions.createNewPlaybackConversation.match(action) || + ConversationsActions.addConversations.match(action), ), map(() => ConversationsSelectors.selectConversations(state$.value)), switchMap((conversations) => { @@ -1379,6 +1385,103 @@ const initEpic: AppEpic = (action$) => ), ); +//TODO: added for development purpose - emulate immediate sharing with yourself +const shareFolderEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.shareFolder.match), + map(({ payload }) => ({ + sharedFolderId: payload.id, + shareUniqueId: payload.shareUniqueId, + conversations: ConversationsSelectors.selectConversations(state$.value), + childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( + state$.value, + payload.id, + ), + folders: ConversationsSelectors.selectFolders(state$.value), + })), + switchMap( + ({ + sharedFolderId, + shareUniqueId, + conversations, + childFolders, + folders, + }) => { + const mapping = new Map(); + childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); + const newFolders = folders + .filter(({ id }) => childFolders.includes(id)) + .map(({ folderId, ...folder }) => ({ + ...folder, + id: mapping.get(folder.id), + folderId: + folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level + sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, + isShared: false, + shareUniqueId: + folder.id === sharedFolderId ? shareUniqueId : undefined, + })); + + const sharedConversations = conversations + .filter( + (conversation) => + conversation.folderId && + childFolders.includes(conversation.folderId), + ) + .map(({ folderId, ...prompt }) => ({ + ...prompt, + id: uuidv4(), + folderId: mapping.get(folderId), + isShared: false, + })); + + return concat( + of( + ConversationsActions.addConversations({ + conversations: sharedConversations, + }), + ), + of( + ConversationsActions.addFolders({ + folders: newFolders, + }), + ), + ); + }, + ), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const shareConversationEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.shareConversation.match), + map(({ payload }) => ({ + sharedConversationId: payload.id, + shareUniqueId: payload.shareUniqueId, + conversations: ConversationsSelectors.selectConversations(state$.value), + })), + switchMap(({ sharedConversationId, shareUniqueId, conversations }) => { + const sharedConversations = conversations + .filter((conversation) => conversation.id === sharedConversationId) + .map(({ folderId: _, ...conversation }) => ({ + ...conversation, + id: uuidv4(), + folderId: undefined, // show on root level + sharedWithMe: true, + isShared: false, + shareUniqueId, + })); + + return concat( + of( + ConversationsActions.addConversations({ + conversations: sharedConversations, + }), + ), + ); + }), + ); + export const ConversationsEpics = combineEpics( initEpic, initConversationsEpic, @@ -1412,4 +1515,7 @@ export const ConversationsEpics = combineEpics( playbackNextMessageEndEpic, playbackPrevMessageEpic, playbackCalncelEpic, + + shareFolderEpic, + shareConversationEpic, ); diff --git a/src/store/conversations/conversations.reducers.ts b/src/store/conversations/conversations.reducers.ts index 2d89ebe1b8..dccb967811 100644 --- a/src/store/conversations/conversations.reducers.ts +++ b/src/store/conversations/conversations.reducers.ts @@ -116,6 +116,38 @@ export const conversationsSlice = createSlice({ return conv; }); }, + shareConversation: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.conversations = state.conversations.map((conv) => { + if (conv.id === payload.id) { + return { + ...conv, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isShared: true, + }; + } + + return conv; + }); + }, + shareFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isShared: true, + }; + } + + return folder; + }); + }, exportConversation: ( state, _action: PayloadAction<{ conversationId: string }>, @@ -222,6 +254,12 @@ export const conversationsSlice = createSlice({ ) => { state.conversations = payload.conversations; }, + addConversations: ( + state, + { payload }: PayloadAction<{ conversations: Conversation[] }>, + ) => { + state.conversations = [...state.conversations, ...payload.conversations]; + }, clearConversations: (state) => { state.conversations = []; state.folders = []; @@ -297,6 +335,12 @@ export const conversationsSlice = createSlice({ ) => { state.folders = payload.folders; }, + addFolders: ( + state, + { payload }: PayloadAction<{ folders: FolderInterface[] }>, + ) => { + state.folders = [...state.folders, ...payload.folders]; + }, setSearchTerm: ( state, { payload }: PayloadAction<{ searchTerm: string }>, @@ -337,8 +381,10 @@ export const conversationsSlice = createSlice({ rate: number; }>, ) => state, - rateMessageFail: (state, _action: PayloadAction<{ error: Response }>) => + rateMessageFail: ( state, + _action: PayloadAction<{ error: Response | string }>, + ) => state, cleanMessage: (state) => state, deleteMessage: (state, _action: PayloadAction<{ index: number }>) => state, sendMessages: ( diff --git a/src/store/conversations/conversations.selectors.ts b/src/store/conversations/conversations.selectors.ts index 0104747444..09b4e8683c 100644 --- a/src/store/conversations/conversations.selectors.ts +++ b/src/store/conversations/conversations.selectors.ts @@ -7,12 +7,12 @@ import { } from '@/src/utils/app/folders'; import { doesConversationContainSearchTerm, - getItemFilter, + getMyItemsFilters, } from '@/src/utils/app/search'; import { Conversation, Role } from '@/src/types/chat'; -import { EntityFilter, EntityType } from '@/src/types/common'; -import { SearchFilters } from '@/src/types/search'; +import { EntityType } from '@/src/types/common'; +import { EntityFilters, SearchFilters } from '@/src/types/search'; import { RootState } from '../index'; import { ModelsSelectors } from '../models/models.reducers'; @@ -28,15 +28,16 @@ export const selectConversations = createSelector([rootSelector], (state) => { export const selectFilteredConversations = createSelector( [ selectConversations, - (_state, filter?: EntityFilter) => filter, - (_state, _filter, searchTerm?: string) => searchTerm, + (_state, filters: EntityFilters) => filters, + (_state, _filters, searchTerm?: string) => searchTerm, ], - (conversations, filter?, searchTerm?) => { + (conversations, filters, searchTerm?) => { return conversations.filter( (conversation) => (!searchTerm || doesConversationContainSearchTerm(conversation, searchTerm)) && - (!filter || filter(conversation)), + filters.searchFilter(conversation) && + (conversation.folderId || filters.sectionFilter(conversation)), ); }, ); @@ -63,30 +64,38 @@ export const selectFilteredFolders = createSelector( (state) => state, selectFolders, selectEmptyFolderIds, - (_state, itemFilter?: EntityFilter) => itemFilter, - (_state, _itemFilter?, searchTerm?: string) => searchTerm, - (_state, _itemFilter?, _searchTerm?, includeEmptyFolders?: boolean) => + (_state, filters: EntityFilters) => filters, + (_state, _filters, searchTerm?: string) => searchTerm, + (_state, _filters, _searchTerm?, includeEmptyFolders?: boolean) => includeEmptyFolders, ], ( state, folders, emptyFolderIds, - itemFilter?, + filters?, searchTerm?, includeEmptyFolders?, ) => { const filteredConversations = selectFilteredConversations( state, - itemFilter, + filters, searchTerm, ); const folderIds = filteredConversations // direct parent folders .map((c) => c.folderId) .filter((fid) => fid); - // include empty folders only if not search - if (includeEmptyFolders && !searchTerm?.trim().length) { - folderIds.push(...emptyFolderIds); + + if (!searchTerm?.trim().length) { + const markedFolderIds = folders + .filter((folder) => filters?.searchFilter(folder)) + .map((f) => f.id); + folderIds.push(...markedFolderIds); + + if (includeEmptyFolders && !searchTerm?.length) { + // include empty folders only if not search + folderIds.push(...emptyFolderIds); + } } const filteredFolderIds = new Set( @@ -95,7 +104,11 @@ export const selectFilteredFolders = createSelector( ), ); - return folders.filter((folder) => filteredFolderIds.has(folder.id)); + return folders.filter( + (folder) => + (folder.folderId || filters.sectionFilter(folder)) && + filteredFolderIds.has(folder.id), + ); }, ); @@ -189,9 +202,9 @@ export const selectIsEmptySearchFilter = createSelector( (state) => state.searchFilters === SearchFilters.None, ); -export const selectItemFilter = createSelector( +export const selectMyItemsFilters = createSelector( [selectSearchFilters], - (searchFilters) => getItemFilter(searchFilters), + (searchFilters) => getMyItemsFilters(searchFilters), ); export const selectSearchedConversations = createSelector( diff --git a/src/store/files/files.epics.ts b/src/store/files/files.epics.ts index 9756d34db8..6e93c76b87 100644 --- a/src/store/files/files.epics.ts +++ b/src/store/files/files.epics.ts @@ -1,5 +1,3 @@ -import { i18n } from 'next-i18next'; - import { catchError, concat, @@ -16,6 +14,7 @@ import { combineEpics } from 'redux-observable'; import { DataService } from '@/src/utils/app/data/data-service'; import { triggerDownload } from '@/src/utils/app/file'; +import { translate } from '@/src/utils/app/translation'; import { AppEpic } from '@/src/types/store'; @@ -170,7 +169,7 @@ const removeFileFailEpic: AppEpic = (action$) => filter(FilesActions.removeFileFail.match), map(({ payload }) => { return UIActions.showToast({ - message: i18n!.t( + message: translate( 'Removing file {{fileName}} failed. Please try again later', { ns: 'file', diff --git a/src/store/models/models.reducers.ts b/src/store/models/models.reducers.ts index 445b635f8f..ceef2910a0 100644 --- a/src/store/models/models.reducers.ts +++ b/src/store/models/models.reducers.ts @@ -1,7 +1,7 @@ -import { i18n } from 'next-i18next'; - import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit'; +import { translate } from '@/src/utils/app/translation'; + import { EntityType } from '@/src/types/common'; import { ErrorMessage } from '@/src/types/error'; import { ModelsMap } from '@/src/types/models'; @@ -54,11 +54,11 @@ export const modelsSlice = createSlice({ getModelsFail: (state, { payload }: PayloadAction<{ error: any }>) => { state.isLoading = false; state.error = { - title: i18n?.t('Error fetching models.'), + title: translate('Error fetching models.'), code: payload.error.status || 'unknown', messageLines: payload.error.statusText ? [payload.error.statusText] - : [i18n?.t(errorsMessages.generalServer, { ns: 'common' })], + : [translate(errorsMessages.generalServer, { ns: 'common' })], } as ErrorMessage; }, initRecentModels: ( diff --git a/src/store/prompts/prompts.epics.ts b/src/store/prompts/prompts.epics.ts index 1f4a451f54..43670cea75 100644 --- a/src/store/prompts/prompts.epics.ts +++ b/src/store/prompts/prompts.epics.ts @@ -1,5 +1,3 @@ -import { i18n } from 'next-i18next'; - import { concat, filter, ignoreElements, map, of, switchMap, tap } from 'rxjs'; import { combineEpics } from 'redux-observable'; @@ -10,6 +8,7 @@ import { exportPrompts, importPrompts, } from '@/src/utils/app/import-export'; +import { translate } from '@/src/utils/app/translation'; import { AppEpic } from '@/src/types/store'; @@ -18,6 +17,8 @@ import { errorsMessages } from '@/src/constants/errors'; import { UIActions } from '../ui/ui.reducers'; import { PromptsActions, PromptsSelectors } from './prompts.reducers'; +import { v4 as uuidv4 } from 'uuid'; + const savePromptsEpic: AppEpic = (action$, state$) => action$.pipe( filter( @@ -26,6 +27,7 @@ const savePromptsEpic: AppEpic = (action$, state$) => PromptsActions.deletePrompts.match(action) || PromptsActions.clearPrompts.match(action) || PromptsActions.updatePrompt.match(action) || + PromptsActions.addPrompts.match(action) || PromptsActions.importPromptsSuccess.match(action), ), map(() => PromptsSelectors.selectPrompts(state$.value)), @@ -43,6 +45,7 @@ const saveFoldersEpic: AppEpic = (action$, state$) => PromptsActions.deleteFolder.match(action) || PromptsActions.renameFolder.match(action) || PromptsActions.moveFolder.match(action) || + PromptsActions.addFolders.match(action) || PromptsActions.clearPrompts.match(action) || PromptsActions.importPromptsSuccess.match(action), ), @@ -135,7 +138,7 @@ const importPromptsEpic: AppEpic = (action$, state$) => if (isError) { return of( UIActions.showToast({ - message: (i18n as any).t(errorsMessages.unsupportedDataFormat, { + message: translate(errorsMessages.unsupportedDataFormat, { ns: 'common', }), type: 'error', @@ -186,6 +189,97 @@ const initEpic: AppEpic = (action$) => ), ); +//TODO: added for development purpose - emulate immediate sharing with yourself +const shareFolderEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(PromptsActions.shareFolder.match), + map(({ payload }) => ({ + sharedFolderId: payload.id, + shareUniqueId: payload.shareUniqueId, + prompts: PromptsSelectors.selectPrompts(state$.value), + childFolders: PromptsSelectors.selectChildAndCurrentFoldersIdsById( + state$.value, + payload.id, + ), + folders: PromptsSelectors.selectFolders(state$.value), + })), + switchMap( + ({ sharedFolderId, shareUniqueId, prompts, childFolders, folders }) => { + const mapping = new Map(); + childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); + const newFolders = folders + .filter(({ id }) => childFolders.includes(id)) + .map(({ folderId, ...folder }) => ({ + ...folder, + id: mapping.get(folder.id), + folderId: + folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level + sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, + shareUniqueId: + folder.id === sharedFolderId ? shareUniqueId : undefined, + isShared: false, + })); + + const sharedPrompts = prompts + .filter( + (prompt) => + prompt.folderId && childFolders.includes(prompt.folderId), + ) + .map(({ folderId, ...prompt }) => ({ + ...prompt, + id: uuidv4(), + folderId: mapping.get(folderId), + isShared: false, + })); + + return concat( + of( + PromptsActions.addPrompts({ + prompts: sharedPrompts, + }), + ), + of( + PromptsActions.addFolders({ + folders: newFolders, + }), + ), + ); + }, + ), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const sharePromptEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(PromptsActions.sharePrompt.match), + map(({ payload }) => ({ + sharedPromptId: payload.promptId, + shareUniqueId: payload.shareUniqueId, + prompts: PromptsSelectors.selectPrompts(state$.value), + })), + switchMap(({ sharedPromptId, shareUniqueId, prompts }) => { + const sharedPrompts = prompts + .filter((prompt) => prompt.id === sharedPromptId) + .map(({ folderId: _, ...prompt }) => ({ + ...prompt, + id: uuidv4(), + folderId: undefined, // show on root level + sharedWithMe: true, + isShared: false, + shareUniqueId: + prompt.id === sharedPromptId ? shareUniqueId : undefined, + })); + + return concat( + of( + PromptsActions.addPrompts({ + prompts: sharedPrompts, + }), + ), + ); + }), + ); + export const PromptsEpics = combineEpics( initEpic, initPromptsEpic, @@ -196,4 +290,6 @@ export const PromptsEpics = combineEpics( exportPromptsEpic, exportPromptEpic, importPromptsEpic, + shareFolderEpic, + sharePromptEpic, ); diff --git a/src/store/prompts/prompts.reducers.ts b/src/store/prompts/prompts.reducers.ts index 25933fdd02..f5f2fac3ad 100644 --- a/src/store/prompts/prompts.reducers.ts +++ b/src/store/prompts/prompts.reducers.ts @@ -63,12 +63,47 @@ export const promptsSlice = createSlice({ return conv; }); }, + sharePrompt: ( + state, + { payload }: PayloadAction<{ promptId: string; shareUniqueId: string }>, + ) => { + state.prompts = state.prompts.map((conv) => { + if (conv.id === payload.promptId) { + return { + ...conv, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isShared: true, + }; + } + + return conv; + }); + }, + shareFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isShared: true, + }; + } + + return folder; + }); + }, updatePrompts: ( state, { payload }: PayloadAction<{ prompts: Prompt[] }>, ) => { state.prompts = payload.prompts; }, + addPrompts: (state, { payload }: PayloadAction<{ prompts: Prompt[] }>) => { + state.prompts = [...state.prompts, ...payload.prompts]; + }, clearPrompts: (state) => { state.prompts = []; state.folders = []; @@ -160,6 +195,12 @@ export const promptsSlice = createSlice({ ) => { state.folders = payload.folders; }, + addFolders: ( + state, + { payload }: PayloadAction<{ folders: FolderInterface[] }>, + ) => { + state.folders = [...state.folders, ...payload.folders]; + }, setSearchTerm: ( state, { payload }: PayloadAction<{ searchTerm: string }>, diff --git a/src/store/prompts/prompts.selectors.ts b/src/store/prompts/prompts.selectors.ts index d77838a8fd..17c02adb85 100644 --- a/src/store/prompts/prompts.selectors.ts +++ b/src/store/prompts/prompts.selectors.ts @@ -7,12 +7,11 @@ import { } from '@/src/utils/app/folders'; import { doesPromptContainSearchTerm, - getItemFilter, + getMyItemsFilters, } from '@/src/utils/app/search'; -import { EntityFilter } from '@/src/types/common'; import { Prompt } from '@/src/types/prompt'; -import { SearchFilters } from '@/src/types/search'; +import { EntityFilters, SearchFilters } from '@/src/types/search'; import { RootState } from '../index'; import { PromptsState } from './prompts.types'; @@ -26,14 +25,15 @@ export const selectPrompts = createSelector([rootSelector], (state) => { export const selectFilteredPrompts = createSelector( [ selectPrompts, - (_state, filter?: EntityFilter) => filter, - (_state, _filter, searchTerm?: string) => searchTerm, + (_state, filters: EntityFilters) => filters, + (_state, _filters, searchTerm?: string) => searchTerm, ], - (prompts, filter?, searchTerm?) => { + (prompts, filters, searchTerm?) => { return prompts.filter( (prompt) => (!searchTerm || doesPromptContainSearchTerm(prompt, searchTerm)) && - (!filter || filter(prompt)), + filters.searchFilter(prompt) && + (prompt.folderId || filters.sectionFilter(prompt)), ); }, ); @@ -67,30 +67,34 @@ export const selectFilteredFolders = createSelector( (state) => state, selectFolders, selectEmptyFolderIds, - (_state, itemFilter?: EntityFilter) => itemFilter, - (_state, _itemFilter?, searchTerm?: string) => searchTerm, - (_state, _itemFilter?, _searchTerm?, includeEmptyFolders?: boolean) => + (_state, filters: EntityFilters) => filters, + (_state, _filters?, searchTerm?: string) => searchTerm, + (_state, _filters?, _searchTerm?, includeEmptyFolders?: boolean) => includeEmptyFolders, ], ( state, folders, emptyFolderIds, - itemFilter?, + filters?, searchTerm?, includeEmptyFolders?, ) => { - const filteredPrompts = selectFilteredPrompts( - state, - itemFilter, - searchTerm, - ); + const filteredPrompts = selectFilteredPrompts(state, filters, searchTerm); const folderIds = filteredPrompts // direct parent folders .map((c) => c.folderId) .filter((fid) => fid); - // include empty folders only if not search - if (includeEmptyFolders && !searchTerm?.trim().length) { - folderIds.push(...emptyFolderIds); + + if (!searchTerm?.trim().length) { + const markedFolderIds = folders + .filter((folder) => filters?.searchFilter(folder)) + .map((f) => f.id); + folderIds.push(...markedFolderIds); + + if (includeEmptyFolders && !searchTerm?.length) { + // include empty folders only if not search + folderIds.push(...emptyFolderIds); + } } const filteredFolderIds = new Set( @@ -99,7 +103,11 @@ export const selectFilteredFolders = createSelector( ), ); - return folders.filter((folder) => filteredFolderIds.has(folder.id)); + return folders.filter( + (folder) => + (folder.folderId || filters.sectionFilter(folder)) && + filteredFolderIds.has(folder.id), + ); }, ); @@ -138,9 +146,9 @@ export const selectIsEmptySearchFilter = createSelector( (state) => state.searchFilters === SearchFilters.None, ); -export const selectItemFilter = createSelector( +export const selectMyItemsFilters = createSelector( [selectSearchFilters], - (searchFilters) => getItemFilter(searchFilters), + (searchFilters) => getMyItemsFilters(searchFilters), ); export const selectSearchedPrompts = createSelector( diff --git a/src/store/settings/settings.reducers.ts b/src/store/settings/settings.reducers.ts index 4c8beaea55..d67b533e8d 100644 --- a/src/store/settings/settings.reducers.ts +++ b/src/store/settings/settings.reducers.ts @@ -129,24 +129,30 @@ const isFeatureEnabled = createSelector( ); const isPublishingEnabled = createSelector( - [selectEnabledFeatures, (_, featureType: FeatureType) => featureType], + [selectEnabledFeatures, (_, featureType?: FeatureType) => featureType], (enabledFeatures, featureType) => { - return enabledFeatures.has( - featureType === FeatureType.Chat - ? Feature.ConversationsPublishing - : Feature.PromptsPublishing, - ); + switch (featureType) { + case FeatureType.Chat: + return enabledFeatures.has(Feature.ConversationsPublishing); + case FeatureType.Prompt: + return enabledFeatures.has(Feature.PromptsPublishing); + default: + return false; + } }, ); const isSharingEnabled = createSelector( - [selectEnabledFeatures, (_, featureType: FeatureType) => featureType], + [selectEnabledFeatures, (_, featureType?: FeatureType) => featureType], (enabledFeatures, featureType) => { - return enabledFeatures.has( - featureType === FeatureType.Chat - ? Feature.ConversationsSharing - : Feature.PromptsSharing, - ); + switch (featureType) { + case FeatureType.Chat: + return enabledFeatures.has(Feature.ConversationsSharing); + case FeatureType.Prompt: + return enabledFeatures.has(Feature.PromptsSharing); + default: + return false; + } }, ); diff --git a/src/types/chat.ts b/src/types/chat.ts index 218a1755e2..167ea213da 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -1,6 +1,5 @@ -import { Entity } from './common'; +import { ShareEntity } from './common'; import { MIMEType } from './files'; -import { ShareInterface } from './share'; export interface Attachment { index?: number; @@ -69,7 +68,7 @@ export interface RateBody { value: boolean; } -export interface Conversation extends ShareInterface, Entity { +export interface Conversation extends ShareEntity { messages: Message[]; model: ConversationEntityModel; prompt: string; diff --git a/src/types/common.ts b/src/types/common.ts index c7c22290b3..8abe9ac77d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,3 +1,5 @@ +import { ShareInterface } from './share'; + export enum EntityType { Model = 'model', Application = 'application', @@ -16,9 +18,9 @@ export enum HighlightColor { Blue = 'blue', } -export type EntityFilter = (item: T) => boolean; - export interface Entity { id: string; name: string; } + +export interface ShareEntity extends Entity, ShareInterface {} diff --git a/src/types/folder.ts b/src/types/folder.ts index 351f2c0440..b810958575 100644 --- a/src/types/folder.ts +++ b/src/types/folder.ts @@ -1,6 +1,7 @@ -import { Entity, EntityFilter } from './common'; +import { ShareEntity } from './common'; +import { EntityFilters } from './search'; -export interface FolderInterface extends Entity { +export interface FolderInterface extends ShareEntity { type: FolderType; folderId?: string; serverSynced?: boolean; @@ -12,13 +13,13 @@ export enum FolderType { File = 'file', } -export interface FolderSectionProps { +export interface FolderSectionProps { hidden?: boolean; name: string; dataQa: string; hideIfEmpty?: boolean; displayRootFiles?: boolean; - itemFilter: EntityFilter; + filters: EntityFilters; showEmptyFolders?: boolean; openByDefault?: boolean; } diff --git a/src/types/prompt.ts b/src/types/prompt.ts index 0cc7c98c3b..51c0c7f9c3 100644 --- a/src/types/prompt.ts +++ b/src/types/prompt.ts @@ -1,7 +1,6 @@ -import { Entity } from './common'; -import { ShareInterface } from './share'; +import { ShareEntity } from './common'; -export interface Prompt extends ShareInterface, Entity { +export interface Prompt extends ShareEntity { description?: string; content?: string; folderId?: string; diff --git a/src/types/search.ts b/src/types/search.ts index 1cbbf7334a..8cf85f5d79 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,5 +1,13 @@ +import { ShareEntity } from './common'; + export enum SearchFilters { None = 0, SharedByMe = 1 << 0, PublishedByMe = 1 << 1, } + +export type EntityFilter = (item: T) => boolean; +export interface EntityFilters { + sectionFilter: EntityFilter; // filter root level folders and items e.g. "Shared with me" + searchFilter: EntityFilter; // filter specific level folders and items e.g. "Shared by me" +} diff --git a/src/types/share.ts b/src/types/share.ts index 0e668260b4..ab2ebf00c0 100644 --- a/src/types/share.ts +++ b/src/types/share.ts @@ -2,4 +2,6 @@ export interface ShareInterface { isShared?: boolean; sharedWithMe?: boolean; isPublished?: boolean; + publishedWithMe?: boolean; + shareUniqueId?: string; } diff --git a/src/utils/app/clean.ts b/src/utils/app/clean.ts index a67e4fb4bc..007e183577 100644 --- a/src/utils/app/clean.ts +++ b/src/utils/app/clean.ts @@ -57,6 +57,8 @@ export const cleanConversationHistory = ( }), isShared: conversation.isShared, sharedWithMe: conversation.sharedWithMe, + isPublished: conversation.isPublished, + shareUniqueId: conversation.shareUniqueId, }; acc.push(cleanConversation); diff --git a/src/utils/app/search.ts b/src/utils/app/search.ts index 3a4d0713fa..94e7866173 100644 --- a/src/utils/app/search.ts +++ b/src/utils/app/search.ts @@ -1,9 +1,8 @@ import { Conversation } from '@/src/types/chat'; -import { EntityFilter } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { OpenAIEntityAddon, OpenAIEntityModel } from '@/src/types/openai'; import { Prompt } from '@/src/types/prompt'; -import { SearchFilters } from '@/src/types/search'; +import { EntityFilter, EntityFilters, SearchFilters } from '@/src/types/search'; import { ShareInterface } from '@/src/types/share'; export const doesConversationContainSearchTerm = ( @@ -67,15 +66,22 @@ export const doesEntityContainSearchItem = < throw new Error('unexpected entity'); }; -//TODO: for development purpose - emulate immediate sharing with yourself -export const PinnedItemsFilter: EntityFilter = (_item) => true; // !item.sharedWithMe; +export const TrueFilter: EntityFilter = () => true; -export const SharedWithMeFilter: EntityFilter = (item) => - !!item.sharedWithMe; +export const MyItemFilter: EntityFilter = (item) => + !item.sharedWithMe && !item.publishedWithMe; + +export const SharedWithMeFilter: EntityFilters = { + sectionFilter: (item) => !!item.sharedWithMe, + searchFilter: TrueFilter, +}; export const SharedByMeFilter: EntityFilter = (item) => !!item.isShared; +export const PublishedWithMeFilter: EntityFilter = (item) => + !!item.publishedWithMe; + export const PublishedByMeFilter: EntityFilter = (item) => !!item.isPublished; @@ -90,11 +96,9 @@ export const isSearchFilterSelected = ( value: SearchFilters, ) => (filter & value) === value; -export const getItemFilter = ( +export const getMyItemsFilter = ( searchFilters: SearchFilters, ): EntityFilter => { - if (searchFilters === SearchFilters.None) return PinnedItemsFilter; - const itemFilters: EntityFilter[] = []; if (isSearchFilterSelected(searchFilters, SearchFilters.SharedByMe)) { itemFilters.push(SharedByMeFilter); @@ -102,7 +106,14 @@ export const getItemFilter = ( if (isSearchFilterSelected(searchFilters, SearchFilters.PublishedByMe)) { itemFilters.push(PublishedByMeFilter); } - if (!itemFilters.length) return PinnedItemsFilter; + if (!itemFilters.length) return TrueFilter; return (item: ShareInterface) => itemFilters.some((filter) => filter(item)); }; + +export const getMyItemsFilters = ( + searchFilters: SearchFilters, +): EntityFilters => ({ + sectionFilter: MyItemFilter, + searchFilter: getMyItemsFilter(searchFilters), +}); From f24b37f162322445943e49fd37f3a0484bca13ee Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 13 Dec 2023 10:28:57 +0100 Subject: [PATCH 02/22] feat: added tooltips (#324)(Issue #191) --- src/components/Chat/ShareModal.tsx | 24 +++++++++-------- src/components/Common/ShareIcon.tsx | 40 ++++++++++++++++------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/components/Chat/ShareModal.tsx b/src/components/Chat/ShareModal.tsx index 402c8fcbf5..88d676a68b 100644 --- a/src/components/Chat/ShareModal.tsx +++ b/src/components/Chat/ShareModal.tsx @@ -122,9 +122,11 @@ export default function ShareModal({

- - {`${t('Share')}: ${entity.name.trim()}`} - + + + {`${t('Share')}: ${entity.name.trim()}`} + +

{t('share.modal.link.description')} @@ -133,13 +135,15 @@ export default function ShareModal({ {t('share.modal.link', { context: type })}

- + + +
{urlCopied ? ( diff --git a/src/components/Common/ShareIcon.tsx b/src/components/Common/ShareIcon.tsx index 13c8c5f37d..449db36cee 100644 --- a/src/components/Common/ShareIcon.tsx +++ b/src/components/Common/ShareIcon.tsx @@ -10,6 +10,8 @@ import { ShareInterface } from '@/src/types/share'; import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; +import Tooltip from './Tooltip'; + import ArrowUpRight from '@/public/images/icons/arrow-up-right.svg'; import World from '@/public/images/icons/world.svg'; @@ -56,27 +58,29 @@ export default function ShareIcon({ isPublished ? 'rounded-md' : 'rounded-sm', )} > - + + 'stroke-1 p-[1px]', + isHighlited && + getByHighlightColor( + highlightColor, + 'bg-green/15', + 'bg-violet/15', + 'bg-blue-500/20', + ), + )} + /> +
); From 4ba9a920d753088675bd3461a6d94951d06d956c Mon Sep 17 00:00:00 2001 From: Mikita Butsko Date: Wed, 13 Dec 2023 11:12:44 +0100 Subject: [PATCH 03/22] feat: use api key in headers only if jwt not presented (#277)(#283) --- src/utils/server/get-headers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/server/get-headers.ts b/src/utils/server/get-headers.ts index 168a6cb6e3..9df28d1e2e 100644 --- a/src/utils/server/get-headers.ts +++ b/src/utils/server/get-headers.ts @@ -9,14 +9,17 @@ export const getApiHeaders = ({ }): Record => { const headers: Record = { 'Content-Type': 'application/json', - 'Api-Key': process.env.OPENAI_API_KEY, }; if (chatId) { headers['X-CONVERSATION-ID'] = chatId; } + if (jwt) { headers['authorization'] = 'Bearer ' + jwt; + } else { + headers['Api-Key'] = process.env.OPENAI_API_KEY; } + if (jobTitle) { headers['X-JOB-TITLE'] = jobTitle; } From f6c8464d2a8f9f7c07369d644d91179249bc7578 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 13 Dec 2023 13:05:51 +0100 Subject: [PATCH 04/22] feat: add Publish options in context menu and Organization section (#312)(Issue #163) --- .env.development | 2 +- e2e/src/testData/expectedConstants.ts | 3 + e2e/src/tests/chatBarConversation.test.ts | 2 + e2e/src/tests/prompts.test.ts | 1 + public/images/icons/unpublish.svg | 3 + src/components/Chat/PublishModal.tsx | 120 ++++++++++++++++ src/components/Chat/ShareModal.tsx | 33 ++--- src/components/Chat/UnpublishModal.tsx | 125 +++++++++++++++++ .../Chatbar/components/ChatFolders.tsx | 19 ++- .../Chatbar/components/Conversation.tsx | 64 ++++++--- src/components/Common/FolderContextMenu.tsx | 70 +++++++++- src/components/Common/ItemContextMenu.tsx | 81 ++++++++--- src/components/Folder/Folder.tsx | 78 ++++++++--- .../Promptbar/components/Prompt.tsx | 69 ++++++---- .../Promptbar/components/PromptFolders.tsx | 15 +- src/constants/chat.ts | 10 ++ .../conversations/conversations.epics.ts | 129 ++++++++++++++++-- .../conversations/conversations.reducers.ts | 71 +++++++++- src/store/prompts/prompts.epics.ts | 116 +++++++++++++++- src/store/prompts/prompts.reducers.ts | 82 +++++++++-- src/types/share.ts | 8 ++ src/utils/app/clean.ts | 1 + src/utils/app/search.ts | 10 +- src/utils/app/share.ts | 49 +++++++ 24 files changed, 1015 insertions(+), 146 deletions(-) create mode 100644 public/images/icons/unpublish.svg create mode 100644 src/components/Chat/PublishModal.tsx create mode 100644 src/components/Chat/UnpublishModal.tsx create mode 100644 src/utils/app/share.ts diff --git a/.env.development b/.env.development index e7f21c5121..9a1be1d76e 100644 --- a/.env.development +++ b/.env.development @@ -59,7 +59,7 @@ IS_IFRAME="false" # Application UI settings -ENABLED_FEATURES="conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,prompts-sharing,input-files,attachments-manager" +ENABLED_FEATURES="conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,prompts-sharing,input-files,attachments-manager,conversations-publishing,prompts-publishing" NEXT_PUBLIC_APP_NAME="Local Development APP Name" NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT="" NEXT_PUBLIC_DEFAULT_TEMPERATURE="1" diff --git a/e2e/src/testData/expectedConstants.ts b/e2e/src/testData/expectedConstants.ts index a073a5544c..ec1a5a6236 100644 --- a/e2e/src/testData/expectedConstants.ts +++ b/e2e/src/testData/expectedConstants.ts @@ -56,6 +56,9 @@ export enum MenuOptions { export = 'Export', moveTo = 'Move to', share = 'Share', + publish = 'Publish', + update = 'Update', + unpublish = 'Unpublish', delete = 'Delete', newFolder = 'New folder', } diff --git a/e2e/src/tests/chatBarConversation.test.ts b/e2e/src/tests/chatBarConversation.test.ts index ac44180dea..6b2e9090a2 100644 --- a/e2e/src/tests/chatBarConversation.test.ts +++ b/e2e/src/tests/chatBarConversation.test.ts @@ -189,6 +189,7 @@ test('Menu for New conversation', async ({ MenuOptions.export, MenuOptions.moveTo, MenuOptions.share, + MenuOptions.publish, MenuOptions.delete, ]); }); @@ -227,6 +228,7 @@ test( MenuOptions.export, MenuOptions.moveTo, MenuOptions.share, + MenuOptions.publish, MenuOptions.delete, ]); }, diff --git a/e2e/src/tests/prompts.test.ts b/e2e/src/tests/prompts.test.ts index 5784e9d788..97f9505ab8 100644 --- a/e2e/src/tests/prompts.test.ts +++ b/e2e/src/tests/prompts.test.ts @@ -70,6 +70,7 @@ test('Prompt menu', async ({ MenuOptions.export, MenuOptions.moveTo, MenuOptions.share, + MenuOptions.publish, MenuOptions.delete, ]); }); diff --git a/public/images/icons/unpublish.svg b/public/images/icons/unpublish.svg new file mode 100644 index 0000000000..72a23d2b44 --- /dev/null +++ b/public/images/icons/unpublish.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Chat/PublishModal.tsx b/src/components/Chat/PublishModal.tsx new file mode 100644 index 0000000000..c9bedf40ec --- /dev/null +++ b/src/components/Chat/PublishModal.tsx @@ -0,0 +1,120 @@ +import { + FloatingFocusManager, + FloatingOverlay, + FloatingPortal, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; +import { IconX } from '@tabler/icons-react'; +import { ClipboardEvent, MouseEvent, useCallback, useRef } from 'react'; + +import { useTranslation } from 'next-i18next'; + +import { getPublishActionByType } from '@/src/utils/app/share'; + +import { ShareEntity } from '@/src/types/common'; +import { SharingType } from '@/src/types/share'; +import { Translation } from '@/src/types/translation'; + +import { useAppDispatch } from '@/src/store/hooks'; + +import { v4 as uuidv4 } from 'uuid'; + +interface Props { + entity: ShareEntity; + type: SharingType; + isOpen: boolean; + onClose: () => void; +} + +export default function PublishModal({ entity, isOpen, onClose, type }: Props) { + const { t } = useTranslation(Translation.SideBar); + const dispatch = useAppDispatch(); + const publishAction = getPublishActionByType(type); + const shareId = useRef(uuidv4()); + + const { refs, context } = useFloating({ + open: isOpen, + onOpenChange: () => { + onClose(); + }, + }); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); + + const handleClose = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + onClose(); + }, + [onClose], + ); + + const handlePublish = useCallback( + (e: MouseEvent | ClipboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + dispatch( + publishAction({ id: entity.id, shareUniqueId: shareId.current }), + ); + onClose(); + }, + [dispatch, entity.id, onClose, publishAction], + ); + + return ( + + + +
+ +
+

+ + {`${t('Publication request for')}: ${entity.name.trim()}`} + +

+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/components/Chat/ShareModal.tsx b/src/components/Chat/ShareModal.tsx index 88d676a68b..c737254178 100644 --- a/src/components/Chat/ShareModal.tsx +++ b/src/components/Chat/ShareModal.tsx @@ -18,36 +18,29 @@ import { import { useTranslation } from 'next-i18next'; -import { Entity } from '@/src/types/common'; +import { getShareActionByType } from '@/src/utils/app/share'; + +import { ShareEntity } from '@/src/types/common'; +import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; +import { useAppDispatch } from '@/src/store/hooks'; + import Tooltip from '../Common/Tooltip'; import { v4 as uuidv4 } from 'uuid'; -export enum SharingType { - Conversation = 'conversation', - ConversationFolder = 'conversations_folder', - Prompt = 'prompt', - PromptFolder = 'prompts_folder', -} - interface Props { - entity: Entity; + entity: ShareEntity; type: SharingType; isOpen: boolean; onClose: () => void; - onShare: (shareId: string) => void; } -export default function ShareModal({ - entity, - isOpen, - onClose, - onShare, - type, -}: Props) { +export default function ShareModal({ entity, isOpen, onClose, type }: Props) { const { t } = useTranslation(Translation.SideBar); + const dispatch = useAppDispatch(); + const shareAction = getShareActionByType(type); const copyButtonRef = useRef(null); const [urlCopied, setUrlCopied] = useState(false); const [urlWasCopied, setUrlWasCopied] = useState(false); @@ -88,11 +81,13 @@ export default function ShareModal({ }, 2000); if (!urlWasCopied) { setUrlWasCopied(true); - onShare(shareId.current); + dispatch( + shareAction({ id: entity.id, shareUniqueId: shareId.current }), + ); } }); }, - [onShare, shareId, url, urlWasCopied], + [dispatch, entity.id, shareAction, url, urlWasCopied], ); useEffect(() => () => clearTimeout(timeoutRef.current), []); diff --git a/src/components/Chat/UnpublishModal.tsx b/src/components/Chat/UnpublishModal.tsx new file mode 100644 index 0000000000..89aed4895b --- /dev/null +++ b/src/components/Chat/UnpublishModal.tsx @@ -0,0 +1,125 @@ +import { + FloatingFocusManager, + FloatingOverlay, + FloatingPortal, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; +import { IconX } from '@tabler/icons-react'; +import { ClipboardEvent, MouseEvent, useCallback, useRef } from 'react'; + +import { useTranslation } from 'next-i18next'; + +import { getUnpublishActionByType } from '@/src/utils/app/share'; + +import { Entity } from '@/src/types/common'; +import { SharingType } from '@/src/types/share'; +import { Translation } from '@/src/types/translation'; + +import { useAppDispatch } from '@/src/store/hooks'; + +import { v4 as uuidv4 } from 'uuid'; + +interface Props { + entity: Entity; + type: SharingType; + isOpen: boolean; + onClose: () => void; +} + +export default function UnpublishModal({ + entity, + isOpen, + onClose, + type, +}: Props) { + const { t } = useTranslation(Translation.SideBar); + const dispatch = useAppDispatch(); + const unpublishAction = getUnpublishActionByType(type); + const shareId = useRef(uuidv4()); + + const { refs, context } = useFloating({ + open: isOpen, + onOpenChange: () => { + onClose(); + }, + }); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); + + const handleClose = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + onClose(); + }, + [onClose], + ); + + const handleUnpublish = useCallback( + (e: MouseEvent | ClipboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + dispatch( + unpublishAction({ id: entity.id, shareUniqueId: shareId.current }), + ); + onClose(); + }, + [dispatch, entity.id, onClose, unpublishAction], + ); + + return ( + + + +
+ +
+

+ + {`${t('Unpublish')}: ${entity.name.trim()}`} + +

+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/components/Chatbar/components/ChatFolders.tsx b/src/components/Chatbar/components/ChatFolders.tsx index e75f1fe35a..6b300c1a1a 100644 --- a/src/components/Chatbar/components/ChatFolders.tsx +++ b/src/components/Chatbar/components/ChatFolders.tsx @@ -2,7 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; -import { SharedWithMeFilter } from '@/src/utils/app/search'; +import { + PublishedWithMeFilter, + SharedWithMeFilter, +} from '@/src/utils/app/search'; import { Conversation } from '@/src/types/chat'; import { FeatureType, HighlightColor } from '@/src/types/common'; @@ -282,7 +285,7 @@ export function ChatFolders() { ConversationsSelectors.selectIsEmptySearchFilter, ); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); - const commonSearchFilter = useAppSelector( + const commonItemFilter = useAppSelector( ConversationsSelectors.selectMyItemsFilters, ); @@ -293,6 +296,14 @@ export function ChatFolders() { const folderItems: FolderSectionProps[] = useMemo( () => [ + { + hidden: !isSharingEnabled || !isFilterEmpty, + name: t('Organization'), + filters: PublishedWithMeFilter, + displayRootFiles: true, + dataQa: 'published-with-me', + openByDefault: !!searchTerm.length, + }, { hidden: !isSharingEnabled || !isFilterEmpty, name: t('Shared with me'), @@ -303,13 +314,13 @@ export function ChatFolders() { }, { name: t('Pinned chats'), - filters: commonSearchFilter, + filters: commonItemFilter, showEmptyFolders: isFilterEmpty, openByDefault: true, dataQa: 'pinned-chats', }, ].filter(({ hidden }) => !hidden), - [commonSearchFilter, isFilterEmpty, isSharingEnabled, searchTerm.length, t], + [commonItemFilter, isFilterEmpty, isSharingEnabled, searchTerm.length, t], ); return ( diff --git a/src/components/Chatbar/components/Conversation.tsx b/src/components/Chatbar/components/Conversation.tsx index 03d4617f24..442a87d9f1 100644 --- a/src/components/Chatbar/components/Conversation.tsx +++ b/src/components/Chatbar/components/Conversation.tsx @@ -15,6 +15,7 @@ import classNames from 'classnames'; import { Conversation } from '@/src/types/chat'; import { FeatureType, HighlightColor } from '@/src/types/common'; +import { SharingType } from '@/src/types/share'; import { ConversationsActions, @@ -22,7 +23,6 @@ import { } from '@/src/store/conversations/conversations.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ModelsSelectors } from '@/src/store/models/models.reducers'; -import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { emptyImage } from '@/src/constants/drag-and-drop'; @@ -30,11 +30,13 @@ import { emptyImage } from '@/src/constants/drag-and-drop'; import SidebarActionButton from '@/src/components/Buttons/SidebarActionButton'; import { PlaybackIcon } from '@/src/components/Chat/PlaybackIcon'; import { ReplayAsIsIcon } from '@/src/components/Chat/ReplayAsIsIcon'; -import ShareModal, { SharingType } from '@/src/components/Chat/ShareModal'; +import ShareModal from '@/src/components/Chat/ShareModal'; import ItemContextMenu from '@/src/components/Common/ItemContextMenu'; import { MoveToFolderMobileModal } from '@/src/components/Common/MoveToFolderMobileModal'; import ShareIcon from '@/src/components/Common/ShareIcon'; +import PublishModal from '../../Chat/PublishModal'; +import UnpublishModal from '../../Chat/UnpublishModal'; import { ModelIcon } from './ModelIcon'; import { v4 as uuidv4 } from 'uuid'; @@ -112,10 +114,6 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { ConversationsSelectors.selectIsPlaybackSelectedConversations, ); - const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isSharingEnabled(state, FeatureType.Chat), - ); - const [isDeleting, setIsDeleting] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(''); @@ -124,8 +122,9 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { const inputRef = useRef(null); const dragImageRef = useRef(); const [isSharing, setIsSharing] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); + const [isUnpublishing, setIsUnpublishing] = useState(false); const [isContextMenu, setIsContextMenu] = useState(false); - const { id: conversationId } = conversation; const isSelected = selectedConversationIds.includes(conversation.id); const { refs, context } = useFloating({ @@ -262,17 +261,23 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { setIsSharing(false); }, []); - const handleShared = useCallback( - (shareUniqueId: string) => { - dispatch( - ConversationsActions.shareConversation({ - id: conversationId, - shareUniqueId, - }), - ); - }, - [conversationId, dispatch], - ); + const handleOpenPublishing: MouseEventHandler = + useCallback(() => { + setIsPublishing(true); + }, []); + + const handleClosePublishModal = useCallback(() => { + setIsPublishing(false); + }, []); + + const handleOpenUnpublishing: MouseEventHandler = + useCallback(() => { + setIsUnpublishing(true); + }, []); + + const handleCloseUnpublishModal = useCallback(() => { + setIsUnpublishing(false); + }, []); const handleMoveToFolder = useCallback( ({ @@ -405,6 +410,7 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { data-qa="dots-menu" > { } onReplay={!isPlayback ? handleStartReplay : undefined} onPlayback={handleCreatePlayback} - onOpenShareModal={isSharingEnabled ? handleOpenSharing : undefined} + onShare={handleOpenSharing} + onPublish={handleOpenPublishing} + onPublishUpdate={handleOpenPublishing} + onUnpublish={handleOpenUnpublishing} onOpenChange={setIsContextMenu} isOpen={isContextMenu} /> @@ -470,7 +479,22 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { type={SharingType.Conversation} isOpen onClose={handleCloseShareModal} - onShare={handleShared} + /> + )} + {isPublishing && ( + + )} + {isUnpublishing && ( + )}
diff --git a/src/components/Common/FolderContextMenu.tsx b/src/components/Common/FolderContextMenu.tsx index 5cc13127d2..9fe6fd9f6c 100644 --- a/src/components/Common/FolderContextMenu.tsx +++ b/src/components/Common/FolderContextMenu.tsx @@ -1,39 +1,63 @@ import { + IconClockShare, IconDots, IconFolderPlus, IconPencilMinus, IconTrashX, IconUserShare, + IconWorldShare, } from '@tabler/icons-react'; import { MouseEventHandler, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; -import { HighlightColor } from '@/src/types/common'; +import { FeatureType, HighlightColor } from '@/src/types/common'; +import { FolderInterface } from '@/src/types/folder'; import { DisplayMenuItemProps } from '@/src/types/menu'; import { Translation } from '@/src/types/translation'; +import { useAppSelector } from '@/src/store/hooks'; +import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; + import ContextMenu from './ContextMenu'; +import UnpublishIcon from '@/public/images/icons/unpublish.svg'; + interface FolderContextMenuProps { + folder: FolderInterface; + featureType?: FeatureType; onDelete?: MouseEventHandler; onRename?: MouseEventHandler; onAddFolder?: MouseEventHandler; onOpenChange?: (isOpen: boolean) => void; highlightColor: HighlightColor; isOpen?: boolean; - onOpenShareModal?: MouseEventHandler; + onShare?: MouseEventHandler; + onPublish?: MouseEventHandler; + onUnpublish?: MouseEventHandler; + onPublishUpdate?: MouseEventHandler; } export const FolderContextMenu = ({ + folder, + featureType, onDelete, onRename, onAddFolder, onOpenChange, - onOpenShareModal, + onShare, + onPublish, + onUnpublish, + onPublishUpdate, highlightColor, isOpen, }: FolderContextMenuProps) => { const { t } = useTranslation(Translation.SideBar); + const isPublishingEnabled = useAppSelector((state) => + SettingsSelectors.isPublishingEnabled(state, featureType), + ); + const isSharingEnabled = useAppSelector((state) => + SettingsSelectors.isPublishingEnabled(state, featureType), + ); const menuItems: DisplayMenuItemProps[] = useMemo( () => [ { @@ -45,10 +69,32 @@ export const FolderContextMenu = ({ }, { name: t('Share'), - display: !!onOpenShareModal, + display: isSharingEnabled && !!onShare, dataQa: 'share', Icon: IconUserShare, - onClick: onOpenShareModal, + onClick: onShare, + }, + { + name: t('Publish'), + dataQa: 'publish', + display: isPublishingEnabled && !folder.isPublished && !!onPublish, + Icon: IconWorldShare, + onClick: onPublish, + }, + { + name: t('Update'), + dataQa: 'update-publishing', + display: + isPublishingEnabled && !!folder.isPublished && !!onPublishUpdate, + Icon: IconClockShare, + onClick: onPublishUpdate, + }, + { + name: t('Unpublish'), + dataQa: 'unpublish', + display: isPublishingEnabled && !!folder.isPublished && !!onUnpublish, + Icon: UnpublishIcon, + onClick: onUnpublish, }, { name: t('Delete'), @@ -65,7 +111,19 @@ export const FolderContextMenu = ({ onClick: onAddFolder, }, ], - [t, onRename, onOpenShareModal, onDelete, onAddFolder], + [ + t, + onRename, + isSharingEnabled, + onShare, + isPublishingEnabled, + folder.isPublished, + onPublish, + onPublishUpdate, + onUnpublish, + onDelete, + onAddFolder, + ], ); if (!onDelete && !onRename && !onAddFolder) { diff --git a/src/components/Common/ItemContextMenu.tsx b/src/components/Common/ItemContextMenu.tsx index b9299ab813..3ac80e0bd6 100644 --- a/src/components/Common/ItemContextMenu.tsx +++ b/src/components/Common/ItemContextMenu.tsx @@ -1,4 +1,5 @@ import { + IconClockShare, IconDots, IconFileArrowRight, IconFolderPlus, @@ -8,6 +9,7 @@ import { IconRefreshDot, IconScale, IconUserShare, + IconWorldShare, } from '@tabler/icons-react'; import { IconTrashX } from '@tabler/icons-react'; import { MouseEventHandler, useMemo } from 'react'; @@ -16,14 +18,20 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; -import { FeatureType, HighlightColor } from '@/src/types/common'; +import { FeatureType, HighlightColor, ShareEntity } from '@/src/types/common'; import { FolderInterface } from '@/src/types/folder'; import { DisplayMenuItemProps } from '@/src/types/menu'; import { Translation } from '@/src/types/translation'; +import { useAppSelector } from '@/src/store/hooks'; +import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; + import ContextMenu from './ContextMenu'; +import UnpublishIcon from '@/public/images/icons/unpublish.svg'; + interface ItemContextMenuProps { + entity: ShareEntity; folders: FolderInterface[]; featureType: FeatureType; highlightColor: HighlightColor; @@ -35,14 +43,18 @@ interface ItemContextMenuProps { onDelete: MouseEventHandler; onRename: MouseEventHandler; onExport: MouseEventHandler; - onReplay?: MouseEventHandler; + onReplay?: MouseEventHandler; onCompare?: MouseEventHandler; - onPlayback?: MouseEventHandler; - onOpenShareModal?: MouseEventHandler; + onPlayback?: MouseEventHandler; + onShare?: MouseEventHandler; + onPublish?: MouseEventHandler; + onUnpublish?: MouseEventHandler; + onPublishUpdate?: MouseEventHandler; onOpenChange?: (isOpen: boolean) => void; } export default function ItemContextMenu({ + entity, featureType, isEmptyConversation, className, @@ -57,10 +69,19 @@ export default function ItemContextMenu({ onPlayback, onMoveToFolder, onOpenMoveToModal, - onOpenShareModal, + onShare, + onPublish, + onUnpublish, + onPublishUpdate, onOpenChange, }: ItemContextMenuProps) { const { t } = useTranslation(Translation.SideBar); + const isPublishingEnabled = useAppSelector((state) => + SettingsSelectors.isPublishingEnabled(state, featureType), + ); + const isSharingEnabled = useAppSelector((state) => + SettingsSelectors.isPublishingEnabled(state, featureType), + ); const menuItems: DisplayMenuItemProps[] = useMemo( () => [ { @@ -133,9 +154,31 @@ export default function ItemContextMenu({ { name: t('Share'), dataQa: 'share', - display: !!onOpenShareModal, + display: isSharingEnabled && !!onShare, Icon: IconUserShare, - onClick: onOpenShareModal, + onClick: onShare, + }, + { + name: t('Publish'), + dataQa: 'publish', + display: isPublishingEnabled && !entity.isPublished && !!onPublish, + Icon: IconWorldShare, + onClick: onPublish, + }, + { + name: t('Update'), + dataQa: 'update-publishing', + display: + isPublishingEnabled && !!entity.isPublished && !!onPublishUpdate, + Icon: IconClockShare, + onClick: onPublishUpdate, + }, + { + name: t('Unpublish'), + dataQa: 'unpublish', + display: isPublishingEnabled && !!entity.isPublished && !!onUnpublish, + Icon: UnpublishIcon, + onClick: onUnpublish, }, { name: t('Delete'), @@ -145,19 +188,25 @@ export default function ItemContextMenu({ }, ], [ + t, featureType, - folders, - isEmptyConversation, + onRename, onCompare, - onDelete, + isEmptyConversation, + onReplay, + onPlayback, onExport, - onMoveToFolder, onOpenMoveToModal, - onOpenShareModal, - onPlayback, - onRename, - onReplay, - t, + folders, + isSharingEnabled, + onShare, + isPublishingEnabled, + entity.isPublished, + onPublish, + onPublishUpdate, + onUnpublish, + onDelete, + onMoveToFolder, ], ); diff --git a/src/components/Folder/Folder.tsx b/src/components/Folder/Folder.tsx index 0a53b4664d..33e8bd7768 100644 --- a/src/components/Folder/Folder.tsx +++ b/src/components/Folder/Folder.tsx @@ -29,11 +29,10 @@ import { FeatureType, HighlightColor } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; +import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; -import { ConversationsActions } from '@/src/store/conversations/conversations.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; -import { PromptsActions } from '@/src/store/prompts/prompts.reducers'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions } from '@/src/store/ui/ui.reducers'; @@ -44,7 +43,9 @@ import CaretIconComponent from '@/src/components/Common/CaretIconComponent'; import CheckIcon from '../../../public/images/icons/check.svg'; import XmarkIcon from '../../../public/images/icons/xmark.svg'; -import ShareModal, { SharingType } from '../Chat/ShareModal'; +import PublishModal from '../Chat/PublishModal'; +import ShareModal from '../Chat/ShareModal'; +import UnpublishModal from '../Chat/UnpublishModal'; import { ConfirmDialog } from '../Common/ConfirmDialog'; import { FolderContextMenu } from '../Common/FolderContextMenu'; import ShareIcon from '../Common/ShareIcon'; @@ -132,6 +133,11 @@ const Folder = ({ const isSharingEnabled = useAppSelector((state) => SettingsSelectors.isSharingEnabled(state, featureType), ); + const [isPublishing, setIsPublishing] = useState(false); + const [isUnpublishing, setIsUnpublishing] = useState(false); + const isPublishingEnabled = useAppSelector((state) => + SettingsSelectors.isPublishingEnabled(state, featureType), + ); const handleOpenSharing: MouseEventHandler = useCallback((e) => { e.stopPropagation(); @@ -142,21 +148,23 @@ const Folder = ({ setIsSharing(false); }, []); - const handleShared = useCallback( - (shareUniqueId: string) => { - const shareFolder = - featureType === FeatureType.Chat - ? ConversationsActions.shareFolder - : PromptsActions.shareFolder; - dispatch( - shareFolder({ - id: currentFolder.id, - shareUniqueId, - }), - ); - }, - [currentFolder.id, dispatch, featureType], - ); + const handleOpenPublishing: MouseEventHandler = useCallback((e) => { + e.stopPropagation(); + setIsPublishing(true); + }, []); + + const handleClosePublishModal = useCallback(() => { + setIsPublishing(false); + }, []); + + const handleOpenUnpublishing: MouseEventHandler = useCallback((e) => { + e.stopPropagation(); + setIsUnpublishing(true); + }, []); + + const handleCloseUnpublishModal = useCallback(() => { + setIsUnpublishing(false); + }, []); const isFolderOpened = useMemo(() => { return openedFoldersIds.includes(currentFolder.id); @@ -529,6 +537,8 @@ const Folder = ({ )} > ({ } onDelete={onDeleteFolder && onDelete} onAddFolder={onAddFolder && onAdd} - onOpenShareModal={ - isSharingEnabled ? handleOpenSharing : undefined - } + onShare={handleOpenSharing} + onPublish={handleOpenPublishing} + onPublishUpdate={handleOpenPublishing} + onUnpublish={handleOpenUnpublishing} highlightColor={highlightColor} onOpenChange={setIsContextMenu} isOpen={isContextMenu} @@ -686,7 +697,30 @@ const Folder = ({ } isOpen onClose={handleCloseShareModal} - onShare={handleShared} + /> + )} + {isPublishing && isPublishingEnabled && ( + + )} + {isUnpublishing && isPublishingEnabled && ( + )} diff --git a/src/components/Promptbar/components/Prompt.tsx b/src/components/Promptbar/components/Prompt.tsx index b2fba62132..6445479864 100644 --- a/src/components/Promptbar/components/Prompt.tsx +++ b/src/components/Promptbar/components/Prompt.tsx @@ -12,13 +12,13 @@ import classNames from 'classnames'; import { FeatureType, HighlightColor } from '@/src/types/common'; import { Prompt } from '@/src/types/prompt'; +import { SharingType } from '@/src/types/share'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { PromptsActions, PromptsSelectors, } from '@/src/store/prompts/prompts.reducers'; -import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { stopBubbling } from '@/src/constants/chat'; @@ -26,7 +26,9 @@ import SidebarActionButton from '@/src/components/Buttons/SidebarActionButton'; import ItemContextMenu from '@/src/components/Common/ItemContextMenu'; import { MoveToFolderMobileModal } from '@/src/components/Common/MoveToFolderMobileModal'; -import ShareModal, { SharingType } from '../../Chat/ShareModal'; +import PublishModal from '../../Chat/PublishModal'; +import ShareModal from '../../Chat/ShareModal'; +import UnpublishModal from '../../Chat/UnpublishModal'; import ShareIcon from '../../Common/ShareIcon'; import { PromptModal } from './PromptModal'; @@ -52,17 +54,14 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { const isSelected = selectedPromptId === prompt.id; const showModal = useAppSelector(PromptsSelectors.selectIsEditModalOpen); - const isSharingEnabled = useAppSelector((state) => - SettingsSelectors.isSharingEnabled(state, FeatureType.Prompt), - ); - const [isDeleting, setIsDeleting] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [isShowMoveToModal, setIsShowMoveToModal] = useState(false); const [isSharing, setIsSharing] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); + const [isUnpublishing, setIsUnpublishing] = useState(false); const [isContextMenu, setIsContextMenu] = useState(false); - const { id: promptId } = prompt; const { refs, context } = useFloating({ open: isContextMenu, @@ -76,23 +75,29 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { setIsSharing(false); }, []); - const handleShared = useCallback( - (shareUniqueId: string) => { - dispatch( - PromptsActions.sharePrompt({ - promptId, - shareUniqueId, - }), - ); - }, - [dispatch, promptId], - ); - const handleOpenSharing: MouseEventHandler = useCallback(() => { setIsSharing(true); }, []); + const handleOpenPublishing: MouseEventHandler = + useCallback(() => { + setIsPublishing(true); + }, []); + + const handleClosePublishModal = useCallback(() => { + setIsPublishing(false); + }, []); + + const handleOpenUnpublishing: MouseEventHandler = + useCallback(() => { + setIsUnpublishing(true); + }, []); + + const handleCloseUnpublishModal = useCallback(() => { + setIsUnpublishing(false); + }, []); + const handleUpdate = useCallback( (prompt: Prompt) => { dispatch( @@ -270,6 +275,7 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { onClick={stopBubbling} > { setIsShowMoveToModal(true); }} highlightColor={HighlightColor.Violet} - onOpenShareModal={ - isSharingEnabled ? handleOpenSharing : undefined - } + onShare={handleOpenSharing} + onPublish={handleOpenPublishing} + onPublishUpdate={handleOpenPublishing} + onUnpublish={handleOpenUnpublishing} onOpenChange={setIsContextMenu} isOpen={isContextMenu} /> @@ -315,7 +322,23 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { type={SharingType.Prompt} isOpen onClose={handleCloseShareModal} - onShare={handleShared} + /> + )} + + {isPublishing && ( + + )} + {isUnpublishing && ( + )} diff --git a/src/components/Promptbar/components/PromptFolders.tsx b/src/components/Promptbar/components/PromptFolders.tsx index e33ed33ffc..4ee34d1f2e 100644 --- a/src/components/Promptbar/components/PromptFolders.tsx +++ b/src/components/Promptbar/components/PromptFolders.tsx @@ -2,7 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; -import { SharedWithMeFilter } from '@/src/utils/app/search'; +import { + PublishedWithMeFilter, + SharedWithMeFilter, +} from '@/src/utils/app/search'; import { FeatureType, HighlightColor } from '@/src/types/common'; import { FolderInterface, FolderSectionProps } from '@/src/types/folder'; @@ -277,12 +280,20 @@ export function PromptFolders() { const folderItems: FolderSectionProps[] = useMemo( () => [ + { + hidden: !isSharingEnabled || !isFilterEmpty, + name: t('Organization'), + filters: PublishedWithMeFilter, + displayRootFiles: true, + dataQa: 'published-with-me', + openByDefault: !!searchTerm.length, + }, { hidden: !isSharingEnabled || !isFilterEmpty, name: t('Shared with me'), filters: SharedWithMeFilter, displayRootFiles: true, - dataQa: 'share-with-me', + dataQa: 'shared-with-me', openByDefault: !!searchTerm.length, }, { diff --git a/src/constants/chat.ts b/src/constants/chat.ts index ee3ff3cf4d..d14c257e6f 100644 --- a/src/constants/chat.ts +++ b/src/constants/chat.ts @@ -1,8 +1,18 @@ import { MouseEvent } from 'react'; +import { ShareInterface } from '../types/share'; + export const modelCursorSign = '▍'; export const modelCursorSignWithBackquote = '`▍`'; export const stopBubbling = (e: MouseEvent) => { e.stopPropagation(); }; + +export const resetShareEntity: ShareInterface = { + isPublished: false, + isShared: false, + publishedWithMe: false, + sharedWithMe: false, + shareUniqueId: undefined, +}; diff --git a/src/store/conversations/conversations.epics.ts b/src/store/conversations/conversations.epics.ts index 32547141c6..862c685ff9 100644 --- a/src/store/conversations/conversations.epics.ts +++ b/src/store/conversations/conversations.epics.ts @@ -61,6 +61,7 @@ import { import { EntityType } from '@/src/types/common'; import { AppEpic } from '@/src/types/store'; +import { resetShareEntity } from './../../constants/chat'; import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-settings'; import { errorsMessages } from '@/src/constants/errors'; @@ -1086,7 +1087,8 @@ const saveFoldersEpic: AppEpic = (action$, state$) => ConversationsActions.moveFolder.match(action) || ConversationsActions.clearConversations.match(action) || ConversationsActions.importConversationsSuccess.match(action) || - ConversationsActions.addFolders.match(action), + ConversationsActions.addFolders.match(action) || + ConversationsActions.unpublishFolder.match(action), ), map(() => ({ conversationsFolders: ConversationsSelectors.selectFolders(state$.value), @@ -1139,7 +1141,8 @@ const saveConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.importConversationsSuccess.match(action) || ConversationsActions.deleteConversations.match(action) || ConversationsActions.createNewPlaybackConversation.match(action) || - ConversationsActions.addConversations.match(action), + ConversationsActions.addConversations.match(action) || + ConversationsActions.unpublishConversation.match(action), ), map(() => ConversationsSelectors.selectConversations(state$.value)), switchMap((conversations) => { @@ -1188,7 +1191,7 @@ const playbackNextMessageStartEpic: AppEpic = (action$, state$) => messages: updatedMessages, isMessageStreaming: true, model: { ...conv.model, ...assistantMessage.model }, - prompt: prompt, + prompt, temperature: temperature, selectedAddons: selectedAddons, assistantModelId: assistantModelId, @@ -1300,8 +1303,8 @@ const playbackPrevMessageEpic: AppEpic = (action$, state$) => values: { messages: updatedMessages, isMessageStreaming: false, - model: model, - prompt: prompt, + model, + prompt, temperature: temperature, selectedAddons: selectedAddons, assistantModelId: assistantModelId, @@ -1414,10 +1417,11 @@ const shareFolderEpic: AppEpic = (action$, state$) => .map(({ folderId, ...folder }) => ({ ...folder, id: mapping.get(folder.id), + originalId: folder.id, folderId: folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level + ...resetShareEntity, sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, - isShared: false, shareUniqueId: folder.id === sharedFolderId ? shareUniqueId : undefined, })); @@ -1428,11 +1432,12 @@ const shareFolderEpic: AppEpic = (action$, state$) => conversation.folderId && childFolders.includes(conversation.folderId), ) - .map(({ folderId, ...prompt }) => ({ - ...prompt, + .map(({ folderId, ...conversation }) => ({ + ...conversation, + ...resetShareEntity, id: uuidv4(), + originalId: conversation.id, folderId: mapping.get(folderId), - isShared: false, })); return concat( @@ -1465,10 +1470,112 @@ const shareConversationEpic: AppEpic = (action$, state$) => .filter((conversation) => conversation.id === sharedConversationId) .map(({ folderId: _, ...conversation }) => ({ ...conversation, + ...resetShareEntity, id: uuidv4(), + originalId: conversation.id, folderId: undefined, // show on root level sharedWithMe: true, - isShared: false, + shareUniqueId, + })); + + return concat( + of( + ConversationsActions.addConversations({ + conversations: sharedConversations, + }), + ), + ); + }), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const publishFolderEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.publishFolder.match), + map(({ payload }) => ({ + sharedFolderId: payload.id, + shareUniqueId: payload.shareUniqueId, + conversations: ConversationsSelectors.selectConversations(state$.value), + childFolders: ConversationsSelectors.selectChildAndCurrentFoldersIdsById( + state$.value, + payload.id, + ), + folders: ConversationsSelectors.selectFolders(state$.value), + })), + switchMap( + ({ + sharedFolderId, + shareUniqueId, + conversations, + childFolders, + folders, + }) => { + const mapping = new Map(); + childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); + const newFolders = folders + .filter(({ id }) => childFolders.includes(id)) + .map(({ folderId, ...folder }) => ({ + ...folder, + ...resetShareEntity, + id: mapping.get(folder.id), + originalId: folder.id, + folderId: + folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level + publishedWithMe: + folder.id === sharedFolderId || folder.sharedWithMe, + shareUniqueId: + folder.id === sharedFolderId ? shareUniqueId : undefined, + })); + + const sharedConversations = conversations + .filter( + (conversation) => + conversation.folderId && + childFolders.includes(conversation.folderId), + ) + .map(({ folderId, ...conversation }) => ({ + ...conversation, + ...resetShareEntity, + id: uuidv4(), + originalId: conversation.id, + folderId: mapping.get(folderId), + })); + + return concat( + of( + ConversationsActions.addConversations({ + conversations: sharedConversations, + }), + ), + of( + ConversationsActions.addFolders({ + folders: newFolders, + }), + ), + ); + }, + ), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const publishConversationEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(ConversationsActions.publishConversation.match), + map(({ payload }) => ({ + sharedConversationId: payload.id, + shareUniqueId: payload.shareUniqueId, + conversations: ConversationsSelectors.selectConversations(state$.value), + })), + switchMap(({ sharedConversationId, shareUniqueId, conversations }) => { + const sharedConversations = conversations + .filter((conversation) => conversation.id === sharedConversationId) + .map(({ folderId: _, ...conversation }) => ({ + ...conversation, + ...resetShareEntity, + id: uuidv4(), + originalId: conversation.id, + folderId: undefined, // show on root level + publishedWithMe: true, shareUniqueId, })); @@ -1518,4 +1625,6 @@ export const ConversationsEpics = combineEpics( shareFolderEpic, shareConversationEpic, + publishFolderEpic, + publishConversationEpic, ); diff --git a/src/store/conversations/conversations.reducers.ts b/src/store/conversations/conversations.reducers.ts index dccb967811..0b69e2e29e 100644 --- a/src/store/conversations/conversations.reducers.ts +++ b/src/store/conversations/conversations.reducers.ts @@ -13,6 +13,7 @@ import { import { SupportedExportFormats } from '@/src/types/export'; import { FolderInterface, FolderType } from '@/src/types/folder'; +import { resetShareEntity } from './../../constants/chat'; import { DEFAULT_CONVERSATION_NAME, DEFAULT_SYSTEM_PROMPT, @@ -148,6 +149,70 @@ export const conversationsSlice = createSlice({ return folder; }); }, + publishConversation: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.conversations = state.conversations.map((conv) => { + if (conv.id === payload.id) { + return { + ...conv, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isPublished: true, + }; + } + + return conv; + }); + }, + publishFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isPublished: true, + }; + } + + return folder; + }); + }, + unpublishConversation: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.conversations = state.conversations.map((conv) => { + if (conv.id === payload.id) { + return { + ...conv, + //TODO: unpublish conversation by API + isPublished: false, + }; + } + + return conv; + }); + }, + unpublishFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: unpublish folder by API + isPublished: false, + }; + } + + return folder; + }); + }, exportConversation: ( state, _action: PayloadAction<{ conversationId: string }>, @@ -174,6 +239,7 @@ export const conversationsSlice = createSlice({ ); const newConversation: Conversation = { ...payload.conversation, + ...resetShareEntity, id: uuidv4(), name: newConversationName, messages: [], @@ -191,8 +257,6 @@ export const conversationsSlice = createSlice({ activePlaybackIndex: 0, messagesStack: [], }, - isShared: false, - sharedWithMe: false, }; state.conversations = state.conversations.concat([newConversation]); state.selectedConversationsIds = [newConversation.id]; @@ -205,6 +269,7 @@ export const conversationsSlice = createSlice({ const newConversation: Conversation = { ...payload.conversation, + ...resetShareEntity, id: uuidv4(), name: newConversationName, messages: [], @@ -222,8 +287,6 @@ export const conversationsSlice = createSlice({ activeReplayIndex: 0, replayAsIs: false, }, - isShared: false, - sharedWithMe: false, }; state.conversations = state.conversations.concat([newConversation]); state.selectedConversationsIds = [newConversation.id]; diff --git a/src/store/prompts/prompts.epics.ts b/src/store/prompts/prompts.epics.ts index 43670cea75..9dd5584bde 100644 --- a/src/store/prompts/prompts.epics.ts +++ b/src/store/prompts/prompts.epics.ts @@ -12,6 +12,7 @@ import { translate } from '@/src/utils/app/translation'; import { AppEpic } from '@/src/types/store'; +import { resetShareEntity } from './../../constants/chat'; import { errorsMessages } from '@/src/constants/errors'; import { UIActions } from '../ui/ui.reducers'; @@ -28,7 +29,8 @@ const savePromptsEpic: AppEpic = (action$, state$) => PromptsActions.clearPrompts.match(action) || PromptsActions.updatePrompt.match(action) || PromptsActions.addPrompts.match(action) || - PromptsActions.importPromptsSuccess.match(action), + PromptsActions.importPromptsSuccess.match(action) || + PromptsActions.unpublishPrompt.match(action), ), map(() => PromptsSelectors.selectPrompts(state$.value)), switchMap((prompts) => { @@ -47,7 +49,8 @@ const saveFoldersEpic: AppEpic = (action$, state$) => PromptsActions.moveFolder.match(action) || PromptsActions.addFolders.match(action) || PromptsActions.clearPrompts.match(action) || - PromptsActions.importPromptsSuccess.match(action), + PromptsActions.importPromptsSuccess.match(action) || + PromptsActions.unpublishFolder.match(action), ), map(() => ({ promptsFolders: PromptsSelectors.selectFolders(state$.value), @@ -211,13 +214,14 @@ const shareFolderEpic: AppEpic = (action$, state$) => .filter(({ id }) => childFolders.includes(id)) .map(({ folderId, ...folder }) => ({ ...folder, + ...resetShareEntity, id: mapping.get(folder.id), + originalId: folder.id, folderId: folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level sharedWithMe: folder.id === sharedFolderId || folder.sharedWithMe, shareUniqueId: folder.id === sharedFolderId ? shareUniqueId : undefined, - isShared: false, })); const sharedPrompts = prompts @@ -227,9 +231,10 @@ const shareFolderEpic: AppEpic = (action$, state$) => ) .map(({ folderId, ...prompt }) => ({ ...prompt, + ...resetShareEntity, id: uuidv4(), + originalId: prompt.id, folderId: mapping.get(folderId), - isShared: false, })); return concat( @@ -253,7 +258,7 @@ const sharePromptEpic: AppEpic = (action$, state$) => action$.pipe( filter(PromptsActions.sharePrompt.match), map(({ payload }) => ({ - sharedPromptId: payload.promptId, + sharedPromptId: payload.id, shareUniqueId: payload.shareUniqueId, prompts: PromptsSelectors.selectPrompts(state$.value), })), @@ -262,10 +267,106 @@ const sharePromptEpic: AppEpic = (action$, state$) => .filter((prompt) => prompt.id === sharedPromptId) .map(({ folderId: _, ...prompt }) => ({ ...prompt, + ...resetShareEntity, id: uuidv4(), + originalId: prompt.id, folderId: undefined, // show on root level sharedWithMe: true, - isShared: false, + shareUniqueId: + prompt.id === sharedPromptId ? shareUniqueId : undefined, + })); + + return concat( + of( + PromptsActions.addPrompts({ + prompts: sharedPrompts, + }), + ), + ); + }), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const publishFolderEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(PromptsActions.publishFolder.match), + map(({ payload }) => ({ + sharedFolderId: payload.id, + shareUniqueId: payload.shareUniqueId, + prompts: PromptsSelectors.selectPrompts(state$.value), + childFolders: PromptsSelectors.selectChildAndCurrentFoldersIdsById( + state$.value, + payload.id, + ), + folders: PromptsSelectors.selectFolders(state$.value), + })), + switchMap( + ({ sharedFolderId, shareUniqueId, prompts, childFolders, folders }) => { + const mapping = new Map(); + childFolders.forEach((folderId) => mapping.set(folderId, uuidv4())); + const newFolders = folders + .filter(({ id }) => childFolders.includes(id)) + .map(({ folderId, ...folder }) => ({ + ...folder, + ...resetShareEntity, + id: mapping.get(folder.id), + originalId: folder.id, + folderId: + folder.id === sharedFolderId ? undefined : mapping.get(folderId), // show shared folder on root level + publishedWithMe: + folder.id === sharedFolderId || folder.sharedWithMe, + shareUniqueId: + folder.id === sharedFolderId ? shareUniqueId : undefined, + })); + + const sharedPrompts = prompts + .filter( + (prompt) => + prompt.folderId && childFolders.includes(prompt.folderId), + ) + .map(({ folderId, ...prompt }) => ({ + ...prompt, + ...resetShareEntity, + id: uuidv4(), + originalId: prompt.id, + folderId: mapping.get(folderId), + })); + + return concat( + of( + PromptsActions.addPrompts({ + prompts: sharedPrompts, + }), + ), + of( + PromptsActions.addFolders({ + folders: newFolders, + }), + ), + ); + }, + ), + ); + +//TODO: added for development purpose - emulate immediate sharing with yourself +const publishPromptEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(PromptsActions.publishPrompt.match), + map(({ payload }) => ({ + sharedPromptId: payload.id, + shareUniqueId: payload.shareUniqueId, + prompts: PromptsSelectors.selectPrompts(state$.value), + })), + switchMap(({ sharedPromptId, shareUniqueId, prompts }) => { + const sharedPrompts = prompts + .filter((prompt) => prompt.id === sharedPromptId) + .map(({ folderId: _, ...prompt }) => ({ + ...prompt, + ...resetShareEntity, + id: uuidv4(), + originalId: prompt.id, + folderId: undefined, // show on root level + publishedWithMe: true, shareUniqueId: prompt.id === sharedPromptId ? shareUniqueId : undefined, })); @@ -290,6 +391,9 @@ export const PromptsEpics = combineEpics( exportPromptsEpic, exportPromptEpic, importPromptsEpic, + shareFolderEpic, sharePromptEpic, + publishFolderEpic, + publishPromptEpic, ); diff --git a/src/store/prompts/prompts.reducers.ts b/src/store/prompts/prompts.reducers.ts index f5f2fac3ad..5d2d171810 100644 --- a/src/store/prompts/prompts.reducers.ts +++ b/src/store/prompts/prompts.reducers.ts @@ -52,31 +52,31 @@ export const promptsSlice = createSlice({ state, { payload }: PayloadAction<{ promptId: string; values: Partial }>, ) => { - state.prompts = state.prompts.map((conv) => { - if (conv.id === payload.promptId) { + state.prompts = state.prompts.map((prompt) => { + if (prompt.id === payload.promptId) { return { - ...conv, + ...prompt, ...payload.values, }; } - return conv; + return prompt; }); }, sharePrompt: ( state, - { payload }: PayloadAction<{ promptId: string; shareUniqueId: string }>, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, ) => { - state.prompts = state.prompts.map((conv) => { - if (conv.id === payload.promptId) { + state.prompts = state.prompts.map((prompt) => { + if (prompt.id === payload.id) { return { - ...conv, + ...prompt, //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} isShared: true, }; } - return conv; + return prompt; }); }, shareFolder: ( @@ -95,6 +95,70 @@ export const promptsSlice = createSlice({ return folder; }); }, + publishPrompt: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.prompts = state.prompts.map((prompt) => { + if (prompt.id === payload.id) { + return { + ...prompt, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isPublished: true, + }; + } + + return prompt; + }); + }, + publishFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: send newShareId to API to store {id, createdDate, type: conversation/prompt/folder} + isPublished: true, + }; + } + + return folder; + }); + }, + unpublishPrompt: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.prompts = state.prompts.map((prompt) => { + if (prompt.id === payload.id) { + return { + ...prompt, + //TODO: unpublish prompt by API + isPublished: false, + }; + } + + return prompt; + }); + }, + unpublishFolder: ( + state, + { payload }: PayloadAction<{ id: string; shareUniqueId: string }>, + ) => { + state.folders = state.folders.map((folder) => { + if (folder.id === payload.id) { + return { + ...folder, + //TODO: unpublish folder by API + isPublished: false, + }; + } + + return folder; + }); + }, updatePrompts: ( state, { payload }: PayloadAction<{ prompts: Prompt[] }>, diff --git a/src/types/share.ts b/src/types/share.ts index ab2ebf00c0..f3a3d609e8 100644 --- a/src/types/share.ts +++ b/src/types/share.ts @@ -4,4 +4,12 @@ export interface ShareInterface { isPublished?: boolean; publishedWithMe?: boolean; shareUniqueId?: string; + originalId?: string; +} + +export enum SharingType { + Conversation = 'conversation', + ConversationFolder = 'conversations_folder', + Prompt = 'prompt', + PromptFolder = 'prompts_folder', } diff --git a/src/utils/app/clean.ts b/src/utils/app/clean.ts index 007e183577..b0ec536913 100644 --- a/src/utils/app/clean.ts +++ b/src/utils/app/clean.ts @@ -58,6 +58,7 @@ export const cleanConversationHistory = ( isShared: conversation.isShared, sharedWithMe: conversation.sharedWithMe, isPublished: conversation.isPublished, + publishedWithMe: conversation.publishedWithMe, shareUniqueId: conversation.shareUniqueId, }; diff --git a/src/utils/app/search.ts b/src/utils/app/search.ts index 94e7866173..b9ee850b07 100644 --- a/src/utils/app/search.ts +++ b/src/utils/app/search.ts @@ -72,15 +72,17 @@ export const MyItemFilter: EntityFilter = (item) => !item.sharedWithMe && !item.publishedWithMe; export const SharedWithMeFilter: EntityFilters = { - sectionFilter: (item) => !!item.sharedWithMe, - searchFilter: TrueFilter, + searchFilter: (item) => !!item.sharedWithMe, + sectionFilter: TrueFilter, }; export const SharedByMeFilter: EntityFilter = (item) => !!item.isShared; -export const PublishedWithMeFilter: EntityFilter = (item) => - !!item.publishedWithMe; +export const PublishedWithMeFilter: EntityFilters = { + searchFilter: (item) => !!item.publishedWithMe, + sectionFilter: TrueFilter, +}; export const PublishedByMeFilter: EntityFilter = (item) => !!item.isPublished; diff --git a/src/utils/app/share.ts b/src/utils/app/share.ts new file mode 100644 index 0000000000..a4cd767f39 --- /dev/null +++ b/src/utils/app/share.ts @@ -0,0 +1,49 @@ +import { SharingType } from '@/src/types/share'; + +import { ConversationsActions } from '@/src/store/conversations/conversations.reducers'; +import { PromptsActions } from '@/src/store/prompts/prompts.reducers'; + +export const getShareActionByType = (type: SharingType) => { + switch (type) { + case SharingType.Conversation: + return ConversationsActions.shareConversation; + case SharingType.ConversationFolder: + return ConversationsActions.shareFolder; + case SharingType.Prompt: + return PromptsActions.sharePrompt; + case SharingType.PromptFolder: + return PromptsActions.shareFolder; + default: + throw new Error('unknown type'); + } +}; + +export const getPublishActionByType = (type: SharingType) => { + switch (type) { + case SharingType.Conversation: + return ConversationsActions.publishConversation; + case SharingType.ConversationFolder: + return ConversationsActions.publishFolder; + case SharingType.Prompt: + return PromptsActions.publishPrompt; + case SharingType.PromptFolder: + return PromptsActions.publishFolder; + default: + throw new Error('unknown type'); + } +}; + +export const getUnpublishActionByType = (type: SharingType) => { + switch (type) { + case SharingType.Conversation: + return ConversationsActions.unpublishConversation; + case SharingType.ConversationFolder: + return ConversationsActions.unpublishFolder; + case SharingType.Prompt: + return PromptsActions.unpublishPrompt; + case SharingType.PromptFolder: + return PromptsActions.unpublishFolder; + default: + throw new Error('unknown type'); + } +}; From 24bc24872ce2b4c0a532cc10c83b42d31e3e9c9c Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 13 Dec 2023 14:29:29 +0100 Subject: [PATCH 05/22] fix: fix share icon tooltip and focus for filters (#327)(Issues #191, #276) --- src/components/Common/ShareIcon.tsx | 2 +- src/components/Search/SearchFilterRenderer.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Common/ShareIcon.tsx b/src/components/Common/ShareIcon.tsx index 449db36cee..706c81ba22 100644 --- a/src/components/Common/ShareIcon.tsx +++ b/src/components/Common/ShareIcon.tsx @@ -58,7 +58,7 @@ export default function ShareIcon({ isPublished ? 'rounded-md' : 'rounded-sm', )} > - + Date: Wed, 13 Dec 2023 15:50:35 +0100 Subject: [PATCH 06/22] feat: switch off powered by header (#328) (#329) --- next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.js b/next.config.js index 24a031335a..59e2c84d6b 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,7 @@ class BasePathResolver { /** @type {import('next').NextConfig} */ const nextConfig = { i18n, + poweredByHeader: false, reactStrictMode: true, experimental: { instrumentationHook: true, From 6db3f3dbb5b2dd21e353964c24d8e3b650d6849d Mon Sep 17 00:00:00 2001 From: Denys_Kolomiitsev <143205282+denys-kolomiitsev@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:54:51 +0100 Subject: [PATCH 07/22] feat: made side panels resizable(#128) (#306) --- e2e/src/tests/folderPrompts.test.ts | 7 +- e2e/src/ui/selectors/sideBarSelectors.ts | 4 +- e2e/src/ui/webElements/folderPrompts.ts | 12 - e2e/src/ui/webElements/promptBar.ts | 18 ++ package-lock.json | 10 + package.json | 1 + src/components/Chat/ConversationSettings.tsx | 2 +- src/components/Common/SidebarMenu.tsx | 50 +++- src/components/Sidebar/ResizeIcons.tsx | 21 ++ src/components/Sidebar/Sidebar.tsx | 228 ++++++++++++++---- src/constants/default-ui-settings.ts | 2 + src/store/ui/ui.epics.ts | 22 ++ src/store/ui/ui.reducers.ts | 22 ++ src/types/storage.ts | 28 +-- src/utils/app/data/data-service.ts | 77 ++++-- .../app/data/storages/browser-storage.ts | 42 ++-- 16 files changed, 415 insertions(+), 131 deletions(-) create mode 100644 src/components/Sidebar/ResizeIcons.tsx create mode 100644 src/constants/default-ui-settings.ts diff --git a/e2e/src/tests/folderPrompts.test.ts b/e2e/src/tests/folderPrompts.test.ts index 7fd242f995..e6bc5afe67 100644 --- a/e2e/src/tests/folderPrompts.test.ts +++ b/e2e/src/tests/folderPrompts.test.ts @@ -169,17 +169,18 @@ test('Prompt is moved out of the folder via drag&drop', async ({ folderPrompts, localStorageManager, prompts, + promptBar, setTestIds, }) => { setTestIds('EPMRTC-961'); const promptInFolder = promptData.prepareDefaultPromptInFolder(); await localStorageManager.setFolders(promptInFolder.folders); await localStorageManager.setPrompts(promptInFolder.prompts[0]); + await localStorageManager.setOpenedFolders(promptInFolder.folders); await dialHomePage.openHomePage(); - await dialHomePage.waitForPageLoaded(); - await folderPrompts.expandCollapseFolder(promptInFolder.folders.name); - await folderPrompts.dropPromptFromFolder( + await dialHomePage.waitForPageLoaded({ isNewConversationVisible: true }); + await promptBar.dropPromptFromFolder( promptInFolder.folders.name, promptInFolder.prompts[0].name, ); diff --git a/e2e/src/ui/selectors/sideBarSelectors.ts b/e2e/src/ui/selectors/sideBarSelectors.ts index 71a564d759..5a5465de3d 100644 --- a/e2e/src/ui/selectors/sideBarSelectors.ts +++ b/e2e/src/ui/selectors/sideBarSelectors.ts @@ -1,8 +1,8 @@ import { Attributes, Tags } from '@/e2e/src/ui/domData'; export const SideBarSelectors = { - chatBar: '[data-qa="sidebar"].fixed.left-0', - promptBar: '[data-qa="sidebar"].fixed.right-0', + chatBar: '[data-qa="chatbar"]', + promptBar: '[data-qa="promptbar"]', folder: '[data-qa="folder"]', dotsMenu: '[aria-haspopup="menu"]', renameInput: (value: string) => diff --git a/e2e/src/ui/webElements/folderPrompts.ts b/e2e/src/ui/webElements/folderPrompts.ts index ce5cdf49b4..e68bfe63d3 100644 --- a/e2e/src/ui/webElements/folderPrompts.ts +++ b/e2e/src/ui/webElements/folderPrompts.ts @@ -46,16 +46,4 @@ export class FolderPrompts extends Folders { await this.folderPromptDotsMenu(folderName, promptName).click(); await this.getDropdownMenu().waitForState(); } - - public async dropPromptFromFolder(folderName: string, promptName: string) { - const folderPrompt = await this.getFolderPrompt(folderName, promptName); - await folderPrompt.hover(); - await this.page.mouse.down(); - const foldersBounding = await this.getElementBoundingBox(); - await this.page.mouse.move( - foldersBounding!.x, - foldersBounding!.y + 1.5 * foldersBounding!.height, - ); - await this.page.mouse.up(); - } } diff --git a/e2e/src/ui/webElements/promptBar.ts b/e2e/src/ui/webElements/promptBar.ts index f0c6184122..b524584998 100644 --- a/e2e/src/ui/webElements/promptBar.ts +++ b/e2e/src/ui/webElements/promptBar.ts @@ -28,6 +28,9 @@ export class PromptBar extends BaseElement { ChatBarSelectors.exportPrompts, ); public importButton = this.getChildElementBySelector(SideBarSelectors.import); + public draggableArea = this.getChildElementBySelector( + SideBarSelectors.draggableArea, + ); getFolderPrompts(): FolderPrompts { if (!this.folderPrompts) { @@ -90,4 +93,19 @@ export class PromptBar extends BaseElement { public async deleteAllPrompts() { await this.deleteAllPromptsButton.click(); } + + public async dropPromptFromFolder(folderName: string, promptName: string) { + const folderPrompt = await this.getFolderPrompts().getFolderPrompt( + folderName, + promptName, + ); + await folderPrompt.hover(); + await this.page.mouse.down(); + const draggableBounding = await this.draggableArea.getElementBoundingBox(); + await this.page.mouse.move( + draggableBounding!.x + draggableBounding!.width / 2, + draggableBounding!.y + draggableBounding!.height / 2, + ); + await this.page.mouse.up(); + } } diff --git a/package-lock.json b/package-lock.json index e9d86cbfe9..5ab8ddccf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "pino": "^8.15.1", "pino-pretty": "^10.2.0", "rc-slider": "^10.2.1", + "re-resizable": "^6.9.11", "react": "18.2.0", "react-dom": "18.2.0", "react-hot-toast": "^2.4.0", @@ -10909,6 +10910,15 @@ "react-dom": ">=16.9.0" } }, + "node_modules/re-resizable": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.11.tgz", + "integrity": "sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "license": "MIT", diff --git a/package.json b/package.json index 15a35919d8..ef6b04297d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "pino": "^8.15.1", "pino-pretty": "^10.2.0", "rc-slider": "^10.2.1", + "re-resizable": "^6.9.11", "react": "18.2.0", "react-dom": "18.2.0", "react-hot-toast": "^2.4.0", diff --git a/src/components/Chat/ConversationSettings.tsx b/src/components/Chat/ConversationSettings.tsx index 729c9cf034..884a7c79e0 100644 --- a/src/components/Chat/ConversationSettings.tsx +++ b/src/components/Chat/ConversationSettings.tsx @@ -121,7 +121,7 @@ export const ConversationSettings = ({ }); ref.current && resizeObserver.observe(ref.current); - () => { + return () => { resizeObserver.disconnect(); }; }, [ref]); diff --git a/src/components/Common/SidebarMenu.tsx b/src/components/Common/SidebarMenu.tsx index f71f93d48e..abd42cf45e 100644 --- a/src/components/Common/SidebarMenu.tsx +++ b/src/components/Common/SidebarMenu.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; @@ -10,6 +10,10 @@ import Tooltip from '@/src/components/Common/Tooltip'; import ContextMenu from './ContextMenu'; +const ICON_WIDTH = 24; +const ITEM_PADDING = 5; +const ITEMS_GAP_IN_PIXELS = 8; +const ITEM_WIDTH = ITEM_PADDING * 2 + ICON_WIDTH + ITEMS_GAP_IN_PIXELS; export function SidebarMenuItemRenderer(props: MenuItemRendererProps) { const { Icon, @@ -20,6 +24,7 @@ export function SidebarMenuItemRenderer(props: MenuItemRendererProps) { className, childMenuItems, } = props; + const item = ( ); @@ -55,19 +67,43 @@ export function SidebarMenuItemRenderer(props: MenuItemRendererProps) { export default function SidebarMenu({ menuItems, highlightColor, - displayMenuItemCount = 5, // calculate in future based on width of container + displayMenuItemCount = 5, isOpen, onOpenChange, }: MenuProps) { + const [displayItemsCount, setDisplayItemsCount] = + useState(displayMenuItemCount); + const containerRef = useRef(null); const [visibleItems, hiddenItems] = useMemo(() => { const displayedItems = menuItems.filter(({ display = true }) => display); - const visibleItems = displayedItems.slice(0, displayMenuItemCount); - const hiddenItems = displayedItems.slice(displayMenuItemCount); + const visibleItems = displayedItems.slice(0, displayItemsCount); + const hiddenItems = displayedItems.slice(displayItemsCount); return [visibleItems, hiddenItems]; - }, [displayMenuItemCount, menuItems]); + }, [menuItems, displayItemsCount]); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.contentBoxSize) { + const itemsContainerWidth = entry.contentBoxSize[0].inlineSize; + + setDisplayItemsCount(itemsContainerWidth / ITEM_WIDTH); + } + } + }); + const containerElement = containerRef.current; + containerElement && resizeObserver.observe(containerElement); + + return () => { + containerElement && resizeObserver.observe(containerElement); + }; + }, []); return ( -
+
{visibleItems.map(({ CustomTriggerRenderer, ...props }) => { const Trigger = CustomTriggerRenderer ? ( { + return ( +
+ +
+ ); +}; + +export const RightSideResizeIcon = ({ className }: ResizeIconProps) => { + return ( +
+ +
+ ); +}; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index b1afae53ca..1551a9826e 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,16 +1,29 @@ -import { ReactNode, useCallback, useRef, useState } from 'react'; +import { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; -import { FeatureType } from '@/src/types/common'; +import { getByHighlightColor } from '@/src/utils/app/folders'; + +import { FeatureType, HighlightColor } from '@/src/types/common'; import { SearchFilters } from '@/src/types/search'; import { Translation } from '@/src/types/translation'; +import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; + +import { + HEADER_HEIGHT, + SIDEBAR_MIN_WIDTH, +} from '@/src/constants/default-ui-settings'; + import { NoData } from '../Common/NoData'; import { NoResultsFound } from '../Common/NoResultsFound'; import Search from '../Search'; +import { LeftSideResizeIcon, RightSideResizeIcon } from './ResizeIcons'; + +import { Resizable, ResizableProps } from 're-resizable'; interface Props { isOpen: boolean; @@ -46,8 +59,56 @@ const Sidebar = ({ }: Props) => { const { t } = useTranslation(Translation.PromptBar); const [isDraggingOver, setIsDraggingOver] = useState(false); + const [isResizing, setIsResizing] = useState(false); const dragDropElement = useRef(null); - const draggingColor = side === 'left' ? 'bg-green/15' : 'bg-violet/15'; + const sideBarElementRef = useRef(null); + const dispatch = useAppDispatch(); + const chatbarWidth = useAppSelector(UISelectors.selectChatbarWidth); + const promptbarWidth = useAppSelector(UISelectors.selectPromptbarWidth); + + const isLeftSidebar = side === 'left'; + const isRightSidebar = side === 'right'; + const dataQa = useMemo( + () => (isLeftSidebar ? 'chatbar' : 'promptbar'), + [isLeftSidebar], + ); + const highlightColor = isLeftSidebar + ? HighlightColor.Green + : HighlightColor.Violet; + const draggingColor = getByHighlightColor( + highlightColor, + 'bg-green/15', + 'bg-violet/15', + ); + + const chatbarColor = classNames( + 'xl:bg-green xl:text-green xl:dark:bg-green', + isResizing ? 'bg-green text-green dark:bg-green' : '', + ); + + const promptbarColor = classNames( + 'xl:bg-violet xl:text-violet xl:dark:bg-violet', + isResizing ? 'bg-violet text-violet dark:bg-violet' : '', + ); + + const resizeTriggerColor = getByHighlightColor( + highlightColor, + chatbarColor, + promptbarColor, + ); + + const resizeTriggerClassName = classNames( + 'invisible h-full w-0.5 bg-gray-500 text-gray-500 group-hover:visible dark:bg-gray-600 md:visible', + resizeTriggerColor, + isResizing ? 'xl:visible' : 'xl:invisible', + ); + + const SIDEBAR_DEFAULT_WIDTH = useMemo( + () => (isLeftSidebar ? chatbarWidth : promptbarWidth), + [isLeftSidebar, chatbarWidth, promptbarWidth], + ); + const SIDEBAR_HEIGHT = `calc(100%-${HEADER_HEIGHT}px)`; + const allowDrop = useCallback((e: any) => { e.preventDefault(); }, []); @@ -71,55 +132,124 @@ const Sidebar = ({ } }, []); + const onResizeStart = useCallback(() => { + setIsResizing(true); + }, []); + + const onResizeStop = useCallback(() => { + setIsResizing(false); + const resizibleWidth = + sideBarElementRef.current?.resizable?.getClientRects()[0].width && + Math.round( + sideBarElementRef.current?.resizable?.getClientRects()[0].width, + ); + + const width = resizibleWidth ?? SIDEBAR_MIN_WIDTH; + + if (isLeftSidebar) { + dispatch(UIActions.setChatbarWidth(width)); + } + + if (isRightSidebar) { + dispatch(UIActions.setPromptbarWidth(width)); + } + }, [dispatch, isLeftSidebar, isRightSidebar]); + + const resizeSettings: ResizableProps = useMemo(() => { + return { + defaultSize: { + width: SIDEBAR_DEFAULT_WIDTH ?? SIDEBAR_MIN_WIDTH, + height: SIDEBAR_HEIGHT, + }, + enable: { + top: false, + right: isLeftSidebar, + bottom: false, + left: isRightSidebar, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }, + handleClasses: { + right: 'group invisible md:visible', + left: 'group invisible md:visible', + }, + handleStyles: { right: { right: '-11px' }, left: { left: '-3px' } }, + handleComponent: { + left: , + right: , + }, + onResizeStart: onResizeStart, + onResizeStop: onResizeStop, + }; + }, [ + onResizeStart, + onResizeStop, + resizeTriggerClassName, + isLeftSidebar, + isRightSidebar, + SIDEBAR_HEIGHT, + SIDEBAR_DEFAULT_WIDTH, + ]); + + const resizableWrapperClassName = classNames( + `!fixed top-12 z-40 flex h-[calc(100%-48px)] min-w-[260px] border-gray-300 dark:border-gray-900 md:max-w-[45%] xl:!relative xl:top-0 xl:h-full`, + isLeftSidebar ? 'left-0 border-r' : 'right-0 border-l', + ); + return isOpen ? ( -
- - {actionButtons} -
- {folderComponent} - - {filteredItems?.length > 0 ? ( -
{ - setIsDraggingOver(false); - handleDrop(e); - }} - onDragOver={allowDrop} - onDragEnter={highlightDrop} - onDragLeave={removeHighlight} - data-qa="draggable-area" - > - {itemComponent} -
- ) : searchTerm.length ? ( -
- -
- ) : ( -
- -
- )} +
+ + + {actionButtons} + +
+ {folderComponent} + + {filteredItems?.length > 0 ? ( +
{ + setIsDraggingOver(false); + handleDrop(e); + }} + onDragOver={allowDrop} + onDragEnter={highlightDrop} + onDragLeave={removeHighlight} + data-qa="draggable-area" + > + {itemComponent} +
+ ) : searchTerm.length ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ {footerComponent}
- {footerComponent} -
+ ) : null; }; diff --git a/src/constants/default-ui-settings.ts b/src/constants/default-ui-settings.ts new file mode 100644 index 0000000000..04c832c456 --- /dev/null +++ b/src/constants/default-ui-settings.ts @@ -0,0 +1,2 @@ +export const SIDEBAR_MIN_WIDTH = 260; +export const HEADER_HEIGHT = 48; diff --git a/src/store/ui/ui.epics.ts b/src/store/ui/ui.epics.ts index ba0ad52df8..58502f97d4 100644 --- a/src/store/ui/ui.epics.ts +++ b/src/store/ui/ui.epics.ts @@ -31,6 +31,8 @@ const initEpic: AppEpic = (action$) => showPromptbar: DataService.getShowPromptbar(), openedFoldersIds: DataService.getOpenedFolderIds(), textOfClosedAnnouncement: DataService.getClosedAnnouncement(), + chatbarWidth: DataService.getChatbarWidth(), + promptbarWidth: DataService.getPromptbarWidth(), }), ), switchMap( @@ -40,6 +42,8 @@ const initEpic: AppEpic = (action$) => showChatbar, showPromptbar, textOfClosedAnnouncement, + chatbarWidth, + promptbarWidth, }) => { const actions = []; @@ -52,6 +56,8 @@ const initEpic: AppEpic = (action$) => announcement: textOfClosedAnnouncement, }), ); + actions.push(UIActions.setChatbarWidth(chatbarWidth)); + actions.push(UIActions.setPromptbarWidth(promptbarWidth)); return concat(actions); }, @@ -151,6 +157,20 @@ const saveOpenedFoldersIdsEpic: AppEpic = (action$, state$) => ignoreElements(), ); +const saveChatbarWidthEpic: AppEpic = (action$) => + action$.pipe( + filter(UIActions.setChatbarWidth.match), + switchMap(({ payload }) => DataService.setChatbarWidth(payload)), + ignoreElements(), + ); + +const savePromptbarWidthEpic: AppEpic = (action$) => + action$.pipe( + filter(UIActions.setPromptbarWidth.match), + switchMap(({ payload }) => DataService.setPromptbarWidth(payload)), + ignoreElements(), + ); + const UIEpics = combineEpics( initEpic, saveThemeEpic, @@ -159,6 +179,8 @@ const UIEpics = combineEpics( showToastErrorEpic, saveOpenedFoldersIdsEpic, closeAnnouncementEpic, + saveChatbarWidthEpic, + savePromptbarWidthEpic, ); export default UIEpics; diff --git a/src/store/ui/ui.reducers.ts b/src/store/ui/ui.reducers.ts index 620a2b2427..760013b502 100644 --- a/src/store/ui/ui.reducers.ts +++ b/src/store/ui/ui.reducers.ts @@ -2,6 +2,8 @@ import { PayloadAction, createSelector, createSlice } from '@reduxjs/toolkit'; import { Theme } from '@/src/types/settings'; +import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; + import { RootState } from '..'; export interface UIState { @@ -13,6 +15,8 @@ export interface UIState { isCompareMode: boolean; openedFoldersIds: string[]; textOfClosedAnnouncement?: string | undefined; + chatbarWidth?: number; + promptbarWidth?: number; } const initialState: UIState = { @@ -24,6 +28,8 @@ const initialState: UIState = { isCompareMode: false, openedFoldersIds: [], textOfClosedAnnouncement: undefined, + chatbarWidth: SIDEBAR_MIN_WIDTH, + promptbarWidth: SIDEBAR_MIN_WIDTH, }; export const uiSlice = createSlice({ @@ -34,6 +40,12 @@ export const uiSlice = createSlice({ setTheme: (state, { payload }: PayloadAction) => { state.theme = payload; }, + setChatbarWidth: (state, { payload }: PayloadAction>) => { + state.chatbarWidth = payload; + }, + setPromptbarWidth: (state, { payload }: PayloadAction>) => { + state.promptbarWidth = payload; + }, setShowChatbar: ( state, { payload }: PayloadAction, @@ -154,6 +166,14 @@ const selectTextOfClosedAnnouncement = createSelector( }, ); +const selectChatbarWidth = createSelector([rootSelector], (state) => { + return state.chatbarWidth; +}); + +const selectPromptbarWidth = createSelector([rootSelector], (state) => { + return state.promptbarWidth; +}); + export const UIActions = uiSlice.actions; export const UISelectors = { @@ -166,4 +186,6 @@ export const UISelectors = { selectOpenedFoldersIds, selectIsFolderOpened, selectTextOfClosedAnnouncement, + selectChatbarWidth, + selectPromptbarWidth, }; diff --git a/src/types/storage.ts b/src/types/storage.ts index 4f62d1a8ac..e803e915a6 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -3,24 +3,24 @@ import { Observable } from 'rxjs'; import { Conversation } from './chat'; import { FolderInterface } from './folder'; import { Prompt } from './prompt'; -import { Theme } from './settings'; export type StorageType = 'browserStorage' | 'api' | 'apiMock'; -// keep track of local storage schema -export interface LocalStorage { - conversationHistory: Conversation[]; - selectedConversationIds: string[]; - theme: Theme; - // added folders (3/23/23) - folders: FolderInterface[]; - // added prompts (3/26/23) - prompts: Prompt[]; - // added showChatbar and showPromptbar (3/26/23) - showChatbar: boolean; - showPromptbar: boolean; +export enum UIStorageKeys { + Prompts = 'prompts', + ConversationHistory = 'conversationHistory', + Folders = 'folders', + SelectedConversationIds = 'selectedConversationIds', + RecentModelsIds = 'recentModelsIds', + RecentAddonsIds = 'recentAddonsIds', + Settings = 'settings', + ShowChatbar = 'showChatbar', + ShowPromptbar = 'showPromptbar', + ChatbarWidth = 'chatbarWidth', + PromptbarWidth = 'promptbarWidth', + OpenedFoldersIds = 'openedFoldersIds', + TextOfClosedAnnouncement = 'textOfClosedAnnouncement', } - export interface DialStorage { getConversationsFolders(): Observable; setConversationsFolders(folders: FolderInterface[]): Observable; diff --git a/src/utils/app/data/data-service.ts b/src/utils/app/data/data-service.ts index dcf2312f94..74d78fb72e 100644 --- a/src/utils/app/data/data-service.ts +++ b/src/utils/app/data/data-service.ts @@ -11,7 +11,9 @@ import { import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; import { Theme } from '@/src/types/settings'; -import { DialStorage } from '@/src/types/storage'; +import { DialStorage, UIStorageKeys } from '@/src/types/storage'; + +import { SIDEBAR_MIN_WIDTH } from '@/src/constants/default-ui-settings'; import { getPathNameId, getRelativePath } from '../file'; import { ApiMockStorage } from './storages/api-mock-storage'; @@ -60,70 +62,107 @@ export class DataService { return this.getDataStorage().setConversations(conversations); } public static getSelectedConversationsIds(): Observable { - return BrowserStorage.getData('selectedConversationIds', []); + return BrowserStorage.getData(UIStorageKeys.SelectedConversationIds, []); } public static setSelectedConversationsIds( selectedConversationsIds: string[], ): Observable { return BrowserStorage.setData( - 'selectedConversationIds', + UIStorageKeys.SelectedConversationIds, selectedConversationsIds, ); } public static getRecentModelsIds(): Observable { - return BrowserStorage.getData('recentModelsIds', []); + return BrowserStorage.getData(UIStorageKeys.RecentModelsIds, []); } public static setRecentModelsIds( recentModelsIds: string[], ): Observable { - return BrowserStorage.setData('recentModelsIds', recentModelsIds); + return BrowserStorage.setData( + UIStorageKeys.RecentModelsIds, + recentModelsIds, + ); } public static getRecentAddonsIds(): Observable { - return BrowserStorage.getData('recentAddonsIds', []); + return BrowserStorage.getData(UIStorageKeys.RecentAddonsIds, []); } public static setRecentAddonsIds( recentAddonsIds: string[], ): Observable { - return BrowserStorage.setData('recentAddonsIds', recentAddonsIds); + return BrowserStorage.setData( + UIStorageKeys.RecentAddonsIds, + recentAddonsIds, + ); } public static getTheme(): Observable { - return BrowserStorage.getData('settings', { theme: 'dark' as Theme }).pipe( - map((settings) => settings.theme), - ); + return BrowserStorage.getData(UIStorageKeys.Settings, { + theme: 'dark' as Theme, + }).pipe(map((settings) => settings.theme)); } + public static setTheme(theme: Theme): Observable { - return BrowserStorage.setData('settings', { theme }); + return BrowserStorage.setData(UIStorageKeys.Settings, { theme }); + } + + public static getChatbarWidth(): Observable { + return BrowserStorage.getData( + UIStorageKeys.ChatbarWidth, + SIDEBAR_MIN_WIDTH, + ); + } + + public static setChatbarWidth(chatBarWidth: number): Observable { + return BrowserStorage.setData(UIStorageKeys.ChatbarWidth, chatBarWidth); } + + public static getPromptbarWidth(): Observable { + return BrowserStorage.getData( + UIStorageKeys.PromptbarWidth, + SIDEBAR_MIN_WIDTH, + ); + } + + public static setPromptbarWidth(promptBarWidth: number): Observable { + return BrowserStorage.setData(UIStorageKeys.PromptbarWidth, promptBarWidth); + } + public static getShowChatbar(): Observable { - return BrowserStorage.getData('showChatbar', true); + return BrowserStorage.getData(UIStorageKeys.ShowChatbar, true); } + public static setShowChatbar(showChatbar: boolean): Observable { - return BrowserStorage.setData('showChatbar', showChatbar); + return BrowserStorage.setData(UIStorageKeys.ShowChatbar, showChatbar); } + public static getShowPromptbar(): Observable { - return BrowserStorage.getData('showPromptbar', true); + return BrowserStorage.getData(UIStorageKeys.ShowPromptbar, true); } + public static setShowPromptbar(showPromptbar: boolean): Observable { - return BrowserStorage.setData('showPromptbar', showPromptbar); + return BrowserStorage.setData(UIStorageKeys.ShowPromptbar, showPromptbar); } + public static getOpenedFolderIds(): Observable { - return BrowserStorage.getData('openedFoldersIds', []); + return BrowserStorage.getData(UIStorageKeys.OpenedFoldersIds, []); } public static setOpenedFolderIds( openedFolderIds: string[], ): Observable { - return BrowserStorage.setData('openedFoldersIds', openedFolderIds); + return BrowserStorage.setData( + UIStorageKeys.OpenedFoldersIds, + openedFolderIds, + ); } public static getClosedAnnouncement(): Observable { - return BrowserStorage.getData('textOfClosedAnnouncement', ''); + return BrowserStorage.getData(UIStorageKeys.TextOfClosedAnnouncement, ''); } public static setClosedAnnouncement( closedAnnouncementText: string | undefined, ): Observable { return BrowserStorage.setData( - 'textOfClosedAnnouncement', + UIStorageKeys.TextOfClosedAnnouncement, closedAnnouncementText || '', ); } diff --git a/src/utils/app/data/storages/browser-storage.ts b/src/utils/app/data/storages/browser-storage.ts index 97bcceb567..b9d4fff2fc 100644 --- a/src/utils/app/data/storages/browser-storage.ts +++ b/src/utils/app/data/storages/browser-storage.ts @@ -6,26 +6,13 @@ import { Observable, map, of, switchMap, throwError } from 'rxjs'; import { Conversation } from '@/src/types/chat'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt } from '@/src/types/prompt'; -import { DialStorage } from '@/src/types/storage'; +import { DialStorage, UIStorageKeys } from '@/src/types/storage'; import { errorsMessages } from '@/src/constants/errors'; import { cleanConversationHistory } from '../../clean'; import { isLocalStorageEnabled } from '../storage'; -type UIStorageKeys = - | 'prompts' - | 'conversationHistory' - | 'folders' - | 'selectedConversationIds' - | 'recentModelsIds' - | 'recentAddonsIds' - | 'settings' - | 'showChatbar' - | 'showPromptbar' - | 'openedFoldersIds' - | 'textOfClosedAnnouncement'; - export class BrowserStorage implements DialStorage { private static storage: globalThis.Storage | undefined; @@ -38,25 +25,28 @@ export class BrowserStorage implements DialStorage { } getConversations(): Observable { - return BrowserStorage.getData('conversationHistory', []).pipe( + return BrowserStorage.getData(UIStorageKeys.ConversationHistory, []).pipe( map((conversations) => cleanConversationHistory(conversations)), ); } setConversations(conversations: Conversation[]): Observable { - return BrowserStorage.setData('conversationHistory', conversations); + return BrowserStorage.setData( + UIStorageKeys.ConversationHistory, + conversations, + ); } getPrompts(): Observable { - return BrowserStorage.getData('prompts', []); + return BrowserStorage.getData(UIStorageKeys.Prompts, []); } setPrompts(prompts: Prompt[]): Observable { - return BrowserStorage.setData('prompts', prompts); + return BrowserStorage.setData(UIStorageKeys.Prompts, prompts); } getConversationsFolders() { - return BrowserStorage.getData('folders', []).pipe( + return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe( map((folders: FolderInterface[]) => { return folders.filter((folder) => folder.type === FolderType.Chat); }), @@ -64,7 +54,7 @@ export class BrowserStorage implements DialStorage { } getPromptsFolders() { - return BrowserStorage.getData('folders', []).pipe( + return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe( map((folders: FolderInterface[]) => { return folders.filter((folder) => folder.type === FolderType.Prompt); }), @@ -74,25 +64,29 @@ export class BrowserStorage implements DialStorage { setConversationsFolders( conversationFolders: FolderInterface[], ): Observable { - return BrowserStorage.getData('folders', []).pipe( + return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe( map((items: FolderInterface[]) => items.filter((item) => item.type !== FolderType.Chat), ), map((promptsFolders: FolderInterface[]) => { return promptsFolders.concat(conversationFolders); }), - switchMap((folders) => BrowserStorage.setData('folders', folders)), + switchMap((folders) => + BrowserStorage.setData(UIStorageKeys.Folders, folders), + ), ); } setPromptsFolders(promptsFolders: FolderInterface[]): Observable { - return BrowserStorage.getData('folders', []).pipe( + return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe( map((items: FolderInterface[]) => items.filter((item) => item.type !== FolderType.Prompt), ), map((convFolders: FolderInterface[]) => { return convFolders.concat(promptsFolders); }), - switchMap((folders) => BrowserStorage.setData('folders', folders)), + switchMap((folders) => + BrowserStorage.setData(UIStorageKeys.Folders, folders), + ), ); } From 178999fcbd71a7caa6e5882ccfeaffb92e4f3281 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:47:42 +0100 Subject: [PATCH 08/22] fix: fix prompt sizing and truncation of chat header tooltip (#325) (Issue #102) --- src/components/Chat/ChatInfoTooltip.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/Chat/ChatInfoTooltip.tsx b/src/components/Chat/ChatInfoTooltip.tsx index 7702bde3d6..729625e9aa 100644 --- a/src/components/Chat/ChatInfoTooltip.tsx +++ b/src/components/Chat/ChatInfoTooltip.tsx @@ -20,6 +20,14 @@ interface Props { temperature: number | null; } +const SM_HEIGHT_THRESHOLDS = [ + { threshold: 480, class: 'line-clamp-3' }, + { threshold: 640, class: 'line-clamp-6' }, + { threshold: 800, class: 'line-clamp-[14]' }, + { threshold: 960, class: 'line-clamp-[20]' }, +]; +const DEFAULT_SM_LINE_CLAMP = 'line-clamp-[28]'; + const getModelTemplate = ( model: OpenAIEntityModel, theme: Theme, @@ -51,6 +59,11 @@ export const ChatInfoTooltip = ({ prompt, temperature, }: Props) => { + const lineClampClass = + SM_HEIGHT_THRESHOLDS.find( + (lineClamp) => window.innerHeight <= lineClamp.threshold, + )?.class || DEFAULT_SM_LINE_CLAMP; + const theme = useAppSelector(UISelectors.selectThemeState); const { t } = useTranslation(Translation.Chat); @@ -76,7 +89,10 @@ export const ChatInfoTooltip = ({ {prompt && ( <> {t('System prompt')}: -
+
{prompt}
From 86b678bb3f759b86a8e1ba02762bfe12f3064a96 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:51:18 +0100 Subject: [PATCH 09/22] fix: add a space between names in chat panels (#332) (Issue #309) --- .../Chatbar/components/ConversationsRenderer.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Chatbar/components/ConversationsRenderer.tsx b/src/components/Chatbar/components/ConversationsRenderer.tsx index 99e9e6ba7b..70588eb556 100644 --- a/src/components/Chatbar/components/ConversationsRenderer.tsx +++ b/src/components/Chatbar/components/ConversationsRenderer.tsx @@ -12,6 +12,7 @@ interface ConversationsRendererProps { conversations: Conversation[]; label: string; } + export const ConversationsRenderer = ({ conversations, label, @@ -36,9 +37,14 @@ export const ConversationsRenderer = ({ isHighlighted={isSectionHighlighted} openByDefault > - {conversations.map((conversation) => ( - - ))} +
+ {conversations.map((conversation) => ( + + ))} +
)} From cd227b997cbc38652b75f5475fb56733726aacf5 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Thu, 14 Dec 2023 12:27:21 +0100 Subject: [PATCH 10/22] fix: fixed tooltips for share icon (#333) (Issue #191) --- package-lock.json | 106 ++++++++++++++++------------ src/components/Common/ShareIcon.tsx | 6 +- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ab8ddccf0..85010013b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,9 +188,10 @@ } }, "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -220,11 +221,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.23.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -234,9 +236,10 @@ } }, "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -258,9 +261,10 @@ } }, "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -282,9 +286,10 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -468,9 +473,10 @@ } }, "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -492,9 +498,10 @@ } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -534,9 +541,10 @@ } }, "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -598,9 +606,10 @@ } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -622,9 +631,10 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -696,9 +706,10 @@ } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -723,9 +734,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -1717,9 +1728,10 @@ } }, "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2014,9 +2026,10 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2115,9 +2128,10 @@ } }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2148,9 +2162,10 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -3795,9 +3810,10 @@ } }, "node_modules/@svgr/hast-util-to-babel-ast/node_modules/@babel/types": { - "version": "7.23.5", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -4226,9 +4242,9 @@ } }, "node_modules/@types/babel__core/node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -4268,9 +4284,9 @@ } }, "node_modules/@types/babel__traverse/node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -8958,9 +8974,9 @@ } }, "node_modules/magicast/node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", diff --git a/src/components/Common/ShareIcon.tsx b/src/components/Common/ShareIcon.tsx index 706c81ba22..71bd2f4394 100644 --- a/src/components/Common/ShareIcon.tsx +++ b/src/components/Common/ShareIcon.tsx @@ -1,11 +1,14 @@ import { ReactNode } from 'react'; +import { useTranslation } from 'next-i18next'; + import classNames from 'classnames'; import { getByHighlightColor } from '@/src/utils/app/folders'; import { FeatureType, HighlightColor } from '@/src/types/common'; import { ShareInterface } from '@/src/types/share'; +import { Translation } from '@/src/types/translation'; import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; @@ -32,6 +35,7 @@ export default function ShareIcon({ children, featureType, }: ShareIsonProps) { + const { t } = useTranslation(Translation.SideBar); const isSharingEnabled = useAppSelector((state) => SettingsSelectors.isSharingEnabled(state, featureType), ); @@ -58,7 +62,7 @@ export default function ShareIcon({ isPublished ? 'rounded-md' : 'rounded-sm', )} > - + Date: Thu, 14 Dec 2023 13:52:45 +0100 Subject: [PATCH 11/22] fix: add search filter tooltip and fix checkbox focus (#338) (Issue #334) --- src/components/Search/SearchFiltersView.tsx | 5 +++-- src/styles/globals.css | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchFiltersView.tsx b/src/components/Search/SearchFiltersView.tsx index 73803153b2..c691cc8b79 100644 --- a/src/components/Search/SearchFiltersView.tsx +++ b/src/components/Search/SearchFiltersView.tsx @@ -21,6 +21,7 @@ import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import ContextMenu from '../Common/ContextMenu'; +import Tooltip from '../Common/Tooltip'; import SearchFilterRenderer from './SearchFilterRenderer'; interface Props { @@ -93,7 +94,7 @@ export default function SearchFiltersView({ highlightColor={highlightColor} triggerIconClassName="absolute right-4 cursor-pointer max-h-[18px]" TriggerCustomRenderer={ - <> + )} - + } /> ); diff --git a/src/styles/globals.css b/src/styles/globals.css index 1daa418266..df3a702be1 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -121,7 +121,7 @@ pre:has(div.codeblock) { @layer components { .checkbox { - @apply relative m-0 mr-2 inline h-4 w-4 shrink-0 appearance-none rounded-sm border border-gray-400 text-blue-500 checked:border-blue-500 hover:border-blue-500 focus:border-blue-500 focus-visible:outline-none dark:border-gray-600 checked:dark:border-blue-500; + @apply relative m-0 mr-2 inline h-4 w-4 shrink-0 appearance-none rounded-sm border border-gray-400 text-blue-500 checked:border-blue-500 hover:border-blue-500 focus-visible:outline-none dark:border-gray-600 checked:dark:border-blue-500; } } From 8116bde367ad0cd21421577f80640e853c0e5fbe Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:15:35 +0100 Subject: [PATCH 12/22] fix: fix background color when hover over a folder (#337) (Issue #91) --- src/components/Folder/Folder.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/Folder/Folder.tsx b/src/components/Folder/Folder.tsx index 33e8bd7768..42dbad958d 100644 --- a/src/components/Folder/Folder.tsx +++ b/src/components/Folder/Folder.tsx @@ -409,6 +409,12 @@ const Folder = ({ 'bg-violet/15', 'bg-blue-500/20', ); + const hoverBgColor = getByHighlightColor( + highlightColor, + 'hover:bg-green/15', + 'hover:bg-violet/15', + 'hover:bg-blue-500/20', + ); return (
({
({ highlightColor={highlightColor} featureType={featureType} > - + )}
Date: Thu, 14 Dec 2023 14:27:44 +0100 Subject: [PATCH 13/22] fix: fix "Save & Submit" button disabling with attachment (#339) (Issue #294) --- src/components/Chat/ChatMessage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Chat/ChatMessage.tsx b/src/components/Chat/ChatMessage.tsx index 1e277ab193..8075ca9c78 100644 --- a/src/components/Chat/ChatMessage.tsx +++ b/src/components/Chat/ChatMessage.tsx @@ -372,7 +372,10 @@ export const ChatMessage: FC = memo( )}
diff --git a/src/components/Files/FileItem.tsx b/src/components/Files/FileItem.tsx index af76891d8b..bc3c7d4f07 100644 --- a/src/components/Files/FileItem.tsx +++ b/src/components/Files/FileItem.tsx @@ -42,6 +42,8 @@ export const FileItem = ({ }: Props) => { const { t } = useTranslation(Translation.Files); + const [isContextMenu, setIsContextMenu] = useState(false); + const [isSelected, setIsSelected] = useState(false); const handleCancelFile = useCallback(() => { onEvent?.(FileItemEventIds.Cancel, item.id); @@ -70,7 +72,10 @@ export const FileItem = ({ return (
)} diff --git a/src/components/Files/FileItemContextMenu.tsx b/src/components/Files/FileItemContextMenu.tsx index 77689c3be0..9644d0ecea 100644 --- a/src/components/Files/FileItemContextMenu.tsx +++ b/src/components/Files/FileItemContextMenu.tsx @@ -17,12 +17,14 @@ interface ContextMenuProps { file: DialFile; className: string; onDelete: MouseEventHandler; + onOpenChange?: (isOpen: boolean) => void; } export function FileItemContextMenu({ file, className, onDelete, + onOpenChange, }: ContextMenuProps) { const { t } = useTranslation(Translation.SideBar); const menuItems: DisplayMenuItemProps[] = useMemo( @@ -49,6 +51,7 @@ export function FileItemContextMenu({ return ( {(folders.length !== 0 || filteredFiles.length !== 0) && ( -
+
{folders.map((folder) => { if (folder.folderId) { return null; diff --git a/src/components/Files/PreUploadModal.tsx b/src/components/Files/PreUploadModal.tsx index 3dd399ef4f..2931a8b33d 100644 --- a/src/components/Files/PreUploadModal.tsx +++ b/src/components/Files/PreUploadModal.tsx @@ -313,7 +313,7 @@ export const PreUploadDialog = ({ className="absolute right-2 top-2" onClick={() => onClose(false)} > - +
@@ -375,10 +375,10 @@ export const PreUploadDialog = ({ 0, file.name.lastIndexOf('.'), )} - className="grow rounded border border-gray-400 bg-transparent py-2 pl-8 pr-12 placeholder:text-gray-500 hover:border-blue-500 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:hover:border-blue-500 dark:focus:border-blue-500" + className="grow text-ellipsis rounded border border-gray-400 bg-transparent px-8 py-2 placeholder:text-gray-500 hover:border-blue-500 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:hover:border-blue-500 dark:focus:border-blue-500" onChange={handleRenameFile(index)} /> - + {file.name.slice(file.name.lastIndexOf('.'))}
diff --git a/src/components/Files/SelectFolderModal.tsx b/src/components/Files/SelectFolderModal.tsx index 56202eda2e..4fd42bef69 100644 --- a/src/components/Files/SelectFolderModal.tsx +++ b/src/components/Files/SelectFolderModal.tsx @@ -24,6 +24,7 @@ import { FilesActions, FilesSelectors } from '@/src/store/files/files.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import CaretIconComponent from '@/src/components/Common/CaretIconComponent'; +import { NoResultsFound } from '@/src/components/Common/NoResultsFound'; import FolderPlus from '../../../public/images/icons/folder-plus.svg'; import { ErrorMessage } from '../Common/ErrorMessage'; @@ -180,7 +181,7 @@ export const SelectFolderModal = ({ className="absolute right-2 top-2" onClick={() => onClose(false)} > - +
@@ -206,7 +207,7 @@ export const SelectFolderModal = ({
@@ -637,6 +652,7 @@ const Folder = ({ handleDrop={handleDrop} onDropBetweenFolders={onDropBetweenFolders} onRenameFolder={onRenameFolder} + onFileUpload={onFileUpload} onDeleteFolder={onDeleteFolder} onAddFolder={onAddFolder} onClickFolder={onClickFolder} diff --git a/src/components/Folder/index.ts b/src/components/Folder/index.ts deleted file mode 100644 index 93815b9591..0000000000 --- a/src/components/Folder/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Folder'; diff --git a/src/components/Promptbar/components/PromptFolders.tsx b/src/components/Promptbar/components/PromptFolders.tsx index 4ee34d1f2e..7fb4ad9ff7 100644 --- a/src/components/Promptbar/components/PromptFolders.tsx +++ b/src/components/Promptbar/components/PromptFolders.tsx @@ -21,7 +21,7 @@ import { import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; -import Folder from '@/src/components/Folder'; +import Folder from '@/src/components/Folder/Folder'; import CollapsableSection from '../../Common/CollapsableSection'; import { BetweenFoldersLine } from '../../Sidebar/BetweenFoldersLine';