in apps/chat/src/store/conversations/conversations.epics.ts [850:1423]
folderId: updateFolderId(conversation.folderId),
},
}),
),
);
});
}
actions.push(
of(
ConversationsActions.updateFolderSuccess({
folders: updatedFolders,
conversations: updatedConversations,
selectedConversationsIds: updatedSelectedConversationsIds,
}),
),
of(
UIActions.setOpenedFoldersIds({
openedFolderIds: updatedOpenedFoldersIds,
featureType: FeatureType.Chat,
}),
),
);
return concat(...actions);
}),
catchError((err) => {
console.error('Error during upload conversations and folders', err);
return of(ConversationsActions.uploadConversationsFail());
}),
);
}),
);
const clearConversationsEpic: AppEpic = (action$) =>
action$.pipe(
filter(ConversationsActions.clearConversations.match),
switchMap(() => {
return concat(
of(ConversationsActions.clearConversationsSuccess()),
of(ConversationsActions.deleteFolder({})),
);
}),
);
const deleteConversationsEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(ConversationsActions.deleteConversations.match),
map(({ payload }) => ({
conversations: ConversationsSelectors.selectConversations(state$.value),
selectedConversationsIds:
ConversationsSelectors.selectSelectedConversationsIds(state$.value),
conversationIds: new Set(payload.conversationIds),
suppressErrorMessage: payload.suppressErrorMessage || false,
})),
switchMap(
({
conversations,
selectedConversationsIds,
conversationIds,
suppressErrorMessage,
}) => {
const otherConversations = conversations.filter(
(conv) => !conversationIds.has(conv.id),
);
const newSelectedConversationsIds = selectedConversationsIds.filter(
(id) => !conversationIds.has(id),
);
const actions: Observable<AnyAction>[] = [];
const isIsolatedView = SettingsSelectors.selectIsIsolatedView(
state$.value,
);
// No need to recreate conversation for isolated view
if (!isIsolatedView) {
if (
otherConversations.length === 0 ||
newSelectedConversationsIds.length === 0
) {
actions.push(
of(
ConversationsActions.createNewConversations({
names: [translate(DEFAULT_CONVERSATION_NAME)],
suspendHideSidebar: isMediumScreen(),
}),
),
);
} else if (
newSelectedConversationsIds.length !==
selectedConversationsIds.length
) {
actions.push(
of(
ConversationsActions.selectConversations({
conversationIds: newSelectedConversationsIds,
suspendHideSidebar: isMediumScreen(),
}),
),
);
}
}
return concat(
zip(
Array.from(conversationIds).map((id) =>
!isEntityIdLocal({ id })
? ConversationService.deleteConversation(
getConversationInfoFromId(id),
).pipe(
map(() => null),
catchError((err) => {
const { name } = getConversationInfoFromId(id);
!suppressErrorMessage &&
console.error(`Error during deleting "${name}"`, err);
return of(name);
}),
)
: of(null),
),
).pipe(
switchMap((failedNames) =>
concat(
iif(
() =>
failedNames.filter(Boolean).length > 0 &&
!suppressErrorMessage,
of(
UIActions.showErrorToast(
translate(
`An error occurred while deleting the conversation(s): "${failedNames.filter(Boolean).join('", "')}"`,
),
),
),
EMPTY,
),
of(
ConversationsActions.deleteConversationsComplete({
conversationIds,
}),
),
),
),
),
...actions,
);
},
),
);
const rateMessageEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(ConversationsActions.rateMessage.match),
map(({ payload }) => ({
payload,
conversations: ConversationsSelectors.selectConversations(state$.value),
})),
switchMap(({ conversations, payload }) => {
const conversation = conversations.find(
(conv) => conv.id === payload.conversationId,
);
if (!conversation) {
return of(
ConversationsActions.rateMessageFail({
error: translate(
'No conversation exists for rating with provided conversation id',
),
}),
);
}
const message = (conversation as Conversation).messages[
payload.messageIndex
];
if (!message || !message.responseId) {
return of(
ConversationsActions.rateMessageFail({
error: translate('Message cannot be rated'),
}),
);
}
const rateBody: RateBody = {
responseId: message.responseId,
modelId: conversation.model.id,
id: conversation.id,
value: payload.rate > 0 ? true : false,
};
return fromFetch('/api/rate', {
method: HTTPMethod.POST,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(rateBody),
}).pipe(
switchMap((resp) => {
if (!resp.ok) {
return throwError(() => resp);
}
return from(resp.json());
}),
map(() => {
return ConversationsActions.rateMessageSuccess(payload);
}),
catchError((e: Response) => {
return of(
ConversationsActions.rateMessageFail({
error: e,
}),
);
}),
);
}),
);
const updateMessageEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(ConversationsActions.updateMessage.match),
map(({ payload }) => ({
payload,
conversations: ConversationsSelectors.selectConversations(state$.value),
})),
switchMap(({ conversations, payload }) => {
const conversation = conversations.find(
(conv) => conv.id === payload.conversationId,
) as Conversation;
if (!conversation || !conversation.messages[payload.messageIndex]) {
return EMPTY;
}
const actions = [];
const messages = [...conversation.messages];
messages[payload.messageIndex] = {
...messages[payload.messageIndex],
...payload.values,
};
actions.push(
of(
ConversationsActions.updateConversation({
id: payload.conversationId,
values: {
messages: [...messages],
},
}),
),
);
const attachments =
messages[payload.messageIndex].custom_content?.attachments;
if (attachments) {
const attachmentParentFolders = uniq(
attachments
.map(
(attachment) =>
attachment.url &&
getParentFolderIdsFromEntityId(
decodeURIComponent(attachment.url),
),
)
.filter(Boolean),
).flat();
if (attachmentParentFolders.length) {
actions.push(
of(
FilesActions.updateFoldersStatus({
foldersIds: attachmentParentFolders,
status: UploadStatus.UNINITIALIZED,
}),
),
);
}
}
return concat(...actions);
}),
);
const rateMessageSuccessEpic: AppEpic = (action$) =>
action$.pipe(
filter(ConversationsActions.rateMessageSuccess.match),
switchMap(({ payload }) => {
return of(
ConversationsActions.updateMessage({
conversationId: payload.conversationId,
messageIndex: payload.messageIndex,
values: {
like: payload.rate,
},
}),
);
}),
);
const sendMessagesEpic: AppEpic = (action$) =>
action$.pipe(
filter(ConversationsActions.sendMessages.match),
switchMap(({ payload }) => {
return concat(
of(ConversationsActions.createAbortController()),
...payload.conversations.map((conv) => {
return of(
ConversationsActions.sendMessage({
conversation: conv,
message: payload.message,
deleteCount: payload.deleteCount,
activeReplayIndex: payload.activeReplayIndex,
}),
);
}),
);
}),
);
const sendMessageEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(ConversationsActions.sendMessage.match),
map(({ payload }) => ({
payload,
modelsMap: ModelsSelectors.selectModelsMap(state$.value),
conversations: ConversationsSelectors.selectConversations(state$.value),
selectedConversationIds:
ConversationsSelectors.selectSelectedConversationsIds(state$.value),
overlaySystemPrompt: OverlaySelectors.selectOverlaySystemPrompt(
state$.value,
),
isOverlay: SettingsSelectors.selectIsOverlay(state$.value),
})),
switchMap(
({
payload,
modelsMap,
conversations,
selectedConversationIds,
overlaySystemPrompt,
isOverlay,
}) => {
const actions: Observable<AnyAction>[] = [];
const messageModel: Message[EntityType.Model] = {
id: payload.conversation.model.id,
};
const messageSettings: Message['settings'] = {
prompt: payload.conversation.prompt,
temperature: payload.conversation.temperature,
selectedAddons: payload.conversation.selectedAddons,
assistantModelId: payload.conversation.assistantModelId,
};
const assistantMessage: Message = {
content: '',
model: messageModel,
settings: messageSettings,
role: Role.Assistant,
};
const userMessage: Message = {
...payload.message,
model: messageModel,
settings: messageSettings,
};
let currentMessages =
payload.deleteCount > 0
? payload.conversation.messages.slice(
0,
payload.deleteCount * -1 || undefined,
)
: payload.conversation.messages;
/*
Overlay needs to share host application state information
We storing state information in systemPrompt (message with role: Role.System)
*/
if (isOverlay && overlaySystemPrompt) {
currentMessages = updateSystemPromptInMessages(
currentMessages,
overlaySystemPrompt,
);
}
const updatedMessages = currentMessages.concat(
userMessage,
assistantMessage,
);
const conversationRootFolderId = getConversationRootId();
const newConversationName =
payload.conversation.replay?.isReplay ||
updatedMessages.filter((msg) => msg.role === Role.User).length > 1 ||
payload.conversation.isNameChanged
? payload.conversation.name
: getNextDefaultName(
getNewConversationName(payload.conversation, payload.message),
conversations.filter(
(conv) =>
(conv.folderId === payload.conversation.folderId ||
(isEntityIdLocal(payload.conversation) &&
conv.folderId === conversationRootFolderId)) &&
!selectedConversationIds.includes(conv.id),
),
Math.max(
selectedConversationIds.indexOf(payload.conversation.id),
0,
),
true,
);
const updatedConversation: Conversation = regenerateConversationId({
...payload.conversation,
lastActivityDate: Date.now(),
replay: payload.conversation.replay
? {
...payload.conversation.replay,
activeReplayIndex: payload.activeReplayIndex,
}
: undefined,
messages: updatedMessages,
name: newConversationName,
isMessageStreaming: true,
});
if (
updatedConversation.selectedAddons.length > 0 &&
modelsMap[updatedConversation.model.id]?.type !==
EntityType.Application
) {
actions.push(
of(
AddonsActions.updateRecentAddons({
addonIds: updatedConversation.selectedAddons,
}),
),
);
}
return concat(
...actions,
of(
ConversationsActions.updateConversation({
id: payload.conversation.id,
values: updatedConversation,
}),
),
of(
ModelsActions.updateRecentModels({
modelId: updatedConversation.model.id,
}),
),
of(
ConversationsActions.streamMessage({
conversation: updatedConversation,
message: assistantMessage,
}),
),
);
},
),
);
const streamMessageEpic: AppEpic = (action$, state$) =>
action$.pipe(
filter(ConversationsActions.streamMessage.match),
map(({ payload }) => ({
payload,
modelsMap: ModelsSelectors.selectModelsMap(state$.value),
})),
map(({ payload, modelsMap }) => {
const lastModel = modelsMap[payload.conversation.model.id];
const selectedAddons = uniq([
...payload.conversation.selectedAddons,
...(lastModel?.selectedAddons ?? []),
]);
const assistantModelId = payload.conversation.assistantModelId;
const conversationModelType = lastModel?.type ?? EntityType.Model;
let modelAdditionalSettings = {};
if (conversationModelType === EntityType.Model) {
modelAdditionalSettings = {
prompt: doesModelAllowSystemPrompt(lastModel)
? payload.conversation.prompt
: undefined,
temperature: doesModelAllowTemperature(lastModel)
? payload.conversation.temperature
: 1,
selectedAddons: doesModelAllowAddons(lastModel) ? selectedAddons : [],
};
}
if (conversationModelType === EntityType.Assistant && assistantModelId) {
modelAdditionalSettings = {
assistantModel: modelsMap[assistantModelId],
temperature: doesModelAllowTemperature(lastModel)
? payload.conversation.temperature
: 1,
selectedAddons: doesModelAllowAddons(lastModel) ? selectedAddons : [],
};
}
const chatBody: ChatBody = {
model: modelsMap[payload.conversation.model.id],
messages: payload.conversation.messages
.filter(
(message, index) =>
message.role !== Role.Assistant ||
index !== payload.conversation.messages.length - 1,
)
.map((message) => ({
content: message.content,
role: message.role,
like: void 0,
...((message.custom_content?.state ||
message.custom_content?.attachments ||
message.custom_content?.form_value ||
message.custom_content?.form_schema) && {
custom_content: {
state: message.custom_content?.state,
attachments: message.custom_content?.attachments,
form_value: message.custom_content?.form_value,
form_schema: message.custom_content?.form_schema,
},
}),
})),
id: payload.conversation.id,
...modelAdditionalSettings,
};
return {
payload,
chatBody,
};
}),
mergeMap(({ payload, chatBody }) => {
const conversationSignal =
ConversationsSelectors.selectConversationSignal(state$.value);
const decoder = new TextDecoder();
let eventData = '';
let message = payload.message;
return from(
fetch('/api/chat', {
method: HTTPMethod.POST,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(chatBody),
signal: conversationSignal.signal,
}),
).pipe(
switchMap((response) => {
const body = response.body;
if (!response.ok) {
return throwError(
() => new Error('ServerError', { cause: response }),
);
}
if (!body) {
return throwError(() => new Error('No body received'));
}
const reader = body.getReader();
const subj = new Subject<ReadableStreamReadResult<Uint8Array>>();
const observable = subj.asObservable();
const observer = async () => {
try {
// eslint-disable-next-line no-constant-condition
while (true) {
const val = await reader.read();
subj.next(val);
if (val.done) {