feat: update main chat and system chat UI
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { chatGateway } from '../data/chatGateway';
|
||||
import type { ChatComposerAttachment, ChatConversationRequest, ChatMessage } from '../../mainChatPanel/types';
|
||||
import type { ChatComposerAttachment, ChatConversationRequest, ChatMessage, ChatPromptContextRef } from '../../mainChatPanel/types';
|
||||
import { buildComposerFilePickKey } from '../../mainChatPanel/composerFilePickKey';
|
||||
import { shouldSkipContextConfirmForSessionToday } from '../../mainChatPanel/contextConfirmPreference';
|
||||
|
||||
export type ComposerFilePickResult = {
|
||||
items: {
|
||||
@@ -19,7 +20,9 @@ type PendingChatRequest = {
|
||||
mode: 'queue' | 'direct';
|
||||
origin?: 'composer' | 'prompt';
|
||||
parentRequestId?: string | null;
|
||||
promptContextRef?: ChatPromptContextRef | null;
|
||||
omitPromptHistory?: boolean;
|
||||
codexModel: string;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
@@ -37,10 +40,13 @@ type PendingChatRequest = {
|
||||
};
|
||||
|
||||
type PendingContextConfirm = {
|
||||
sessionId: string;
|
||||
mode: 'queue' | 'direct';
|
||||
text: string;
|
||||
origin?: 'composer' | 'prompt';
|
||||
parentRequestId?: string | null;
|
||||
promptContextRef?: ChatPromptContextRef | null;
|
||||
codexModel: string;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
@@ -87,6 +93,7 @@ type UseConversationComposerControllerOptions = {
|
||||
getDraft: () => string;
|
||||
composerAttachments: ChatComposerAttachment[];
|
||||
isComposerAttachmentUploading: boolean;
|
||||
selectedCodexModel: string;
|
||||
selectedChatType: SelectedChatType;
|
||||
socketRef: { current: WebSocket | null };
|
||||
composerRef: { current: { focus: (options?: { cursor?: 'start' | 'end' | 'all' }) => void } | null };
|
||||
@@ -128,13 +135,23 @@ type UseConversationComposerControllerOptions = {
|
||||
ensureSessionReady?: (sessionId: string) => Promise<boolean>;
|
||||
sendChatRequest: (socket: WebSocket, request: PendingChatRequest) => void;
|
||||
scrollViewportToBottom: () => void;
|
||||
releaseAutoScrollSuspension: () => void;
|
||||
};
|
||||
|
||||
type SendMessageOptions = {
|
||||
sessionId?: string;
|
||||
mode: 'queue' | 'direct';
|
||||
draftText?: string;
|
||||
};
|
||||
|
||||
export type SendMessageResult = 'sent' | 'pending' | 'blocked';
|
||||
const COMPOSER_SUBMISSION_DEDUP_WINDOW_MS = 1200;
|
||||
|
||||
type RecentComposerSubmission = {
|
||||
key: string;
|
||||
submittedAt: number;
|
||||
};
|
||||
|
||||
function createClientRequestId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `client-${crypto.randomUUID()}`;
|
||||
@@ -149,6 +166,7 @@ export function useConversationComposerController({
|
||||
getDraft,
|
||||
composerAttachments,
|
||||
isComposerAttachmentUploading,
|
||||
selectedCodexModel,
|
||||
selectedChatType,
|
||||
socketRef,
|
||||
composerRef,
|
||||
@@ -176,10 +194,47 @@ export function useConversationComposerController({
|
||||
ensureSessionReady,
|
||||
sendChatRequest,
|
||||
scrollViewportToBottom,
|
||||
releaseAutoScrollSuspension,
|
||||
}: UseConversationComposerControllerOptions) {
|
||||
const composerUploadQueueRef = useRef(Promise.resolve<ComposerFilePickResult>({ items: [] }));
|
||||
const activeComposerUploadCountRef = useRef(0);
|
||||
const latestComposerUploadAttemptByKeyRef = useRef(new Map<string, string>());
|
||||
const activeComposerSubmissionKeyRef = useRef<string | null>(null);
|
||||
const recentComposerSubmissionRef = useRef<RecentComposerSubmission | null>(null);
|
||||
|
||||
const isSocketOpen = useCallback(() => {
|
||||
return Boolean(socketRef.current && socketRef.current.readyState === WebSocket.OPEN);
|
||||
}, [socketRef]);
|
||||
|
||||
const buildComposerSubmissionKey = useCallback(
|
||||
({
|
||||
sessionId,
|
||||
mode,
|
||||
text,
|
||||
codexModel,
|
||||
chatTypeId,
|
||||
parentRequestId,
|
||||
omitPromptHistory,
|
||||
}: {
|
||||
sessionId: string;
|
||||
mode: 'queue' | 'direct';
|
||||
text: string;
|
||||
codexModel: string;
|
||||
chatTypeId: string;
|
||||
parentRequestId?: string | null;
|
||||
omitPromptHistory?: boolean;
|
||||
}) =>
|
||||
JSON.stringify({
|
||||
sessionId: sessionId.trim(),
|
||||
mode,
|
||||
text: text.trim(),
|
||||
codexModel: codexModel.trim(),
|
||||
chatTypeId: chatTypeId.trim(),
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
omitPromptHistory: omitPromptHistory === true,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleComposerFilesPicked = useCallback(
|
||||
async (files: File[]): Promise<ComposerFilePickResult> => {
|
||||
@@ -286,18 +341,47 @@ export function useConversationComposerController({
|
||||
const focusComposerAfterSend = useCallback(() => {
|
||||
window.setTimeout(() => {
|
||||
composerRef.current?.focus({ cursor: 'end' });
|
||||
scrollViewportToBottom();
|
||||
}, 0);
|
||||
}, [composerRef, scrollViewportToBottom]);
|
||||
}, [composerRef]);
|
||||
|
||||
const scheduleViewportBottomSyncAfterSend = useCallback(() => {
|
||||
releaseAutoScrollSuspension();
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
scrollViewportToBottom();
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
scrollViewportToBottom();
|
||||
});
|
||||
});
|
||||
}, [releaseAutoScrollSuspension, scrollViewportToBottom, setShowScrollToBottom, shouldStickToBottomRef]);
|
||||
|
||||
const handleExecuteSendError = useCallback(
|
||||
(error: unknown) => {
|
||||
const reason =
|
||||
error instanceof Error && error.message.trim()
|
||||
? error.message.trim()
|
||||
: '요청 전송 중 오류가 발생했습니다.';
|
||||
setActiveSystemStatus(null);
|
||||
setIsSystemStatusPending(false);
|
||||
setMessages((previous) => [...previous.slice(-39), createLocalMessage(reason)]);
|
||||
},
|
||||
[createLocalMessage, setActiveSystemStatus, setIsSystemStatusPending, setMessages],
|
||||
);
|
||||
|
||||
const executeSendMessage = useCallback(
|
||||
async (request: PendingContextConfirm) => {
|
||||
const {
|
||||
sessionId,
|
||||
mode,
|
||||
text,
|
||||
origin,
|
||||
parentRequestId,
|
||||
promptContextRef,
|
||||
chatTypeId,
|
||||
codexModel,
|
||||
chatTypeLabel,
|
||||
chatTypeDescription,
|
||||
chatTypeBaseDescription,
|
||||
@@ -307,176 +391,280 @@ export function useConversationComposerController({
|
||||
customContextContent,
|
||||
omitPromptHistory,
|
||||
} = request;
|
||||
const requestChatTypeId = chatTypeId.trim();
|
||||
const requestChatTypeLabel = chatTypeLabel.trim() || requestChatTypeId || '기본 요청';
|
||||
const targetSessionId = sessionId.trim() || activeSessionId.trim();
|
||||
const submissionKey = buildComposerSubmissionKey({
|
||||
mode,
|
||||
text,
|
||||
codexModel,
|
||||
chatTypeId: requestChatTypeId,
|
||||
parentRequestId,
|
||||
omitPromptHistory,
|
||||
sessionId: targetSessionId,
|
||||
});
|
||||
|
||||
if (ensureSessionReady) {
|
||||
setActiveSystemStatus('새 채팅방 준비 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
const isSessionReady = await ensureSessionReady(activeSessionId);
|
||||
if (origin !== 'prompt' && activeComposerSubmissionKeyRef.current === submissionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSessionReady) {
|
||||
setActiveSystemStatus(null);
|
||||
setIsSystemStatusPending(false);
|
||||
if (origin !== 'prompt') {
|
||||
const recentComposerSubmission = recentComposerSubmissionRef.current;
|
||||
if (
|
||||
recentComposerSubmission &&
|
||||
recentComposerSubmission.key === submissionKey &&
|
||||
Date.now() - recentComposerSubmission.submittedAt < COMPOSER_SUBMISSION_DEDUP_WINDOW_MS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = createClientRequestId();
|
||||
const outgoingRequest: PendingChatRequest = {
|
||||
sessionId: activeSessionId,
|
||||
requestId,
|
||||
text,
|
||||
mode,
|
||||
origin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
omitPromptHistory: omitPromptHistory === true,
|
||||
chatTypeId,
|
||||
chatTypeLabel,
|
||||
chatTypeDescription,
|
||||
chatTypeBaseDescription,
|
||||
defaultContextIds,
|
||||
defaultContexts,
|
||||
customContextTitle,
|
||||
customContextContent,
|
||||
retryCount: 0,
|
||||
failed: false,
|
||||
if (origin !== 'prompt') {
|
||||
activeComposerSubmissionKeyRef.current = submissionKey;
|
||||
recentComposerSubmissionRef.current = {
|
||||
key: submissionKey,
|
||||
submittedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
const shouldOptimisticallyClearComposer = origin !== 'prompt';
|
||||
const previousDraft = shouldOptimisticallyClearComposer ? getDraft() : '';
|
||||
const previousAttachments = shouldOptimisticallyClearComposer ? composerAttachments : [];
|
||||
let composerRestoreNeeded = shouldOptimisticallyClearComposer;
|
||||
|
||||
const restoreComposerOnFailure = () => {
|
||||
if (!composerRestoreNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
composerRestoreNeeded = false;
|
||||
|
||||
if (!getDraft().trim()) {
|
||||
setDraft(previousDraft);
|
||||
}
|
||||
|
||||
setComposerAttachments((current) => {
|
||||
if (current.length > 0 || previousAttachments.length === 0) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return previousAttachments;
|
||||
});
|
||||
};
|
||||
|
||||
if (origin === 'prompt') {
|
||||
promptRequestIdsRef?.current.add(requestId);
|
||||
}
|
||||
|
||||
if (mode === 'queue') {
|
||||
const queuedAt = new Date().toISOString();
|
||||
const optimisticUserMessage: ChatMessage = {
|
||||
...createChatMessage('user', text, requestId),
|
||||
deliveryStatus: null,
|
||||
retryCount: 0,
|
||||
};
|
||||
const optimisticActivityMessage = createActivityLogPlaceholder(requestId, [
|
||||
'# 상태: 요청을 접수했습니다.',
|
||||
'# 진행: 순서를 기다리기 전에 요청 내용을 정리하고 있습니다.',
|
||||
]);
|
||||
upsertRequestItem({
|
||||
sessionId: activeSessionId,
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
status: 'queued',
|
||||
statusMessage: '대기열 등록',
|
||||
userMessageId: optimisticUserMessage.id,
|
||||
userText: text,
|
||||
responseMessageId: null,
|
||||
responseText: '',
|
||||
hasResponse: false,
|
||||
canDelete: false,
|
||||
createdAt: queuedAt,
|
||||
updatedAt: queuedAt,
|
||||
answeredAt: null,
|
||||
terminalAt: null,
|
||||
});
|
||||
syncConversationPreviewForRequest(activeSessionId, text, queuedAt, {
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
mode: 'queue',
|
||||
queueSize: 1,
|
||||
jobMessage: '대기열 등록 중',
|
||||
});
|
||||
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
setMessages((previous) => {
|
||||
const nextMessages = [...previous, optimisticUserMessage];
|
||||
return optimisticActivityMessage ? [...nextMessages, optimisticActivityMessage] : nextMessages;
|
||||
});
|
||||
setActiveSystemStatus('대기열 등록 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
} else {
|
||||
const optimisticUserMessage: ChatMessage = {
|
||||
...createChatMessage('user', text, requestId),
|
||||
deliveryStatus: null,
|
||||
retryCount: 0,
|
||||
};
|
||||
const optimisticActivityMessage = createActivityLogPlaceholder(requestId, [
|
||||
'# 상태: 즉시 요청을 접수했습니다.',
|
||||
'# 진행: 요청 내용을 정리한 뒤 답변을 준비합니다.',
|
||||
]);
|
||||
upsertRequestItem({
|
||||
sessionId: activeSessionId,
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
status: 'accepted',
|
||||
statusMessage: '요청을 접수했습니다.',
|
||||
userMessageId: optimisticUserMessage.id,
|
||||
userText: text,
|
||||
responseMessageId: null,
|
||||
responseText: '',
|
||||
hasResponse: false,
|
||||
canDelete: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
answeredAt: null,
|
||||
terminalAt: null,
|
||||
});
|
||||
syncConversationPreviewForRequest(activeSessionId, text, new Date().toISOString(), {
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
mode: 'direct',
|
||||
queueSize: 0,
|
||||
jobMessage: '즉시 요청 실행 대기 중',
|
||||
});
|
||||
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
setMessages((previous) => {
|
||||
const nextMessages = [...previous, optimisticUserMessage];
|
||||
return optimisticActivityMessage ? [...nextMessages, optimisticActivityMessage] : nextMessages;
|
||||
});
|
||||
setActiveSystemStatus('즉시 응답 준비 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
}
|
||||
|
||||
if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) {
|
||||
setActiveSystemStatus('전송 재시도 중...');
|
||||
pendingRequestsRef.current = [
|
||||
...pendingRequestsRef.current.filter((pending) => pending.requestId !== requestId),
|
||||
outgoingRequest,
|
||||
];
|
||||
if (mode === 'direct') {
|
||||
updatePendingMessageStatus(requestId, 'retrying', 0);
|
||||
}
|
||||
setDraft('');
|
||||
setComposerAttachments([]);
|
||||
focusComposerAfterSend();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sendChatRequest(socketRef.current, outgoingRequest);
|
||||
} catch {
|
||||
setActiveSystemStatus('전송 재시도 중...');
|
||||
pendingRequestsRef.current = [
|
||||
...pendingRequestsRef.current.filter((pending) => pending.requestId !== requestId),
|
||||
outgoingRequest,
|
||||
];
|
||||
if (mode === 'direct') {
|
||||
updatePendingMessageStatus(requestId, 'retrying', 0);
|
||||
if (shouldOptimisticallyClearComposer) {
|
||||
setDraft('');
|
||||
setComposerAttachments([]);
|
||||
focusComposerAfterSend();
|
||||
}
|
||||
|
||||
if (!targetSessionId) {
|
||||
restoreComposerOnFailure();
|
||||
setActiveSystemStatus(null);
|
||||
setIsSystemStatusPending(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ensureSessionReady) {
|
||||
setActiveSystemStatus('새 채팅방 준비 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
const isSessionReady = await ensureSessionReady(targetSessionId);
|
||||
|
||||
if (!isSessionReady) {
|
||||
restoreComposerOnFailure();
|
||||
setActiveSystemStatus(null);
|
||||
setIsSystemStatusPending(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSocketOpen()) {
|
||||
restoreComposerOnFailure();
|
||||
setActiveSystemStatus(null);
|
||||
setIsSystemStatusPending(false);
|
||||
setMessages((previous) => [
|
||||
...previous.slice(-39),
|
||||
createLocalMessage('워크서버 연결이 아직 준비되지 않았습니다. 연결이 복구된 뒤 다시 전송해 주세요.'),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestId = createClientRequestId();
|
||||
const outgoingRequest: PendingChatRequest = {
|
||||
sessionId: targetSessionId,
|
||||
requestId,
|
||||
text,
|
||||
mode,
|
||||
origin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
promptContextRef: promptContextRef ?? null,
|
||||
omitPromptHistory: omitPromptHistory === true,
|
||||
codexModel,
|
||||
chatTypeId: requestChatTypeId,
|
||||
chatTypeLabel: requestChatTypeLabel,
|
||||
chatTypeDescription,
|
||||
chatTypeBaseDescription,
|
||||
defaultContextIds,
|
||||
defaultContexts,
|
||||
customContextTitle,
|
||||
customContextContent,
|
||||
retryCount: 0,
|
||||
failed: false,
|
||||
};
|
||||
|
||||
if (origin === 'prompt') {
|
||||
promptRequestIdsRef?.current.add(requestId);
|
||||
}
|
||||
|
||||
if (mode === 'queue') {
|
||||
const queuedAt = new Date().toISOString();
|
||||
const optimisticUserMessage: ChatMessage = {
|
||||
...createChatMessage('user', text, requestId),
|
||||
deliveryStatus: null,
|
||||
retryCount: 0,
|
||||
};
|
||||
const optimisticActivityMessage = createActivityLogPlaceholder(requestId, [
|
||||
'# 상태: 요청을 접수했습니다.',
|
||||
'# 진행: 순서를 기다리기 전에 요청 내용을 정리하고 있습니다.',
|
||||
]);
|
||||
upsertRequestItem({
|
||||
sessionId: targetSessionId,
|
||||
requestId,
|
||||
chatTypeId: requestChatTypeId,
|
||||
chatTypeLabel: requestChatTypeLabel,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
status: 'queued',
|
||||
statusMessage: '대기열 등록',
|
||||
userMessageId: optimisticUserMessage.id,
|
||||
userText: text,
|
||||
responseMessageId: null,
|
||||
responseText: '',
|
||||
hasResponse: false,
|
||||
canDelete: false,
|
||||
createdAt: queuedAt,
|
||||
updatedAt: queuedAt,
|
||||
answeredAt: null,
|
||||
terminalAt: null,
|
||||
});
|
||||
syncConversationPreviewForRequest(targetSessionId, text, queuedAt, {
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
mode: 'queue',
|
||||
queueSize: 1,
|
||||
jobMessage: '대기열 등록 중',
|
||||
});
|
||||
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
setMessages((previous) => {
|
||||
const nextMessages = [...previous, optimisticUserMessage];
|
||||
return optimisticActivityMessage ? [...nextMessages, optimisticActivityMessage] : nextMessages;
|
||||
});
|
||||
scheduleViewportBottomSyncAfterSend();
|
||||
setActiveSystemStatus('대기열 등록 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
} else {
|
||||
const optimisticUserMessage: ChatMessage = {
|
||||
...createChatMessage('user', text, requestId),
|
||||
deliveryStatus: null,
|
||||
retryCount: 0,
|
||||
};
|
||||
const optimisticActivityMessage = createActivityLogPlaceholder(requestId, [
|
||||
'# 상태: 즉시 요청을 접수했습니다.',
|
||||
'# 진행: 요청 내용을 정리한 뒤 답변을 준비합니다.',
|
||||
]);
|
||||
upsertRequestItem({
|
||||
sessionId: targetSessionId,
|
||||
requestId,
|
||||
chatTypeId: requestChatTypeId,
|
||||
chatTypeLabel: requestChatTypeLabel,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
parentRequestId: parentRequestId?.trim() || null,
|
||||
status: 'accepted',
|
||||
statusMessage: '요청을 접수했습니다.',
|
||||
userMessageId: optimisticUserMessage.id,
|
||||
userText: text,
|
||||
responseMessageId: null,
|
||||
responseText: '',
|
||||
hasResponse: false,
|
||||
canDelete: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
answeredAt: null,
|
||||
terminalAt: null,
|
||||
});
|
||||
syncConversationPreviewForRequest(targetSessionId, text, new Date().toISOString(), {
|
||||
requestId,
|
||||
requestOrigin: origin ?? 'composer',
|
||||
mode: 'direct',
|
||||
queueSize: 0,
|
||||
jobMessage: '즉시 요청 실행 대기 중',
|
||||
});
|
||||
|
||||
shouldStickToBottomRef.current = true;
|
||||
setShowScrollToBottom(false);
|
||||
setMessages((previous) => {
|
||||
const nextMessages = [...previous, optimisticUserMessage];
|
||||
return optimisticActivityMessage ? [...nextMessages, optimisticActivityMessage] : nextMessages;
|
||||
});
|
||||
scheduleViewportBottomSyncAfterSend();
|
||||
setActiveSystemStatus('즉시 응답 준비 중...');
|
||||
setIsSystemStatusPending(true);
|
||||
}
|
||||
|
||||
if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) {
|
||||
setActiveSystemStatus('전송 재시도 중...');
|
||||
pendingRequestsRef.current = [
|
||||
...pendingRequestsRef.current.filter((pending) => pending.requestId !== requestId),
|
||||
outgoingRequest,
|
||||
];
|
||||
if (mode === 'direct') {
|
||||
updatePendingMessageStatus(requestId, 'retrying', 0);
|
||||
}
|
||||
composerRestoreNeeded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
sendChatRequest(socketRef.current, outgoingRequest);
|
||||
} catch {
|
||||
setActiveSystemStatus('전송 재시도 중...');
|
||||
pendingRequestsRef.current = [
|
||||
...pendingRequestsRef.current.filter((pending) => pending.requestId !== requestId),
|
||||
outgoingRequest,
|
||||
];
|
||||
if (mode === 'direct') {
|
||||
updatePendingMessageStatus(requestId, 'retrying', 0);
|
||||
}
|
||||
}
|
||||
|
||||
composerRestoreNeeded = false;
|
||||
return true;
|
||||
} catch (error) {
|
||||
restoreComposerOnFailure();
|
||||
throw error;
|
||||
} finally {
|
||||
if (origin !== 'prompt' && activeComposerSubmissionKeyRef.current === submissionKey) {
|
||||
activeComposerSubmissionKeyRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
setDraft('');
|
||||
setComposerAttachments([]);
|
||||
focusComposerAfterSend();
|
||||
return true;
|
||||
},
|
||||
[
|
||||
activeSessionId,
|
||||
buildComposerSubmissionKey,
|
||||
composerAttachments,
|
||||
createActivityLogPlaceholder,
|
||||
createChatMessage,
|
||||
createLocalMessage,
|
||||
ensureSessionReady,
|
||||
focusComposerAfterSend,
|
||||
getDraft,
|
||||
isSocketOpen,
|
||||
pendingRequestsRef,
|
||||
promptRequestIdsRef,
|
||||
scheduleViewportBottomSyncAfterSend,
|
||||
sendChatRequest,
|
||||
setActiveSystemStatus,
|
||||
setComposerAttachments,
|
||||
@@ -493,15 +681,15 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
({ mode, draftText }: SendMessageOptions) => {
|
||||
({ sessionId, mode, draftText }: SendMessageOptions): SendMessageResult => {
|
||||
if (isComposerAttachmentUploading) {
|
||||
return;
|
||||
return 'blocked';
|
||||
}
|
||||
|
||||
const trimmed = buildOutgoingMessageText(draftText ?? getDraft(), composerAttachments).trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return;
|
||||
return 'blocked';
|
||||
}
|
||||
|
||||
if (!selectedChatType) {
|
||||
@@ -509,7 +697,15 @@ export function useConversationComposerController({
|
||||
...previous.slice(-39),
|
||||
createLocalMessage('현재 사용자에게 허용된 컨텍스트가 없습니다. 관리 페이지에서 컨텍스트 권한을 확인하세요.'),
|
||||
]);
|
||||
return;
|
||||
return 'blocked';
|
||||
}
|
||||
|
||||
if (!isSocketOpen()) {
|
||||
setMessages((previous) => [
|
||||
...previous.slice(-39),
|
||||
createLocalMessage('워크서버 연결이 아직 준비되지 않았습니다. 연결이 복구된 뒤 다시 전송해 주세요.'),
|
||||
]);
|
||||
return 'blocked';
|
||||
}
|
||||
|
||||
const recentContext = summarizeRecentContext(
|
||||
@@ -519,9 +715,34 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
if (recentContext.omittedCount > 0) {
|
||||
setPendingContextConfirm({
|
||||
const targetSessionId = sessionId?.trim() || activeSessionId.trim();
|
||||
const nextRequest = {
|
||||
sessionId: targetSessionId,
|
||||
mode,
|
||||
text: trimmed,
|
||||
codexModel: selectedCodexModel,
|
||||
chatTypeId: selectedChatType.id,
|
||||
chatTypeLabel: selectedChatType.name,
|
||||
chatTypeDescription: selectedChatType.description,
|
||||
chatTypeBaseDescription: selectedChatType.baseDescription,
|
||||
defaultContextIds: selectedChatType.defaultContextIds,
|
||||
defaultContexts: selectedChatType.defaultContexts,
|
||||
customContextTitle: selectedChatType.customContextTitle,
|
||||
customContextContent: selectedChatType.customContextContent,
|
||||
includedContextCount: recentContext.includedCount,
|
||||
omittedContextCount: recentContext.omittedCount,
|
||||
} satisfies PendingContextConfirm;
|
||||
|
||||
if (shouldSkipContextConfirmForSessionToday(targetSessionId)) {
|
||||
void executeSendMessage(nextRequest).catch(handleExecuteSendError);
|
||||
return 'sent';
|
||||
}
|
||||
|
||||
setPendingContextConfirm({
|
||||
sessionId: targetSessionId,
|
||||
mode,
|
||||
text: trimmed,
|
||||
codexModel: selectedCodexModel,
|
||||
chatTypeId: selectedChatType.id,
|
||||
chatTypeLabel: selectedChatType.name,
|
||||
chatTypeDescription: selectedChatType.description,
|
||||
@@ -533,12 +754,14 @@ export function useConversationComposerController({
|
||||
includedContextCount: recentContext.includedCount,
|
||||
omittedContextCount: recentContext.omittedCount,
|
||||
});
|
||||
return;
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
executeSendMessage({
|
||||
void executeSendMessage({
|
||||
sessionId: sessionId?.trim() || activeSessionId.trim(),
|
||||
mode,
|
||||
text: trimmed,
|
||||
codexModel: selectedCodexModel,
|
||||
chatTypeId: selectedChatType.id,
|
||||
chatTypeLabel: selectedChatType.name,
|
||||
chatTypeDescription: selectedChatType.description,
|
||||
@@ -549,7 +772,8 @@ export function useConversationComposerController({
|
||||
customContextContent: selectedChatType.customContextContent,
|
||||
includedContextCount: 0,
|
||||
omittedContextCount: 0,
|
||||
});
|
||||
}).catch(handleExecuteSendError);
|
||||
return 'sent';
|
||||
},
|
||||
[
|
||||
appConfigChat.maxContextChars,
|
||||
@@ -558,8 +782,12 @@ export function useConversationComposerController({
|
||||
composerAttachments,
|
||||
createLocalMessage,
|
||||
getDraft,
|
||||
handleExecuteSendError,
|
||||
executeSendMessage,
|
||||
isSocketOpen,
|
||||
isComposerAttachmentUploading,
|
||||
selectedCodexModel,
|
||||
activeSessionId,
|
||||
messagesRef,
|
||||
selectedChatType,
|
||||
setMessages,
|
||||
|
||||
Reference in New Issue
Block a user