Skip to content

Commit

Permalink
feat(chat): Add opportunity to share applications (Issue #2044) (#2909)
Browse files Browse the repository at this point in the history
Co-authored-by: Denys Kolomiitsev <[email protected]>
  • Loading branch information
Derikyan and denys-kolomiitsev authored Jan 20, 2025
1 parent 245c974 commit 2e396c5
Show file tree
Hide file tree
Showing 32 changed files with 918 additions and 145 deletions.
2 changes: 2 additions & 0 deletions apps/chat-e2e/src/testData/expectedConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export const ExpectedConstants = {
'This link is temporary and will be active for 3 days. This conversation and future changes to it will be visible to users who follow the link. Only owner will be able to make changes. Renaming or changing the model will stop sharing.',
sharePromptText:
'This link is temporary and will be active for 3 days. This prompt and future changes to it will be visible to users who follow the link. Only owner will be able to make changes. Renaming will stop sharing.',
shareApplicationText:
'This application and its updates will be visible to users with the link. Renaming or changing the version will stop sharing.',
shareConversationFolderText:
'This link is temporary and will be active for 3 days. 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. Renaming will stop sharing.',
sharePromptFolderText:
Expand Down
2 changes: 1 addition & 1 deletion apps/chat/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ DIAL_ROLES_FIELD="dial_roles"


# 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,conversations-publishing,prompts-publishing,custom-logo,input-links,custom-applications,message-templates,marketplace,quick-apps,code-apps"
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,custom-logo,input-links,custom-applications,message-templates,marketplace,quick-apps,code-apps,applications-sharing"
NEXT_PUBLIC_APP_NAME="Local Development APP Name"
NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT=""
NEXT_PUBLIC_DEFAULT_TEMPERATURE="1"
Expand Down
3 changes: 3 additions & 0 deletions apps/chat/public/images/icons/unshare-user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/chat/public/locales/en/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"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. Renaming or changing the model will stop sharing.",
"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. Renaming will stop sharing.",
"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. Renaming will stop sharing.",
"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. Renaming will stop sharing."
"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. Renaming will stop sharing.",
"share.modal.link_application": "This application and its updates will be visible to users with the link. Renaming or changing the version will stop sharing."
}
78 changes: 76 additions & 2 deletions apps/chat/src/components/Chat/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useTranslation } from 'next-i18next';

import { getShareType } from '@/src/utils/app/share';

import { FeatureType } from '@/src/types/common';
import { ModalState } from '@/src/types/modal';
import { Translation } from '@/src/types/translation';

Expand All @@ -24,6 +25,8 @@ import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal';
import Modal from '../Common/Modal';
import Tooltip from '../Common/Tooltip';

import { SharePermission } from '@epam/ai-dial-shared';

export const ShareModal = () => {
const isShareModalClosed = useAppSelector(
ShareSelectors.selectShareModalClosed,
Expand All @@ -33,6 +36,37 @@ export const ShareModal = () => {
}
};

interface ShareAccessOptionProps {
filterValue: string;
selected: boolean;
onSelect: (value: boolean) => void;
}

const ShareAccessOption = ({
filterValue,
selected,
onSelect,
}: ShareAccessOptionProps) => {
return (
<label
className="relative flex size-[18px] w-full shrink-0 cursor-pointer items-center"
data-qa="share-option"
>
<input
className="checkbox peer size-[18px] bg-layer-3"
type="checkbox"
checked={selected}
onChange={(e) => onSelect(e.target.checked)}
/>
<IconCheck
size={18}
className="invisible absolute text-accent-primary peer-checked:visible"
/>
<span className="ml-2 whitespace-nowrap text-sm">{filterValue}</span>
</label>
);
};

export default function ShareModalView() {
const { t } = useTranslation(Translation.SideBar);
const dispatch = useAppDispatch();
Expand All @@ -42,11 +76,22 @@ export default function ShareModalView() {
const [urlWasCopied, setUrlWasCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

const [editAccess, setEditAccess] = useState(false);
const modalState = useAppSelector(ShareSelectors.selectShareModalState);
const invitationId = useAppSelector(ShareSelectors.selectInvitationId);
const readInvitationId = useAppSelector(ShareSelectors.selectInvitationId);
const writeInvitationId = useAppSelector(
ShareSelectors.selectWriteInvitationId,
);
const invitationId = editAccess ? writeInvitationId : readInvitationId;

const shareResourceId = useAppSelector(ShareSelectors.selectShareResourceId);

const shareResourceName = useAppSelector(
ShareSelectors.selectShareResourceName,
);
const shareResourceVersion = useAppSelector(
ShareSelectors.selectShareResourceVersion,
);
const shareFeatureType = useAppSelector(
ShareSelectors.selectShareFeatureType,
);
Expand All @@ -57,6 +102,26 @@ export default function ShareModalView() {
}, [shareFeatureType, isFolder]);
const [url, setUrl] = useState('');

const onChangeSharePermissionHandler = useCallback(
(isWrite: boolean) => {
setEditAccess(isWrite);
const shouldGetNewInvitationId =
(isWrite && !writeInvitationId) || (!isWrite && !readInvitationId);

if (shareResourceId && shouldGetNewInvitationId) {
dispatch(
ShareActions.shareApplication({
resourceId: shareResourceId,
permissions: isWrite
? [SharePermission.READ, SharePermission.WRITE]
: [SharePermission.READ],
}),
);
}
},
[dispatch, readInvitationId, shareResourceId, writeInvitationId],
);

useEffect(() => {
setUrl(`${window?.location.origin}/share/${invitationId || ''}`);
}, [invitationId]);
Expand Down Expand Up @@ -86,7 +151,6 @@ export default function ShareModalView() {
);

useEffect(() => () => clearTimeout(timeoutRef.current), []);

return (
<Modal
portalId="theme-main"
Expand All @@ -98,12 +162,22 @@ export default function ShareModalView() {
dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT}
>
<div className="flex flex-col justify-between gap-2">
{shareResourceVersion && <span>Version: {shareResourceVersion}</span>}
<p className="text-sm text-secondary">
{t('share.modal.link.description')}
</p>
<p className="text-sm text-secondary">
{t('share.modal.link', { context: sharingType })}
</p>
{shareFeatureType === FeatureType.Application && (
<div className="my-2 flex flex-col gap-2">
<ShareAccessOption
filterValue="Allow editing by other users"
selected={editAccess}
onSelect={onChangeSharePermissionHandler}
/>
</div>
)}
<div className="relative mt-2">
<Tooltip tooltip={url}>
<input
Expand Down
66 changes: 58 additions & 8 deletions apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
IconPlayerPlay,
IconPlaystationSquare,
IconTrashX,
IconUserShare,
IconWorldShare,
} from '@tabler/icons-react';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import { useTranslation } from 'next-i18next';

Expand All @@ -22,6 +23,7 @@ import {
isExecutableApp,
} from '@/src/utils/app/application';
import { getRootId } from '@/src/utils/app/id';
import { canWriteSharedWithMe } from '@/src/utils/app/share';
import { PseudoModel, isPseudoModel } from '@/src/utils/server/api';

import {
Expand All @@ -39,6 +41,7 @@ import { AuthSelectors } from '@/src/store/auth/auth.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 { ShareActions } from '@/src/store/share/share.reducers';

import { REPLAY_AS_IS_MODEL } from '@/src/constants/chat';

Expand All @@ -51,7 +54,10 @@ import { EntityMarkdownDescription } from '@/src/components/Common/MarkdownDescr
import { ApplicationTopic } from '@/src/components/Marketplace/ApplicationTopic';
import { FunctionStatusIndicator } from '@/src/components/Marketplace/FunctionStatusIndicator';

import UnshareDialog from '../../Common/UnshareDialog';

import LoaderIcon from '@/public/images/icons/loader.svg';
import IconUserUnshare from '@/public/images/icons/unshare-user.svg';
import { Feature } from '@epam/ai-dial-shared';

const DESKTOP_ICON_SIZE = 80;
Expand Down Expand Up @@ -104,6 +110,8 @@ export const TalkToCard = ({

const dispatch = useAppDispatch();

const [isUnshareConfirmOpened, setIsUnshareConfirmOpened] = useState(false);

const installedModelIds = useAppSelector(
ModelsSelectors.selectInstalledModelIds,
);
Expand All @@ -113,12 +121,20 @@ export const TalkToCard = ({
);
const isAdmin = useAppSelector(AuthSelectors.selectIsAdmin);

const isMyApp = entity.id.startsWith(
const isMyEntity = entity.id.startsWith(
getRootId({ featureType: FeatureType.Application }),
);
const isExecutable = isExecutableApp(entity) && (isMyApp || isAdmin);

const canWrite = canWriteSharedWithMe(entity);

const isExecutable =
isExecutableApp(entity) && (isMyEntity || isAdmin || canWrite);
const screenState = useScreenState();

const isApplicationsSharingEnabled = useAppSelector((state) =>
SettingsSelectors.isFeatureEnabled(state, Feature.ApplicationsSharing),
);

const versionsToSelect = useMemo(() => {
return allModels.filter(
(model) =>
Expand All @@ -136,9 +152,6 @@ export const TalkToCard = ({
isSelected,
]);

const isMyEntity = entity.id.startsWith(
getRootId({ featureType: FeatureType.Application }),
);
const isModifyDisabled = isApplicationStatusUpdating(entity);
const playerStatus = getApplicationSimpleStatus(entity);

Expand Down Expand Up @@ -170,6 +183,15 @@ export const TalkToCard = ({
[onSelectVersion],
);

const handleOpenSharing = useCallback(() => {
dispatch(
ShareActions.share({
featureType: FeatureType.Application,
resourceId: entity.id,
}),
);
}, [dispatch, entity.id]);

const isOldReplay = useMemo(() => {
return (
entity.id === REPLAY_AS_IS_MODEL &&
Expand Down Expand Up @@ -208,13 +230,35 @@ export const TalkToCard = ({
{
name: t('Edit'),
dataQa: 'edit',
display: isMyEntity && !!onEdit,
display: (isMyEntity || !!canWrite) && !!onEdit,
Icon: IconPencilMinus,
onClick: (e: React.MouseEvent) => {
e.stopPropagation();
onEdit(entity);
},
},
{
name: t('Share'),
dataQa: 'share',
display: isMyEntity && isApplicationsSharingEnabled,
Icon: IconUserShare,
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleOpenSharing();
},
},
{
name: t('Unshare'),
dataQa: 'unshare',
display:
(!!entity.sharedWithMe || !!entity.isShared) &&
isApplicationsSharingEnabled,
Icon: IconUserUnshare,
onClick: (e: React.MouseEvent) => {
setIsUnshareConfirmOpened(true);
e.stopPropagation();
},
},
{
name: t('Publish'),
dataQa: 'publish',
Expand All @@ -229,7 +273,7 @@ export const TalkToCard = ({
name: t('Logs'),
dataQa: 'app-logs',
display:
isExecutable && playerStatus === SimpleApplicationStatus.UNDEPLOY,
!!isExecutable && playerStatus === SimpleApplicationStatus.UNDEPLOY,
Icon: IconFileDescription,
onClick: (e: React.MouseEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -259,11 +303,14 @@ export const TalkToCard = ({
isCodeAppsEnabled,
PlayerIcon,
onEdit,
canWrite,
isApplicationsSharingEnabled,
onPublish,
isExecutable,
onDelete,
isModifyDisabled,
handleUpdateFunctionStatus,
handleOpenSharing,
onOpenLogs,
],
);
Expand Down Expand Up @@ -388,6 +435,9 @@ export const TalkToCard = ({
))}
</div>
</div>
{isUnshareConfirmOpened && (
<UnshareDialog entity={entity} setOpened={setIsUnshareConfirmOpened} />
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const ApplicationWizard: React.FC<ApplicationWizardProps> = ({
const selectedApplication = useAppSelector(
ApplicationSelectors.selectApplicationDetail,
);
const isSharedWithMe = selectedApplication?.sharedWithMe;

const handleClose = useCallback(() => {
onClose(false);
Expand Down Expand Up @@ -81,6 +82,7 @@ export const ApplicationWizard: React.FC<ApplicationWizardProps> = ({
isEdit={isEdit}
currentReference={currentReference}
selectedApplication={isEdit ? selectedApplication : undefined}
isSharedWithMe={!!isSharedWithMe}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ export const ApplicationWizardFooter: FC<ApplicationWizardFooterProps> = ({
<div
className={classNames(
'flex gap-2 border-t border-tertiary p-4 md:px-6',
isEdit ? 'justify-between' : 'justify-end',
isEdit && !selectedApplication?.sharedWithMe
? 'justify-between'
: 'justify-end',
)}
>
{isEdit && (
{isEdit && !selectedApplication?.sharedWithMe && (
<div className="flex items-center gap-2">
<Tooltip
tooltip={
Expand Down
Loading

0 comments on commit 2e396c5

Please sign in to comment.