apps/chat/src/store/prompts/prompts.epics.ts (846 lines of code) (raw):
import {
EMPTY,
Observable,
catchError,
concat,
concatMap,
filter,
forkJoin,
ignoreElements,
iif,
map,
mergeMap,
of,
switchMap,
zip,
} from 'rxjs';
import { AnyAction } from '@reduxjs/toolkit';
import { combineEpics } from 'redux-observable';
import {
combineEntities,
updateEntitiesFoldersAndIds,
} from '@/src/utils/app/common';
import { PromptService } from '@/src/utils/app/data/prompt-service';
import { getOrUploadPrompt } from '@/src/utils/app/data/storages/api/prompt-api-storage';
import { constructPath } from '@/src/utils/app/file';
import {
addGeneratedFolderId,
generateNextName,
getFolderFromId,
getParentFolderIdsFromFolderId,
splitEntityId,
updateMovedFolderId,
} from '@/src/utils/app/folders';
import { getPromptRootId, isEntityIdExternal } from '@/src/utils/app/id';
import {
getPromptInfoFromId,
regeneratePromptId,
} from '@/src/utils/app/prompts';
import {
isEntityIdPublic,
mapPublishedItems,
} from '@/src/utils/app/publications';
import { translate } from '@/src/utils/app/translation';
import { getPromptApiKey } from '@/src/utils/server/api';
import { FeatureType } from '@/src/types/common';
import { FolderType } from '@/src/types/folder';
import { Prompt, PromptInfo } from '@/src/types/prompt';
import { AppEpic } from '@/src/types/store';
import { resetShareEntity } from '@/src/constants/chat';
import { DEFAULT_PROMPT_NAME } from '@/src/constants/default-ui-settings';
import { PublicationActions } from '../publication/publication.reducers';
import { ShareActions } from '../share/share.reducers';
import { UIActions, UISelectors } from '../ui/ui.reducers';
import { PromptsActions, PromptsSelectors } from './prompts.reducers';
import { UploadStatus } from '@epam/ai-dial-shared';
import omit from 'lodash-es/omit';
import uniq from 'lodash-es/uniq';
const initEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(
(action) =>
PromptsActions.init.match(action) &&
!PromptsSelectors.selectInitialized(state$.value),
),
switchMap(() =>
PromptService.getPrompts(undefined, true).pipe(
mergeMap((prompts) => {
const paths = uniq(
prompts.flatMap((p) => getParentFolderIdsFromFolderId(p.folderId)),
);
return concat(
of(
PromptsActions.addPrompts({
prompts,
}),
),
of(
PromptsActions.addFolders({
folders: paths.map((path) => ({
...getFolderFromId(path, FolderType.Prompt),
status: UploadStatus.LOADED,
})),
}),
),
of(PromptsActions.initFoldersAndPromptsSuccess()),
of(PromptsActions.initFinish()),
);
}),
),
),
);
const createNewPromptEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.createNewPrompt.match),
switchMap(({ payload: newPrompt }) => {
return PromptService.createPrompt(newPrompt).pipe(
switchMap((apiPrompt) => {
const collapsedSections = UISelectors.selectCollapsedSections(
FeatureType.Prompt,
)(state$.value);
return concat(
iif(
// check if something renamed
() => apiPrompt?.name !== newPrompt.name,
concat(
of(PromptsActions.uploadPromptsWithFoldersRecursive()),
of(ShareActions.triggerGettingSharedPromptListings()),
),
of(
PromptsActions.createNewPromptSuccess({
newPrompt,
}),
),
),
of(PromptsActions.setIsNewPromptCreating(false)),
of(
UIActions.setCollapsedSections({
featureType: FeatureType.Prompt,
collapsedSections: collapsedSections.filter(
(section) => section !== translate('Recent'),
),
}),
),
);
}),
catchError((err) => {
console.error("New prompt wasn't created:", err);
return concat(
of(
UIActions.showErrorToast(
translate(
'An error occurred while creating a new prompt. Most likely the prompt already exists. Please refresh the page.',
),
),
),
of(PromptsActions.setIsNewPromptCreating(false)),
);
}),
);
}),
);
const saveNewPromptEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.saveNewPrompt.match),
switchMap(({ payload }) =>
PromptService.createPrompt(payload.newPrompt).pipe(
switchMap(() => of(PromptsActions.createNewPromptSuccess(payload))),
catchError((err) => {
console.error(err);
return of(
UIActions.showErrorToast(
translate(
'An error occurred while saving the prompt. Most likely the prompt already exists. Please refresh the page.',
),
),
);
}),
),
),
);
const saveFoldersEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(
(action) =>
PromptsActions.createFolder.match(action) ||
PromptsActions.deleteFolder.match(action) ||
PromptsActions.addFolders.match(action) ||
PromptsActions.clearPrompts.match(action) ||
PromptsActions.importPromptsSuccess.match(action) ||
PromptsActions.setFolders.match(action),
),
map(() => ({
promptsFolders: PromptsSelectors.selectFolders(state$.value),
})),
switchMap(({ promptsFolders }) => {
return PromptService.setPromptFolders(promptsFolders).pipe(
catchError((err) => {
console.error('An error occurred during the saving folders', err);
return of(
UIActions.showErrorToast(
translate('An error occurred during the saving folders'),
),
);
}),
);
}),
ignoreElements(),
);
const savePromptEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.savePrompt.match),
concatMap(({ payload: newPrompt }) =>
PromptService.updatePrompt(newPrompt),
),
catchError((err) => {
console.error(err);
return of(
UIActions.showErrorToast(
translate(
'An error occurred while saving the prompt. Most likely the prompt already exists. Please refresh the page.',
),
),
);
}),
ignoreElements(),
);
const recreatePromptEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.recreatePrompt.match),
mergeMap(({ payload }) => {
const { parentPath } = splitEntityId(payload.old.id);
return PromptService.createPrompt(payload.new).pipe(
switchMap(() =>
PromptService.deletePrompt({
id: payload.old.id,
folderId: parentPath || getPromptRootId(),
name: payload.old.name,
}),
),
catchError((err) => {
console.error(err);
return concat(
of(
PromptsActions.recreatePromptFail({
newId: payload.new.id,
oldPrompt: payload.old,
}),
),
of(
UIActions.showErrorToast(
translate(
'An error occurred while saving the prompt. Please refresh the page.',
),
),
),
);
}),
ignoreElements(),
);
}),
);
const updatePromptEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.updatePrompt.match),
mergeMap(({ payload }) => getOrUploadPrompt(payload, state$.value)),
mergeMap(({ payload, prompt }) => {
const { values, id } = payload as {
id: string;
values: Partial<Prompt>;
};
if (!prompt) {
return of(
UIActions.showErrorToast(
translate(
'It looks like this prompt has been deleted. Please reload the page',
),
),
);
}
const newPrompt: Prompt = {
...prompt,
...values,
id: constructPath(
values.folderId || prompt.folderId,
getPromptApiKey({ ...prompt, ...values }),
),
};
return concat(
of(PromptsActions.updatePromptSuccess({ prompt: newPrompt, id })),
iif(
() => !!prompt && prompt.id !== newPrompt.id,
of(PromptsActions.recreatePrompt({ old: prompt, new: newPrompt })),
of(PromptsActions.savePrompt(newPrompt)),
),
);
}),
);
export const deletePromptEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.deletePrompt.match),
switchMap(({ payload }) => {
return PromptService.deletePrompt(payload.prompt).pipe(
switchMap(() => EMPTY),
catchError((err) => {
console.error(err);
return of(
UIActions.showErrorToast(
translate(
`An error occurred while deleting the prompt "${payload.prompt.name}"`,
),
),
);
}),
);
}),
);
export const clearPromptsEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.clearPrompts.match),
switchMap(() =>
concat(
of(PromptsActions.clearPromptsSuccess()),
of(PromptsActions.deleteFolder({})),
),
),
);
const deletePromptsEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.deletePrompts.match),
map(({ payload }) => ({
promptIds: new Set(payload.promptIds),
})),
switchMap(({ promptIds }) =>
zip(
Array.from(promptIds).map((id) =>
PromptService.deletePrompt(getPromptInfoFromId(id)).pipe(
map(() => null),
catchError((err) => {
const { name } = getPromptInfoFromId(id);
console.error(
`An error occurred while deleting the prompt "${name}"`,
err,
);
return of(name);
}),
),
),
).pipe(
switchMap((failedNames) =>
concat(
iif(
() => failedNames.filter(Boolean).length > 0,
of(
UIActions.showErrorToast(
translate(
`An error occurred while deleting the prompt(s): "${failedNames.filter(Boolean).join('", "')}"`,
),
),
),
EMPTY,
),
of(PromptsActions.deletePromptsComplete({ promptIds })),
),
),
),
),
);
const updateFolderEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.updateFolder.match),
switchMap(({ payload }) => {
const folder = getFolderFromId(payload.folderId, FolderType.Prompt);
const newFolder = addGeneratedFolderId({ ...folder, ...payload.values });
if (payload.folderId === newFolder.id) {
return EMPTY;
}
return PromptService.getPrompts(payload.folderId, true).pipe(
switchMap((prompts) => {
const updateFolderId = updateMovedFolderId.bind(
null,
payload.folderId,
newFolder.id,
);
const folders = PromptsSelectors.selectFolders(state$.value);
const allPrompts = PromptsSelectors.selectPrompts(state$.value);
const openedFoldersIds = UISelectors.selectOpenedFoldersIds(
FeatureType.Prompt,
)(state$.value);
const { updatedFolders, updatedOpenedFoldersIds } =
updateEntitiesFoldersAndIds(
prompts,
folders,
updateFolderId,
openedFoldersIds,
);
const updatedPrompts = combineEntities(
allPrompts.map((prompt) =>
regeneratePromptId({
...prompt,
folderId: updateFolderId(prompt.folderId),
}),
),
prompts.map((prompt) =>
regeneratePromptId({
...prompt,
folderId: updateFolderId(prompt.folderId),
}),
),
);
const actions: Observable<AnyAction>[] = [];
if (prompts.length) {
prompts.forEach((prompt) => {
actions.push(
of(
PromptsActions.updatePrompt({
id: prompt.id,
values: {
folderId: updateFolderId(prompt.folderId),
},
}),
),
);
});
}
actions.push(
of(
PromptsActions.updateFolderSuccess({
folders: updatedFolders,
prompts: updatedPrompts,
}),
),
of(
UIActions.setOpenedFoldersIds({
openedFolderIds: updatedOpenedFoldersIds,
featureType: FeatureType.Prompt,
}),
),
);
return concat(...actions);
}),
catchError((err) => {
console.error('An error occurred while updating the folder:', err);
return of(
UIActions.showErrorToast(
translate('An error occurred while updating the folder.'),
),
);
}),
);
}),
);
const deleteFolderEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.deleteFolder.match),
switchMap(({ payload }) =>
forkJoin({
folderId: of(payload.folderId),
promptsToDelete: PromptService.getPrompts(payload.folderId, true).pipe(
catchError((err) => {
console.error(
'An error occurred while uploading prompts and folders:',
err,
);
return of([]);
}),
),
folders: of(PromptsSelectors.selectFolders(state$.value)),
}),
),
switchMap(({ folderId, promptsToDelete, folders }) => {
const actions: Observable<AnyAction>[] = [];
const promptIds = promptsToDelete.map((p) => p.id);
if (promptIds.length) {
actions.push(of(PromptsActions.deletePrompts({ promptIds })));
} else {
actions.push(
of(PromptsActions.deletePromptsComplete({ promptIds: new Set([]) })),
);
}
return concat(
of(
PromptsActions.setFolders({
folders: folders.filter(
(folder) =>
folder.id !== folderId && !folder.id.startsWith(`${folderId}/`),
),
}),
),
...actions,
);
}),
);
const toggleFolderEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.toggleFolder.match),
switchMap(({ payload }) => {
const openedFoldersIds = UISelectors.selectOpenedFoldersIds(
FeatureType.Prompt,
)(state$.value);
const isOpened = openedFoldersIds.includes(payload.id);
const action = isOpened ? UIActions.closeFolder : UIActions.openFolder;
return of(
action({
id: payload.id,
featureType: FeatureType.Prompt,
}),
);
}),
);
const openFolderEpic: AppEpic = (action$) =>
action$.pipe(
filter(
(action) =>
UIActions.openFolder.match(action) &&
action.payload.featureType === FeatureType.Prompt,
),
switchMap(({ payload }) =>
of(PromptsActions.uploadFoldersIfNotLoaded({ ids: [payload.id] })),
),
);
const duplicatePromptEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.duplicatePrompt.match),
switchMap(({ payload }) =>
forkJoin({
prompt: getOrUploadPrompt(payload, state$.value).pipe(
map((data) => data.prompt),
),
}),
),
switchMap(({ prompt }) => {
if (!prompt) {
return of(
UIActions.showErrorToast(
translate(
'It looks like this prompt has been deleted. Please reload the page',
),
),
);
}
const prompts = PromptsSelectors.selectPrompts(state$.value);
const promptFolderId = isEntityIdExternal(prompt)
? getPromptRootId() // duplicate external entities in the root only
: prompt.folderId;
const newPrompt = regeneratePromptId({
...omit(prompt, ['publicationInfo']),
...resetShareEntity,
folderId: promptFolderId,
name: generateNextName(
DEFAULT_PROMPT_NAME,
prompt.name,
prompts.filter((p) => p.folderId === promptFolderId), // only root prompts for external entities
),
});
return of(PromptsActions.saveNewPrompt({ newPrompt }));
}),
);
const uploadPromptsFromMultipleFoldersEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.uploadPromptsFromMultipleFolders.match),
mergeMap(({ payload }) => {
return PromptService.getMultipleFoldersPrompts(
payload.paths,
payload.recursive,
).pipe(
switchMap((prompts) => {
const actions: Observable<AnyAction>[] = [];
const paths = uniq(
prompts.flatMap((prompt) =>
getParentFolderIdsFromFolderId(prompt.folderId),
),
);
if (!!payload?.pathToSelectFrom && !!prompts.length) {
const openedFolders = UISelectors.selectOpenedFoldersIds(
FeatureType.Prompt,
)(state$.value);
const topLevelPrompt = prompts
.filter((prompt) =>
prompt.id.startsWith(`${payload.pathToSelectFrom}/`),
)
.toSorted((a, b) => a.folderId.length - b.folderId.length)[0];
actions.push(
concat(
of(
PromptsActions.setIsEditModalOpen({
isOpen: true,
isPreview: true,
}),
),
of(
PromptsActions.uploadPrompt({ promptId: topLevelPrompt.id }),
),
of(
PromptsActions.setSelectedPrompt({
promptId: topLevelPrompt.id,
}),
),
of(
UIActions.setOpenedFoldersIds({
featureType: FeatureType.Prompt,
openedFolderIds: [
...openedFolders,
...paths.filter(
(path) =>
path === payload.pathToSelectFrom ||
path.startsWith(`${payload.pathToSelectFrom}/`),
),
],
}),
),
),
);
}
return concat(
of(
PromptsActions.addPrompts({
prompts,
}),
),
of(
PromptsActions.addFolders({
folders: paths.map((path) => ({
...getFolderFromId(path, FolderType.Prompt),
status: UploadStatus.LOADED,
})),
}),
),
...actions,
);
}),
);
}),
);
const uploadPromptsWithFoldersRecursiveEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.uploadPromptsWithFoldersRecursive.match),
mergeMap(({ payload }) =>
PromptService.getPrompts(payload?.path, true).pipe(
mergeMap((prompts) => {
const actions: Observable<AnyAction>[] = [];
const paths = uniq(
prompts.flatMap((prompt) =>
getParentFolderIdsFromFolderId(prompt.folderId),
),
);
const publicPrompts = prompts.filter((prompt) =>
isEntityIdPublic(prompt),
);
const publicPromptIds = publicPrompts.map((prompt) => prompt.id);
const { publicVersionGroups, items: mappedPublicPrompts } =
mapPublishedItems<PromptInfo>(publicPrompts, FeatureType.Prompt);
const notPublicPrompts = prompts.filter(
(prompt) => !publicPromptIds.includes(prompt.id),
);
if (publicPromptIds.length) {
actions.push(
of(
PublicationActions.addPublicVersionGroups({
publicVersionGroups,
}),
),
);
}
return concat(
of(
PromptsActions.addPrompts({
prompts: [...mappedPublicPrompts, ...notPublicPrompts],
}),
),
of(
PromptsActions.addFolders({
folders: paths.map((path) => ({
...getFolderFromId(path, FolderType.Prompt),
status: UploadStatus.LOADED,
})),
}),
),
of(PromptsActions.uploadPromptsWithFoldersRecursiveSuccess()),
...actions,
);
}),
catchError((err) => {
console.error(
'An error occurred while uploading prompts and folders:',
err,
);
return [];
}),
),
),
);
const uploadFolderIfNotLoadedEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.uploadFoldersIfNotLoaded.match),
mergeMap(({ payload }) => {
const folders = PromptsSelectors.selectFolders(state$.value);
const notUploadedPaths = folders
.filter(
(folder) =>
payload.ids.includes(folder.id) &&
folder.status !== UploadStatus.LOADED,
)
.map((folder) => folder.id);
if (!notUploadedPaths.length) {
return EMPTY;
}
return of(PromptsActions.uploadFolders({ ids: notUploadedPaths }));
}),
);
const uploadFoldersEpic: AppEpic = (action$) =>
action$.pipe(
filter(PromptsActions.uploadFolders.match),
mergeMap(({ payload }) =>
zip(
payload.ids.map((path) => PromptService.getPromptsAndFolders(path)),
).pipe(
switchMap((foldersAndEntities) => {
const actions: Observable<AnyAction>[] = [];
const folders = foldersAndEntities.flatMap((items) => items.folders);
const prompts = foldersAndEntities.flatMap((items) => items.entities);
const publicPrompts = prompts.filter((prompt) =>
isEntityIdPublic(prompt),
);
const publicPromptIds = prompts.map((prompt) => prompt.id);
const { publicVersionGroups, items: mappedPublicPrompts } =
mapPublishedItems<PromptInfo>(publicPrompts, FeatureType.Prompt);
const notPublicPrompts = prompts.filter(
(prompt) => !publicPromptIds.includes(prompt.id),
);
if (publicPromptIds.length) {
actions.push(
of(
PublicationActions.addPublicVersionGroups({
publicVersionGroups,
}),
),
);
}
return concat(
...actions,
of(
PromptsActions.uploadChildPromptsWithFoldersSuccess({
parentIds: payload.ids,
folders,
prompts: [...mappedPublicPrompts, ...notPublicPrompts],
}),
),
...payload.ids.map((id) =>
of(
PromptsActions.updateFolder({
folderId: id,
values: { status: UploadStatus.LOADED },
}),
),
),
);
}),
catchError((err) => {
console.error('Error during upload prompts and folders', err);
return of(
UIActions.showErrorToast(
translate('Error during upload prompts and folders'),
),
);
}),
),
),
);
export const uploadPromptEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(PromptsActions.uploadPrompt.match),
switchMap(({ payload }) => {
const originalPrompt = PromptsSelectors.selectPrompt(
state$.value,
payload.promptId,
);
return PromptService.getPrompt(
originalPrompt || ({ id: payload.promptId } as PromptInfo),
);
}),
map((servicePrompt) => {
return PromptsActions.uploadPromptSuccess({
prompt: servicePrompt,
});
}),
catchError((err) => {
console.error('An error occurred while uploading the prompt:', err);
return of(
UIActions.showErrorToast(
translate('An error occurred while uploading the prompt'),
),
);
}),
);
const deleteChosenPromptsEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter((action) => PromptsActions.deleteChosenPrompts.match(action)),
switchMap(() => {
const actions: Observable<AnyAction>[] = [];
const prompts = PromptsSelectors.selectPrompts(state$.value);
const chosenPromptIds = PromptsSelectors.selectSelectedItems(
state$.value,
);
const { fullyChosenFolderIds } = PromptsSelectors.selectChosenFolderIds(
prompts,
)(state$.value);
const promptIds = PromptsSelectors.selectPrompts(state$.value).map(
(prompt) => prompt.id,
);
const folders = PromptsSelectors.selectFolders(state$.value);
const emptyFoldersIds = PromptsSelectors.selectEmptyFolderIds(
state$.value,
);
const deletedPromptIds = uniq([
...chosenPromptIds,
...promptIds.filter((id) =>
fullyChosenFolderIds.some((folderId) => id.startsWith(folderId)),
),
]);
if (promptIds.length) {
actions.push(
of(
PromptsActions.deletePrompts({
promptIds: deletedPromptIds,
}),
),
);
}
return concat(
of(
PromptsActions.setFolders({
folders: folders.filter(
(folder) =>
!fullyChosenFolderIds.includes(`${folder.id}/`) &&
(prompts.some((p) => p.id.startsWith(`${folder.id}/`)) ||
emptyFoldersIds.some((id) => id === folder.id)),
),
}),
),
of(PromptsActions.resetChosenPrompts()),
...actions,
);
}),
);
export const PromptsEpics = combineEpics(
initEpic,
uploadPromptsFromMultipleFoldersEpic,
uploadPromptsWithFoldersRecursiveEpic,
uploadFolderIfNotLoadedEpic,
uploadFoldersEpic,
openFolderEpic,
toggleFolderEpic,
saveFoldersEpic,
saveNewPromptEpic,
deleteFolderEpic,
savePromptEpic,
recreatePromptEpic,
updatePromptEpic,
deletePromptEpic,
clearPromptsEpic,
deletePromptsEpic,
updateFolderEpic,
createNewPromptEpic,
duplicatePromptEpic,
uploadPromptEpic,
deleteChosenPromptsEpic,
);