diff --git a/src/components/Form/response.tsx b/src/components/Form/response.tsx index bfd1021..3fbb57a 100644 --- a/src/components/Form/response.tsx +++ b/src/components/Form/response.tsx @@ -3,11 +3,11 @@ import { useEffect, useRef, useState } from 'react'; import { IconBtn } from '../../components/Button'; import { Input } from '../../components/Input'; import { Roles } from '../../config/enums'; +import { TYPING_TIMEOUT } from '../../config/env'; import { useAppDispatch, useAppSelector } from '../../hooks'; import { fillUserHistoryData, getChat, setLastGroupPointer, setTypingTimeoutExpired } from '../../store/slices/chat'; import { getQueryParam, uuidV4 } from '../../utils'; import { layoutFoot as variant } from '../Layout/variants'; -import { TYPING_TIMEOUT } from '../../config/env'; export const ResponseForm = () => { const dispatch = useAppDispatch(); diff --git a/src/components/Stream/assistant.tsx b/src/components/Stream/assistant.tsx index 643a3cd..b459f3b 100644 --- a/src/components/Stream/assistant.tsx +++ b/src/components/Stream/assistant.tsx @@ -6,18 +6,21 @@ import { useAppSelector } from '../../hooks'; import { AssistantProps } from '../../interfaces/component'; import { getChat, sortBySequence } from '../../store/slices/chat'; import { getMeta } from '../../store/slices/meta'; -import { getQueryParam, extractVideoCode, uuidV4 } from '../../utils'; +import { extractVideoCode, getQueryParam, uuidV4 } from '../../utils'; +import { replaceNewRowSymbols } from '../../utils/formatting'; import MarkdownLink from '../Markdown/link'; import { Media } from '../Media'; import OptionList from './options'; import { flickerEffect } from './variants'; -import { replaceNewRowSymbols } from '../../utils/formatting'; const Assistant = ({ message, itemId }: AssistantProps) => { const term = getQueryParam(); const { isStreaming } = useAppSelector(getChat); const { pd } = useAppSelector(getMeta); - const isLast = useAppSelector((state) => state.chat.record[term].historyIds.length - 1 === state.chat.record[term].historyIds.indexOf(itemId)); + const isLast = useAppSelector((state) => { + const threadId = state.chat.thread[term]; + return state.chat.record[threadId].historyIds.length - 1 === state.chat.record[threadId].historyIds.indexOf(itemId) + }); const { base: baseFlicker } = flickerEffect({ isTyping: isStreaming && isLast }); const sortedContent = [...message.content].sort(sortBySequence); @@ -72,7 +75,7 @@ const Assistant = ({ message, itemId }: AssistantProps) => { key={uuidV4()} e2e="assistant-img" image={it[it.type]?.url || ''} - background={'url("' + it[it.type]?.url + '")'} + background={`url("${it[it.type]?.url}")`} type={Definition.image} title={it[it.type]?.title} /> diff --git a/src/components/Stream/base.tsx b/src/components/Stream/base.tsx index d31ac1b..66e38d5 100644 --- a/src/components/Stream/base.tsx +++ b/src/components/Stream/base.tsx @@ -8,8 +8,15 @@ import { streamBase, streamRow } from './variants'; export const StreamBase = () => { const { aiProfile } = useAppSelector(getConfig); const term = getQueryParam(); - const historyIds = useAppSelector((state) => state.chat.record[term]?.historyIds || []); - const firstMessage = useAppSelector((state) => state.chat.record[term]?.historyData[historyIds[0]]); + const historyIds = useAppSelector((state) => { + const threadId = state.chat.thread[term]; + return state.chat.record[threadId]?.historyIds || [] + }); + const firstMessage = useAppSelector((state) => { + const threadId = state.chat.thread[term]; + + return state.chat.record[threadId]?.historyData[historyIds[0]] + }); const { base, second, date } = streamBase(); const { base: baseRow } = streamRow(); const time = formatDateByLocale(firstMessage?.time || new Date().getTime()); diff --git a/src/components/Stream/bubble.tsx b/src/components/Stream/bubble.tsx index 576c2e1..b9895ed 100644 --- a/src/components/Stream/bubble.tsx +++ b/src/components/Stream/bubble.tsx @@ -10,7 +10,10 @@ import { streamBubble as variant } from './variants'; const StreamBubble = ({ itemId }: { itemId: string }) => { const term = getQueryParam(); - const record = useAppSelector((state) => state.chat.record[term].historyData[itemId]); + const record = useAppSelector((state) => { + const threadId = state.chat.thread[term]; + return state.chat.record[threadId].historyData[itemId]; + }); return (record && (
{ const term = getQueryParam(); const { cid, systemType, marketing, pd } = useAppSelector(getMeta); const { translations, purpose, specialUrls } = useAppSelector(getConfig); - const { isLoading, isStreaming, record } = useAppSelector(getChat); + const { isLoading, isStreaming, record, thread } = useAppSelector(getChat); const { error: streamError } = useAppSelector((store) => store.chat); const storedLink = useAppSelector((store) => store.intentions.link); const { error: emailError, current: currentEmail } = useAppSelector(getEmailIntentions); @@ -39,12 +39,13 @@ export const useFootProps = () => { ctaHref: '', }; - if (!record[term]) { - return staticProps + const currentThread = thread[term]; + if (!currentThread) { + return staticProps; } - const lastMsgId = [...record[term].historyIds].pop(); - const lastMsg = lastMsgId && record[term].historyData[lastMsgId]; + const lastMsgId = [...record[currentThread].historyIds].pop(); + const lastMsg = lastMsgId && record[currentThread].historyData[lastMsgId]; const isLastAssistantMsg = lastMsg && lastMsg.role === Roles.assistant; const link = isLastAssistantMsg && storedLink; const noButtonChoices = !(lastMsg && lastMsg?.content.find((m) => m.buttons)); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index e9aad32..929a83d 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -26,17 +26,17 @@ interface VideoProps { export interface VideoMessage { type: Definition.video; - video: VideoProps + video: VideoProps; } interface ImageProps { url: string; - title: string - alt: string | null + title: string; + alt: string | null; } export interface ImageMessage { type: Definition.image; - image: ImageProps + image: ImageProps; } export interface ButtonOptions { @@ -49,22 +49,34 @@ export interface ButtonOptions { } export interface BaseOptions extends ButtonOptions { - id: string, link: string, noStream: boolean + id: string; + link: string; + noStream: boolean; } export interface OptionsListProps { - options: Array | undefined + options: Array | undefined; } export type AssistantMessageTypeUnion = - TextMessage | ButtonsMessage | EmailMessage | VideoMessage | ImageMessage | PaymentMessage; + | TextMessage + | ButtonsMessage + | EmailMessage + | VideoMessage + | ImageMessage + | PaymentMessage; export type SupportedMessageTypes = - Definition.text | Definition.buttons | Definition.payment | Definition.email | Definition.video | Definition.image; + | Definition.text + | Definition.buttons + | Definition.payment + | Definition.email + | Definition.video + | Definition.image; export interface AssistantRecord { - type: SupportedMessageTypes, - sequence: number, + type: SupportedMessageTypes; + sequence: number; text?: string; video?: VideoProps; image?: ImageProps; @@ -109,18 +121,22 @@ export interface MessageProperties { } export type PredefinedMessagePayload = { - content: string, - buttons?: Array, -} + content: string; + buttons?: Array; +}; export type UserMessageContent = { - groupId: string, sent: boolean, resend: boolean, text: string, sequence: number -} + groupId: string; + sent: boolean; + resend: boolean; + text: string; + sequence: number; +}; export interface UserHistoryData { id: string; role: Roles.user; - content: Array + content: Array; } export interface UserHistoryDataFiller { @@ -147,7 +163,7 @@ export interface AssistantHistoryInitialMessage { export interface ClientMessage { role: Roles.assistant | Roles.user; term: string; - user_id: string; + userId: string; message: string; messageId: string; region: string; diff --git a/src/interfaces/store.ts b/src/interfaces/store.ts index eae63ad..d448a03 100644 --- a/src/interfaces/store.ts +++ b/src/interfaces/store.ts @@ -7,7 +7,7 @@ interface Outgoing { user_id: string; role: Roles.user; message: string; -}; +} export interface ChatState { outgoing: Outgoing; @@ -30,8 +30,11 @@ export interface ChatState { } >; historyIds: Array; - } - } + }; + }; + thread: { + [key: string]: string; + }; } export interface ConfigState { @@ -78,15 +81,15 @@ export interface MetaState { export interface IntentionsState { email: { - current: string, - success: boolean, - error: boolean, - isLoading: boolean, - }, + current: string; + success: boolean; + error: boolean; + isLoading: boolean; + }; response: { - isFormVisible: boolean, - error: boolean, - }, + isFormVisible: boolean; + error: boolean; + }; payment: { isButtonVisible: boolean; isFormVisible: boolean; @@ -96,5 +99,5 @@ export interface IntentionsState { messaging: { isVisible: boolean; }; - link: string + link: string; } diff --git a/src/middleware/socket.ts b/src/middleware/socket.ts index 6ed1a4d..0343a23 100644 --- a/src/middleware/socket.ts +++ b/src/middleware/socket.ts @@ -9,12 +9,12 @@ import { AssistantRecord, ClientMessage, SocketHistoryRecord, - UserMessageContent -} from '../interfaces' + UserMessageContent, +} from '../interfaces'; import { fillAssistantHistoryData, fillInitialMessage, - initiateRecordByTerm, + initiateRecordByThread, resendMessage, resetError, resetHistory, @@ -23,7 +23,6 @@ import { setClosed, setConnected, setError, - updateHistoryByTerm, setIsLoading, setIsStreaming, setLastGroupPointer, @@ -31,6 +30,7 @@ import { setTypingTimeoutExpired, showResendIcon, syncMessageStatus, + updateHistoryByTerm, } from '../store/slices/chat'; import { setConfig } from '../store/slices/config'; import { setRegion } from '../store/slices/meta'; @@ -40,7 +40,7 @@ let socket: Socket; const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isSync: boolean | undefined }) => { const { meta, chat, config } = store.getState(); - const term = getQueryParam(); + const thread = chat.thread[getQueryParam()]; const onError = () => { const { config } = store.getState(); @@ -49,7 +49,7 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS }; const dispatchRetry = () => { - store.dispatch(showResendIcon({ itemId: [...chat.record[term].historyIds].pop() })); + store.dispatch(showResendIcon({ itemId: [...chat.record[thread].historyIds].pop() })); onError(); }; @@ -59,7 +59,15 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS } if (socket && socket.connected && data.message.trim() !== '') { - socket.emit(Events.chat, { time: new Date().getTime(), ...data }, withTimeout(dispatchRetry)); + socket.emit( + Events.chat, + { + time: new Date().getTime(), + threadId: chat.thread[getQueryParam()], + ...data, + }, + withTimeout(dispatchRetry), + ); store.dispatch(resetError()); return; } @@ -77,7 +85,9 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS store.dispatch(setError(config.translations.error)); }; - const message = chat.record[term].historyData[itemId].content.map((item: { text: string }) => item.text).join(['\n']); + const message = chat.record[thread].historyData[itemId].content + .map((item: { text: string }) => item.text) + .join(['\n']); if (socket?.connected && message.trim() !== '') { socket.volatile.emit( Events.chat, @@ -85,11 +95,11 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS role: Roles.user, message, term: getQueryParam(), - user_id: meta.cid, - messageId: itemId, region: meta.region, + messageId: itemId, + userId: meta.cid, }, - withTimeout(onResendError) + withTimeout(onResendError), ); store.dispatch(resetError()); } else { @@ -101,10 +111,10 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS handleMessageSending({ role: Roles.user, message: action.payload, - term, - user_id: meta.cid, + term: getQueryParam(), region: meta.region, - messageId: [...chat.record[term].historyIds].pop(), + userId: meta.cid, + messageId: [...chat.record[thread].historyIds].pop(), }); } @@ -126,24 +136,26 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS } if (setTypingTimeoutExpired.match(action) && action.payload && !('$isSync' in action)) { - const messageId = [...chat.record[term].historyIds].pop(); - const currentMessage = chat.record[term].historyData[messageId]; - const lastMessage = chat.record[term].historyData[messageId].content.map(({ text }: { text: UserMessageContent }) => text).join('\n'); + const messageId = [...chat.record[thread].historyIds].pop(); + const currentMessage = chat.record[thread].historyData[messageId]; + const lastMessage = chat.record[thread].historyData[messageId].content + .map(({ text }: { text: UserMessageContent }) => text) + .join('\n'); if (currentMessage.role === Roles.user && lastMessage.trim() !== '') { handleMessageSending({ role: Roles.user, message: lastMessage, - term, - user_id: meta.cid, + term: getQueryParam(), region: meta.region, - messageId + messageId: messageId, + userId: meta.cid, }); } } if (resendMessage.match(action) && !('$isSync' in action)) { - // @ts-ignore must understand why the action is of type never?? + // @ts-expect-error must understand why the action is of type never?? handleMessageResending(action.payload); } @@ -159,91 +171,114 @@ const chatMiddleware: Middleware = (store) => (next) => (action: Action & { $isS socket.on(Events.connect, () => { const { meta } = store.getState(); socket.sendBuffer = []; - socket.emit(Events.chatHistory, { user_id: meta.cid, region: meta.region, term: getQueryParam() }); + socket.emit(Events.chatHistory, { userId: meta.cid, region: meta.region, term: getQueryParam() }); store.dispatch(setConnected(true)); store.dispatch(setLastGroupPointer(uuidV4())); - store.dispatch(initiateRecordByTerm()); }); - socket.on(Events.chatHistory, ({ history: servedHistory, errors, region, term: servedTerm, threadId } - : { history: Array, errors: string[], region: string, term: string, threadId: string }) => { + socket.on( + Events.chatHistory, + ({ + history: servedHistory, + errors, + region, + term: servedTerm, + threadId, + }: { + history: Array; + errors: string[]; + region: string; + term: string; + threadId: string; + }) => { + store.dispatch(resetIsLoading()); + store.dispatch(setIsStreaming(false)); + store.dispatch(setRegion(region)); + store.dispatch(initiateRecordByThread({ threadId })); - store.dispatch(resetIsLoading()); - store.dispatch(setIsStreaming(false)); - store.dispatch(setRegion(region)); - const { config, meta } = store.getState(); + const { config, meta } = store.getState(); - if (errors.length) { - store.dispatch(setError(errors[0])); - return; - } - - if (servedHistory.length) { - store.dispatch(syncMessageStatus({ history: servedHistory, term: servedTerm })); - store.dispatch(updateHistoryByTerm({ history: servedHistory, term: servedTerm })); - return - } + if (errors.length) { + store.dispatch(setError(errors[0])); + return; + } - !action.$isSync && store.dispatch(resetHistory({ term })); - !action.$isSync && store.dispatch(setIsLoading()); - let interval = 0; - config.aiProfile.initialMessage - .forEach((element: AssistantHistoryInitialMessage, index: number, arr: Array) => { - interval += 1000; - - setTimeout(() => { - - !action.$isSync && store.dispatch(fillInitialMessage({ message: element, term })); - - if (arr.length === index + 1) { - !action.$isSync && config.aiProfile.initialMessage.forEach((message: SocketHistoryRecord) => - handleMessageSending({ - role: Roles.assistant, - term: getQueryParam(), - user_id: meta.cid, - message: JSON.stringify(message.content), - messageId: message.id, - region: meta.region, - })); - store.dispatch(resetIsLoading()); - } - }, interval); + if (servedHistory.length) { + store.dispatch(syncMessageStatus({ history: servedHistory, term: servedTerm })); + store.dispatch(updateHistoryByTerm({ history: servedHistory, term: servedTerm })); + return; + } - }); - }); + !action.$isSync && store.dispatch(resetHistory({ term: getQueryParam(), thread })); + !action.$isSync && store.dispatch(setIsLoading()); + let interval = 0; + config.aiProfile.initialMessage.forEach( + (element: AssistantHistoryInitialMessage, index: number, arr: Array) => { + interval += 1000; + + setTimeout(() => { + !action.$isSync && store.dispatch(fillInitialMessage({ message: element, term: getQueryParam() })); + + if (arr.length === index + 1) { + !action.$isSync && + config.aiProfile.initialMessage.forEach((message: SocketHistoryRecord) => + handleMessageSending({ + role: Roles.assistant, + term: getQueryParam(), + message: JSON.stringify(message.content), + region: meta.region, + userId: meta.cid, + messageId: message.id, + }), + ); + store.dispatch(resetIsLoading()); + } + }, interval); + }, + ); + }, + ); - socket.on(Events.streamStart, ({ id, term }: { id: string, term: string }) => { + socket.on(Events.streamStart, ({ id, term }: { id: string; term: string }) => { store.dispatch(setIsStreaming(true)); store.dispatch(resetIsLoading()); store.dispatch(resetOutgoing()); store.dispatch(resetError()); - store.dispatch(fillAssistantHistoryData({ - id, term - })); + store.dispatch( + fillAssistantHistoryData({ + id, + term, + }), + ); }); - socket.on(Events.streamData, (data: AssistantRecord - & { - id: string; - term: string; - errors: Array; - }) => { + socket.on( + Events.streamData, + ( + data: AssistantRecord & { + id: string; + term: string; + errors: Array; + }, + ) => { + store.dispatch( + fillAssistantHistoryData({ + id: data.id, + sequence: data.sequence, + term: data.term, + content: { + type: data.type, + [data.type]: data[data.type], + sequence: data.sequence, + }, + }), + ); - store.dispatch(fillAssistantHistoryData({ - id: data.id, - sequence: data.sequence, - term: data.term, - content: { - type: data.type, - [data.type]: data[data.type], - sequence: data.sequence + if (data.errors?.length && !chat.error) { + store.dispatch(setError(data.errors[0])); } - })); - - if (data.errors?.length && !chat.error) { - store.dispatch(setError(data.errors[0])); - } - }); + }, + ); socket.on(Events.streamEnd, () => { store.dispatch(setIsStreaming(false)); diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 5aa7089..258414c 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -11,7 +11,7 @@ export const meta: MetaState = { marketing: { screen: {}, lastUtmParams: {}, - utmParams: {} + utmParams: {}, }, }; @@ -39,7 +39,7 @@ export const config: ConfigState = { code: '', image: '', title: '', - } + }, }; export const chat: ChatState = { @@ -57,6 +57,7 @@ export const chat: ChatState = { closed: false, isStreaming: false, record: {}, + thread: {}, }; export const intentions: IntentionsState = { @@ -79,7 +80,7 @@ export const intentions: IntentionsState = { messaging: { isVisible: false, }, - link: '' + link: '', }; export default { meta, config, intentions, chat }; diff --git a/src/store/slices/chat.ts b/src/store/slices/chat.ts index 2368eb7..64573f9 100644 --- a/src/store/slices/chat.ts +++ b/src/store/slices/chat.ts @@ -2,7 +2,7 @@ import { createSlice, Draft, PayloadAction } from '@reduxjs/toolkit'; import produce from 'immer'; import { getUnifiedSequence } from '../../config'; -import { Definition, QueryParams, Roles } from '../../config/enums'; +import { Definition, Roles } from '../../config/enums'; import { AssistantHistoryDataFiller, AssistantHistoryInitialMessage, @@ -30,63 +30,72 @@ const configSlice = createSlice({ resetOutgoing(state) { state.outgoing = initialState.outgoing; }, - initiateRecordByTerm(state) { - const term = getQueryParam(); - if (!state.record[term]) { - state.record[term] = { - historyIds: [], - historyData: {}, - }; - } + initiateRecordByThread(state, { payload: { threadId } }) { + return produce(state, (draft) => { + if (!draft.record[threadId]) { + draft.record[threadId] = { + historyIds: [], + historyData: {}, + }; + draft.thread[getQueryParam()] = threadId; + } + }); }, syncMessageStatus(state, { payload: { term, history } }) { if (term !== getQueryParam()) { return; } + const thread = state.thread[term]; return produce(state, (draft) => { - const unsentMessages = draft.record[term].historyIds.filter((id) => !history.find((record: SocketHistoryRecord) => record.id === id)); + const unsentMessages = draft.record[thread].historyIds.filter( + (id) => !history.find((record: SocketHistoryRecord) => record.id === id), + ); unsentMessages.forEach( (id) => - (draft.record[term].historyData[id].content = draft.record[term].historyData[id].content.map((record) => ({ - ...record, - sent: false, - resend: true, - }))), + (draft.record[thread].historyData[id].content = draft.record[thread].historyData[id].content.map( + (record) => ({ + ...record, + sent: false, + resend: true, + }), + )), ); - }) + }); }, - updateHistoryByTerm(state, { payload: { history, term } }: PayloadAction<{ history: Array, term: string }>) { + updateHistoryByTerm( + state, + { payload: { history, term } }: PayloadAction<{ history: Array; term: string }>, + ) { if (term !== getQueryParam()) { - return + return; } return produce(state, (draft) => { const clientIds = draft.record[term].historyIds; const serverIds = history.map(({ id }) => id); + const thread = state.thread[term]; // DEV NOTE: here i search for ids that are not recorded in the client for (let i = 0; i < serverIds.length; i++) { const currentId = serverIds[i]; if (!clientIds.includes(currentId)) { clientIds.splice(i, 0, currentId); - draft.record[term].historyData[currentId] = history[i]; + draft.record[thread].historyData[currentId] = history[i]; } } - draft.record[term].historyIds = clientIds; + draft.record[thread].historyIds = clientIds; }); }, - addPredefinedAssistantMessage( - state, - { payload }: PayloadAction - ) { + addPredefinedAssistantMessage(state, { payload }: PayloadAction) { const id = uuidV4(); const term = getQueryParam(); + const thread = state.thread[term]; return produce(state, (draft: Draft): void => { - draft.record[term].historyIds.push(id); - draft.record[term].historyData[id] = { + draft.record[thread].historyIds.push(id); + draft.record[thread].historyData[id] = { id, role: Roles.assistant, content: [ @@ -96,12 +105,16 @@ const configSlice = createSlice({ }; }); }, - fillAssistantHistoryData(state, { payload: { id, content, sequence, term } }: PayloadAction) { + fillAssistantHistoryData( + state, + { payload: { id, content, sequence, term } }: PayloadAction, + ) { return produce(state, (draft: Draft) => { + const thread = state.thread[term]; - if (draft.record[term] && !draft.record[term].historyData[id]) { - draft.record[term].historyData[id] = { id, role: Roles.assistant, content: [] }; - draft.record[term].historyIds.push(id); + if (draft.record[thread] && !draft.record[thread].historyData[id]) { + draft.record[thread].historyData[id] = { id, role: Roles.assistant, content: [] }; + draft.record[thread].historyIds.push(id); return; } @@ -116,8 +129,10 @@ const configSlice = createSlice({ }; // check for duplicates and unite them - if (draft.record[term].historyData[id].content.some( - (record) => record.sequence === data.sequence && record.type === data.type) + if ( + draft.record[term].historyData[id].content.some( + (record) => record.sequence === data.sequence && record.type === data.type, + ) ) { draft.record[term].historyData[id].content = getUnifiedSequence( draft.record[term].historyData[id].content as Array, @@ -129,15 +144,13 @@ const configSlice = createSlice({ draft.record[term].historyData[id].content.push(data); }); }, - fillUserHistoryData( - state, - { payload: { id, content, term } }: PayloadAction - ) { - + fillUserHistoryData(state, { payload: { id, content, term } }: PayloadAction) { return produce(state, (draft: Draft) => { let belongsTo; - if (content.groupId && draft.record[term]?.historyData) { - Object.entries(draft.record[term].historyData).forEach(([key, value]) => { + const thread = state.thread[term]; + + if (content.groupId && draft.record[thread]?.historyData) { + Object.entries(draft.record[thread].historyData).forEach(([key, value]) => { if ([...value.content].find((el) => el.groupId === content.groupId)) { belongsTo = key; } @@ -146,27 +159,30 @@ const configSlice = createSlice({ if (belongsTo) { // this is due to keyboard interaction we send messages after timeout - const userMessageRecord = draft.record[term].historyData[belongsTo]; + const userMessageRecord = draft.record[thread].historyData[belongsTo]; userMessageRecord.content.push(content); return; } - if (!draft.record[term].historyData[id]) { - draft.record[term].historyData[id] = { id, role: Roles.user, content: [content] }; - draft.record[term].historyIds.push(id); + if (!draft.record[thread].historyData[id]) { + draft.record[thread].historyData[id] = { id, role: Roles.user, content: [content] }; + draft.record[thread].historyIds.push(id); } }); }, fillInitialMessage( state, - { payload: { message, term } }: PayloadAction<{ message: AssistantHistoryInitialMessage, term: string }> + { payload: { message, term } }: PayloadAction<{ message: AssistantHistoryInitialMessage; term: string }>, ) { const currentTerm = getQueryParam(); if (term !== currentTerm) { - return + return; } - state.record[term].historyIds.push(message.id); - state.record[term].historyData[message.id] = { + + const thread = state.thread[term]; + + state.record[thread].historyIds.push(message.id); + state.record[thread].historyData[message.id] = { id: message.id, role: Roles.assistant, time: new Date().getTime(), @@ -191,7 +207,11 @@ const configSlice = createSlice({ showResendIcon(state, { payload }: PayloadAction<{ itemId: string }>) { const term = getQueryParam(); return produce(state, (draft: Draft) => { - draft.record[term].historyData[payload.itemId].content = draft.record[term].historyData[payload.itemId].content.map((record) => ({ + const thread = state.thread[term]; + + draft.record[thread].historyData[payload.itemId].content = draft.record[thread].historyData[ + payload.itemId + ].content.map((record) => ({ ...record, sent: false, resend: true, @@ -204,7 +224,11 @@ const configSlice = createSlice({ resendMessage(state, { payload }: PayloadAction<{ itemId: string }>) { const term = getQueryParam(); return produce(state, (draft: Draft) => { - draft.record[term].historyData[payload.itemId].content = draft.record[term].historyData[payload.itemId].content.map((record) => ({ + const thread = state.thread[term]; + + draft.record[thread].historyData[payload.itemId].content = draft.record[thread].historyData[ + payload.itemId + ].content.map((record) => ({ ...record, sent: true, resend: false, @@ -220,14 +244,14 @@ const configSlice = createSlice({ setIsStreaming(state, { payload }: PayloadAction) { state.isStreaming = payload; }, - resetHistory(state, { payload: { term } }) { + resetHistory(state, { payload: { term, thread } }) { const currentTerm = getQueryParam(); if (currentTerm !== term) { - return + return; } - state.record[term] = { historyData: {}, historyIds: [] }; + state.record[thread] = { historyData: {}, historyIds: [] }; }, }, }); @@ -235,8 +259,11 @@ const configSlice = createSlice({ export const getChat = (state: { chat: ChatState }) => state.chat; export const userMessageFindOne = (state: { chat: ChatState }) => { const term = getQueryParam(); - return state.chat.record[term].historyIds.find((historyId) => state.chat.record[term].historyData[historyId].role === Roles.user); -} + const threadId = state.chat.thread[term]; + return state.chat.record[threadId].historyIds.find( + (historyId) => state.chat.record[threadId].historyData[historyId].role === Roles.user, + ); +}; export const sortBySequence = (a: AssistantRecord, b: AssistantRecord) => a.sequence - b.sequence; export const { @@ -259,8 +286,8 @@ export const { fillUserHistoryData, resetHistory, fillInitialMessage, - initiateRecordByTerm, - syncMessageStatus + initiateRecordByThread, + syncMessageStatus, } = configSlice.actions; export default configSlice.reducer;