feat: update codex live chat workflow

This commit is contained in:
2026-04-22 20:00:38 +09:00
parent 9e4b70f1f1
commit b0b9980a6c
70 changed files with 5178 additions and 2401 deletions

View File

@@ -1,6 +1,6 @@
import type { Dispatch, SetStateAction } from 'react';
import { appendClientIdHeader, getOrCreateClientId } from '../clientIdentity';
import { getRegisteredAccessToken } from '../tokenAccess';
import { getRegisteredAccessToken, hasRegisteredAccessTokenAccess } from '../tokenAccess';
import { reportClientError } from '../errorLogApi';
import type {
ChatActivityEvent,
@@ -17,28 +17,66 @@ import type {
ChatViewContext,
} from './types';
const CONNECT_TIMEOUT_MS = 8000;
const CONNECT_TIMEOUT_MS = 20000;
const CHAT_SESSION_ID_KEY = 'main-chat-panel:session-id';
const CHAT_LAST_EVENT_ID_STORAGE_PREFIX = 'main-chat-panel:last-event-id:';
const CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX = 'main-chat-panel:notify-offline:';
const CHAT_SESSION_MESSAGES_STORAGE_PREFIX = 'main-chat-panel:messages:';
const CHAT_SESSION_LAST_TYPE_STORAGE_PREFIX = 'main-chat-panel:last-chat-type:';
const CHAT_INTRO_MESSAGE = '요청은 기본적으로 순차 처리됩니다. 급한 요청만 즉시 실행을 사용하세요.';
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
const KST_TIME_ZONE = 'Asia/Seoul';
const CHAT_CONVERSATION_LIST_CACHE_WINDOW_MS = 1500;
const chatSessionLastTypeMemory = new Map<string, string>();
const chatLastEventIdMemory = new Map<string, number>();
const chatOfflineNotificationMemory = new Map<string, boolean>();
let chatClientSessionIdMemory = '';
let localMessageSequence = 0;
let cachedChatConversationList: ChatConversationSummary[] | null = null;
let cachedChatConversationListAt = 0;
let chatConversationListRequestPromise: Promise<ChatConversationSummary[]> | null = null;
export function invalidateChatConversationListCache() {
cachedChatConversationList = null;
cachedChatConversationListAt = 0;
chatConversationListRequestPromise = null;
}
function toConversationSortTime(value: string | null | undefined) {
if (typeof value !== 'string' || !value.trim()) {
return 0;
}
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
export function sortChatConversationSummaries(items: ChatConversationSummary[]) {
return [...items].sort((left, right) => {
const leftTime = Math.max(
toConversationSortTime(left.lastMessageAt),
toConversationSortTime(left.updatedAt),
toConversationSortTime(left.createdAt),
);
const rightTime = Math.max(
toConversationSortTime(right.lastMessageAt),
toConversationSortTime(right.updatedAt),
toConversationSortTime(right.createdAt),
);
if (rightTime !== leftTime) {
return rightTime - leftTime;
}
return left.sessionId.localeCompare(right.sessionId, 'ko-KR');
});
}
export const CHAT_CONNECTION = {
reconnectDelayMs: 3000,
reconnectDelayMs: 1500,
connectTimeoutMs: CONNECT_TIMEOUT_MS,
sessionIdKey: CHAT_SESSION_ID_KEY,
lastEventIdStoragePrefix: CHAT_LAST_EVENT_ID_STORAGE_PREFIX,
notifyOfflineStoragePrefix: CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX,
sessionMessagesStoragePrefix: CHAT_SESSION_MESSAGES_STORAGE_PREFIX,
sessionLastTypeStoragePrefix: CHAT_SESSION_LAST_TYPE_STORAGE_PREFIX,
introMessage: CHAT_INTRO_MESSAGE,
} as const;
@@ -49,21 +87,6 @@ function buildNotifyOfflineStorageKey(sessionId: string, clientId?: string | nul
return `${CHAT_CONNECTION.notifyOfflineStoragePrefix}${normalizedSessionId}:${normalizedClientId}`;
}
function buildLastEventIdStorageKey(sessionId: string) {
const normalizedSessionId = sessionId.trim() || 'default';
return `${CHAT_CONNECTION.lastEventIdStoragePrefix}${normalizedSessionId}`;
}
function buildSessionMessagesStorageKey(sessionId: string) {
const normalizedSessionId = sessionId.trim() || 'default';
return `${CHAT_CONNECTION.sessionMessagesStoragePrefix}${normalizedSessionId}`;
}
function buildSessionLastChatTypeStorageKey(sessionId: string) {
const normalizedSessionId = sessionId.trim() || 'default';
return `${CHAT_CONNECTION.sessionLastTypeStoragePrefix}${normalizedSessionId}`;
}
function createBrowserSessionId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
@@ -77,29 +100,10 @@ export function clearStoredChatClientConversationState() {
return;
}
const keysToRemove: string[] = [];
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index);
if (!key) {
continue;
}
if (
key === CHAT_CONNECTION.sessionIdKey ||
key.startsWith(CHAT_CONNECTION.lastEventIdStoragePrefix) ||
key.startsWith(CHAT_CONNECTION.notifyOfflineStoragePrefix) ||
key.startsWith(CHAT_CONNECTION.sessionMessagesStoragePrefix) ||
key.startsWith(CHAT_CONNECTION.sessionLastTypeStoragePrefix)
) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => {
window.localStorage.removeItem(key);
});
chatClientSessionIdMemory = '';
chatSessionLastTypeMemory.clear();
chatLastEventIdMemory.clear();
chatOfflineNotificationMemory.clear();
}
function normalizeChatConversationRequest(item: ChatConversationRequest): ChatConversationRequest {
@@ -123,15 +127,12 @@ export function getChatClientSessionId() {
return '';
}
const existing = window.localStorage.getItem(CHAT_CONNECTION.sessionIdKey)?.trim();
if (existing) {
return existing;
if (chatClientSessionIdMemory) {
return chatClientSessionIdMemory;
}
const nextSessionId = createBrowserSessionId();
window.localStorage.setItem(CHAT_CONNECTION.sessionIdKey, nextSessionId);
return nextSessionId;
chatClientSessionIdMemory = createBrowserSessionId();
return chatClientSessionIdMemory;
}
export function setChatClientSessionId(sessionId: string) {
@@ -145,7 +146,7 @@ export function setChatClientSessionId(sessionId: string) {
return;
}
window.localStorage.setItem(CHAT_CONNECTION.sessionIdKey, normalizedSessionId);
chatClientSessionIdMemory = normalizedSessionId;
}
export function getLastReceivedChatEventId(sessionId: string) {
@@ -159,9 +160,7 @@ export function getLastReceivedChatEventId(sessionId: string) {
return 0;
}
const raw = window.localStorage.getItem(buildLastEventIdStorageKey(normalizedSessionId));
const parsed = raw ? Number(raw) : NaN;
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
return chatLastEventIdMemory.get(normalizedSessionId) ?? 0;
}
export function persistLastReceivedChatEventId(sessionId: string, eventId: number) {
@@ -181,7 +180,7 @@ export function persistLastReceivedChatEventId(sessionId: string, eventId: numbe
return;
}
window.localStorage.setItem(buildLastEventIdStorageKey(normalizedSessionId), String(eventId));
chatLastEventIdMemory.set(normalizedSessionId, eventId);
}
export function resetLastReceivedChatEventId(sessionId: string) {
@@ -195,7 +194,7 @@ export function resetLastReceivedChatEventId(sessionId: string) {
return;
}
window.localStorage.removeItem(buildLastEventIdStorageKey(normalizedSessionId));
chatLastEventIdMemory.delete(normalizedSessionId);
}
export function getStoredChatOfflineNotificationSetting(sessionId: string, clientId?: string | null) {
@@ -203,13 +202,13 @@ export function getStoredChatOfflineNotificationSetting(sessionId: string, clien
return null;
}
const raw = window.localStorage.getItem(buildNotifyOfflineStorageKey(sessionId, clientId));
const key = buildNotifyOfflineStorageKey(sessionId, clientId);
if (raw === null) {
if (!chatOfflineNotificationMemory.has(key)) {
return null;
}
return raw === 'true';
return chatOfflineNotificationMemory.get(key) ?? null;
}
export function setStoredChatOfflineNotificationSetting(sessionId: string, enabled: boolean, clientId?: string | null) {
@@ -217,7 +216,7 @@ export function setStoredChatOfflineNotificationSetting(sessionId: string, enabl
return;
}
window.localStorage.setItem(buildNotifyOfflineStorageKey(sessionId, clientId), enabled ? 'true' : 'false');
chatOfflineNotificationMemory.set(buildNotifyOfflineStorageKey(sessionId, clientId), enabled);
}
export function clearStoredChatOfflineNotificationSetting(sessionId: string, clientId?: string | null) {
@@ -225,7 +224,7 @@ export function clearStoredChatOfflineNotificationSetting(sessionId: string, cli
return;
}
window.localStorage.removeItem(buildNotifyOfflineStorageKey(sessionId, clientId));
chatOfflineNotificationMemory.delete(buildNotifyOfflineStorageKey(sessionId, clientId));
}
function resolveSyncedChatOfflineNotificationSetting(
@@ -247,106 +246,31 @@ function resolveSyncedChatOfflineNotificationSetting(
return serverValue;
}
export function loadStoredChatMessages(sessionId: string) {
if (typeof window === 'undefined') {
return [] as ChatMessage[];
}
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
return [] as ChatMessage[];
}
try {
const raw = window.localStorage.getItem(buildSessionMessagesStorageKey(normalizedSessionId));
if (!raw) {
return [] as ChatMessage[];
}
const parsed = JSON.parse(raw) as ChatMessage[];
if (!Array.isArray(parsed)) {
return [] as ChatMessage[];
}
return parsed
.filter((message) =>
Boolean(message) &&
(message.author === 'codex' || message.author === 'system' || message.author === 'user') &&
typeof message.text === 'string' &&
typeof message.timestamp === 'string' &&
typeof message.id === 'number',
)
.filter((message) => message.author !== 'system' || isActivityLogMessage(message));
} catch {
return [] as ChatMessage[];
}
}
export function getStoredChatSessionLastTypeId(sessionId: string) {
if (typeof window === 'undefined') {
return null;
}
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
return null;
}
const raw = window.localStorage.getItem(buildSessionLastChatTypeStorageKey(normalizedSessionId))?.trim() ?? '';
const raw = chatSessionLastTypeMemory.get(normalizedSessionId)?.trim() ?? '';
return raw || null;
}
export function setStoredChatSessionLastTypeId(sessionId: string, chatTypeId: string) {
if (typeof window === 'undefined') {
return;
}
const normalizedSessionId = sessionId.trim();
const normalizedChatTypeId = chatTypeId.trim();
if (!normalizedSessionId || !normalizedChatTypeId) {
return;
}
window.localStorage.setItem(
buildSessionLastChatTypeStorageKey(normalizedSessionId),
normalizedChatTypeId,
);
}
export function persistStoredChatMessages(sessionId: string, messages: ChatMessage[]) {
if (typeof window === 'undefined') {
return;
}
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
return;
}
window.localStorage.setItem(
buildSessionMessagesStorageKey(normalizedSessionId),
JSON.stringify(messages.filter((message) => message.author !== 'system' || isActivityLogMessage(message))),
);
}
export function clearStoredChatMessages(sessionId: string) {
if (typeof window === 'undefined') {
if (!normalizedChatTypeId) {
chatSessionLastTypeMemory.delete(normalizedSessionId);
return;
}
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
return;
}
window.localStorage.removeItem(buildSessionMessagesStorageKey(normalizedSessionId));
chatSessionLastTypeMemory.set(normalizedSessionId, normalizedChatTypeId);
}
export function formatTime(date: Date) {
@@ -369,6 +293,20 @@ function createLocalMessageId() {
return Date.now() * 1_000 + localMessageSequence;
}
function createRecoveredMessageId(requestId: string, variant: 'user' | 'codex' | 'activity') {
const baseId = hashRequestId(requestId) * 10;
if (variant === 'user') {
return -(baseId + 3);
}
if (variant === 'activity') {
return -(baseId + 2);
}
return -(baseId + 1);
}
function hashRequestId(value: string) {
let hash = 0;
@@ -606,6 +544,7 @@ export function buildOfflineReply(context: ChatViewContext, input: string) {
export function resolveChatWebSocketUrl(sessionId?: string, lastEventId?: number, clientId?: string) {
const configuredBaseUrl = import.meta.env.VITE_WORK_SERVER_URL;
const resolvedClientId = clientId || getOrCreateClientId();
const accessToken = getRegisteredAccessToken();
if (typeof window === 'undefined') {
return '';
@@ -626,6 +565,9 @@ export function resolveChatWebSocketUrl(sessionId?: string, lastEventId?: number
if (resolvedClientId) {
normalizedUrl.searchParams.set('clientId', resolvedClientId);
}
if (accessToken) {
normalizedUrl.searchParams.set('accessToken', accessToken);
}
if (Number.isFinite(lastEventId) && (lastEventId ?? 0) > 0) {
normalizedUrl.searchParams.set('lastEventId', String(lastEventId));
}
@@ -641,6 +583,9 @@ export function resolveChatWebSocketUrl(sessionId?: string, lastEventId?: number
if (resolvedClientId) {
url.searchParams.set('clientId', resolvedClientId);
}
if (accessToken) {
url.searchParams.set('accessToken', accessToken);
}
if (Number.isFinite(lastEventId) && (lastEventId ?? 0) > 0) {
url.searchParams.set('lastEventId', String(lastEventId));
}
@@ -736,6 +681,106 @@ export async function copyText(text: string) {
}
}
export type PreviewCopyResult = 'text' | 'image' | 'url';
async function copyImagePreview(url: string): Promise<PreviewCopyResult> {
const response = await fetch(url, {
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`preview 이미지를 가져오지 못했습니다. (${response.status})`);
}
const imageBlob = await response.blob();
if (!imageBlob.type.startsWith('image/')) {
throw new Error('이미지 preview만 이미지 자체를 복사할 수 있습니다.');
}
if (typeof navigator !== 'undefined' && navigator.clipboard?.write && typeof ClipboardItem !== 'undefined') {
await navigator.clipboard.write([
new ClipboardItem({
[imageBlob.type]: imageBlob,
}),
]);
return 'image';
}
await copyText(url);
return 'url';
}
function canCopyPreviewBody(kind: string | null | undefined) {
return !['image', 'video', 'pdf', 'file'].includes(String(kind ?? '').trim().toLowerCase());
}
export async function copyPreviewContent({
kind,
url,
fallbackText,
}: {
kind: string | null | undefined;
url: string;
fallbackText?: string | null;
}): Promise<PreviewCopyResult> {
const normalizedKind = String(kind ?? '').trim().toLowerCase();
if (normalizedKind === 'image') {
return copyImagePreview(url);
}
const previewBody = await resolvePreviewBodyForCopy({
kind,
url,
fallbackText,
});
await copyText(previewBody);
return 'text';
}
export async function resolvePreviewBodyForCopy({
kind,
url,
fallbackText,
}: {
kind: string | null | undefined;
url: string;
fallbackText?: string | null;
}) {
const normalizedFallbackText = String(fallbackText ?? '');
if (!canCopyPreviewBody(kind)) {
throw new Error('이 미리보기는 본문 텍스트를 복사할 수 없습니다.');
}
try {
const response = await fetch(url, {
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`preview 본문을 가져오지 못했습니다. (${response.status})`);
}
const bodyText = await response.text();
if (bodyText.trim()) {
return bodyText;
}
} catch (error) {
if (!normalizedFallbackText.trim()) {
throw error;
}
}
if (normalizedFallbackText.trim()) {
return normalizedFallbackText;
}
throw new Error('복사할 preview 본문이 없습니다.');
}
function resolveChatApiBaseUrl() {
if (import.meta.env.VITE_WORK_SERVER_URL) {
return import.meta.env.VITE_WORK_SERVER_URL;
@@ -751,6 +796,11 @@ async function requestChatApi<T>(path: string, init?: RequestInit): Promise<T> {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 8000);
if (!hasRegisteredAccessTokenAccess()) {
window.clearTimeout(timeoutId);
throw new Error('등록된 접근 토큰이 없어 채팅 요청을 보낼 수 없습니다.');
}
if (accessToken && !headers.has('X-Access-Token')) {
headers.set('X-Access-Token', accessToken);
}
@@ -775,17 +825,43 @@ async function requestChatApi<T>(path: string, init?: RequestInit): Promise<T> {
throw new Error('채팅 서버 응답이 지연됩니다.');
}
throw error;
throw new Error('채팅 서버 연결에 실패했습니다.');
}
window.clearTimeout(timeoutId);
if (!response.ok) {
const text = await response.text();
throw new Error(text || '채팅 API 요청에 실패했습니다.');
if (text.trim()) {
try {
const payload = JSON.parse(text) as { message?: string };
const normalizedMessage = String(payload.message ?? '').trim();
if (normalizedMessage) {
throw new Error(normalizedMessage === 'fetch failed' ? '채팅 서버 연결에 실패했습니다.' : normalizedMessage);
}
} catch (error) {
if (error instanceof Error && error.message) {
throw error;
}
}
}
throw new Error('채팅 API 요청에 실패했습니다.');
}
return response.json() as Promise<T>;
const text = await response.text();
if (!text.trim()) {
throw new Error('채팅 서버 응답이 비어 있습니다.');
}
try {
return JSON.parse(text) as T;
} catch {
throw new Error('채팅 서버 응답을 해석하지 못했습니다.');
}
}
async function readFileAsBase64(file: File) {
@@ -827,10 +903,12 @@ export async function fetchChatConversations() {
const clientId = getOrCreateClientId();
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>('/conversations')
.then((response) => {
const items = response.items.map((item) => ({
...item,
notifyOffline: resolveSyncedChatOfflineNotificationSetting(item.sessionId, item.notifyOffline, clientId),
}));
const items = sortChatConversationSummaries(
response.items.map((item) => ({
...item,
notifyOffline: resolveSyncedChatOfflineNotificationSetting(item.sessionId, item.notifyOffline, clientId),
})),
);
cachedChatConversationList = items;
cachedChatConversationListAt = Date.now();
@@ -864,19 +942,23 @@ export async function fetchChatConversationDetail(
const response = await requestChatApi<ChatConversationDetailResponse>(
`/conversations/${encodeURIComponent(sessionId)}${query.toString() ? `?${query.toString()}` : ''}`,
);
const normalizedRequests = response.requests.map((item) => normalizeChatConversationRequest(item));
const visibleRequestIds = new Set(
response.messages
.map((message) => message.clientRequestId?.trim() ?? '')
.filter(Boolean),
);
const hydratedMessages = hydrateActivityLogMessages(
response.messages,
response.activityLogs.filter((item) => visibleRequestIds.has(item.requestId?.trim() ?? '')),
).filter(
(message) => message.author !== 'system' || isActivityLogMessage(message),
);
const recoveredMessages = buildRecoveredMessagesFromConversationDetail(normalizedRequests, response.activityLogs);
return {
...response,
messages: hydrateActivityLogMessages(
response.messages,
response.activityLogs.filter((item) => visibleRequestIds.has(item.requestId?.trim() ?? '')),
).filter(
(message) => message.author !== 'system' || isActivityLogMessage(message),
),
messages: mergeRecoveredChatMessages(recoveredMessages, hydratedMessages),
item: {
...response.item,
notifyOffline: resolveSyncedChatOfflineNotificationSetting(
@@ -885,7 +967,7 @@ export async function fetchChatConversationDetail(
clientId,
),
},
requests: response.requests.map((item) => normalizeChatConversationRequest(item)),
requests: normalizedRequests,
};
}
@@ -945,6 +1027,7 @@ export async function uploadChatComposerFile(sessionId: string, file: File) {
export async function createChatConversationRoom(args: {
sessionId: string;
title?: string;
chatTypeId?: string | null;
contextLabel?: string;
contextDescription?: string;
notifyOffline?: boolean;
@@ -956,6 +1039,7 @@ export async function createChatConversationRoom(args: {
body: JSON.stringify({
sessionId: args.sessionId,
title: args.title ?? '새 대화',
chatTypeId: args.chatTypeId ?? null,
contextLabel: args.contextLabel ?? null,
contextDescription: args.contextDescription ?? null,
notifyOffline,
@@ -963,6 +1047,8 @@ export async function createChatConversationRoom(args: {
}),
});
invalidateChatConversationListCache();
return {
...response.item,
notifyOffline: resolveSyncedChatOfflineNotificationSetting(response.item.sessionId, response.item.notifyOffline, clientId),
@@ -980,6 +1066,8 @@ export async function renameChatConversationRoom(sessionId: string, title: strin
},
);
invalidateChatConversationListCache();
return response.item;
}
@@ -987,6 +1075,9 @@ export async function updateChatConversationRoom(
sessionId: string,
payload: {
title?: string;
chatTypeId?: string | null;
contextLabel?: string | null;
contextDescription?: string | null;
notifyOffline?: boolean;
},
) {
@@ -999,6 +1090,8 @@ export async function updateChatConversationRoom(
},
);
invalidateChatConversationListCache();
return {
...response.item,
notifyOffline: resolveSyncedChatOfflineNotificationSetting(response.item.sessionId, response.item.notifyOffline, clientId),
@@ -1025,6 +1118,8 @@ export async function deleteChatConversationRoom(sessionId: string) {
},
);
invalidateChatConversationListCache();
return response;
}
@@ -1116,8 +1211,8 @@ function isSameChatMessage(left: ChatMessage, right: ChatMessage) {
}
return Boolean(
left.author === 'user' &&
right.author === 'user' &&
(left.author === 'user' || left.author === 'codex') &&
left.author === right.author &&
left.clientRequestId &&
right.clientRequestId &&
left.clientRequestId === right.clientRequestId,
@@ -1133,9 +1228,78 @@ function buildComparableChatMessageKey(message: ChatMessage) {
return `user-request:${message.clientRequestId}`;
}
if (message.author === 'codex' && message.clientRequestId) {
return `codex-request:${message.clientRequestId}`;
}
return `id:${message.id}`;
}
function getComparableChatMessageTime(message: ChatMessage) {
const parsed = Date.parse(String(message.timestamp ?? '').trim());
return Number.isFinite(parsed) ? parsed : 0;
}
function buildRecoveredMessagesFromConversationDetail(
requests: ChatConversationRequest[],
activityLogs: ChatConversationActivityLog[],
) {
const nextMessages: ChatMessage[] = [];
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
requests.forEach((request) => {
const requestId = request.requestId.trim();
if (!requestId || request.status === 'removed') {
return;
}
const userText = String(request.userText ?? '').trim();
const responseText = String(request.responseText ?? '').trim();
const activityLog = activityLogMap.get(requestId);
if (userText) {
nextMessages.push({
id: request.userMessageId ?? createRecoveredMessageId(requestId, 'user'),
author: 'user',
text: userText,
timestamp: request.createdAt || request.updatedAt || '',
clientRequestId: requestId,
});
}
if (responseText) {
nextMessages.push({
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'codex'),
author: 'codex',
text: responseText,
timestamp: request.answeredAt || request.updatedAt || request.createdAt || '',
clientRequestId: requestId,
});
}
if (activityLog && activityLog.lines.length > 0) {
nextMessages.push({
id: createRecoveredMessageId(requestId, 'activity'),
author: 'system',
text: `${CHAT_ACTIVITY_MESSAGE_PREFIX}\n${activityLog.lines.join('\n\n')}`,
timestamp: request.createdAt || request.updatedAt || activityLog.updatedAt || '',
clientRequestId: requestId,
});
}
});
return nextMessages.sort((left, right) => {
const timeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
if (timeDiff !== 0) {
return timeDiff;
}
return left.id - right.id;
});
}
export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: ChatMessage[]) {
if (previous.length === 0) {
return incoming;