chore: sync local workspace changes
This commit is contained in:
@@ -47,7 +47,13 @@ export type ChatGateway = {
|
||||
payload: Partial<
|
||||
Pick<
|
||||
ChatConversationSummary,
|
||||
'title' | 'chatTypeId' | 'lastChatTypeId' | 'contextLabel' | 'contextDescription' | 'notifyOffline'
|
||||
| 'title'
|
||||
| 'chatTypeId'
|
||||
| 'lastChatTypeId'
|
||||
| 'generalSectionName'
|
||||
| 'contextLabel'
|
||||
| 'contextDescription'
|
||||
| 'notifyOffline'
|
||||
>
|
||||
>,
|
||||
) => Promise<ChatConversationSummary>;
|
||||
|
||||
@@ -20,6 +20,7 @@ type PendingChatRequest = {
|
||||
requestId: string;
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
omitPromptHistory?: boolean;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
@@ -35,6 +36,7 @@ type PendingContextConfirm = {
|
||||
chatTypeDescription: string;
|
||||
includedContextCount: number;
|
||||
omittedContextCount: number;
|
||||
omitPromptHistory?: boolean;
|
||||
};
|
||||
|
||||
type SelectedChatType = {
|
||||
@@ -87,6 +89,11 @@ type UseConversationComposerControllerOptions = {
|
||||
scrollViewportToBottom: () => void;
|
||||
};
|
||||
|
||||
type SendMessageOptions = {
|
||||
mode: 'queue' | 'direct';
|
||||
draftText?: string;
|
||||
};
|
||||
|
||||
export function useConversationComposerController({
|
||||
activeSessionId,
|
||||
appConfigChat,
|
||||
@@ -219,13 +226,14 @@ export function useConversationComposerController({
|
||||
|
||||
const executeSendMessage = useCallback(
|
||||
(request: PendingContextConfirm) => {
|
||||
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription } = request;
|
||||
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription, omitPromptHistory } = request;
|
||||
const requestId = `client-${Date.now().toString(36)}`;
|
||||
const outgoingRequest: PendingChatRequest = {
|
||||
sessionId: activeSessionId,
|
||||
requestId,
|
||||
text,
|
||||
mode,
|
||||
omitPromptHistory: omitPromptHistory === true,
|
||||
chatTypeId,
|
||||
chatTypeLabel,
|
||||
chatTypeDescription,
|
||||
@@ -361,12 +369,12 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(mode: 'queue' | 'direct') => {
|
||||
({ mode, draftText }: SendMessageOptions) => {
|
||||
if (isComposerAttachmentUploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = buildOutgoingMessageText(draft, composerAttachments).trim();
|
||||
const trimmed = buildOutgoingMessageText(draftText ?? draft, composerAttachments).trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return;
|
||||
@@ -427,11 +435,11 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
sendMessage('queue');
|
||||
sendMessage({ mode: 'queue' });
|
||||
}, [sendMessage]);
|
||||
|
||||
const handleSendImmediate = useCallback(() => {
|
||||
sendMessage('direct');
|
||||
sendMessage({ mode: 'direct' });
|
||||
}, [sendMessage]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { chatGateway } from '../data/chatGateway';
|
||||
|
||||
type UseConversationListDataOptions = {
|
||||
requestedSessionId: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type UseConversationListDataResult = {
|
||||
@@ -17,37 +18,71 @@ type UseConversationListDataResult = {
|
||||
setConversationSearch: Dispatch<SetStateAction<string>>;
|
||||
};
|
||||
|
||||
const CONVERSATION_LIST_POLL_INTERVAL_MS = 5000;
|
||||
|
||||
function mergeConversationItemsPreservingRequestedSession(
|
||||
nextItems: ChatConversationSummary[],
|
||||
previousItems: ChatConversationSummary[],
|
||||
requestedSessionId: string,
|
||||
) {
|
||||
const previousBySessionId = new Map(previousItems.map((item) => [item.sessionId, item] as const));
|
||||
const normalizedNextItems = nextItems.map((item) => {
|
||||
const previousItem = previousBySessionId.get(item.sessionId);
|
||||
|
||||
if (!previousItem) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const chatTypeId = item.chatTypeId?.trim() || previousItem.chatTypeId?.trim() || null;
|
||||
const lastChatTypeId =
|
||||
item.lastChatTypeId?.trim() ||
|
||||
chatTypeId ||
|
||||
previousItem.lastChatTypeId?.trim() ||
|
||||
previousItem.chatTypeId?.trim() ||
|
||||
null;
|
||||
|
||||
return {
|
||||
...item,
|
||||
chatTypeId,
|
||||
lastChatTypeId,
|
||||
generalSectionName: item.generalSectionName?.trim() || previousItem.generalSectionName?.trim() || null,
|
||||
contextLabel: item.contextLabel?.trim() || previousItem.contextLabel?.trim() || null,
|
||||
contextDescription: item.contextDescription?.trim() || previousItem.contextDescription?.trim() || null,
|
||||
lastMessagePreview: item.lastMessagePreview.trim() || previousItem.lastMessagePreview.trim(),
|
||||
lastResponsePreview: item.lastResponsePreview.trim() || previousItem.lastResponsePreview.trim(),
|
||||
};
|
||||
});
|
||||
const normalizedRequestedSessionId = requestedSessionId.trim();
|
||||
const nextSessionIds = new Set(normalizedNextItems.map((item) => item.sessionId));
|
||||
const preservedTransientItems = previousItems.filter((item) => {
|
||||
if (!item.sessionId || nextSessionIds.has(item.sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.isDraftOnly || item.currentJobStatus === 'queued' || item.currentJobStatus === 'started';
|
||||
});
|
||||
|
||||
if (!normalizedRequestedSessionId) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
const hasRequestedSession = nextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
|
||||
const hasRequestedSession = normalizedNextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
|
||||
|
||||
if (hasRequestedSession) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
const preservedRequestedSession =
|
||||
previousItems.find((item) => item.sessionId === normalizedRequestedSessionId) ?? null;
|
||||
|
||||
if (!preservedRequestedSession) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
return sortChatConversationSummaries([preservedRequestedSession, ...nextItems]);
|
||||
return sortChatConversationSummaries([preservedRequestedSession, ...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
export function useConversationListData({
|
||||
requestedSessionId,
|
||||
enabled = true,
|
||||
}: UseConversationListDataOptions): UseConversationListDataResult {
|
||||
const [conversationItems, setConversationItems] = useState<ChatConversationSummary[]>([]);
|
||||
const [isConversationListLoading, setIsConversationListLoading] = useState(false);
|
||||
@@ -104,65 +139,17 @@ export function useConversationListData({
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
setIsConversationListLoading(true);
|
||||
void loadConversationItems();
|
||||
if (enabled) {
|
||||
setIsConversationListLoading(true);
|
||||
void loadConversationItems();
|
||||
} else {
|
||||
setIsConversationListLoading(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [loadConversationItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let intervalId: number | null = null;
|
||||
|
||||
const startPolling = () => {
|
||||
if (intervalId != null || document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
intervalId = window.setInterval(() => {
|
||||
void loadConversationItems({ silent: true });
|
||||
}, CONVERSATION_LIST_POLL_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (intervalId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void loadConversationItems({ silent: true });
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
void loadConversationItems({ silent: true });
|
||||
startPolling();
|
||||
};
|
||||
|
||||
startPolling();
|
||||
window.addEventListener('focus', handleFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [loadConversationItems]);
|
||||
}, [enabled, loadConversationItems]);
|
||||
|
||||
return {
|
||||
conversationItems,
|
||||
|
||||
@@ -14,6 +14,7 @@ type PendingChatRequest = {
|
||||
requestId: string;
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
omitPromptHistory?: boolean;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
|
||||
@@ -18,14 +18,40 @@ function mergeConversationRequests(
|
||||
sessionId: string,
|
||||
) {
|
||||
const previousSessionItems = previous.filter((item) => item.sessionId === sessionId);
|
||||
const previousByRequestId = new Map(previousSessionItems.map((item) => [item.requestId, item] as const));
|
||||
const incomingRequestIds = new Set(incoming.map((item) => item.requestId));
|
||||
const mergedIncoming = incoming.map((item) => {
|
||||
const previousItem = previousByRequestId.get(item.requestId);
|
||||
|
||||
if (!previousItem) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const nextUserText = item.userText.trim() || previousItem.userText.trim();
|
||||
const nextResponseText = item.responseText.trim() || previousItem.responseText.trim();
|
||||
const nextStatusMessage = item.statusMessage?.trim() || previousItem.statusMessage?.trim() || null;
|
||||
|
||||
return {
|
||||
...item,
|
||||
statusMessage: nextStatusMessage,
|
||||
userMessageId: item.userMessageId ?? previousItem.userMessageId,
|
||||
userText: nextUserText,
|
||||
responseMessageId: item.responseMessageId ?? previousItem.responseMessageId,
|
||||
responseText: nextResponseText,
|
||||
hasResponse: item.hasResponse || previousItem.hasResponse || nextResponseText.length > 0,
|
||||
answeredAt: item.answeredAt ?? previousItem.answeredAt,
|
||||
terminalAt: item.terminalAt ?? previousItem.terminalAt,
|
||||
};
|
||||
});
|
||||
const preservedLocalItems = previousSessionItems.filter((item) => !incomingRequestIds.has(item.requestId));
|
||||
|
||||
return [...incoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
||||
return [...mergedIncoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
||||
}
|
||||
|
||||
type UseConversationRoomDataOptions = {
|
||||
activeSessionId: string;
|
||||
activeConversationIsDraftOnly?: boolean;
|
||||
activeConversationHasLocalActivity?: boolean;
|
||||
oldestLoadedMessageId: number | null;
|
||||
reloadKey: number;
|
||||
shouldForceStickToBottomOnNextLoadRef: MutableRefObject<boolean>;
|
||||
@@ -50,6 +76,8 @@ type UseConversationRoomDataOptions = {
|
||||
|
||||
export function useConversationRoomData({
|
||||
activeSessionId,
|
||||
activeConversationIsDraftOnly = false,
|
||||
activeConversationHasLocalActivity = false,
|
||||
oldestLoadedMessageId,
|
||||
reloadKey,
|
||||
shouldForceStickToBottomOnNextLoadRef,
|
||||
@@ -85,6 +113,18 @@ export function useConversationRoomData({
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeConversationIsDraftOnly && !activeConversationHasLocalActivity) {
|
||||
previousSessionIdRef.current = activeSessionId;
|
||||
setMessages([]);
|
||||
setRequestItems((previous) => previous.filter((item) => item.sessionId !== activeSessionId));
|
||||
setConversationLoadingLabel('첫 요청을 보내면 대화가 저장됩니다.');
|
||||
setIsConversationContentLoading(false);
|
||||
setIsLoadingOlderMessages(false);
|
||||
setHasOlderMessages(false);
|
||||
setOldestLoadedMessageId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
const requestedSessionId = activeSessionId;
|
||||
|
||||
@@ -195,6 +235,8 @@ export function useConversationRoomData({
|
||||
};
|
||||
}, [
|
||||
activeSessionId,
|
||||
activeConversationHasLocalActivity,
|
||||
activeConversationIsDraftOnly,
|
||||
captureViewportRestoreSnapshot,
|
||||
messagesRef,
|
||||
pendingViewportRestoreRef,
|
||||
|
||||
Reference in New Issue
Block a user