chore: sync local workspace changes
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
import { Alert, Button, Checkbox, Input, Select, Spin, message } from 'antd';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import {
|
||||
startTransition,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -33,12 +34,17 @@ import {
|
||||
import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import type { ComposerFilePickResult } from '../chatV2/hooks/useConversationComposerController';
|
||||
import { ChatPreviewBody, resolveChatPreviewKindLabel, type ChatPreviewKind } from './ChatPreviewBody';
|
||||
import {
|
||||
ChatPreviewBody,
|
||||
resolveChatPreviewGlyph,
|
||||
resolveChatPreviewKindLabel,
|
||||
type ChatPreviewKind,
|
||||
} from './ChatPreviewBody';
|
||||
import { triggerResourceDownload } from './downloadUtils';
|
||||
import { extractAutoDetectedPreviewUrls } from './inlinePreviewUrls';
|
||||
import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from './previewMarkers';
|
||||
import { normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { copyPreviewContent, copyText } from './chatUtils';
|
||||
import { copyPreviewContent, copyText, isExecutionFailureMessage, isMissingRequestMessage, isPreparingChatReplyText } from './chatUtils';
|
||||
import { ChatLinkCardPreview } from './ChatLinkCardPreview';
|
||||
import { openChatExternalLink } from './linkNavigation';
|
||||
import { ChatRankedLinkPreview, type RankedLinkPreviewTarget } from './ChatRankedLinkPreview';
|
||||
@@ -110,6 +116,8 @@ type PreviewFetchError = Error & {
|
||||
const MARKDOWN_IMAGE_LINE_PATTERN = /^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/;
|
||||
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const COLLAPSIBLE_MESSAGE_LINE_COUNT = 6;
|
||||
const COLLAPSIBLE_MESSAGE_CHAR_COUNT = 280;
|
||||
const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g;
|
||||
@@ -199,6 +207,25 @@ function buildPreviewFileName(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePreviewFileExtension(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
const fileName = buildPreviewFileName(item).toLowerCase();
|
||||
const match = fileName.match(/\.([a-z0-9]{1,16})$/i);
|
||||
return match?.[1] ?? '';
|
||||
}
|
||||
|
||||
function buildResourceChipMeta(item: Pick<PreviewOption, 'url' | 'label' | 'kind'>) {
|
||||
const extension = resolvePreviewFileExtension(item);
|
||||
|
||||
if (extension) {
|
||||
return extension.toUpperCase();
|
||||
}
|
||||
|
||||
return resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)
|
||||
.replace(/\s+preview$/i, '')
|
||||
.replace(/\s+download$/i, '')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeRankedLinkTitle(value: string) {
|
||||
return value
|
||||
.replace(/^\[(.+)\]\([^)]+\)$/u, '$1')
|
||||
@@ -561,6 +588,22 @@ function isActivityLogMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function getMissingRequestMessageText(message: ChatMessage) {
|
||||
if (!isMissingRequestMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function getExecutionFailureMessageText(message: ChatMessage) {
|
||||
if (!isExecutionFailureMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function extractActivityLines(message: ChatMessage) {
|
||||
return message.text
|
||||
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
|
||||
@@ -570,8 +613,20 @@ function extractActivityLines(message: ChatMessage) {
|
||||
}
|
||||
|
||||
function summarizeActivityLines(lines: string[]) {
|
||||
const latestLine = lines.at(-1) ?? '';
|
||||
return latestLine;
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
const summary = lines[index]
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('# 이유:') || line.startsWith('# 진행:') || line.startsWith('# 상태:'));
|
||||
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return summary.replace(/^#\s*(이유|진행|상태):\s*/u, '').trim();
|
||||
}
|
||||
|
||||
return lines.at(-1) ?? '';
|
||||
}
|
||||
|
||||
function isLikelyCollapsibleMessage(text: string) {
|
||||
@@ -948,8 +1003,8 @@ type ChatConversationViewProps = {
|
||||
onPickComposerFiles: (files: File[]) => ComposerFilePickResult | Promise<ComposerFilePickResult>;
|
||||
onRemoveComposerAttachment: (attachmentId: string) => void;
|
||||
onSelectChatType: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onSendImmediate: () => void;
|
||||
onSend: (draftText?: string) => void;
|
||||
onSendImmediate: (draftText?: string) => void;
|
||||
onToggleSendWithoutContext: () => void;
|
||||
onClearDraft: () => void;
|
||||
onScrollToBottom: () => void;
|
||||
@@ -1018,12 +1073,58 @@ export function ChatConversationView({
|
||||
const [showBusyOverlay, setShowBusyOverlay] = useState(false);
|
||||
const [showLatestResourceOnly, setShowLatestResourceOnly] = useState(true);
|
||||
const [pendingComposerUploads, setPendingComposerUploads] = useState<PendingComposerUpload[]>([]);
|
||||
const [composerDraft, setComposerDraft] = useState(draft);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const activitySectionRefs = useRef(new Map<string, HTMLElement>());
|
||||
const messageBodyRefs = useRef(new Map<number, HTMLDivElement>());
|
||||
const autoCollapsedActivityRequestIdsRef = useRef(new Set<string>());
|
||||
const lastReportedDraftRef = useRef(draft);
|
||||
|
||||
useEffect(() => {
|
||||
if (draft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setComposerDraft(draft);
|
||||
}, [draft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (composerDraft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
startTransition(() => {
|
||||
onDraftChange(composerDraft);
|
||||
});
|
||||
}, 120);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [composerDraft, onDraftChange]);
|
||||
|
||||
const orderedMessages = useMemo(() => {
|
||||
const shouldDisplayActivityMessage = (activityMessage: ChatMessage) => {
|
||||
const requestId = activityMessage.clientRequestId?.trim();
|
||||
|
||||
if (!requestId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestState = requestStateMap.get(requestId);
|
||||
const hasCodexResponse = visibleMessages.some(
|
||||
(candidate) =>
|
||||
candidate.clientRequestId?.trim() === requestId &&
|
||||
candidate.author === 'codex' &&
|
||||
candidate.text.trim().length > 0 &&
|
||||
!isPreparingChatReplyText(candidate.text),
|
||||
);
|
||||
|
||||
return !isTerminalRequestStatus(requestState?.status) || !hasCodexResponse;
|
||||
};
|
||||
|
||||
const latestActivityByRequestId = new Map<string, ChatMessage>();
|
||||
const orphanActivityMessages: ChatMessage[] = [];
|
||||
const baseMessages = visibleMessages.filter((message) => {
|
||||
@@ -1038,7 +1139,9 @@ export function ChatConversationView({
|
||||
return false;
|
||||
}
|
||||
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
if (shouldDisplayActivityMessage(message)) {
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const insertedActivityRequestIds = new Set<string>();
|
||||
@@ -1074,19 +1177,9 @@ export function ChatConversationView({
|
||||
});
|
||||
|
||||
return [...ordered, ...orphanActivityMessages];
|
||||
}, [visibleMessages]);
|
||||
}, [requestStateMap, visibleMessages]);
|
||||
const previewItemsByUrl = useMemo(() => new Map(previewItems.map((item) => [item.url, item])), [previewItems]);
|
||||
const isChatTypeReadonly = useMemo(() => {
|
||||
if (isChatTypeSelectionLocked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(new URLSearchParams(window.location.search).get('sessionId')?.trim());
|
||||
}, [isChatTypeSelectionLocked]);
|
||||
const isChatTypeReadonly = isChatTypeSelectionLocked;
|
||||
const visiblePreviewItems = useMemo(() => {
|
||||
if (!showLatestResourceOnly) {
|
||||
return previewItems;
|
||||
@@ -1590,8 +1683,20 @@ export function ChatConversationView({
|
||||
onOpenPreview(item.id);
|
||||
}}
|
||||
>
|
||||
<span title={item.label}>{item.label}</span>
|
||||
<span>{item.kind}</span>
|
||||
<span className="app-chat-panel__resource-chip-main">
|
||||
<span className="app-chat-panel__resource-chip-icon" aria-hidden="true">
|
||||
{resolveChatPreviewGlyph(item.kind as ChatPreviewKind)}
|
||||
</span>
|
||||
<span className="app-chat-panel__resource-chip-label" title={item.label}>
|
||||
{item.label}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="app-chat-panel__resource-chip-meta"
|
||||
aria-label={`${resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)} 형식`}
|
||||
>
|
||||
{buildResourceChipMeta(item)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1638,8 +1743,17 @@ export function ChatConversationView({
|
||||
const canCollapseMessage = collapsibleMessageIds.includes(message.id);
|
||||
const isExpandedMessage = expandedMessageIds.includes(message.id);
|
||||
const shouldTruncateMessage = canCollapseMessage && !isExpandedMessage;
|
||||
const messageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}`;
|
||||
const isRecoveredMissingRequest = isMissingRequestMessage(message);
|
||||
const isRecoveredExecutionFailure = isExecutionFailureMessage(message);
|
||||
const baseMessageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure ? ' app-chat-message__body--system-status' : ''
|
||||
}`;
|
||||
const { previewSourceText, visibleText, diffBlocks, rankedLinkTargets, linkCardTargets } = extractMessageRenderPayload(message);
|
||||
const renderedText = isRecoveredMissingRequest
|
||||
? getMissingRequestMessageText(message)
|
||||
: isRecoveredExecutionFailure
|
||||
? getExecutionFailureMessageText(message)
|
||||
: visibleText;
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return renderActivityCard(message);
|
||||
@@ -1651,9 +1765,9 @@ export function ChatConversationView({
|
||||
const shouldRenderStandalonePreview =
|
||||
hasPreviewCards && !visibleText && (message.author === 'codex' || message.author === 'system');
|
||||
const stackClassName = [
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const requestState = message.clientRequestId ? requestStateMap.get(message.clientRequestId) : undefined;
|
||||
@@ -1663,10 +1777,26 @@ export function ChatConversationView({
|
||||
return (
|
||||
<div key={message.id} className={stackClassName}>
|
||||
{shouldRenderStandalonePreview ? null : (
|
||||
<article className={`app-chat-message app-chat-message--${message.author}`}>
|
||||
<article
|
||||
className={`app-chat-message ${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure
|
||||
? 'app-chat-message--system-inline'
|
||||
: `app-chat-message--${message.author}`
|
||||
}`}
|
||||
>
|
||||
<div className="app-chat-message__header">
|
||||
<div className="app-chat-message__header-meta">
|
||||
<strong>{message.author === 'codex' ? 'Codex' : message.author === 'user' ? 'You' : 'System'}</strong>
|
||||
<strong>
|
||||
{isRecoveredMissingRequest
|
||||
? '원문 누락'
|
||||
: isRecoveredExecutionFailure
|
||||
? '실행 실패'
|
||||
: message.author === 'codex'
|
||||
? 'Codex'
|
||||
: message.author === 'user'
|
||||
? 'You'
|
||||
: 'System'}
|
||||
</strong>
|
||||
<span>{formatChatTimestamp(message.timestamp)}</span>
|
||||
{message.author === 'user' && requestStatusLabel ? (
|
||||
<span className="app-chat-message__status" aria-label={`요청 상태 ${requestStatusLabel}`}>
|
||||
@@ -1743,13 +1873,13 @@ export function ChatConversationView({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={messageBodyClassName}
|
||||
>
|
||||
{visibleText ? renderMessageBody(visibleText) : null}
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={baseMessageBodyClassName}
|
||||
>
|
||||
{renderedText ? renderMessageBody(renderedText) : null}
|
||||
</div>
|
||||
{message.author === 'user' && requestDetailText ? (
|
||||
<div className="app-chat-message__request-detail" role="status" aria-live="polite">
|
||||
@@ -1917,22 +2047,38 @@ export function ChatConversationView({
|
||||
isSendWithoutContextEnabled ? ' app-chat-panel__composer-contextless-toggle--active' : ''
|
||||
}`}
|
||||
icon={<DisconnectOutlined />}
|
||||
aria-label={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
title={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
aria-label={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
title={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
onClick={onToggleSendWithoutContext}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
aria-label="즉시 요청"
|
||||
onClick={onSendImmediate}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSendImmediate(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
aria-label="큐로 보내기"
|
||||
onClick={onSend}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSend(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
</div>
|
||||
@@ -1987,12 +2133,12 @@ export function ChatConversationView({
|
||||
|
||||
<Input.TextArea
|
||||
ref={composerRef}
|
||||
value={draft}
|
||||
value={composerDraft}
|
||||
autoSize={false}
|
||||
placeholder={composerPlaceholder}
|
||||
disabled={isComposerDisabled}
|
||||
onChange={(event) => {
|
||||
onDraftChange(event.target.value);
|
||||
setComposerDraft(event.target.value);
|
||||
}}
|
||||
onPaste={handleComposerPaste}
|
||||
onKeyDown={(event) => {
|
||||
@@ -2012,16 +2158,18 @@ export function ChatConversationView({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onSend();
|
||||
lastReportedDraftRef.current = event.currentTarget.value;
|
||||
onDraftChange(event.currentTarget.value);
|
||||
onSend(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className={`app-chat-panel__composer-clear${draft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
className={`app-chat-panel__composer-clear${composerDraft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
aria-label="입력창 비우기"
|
||||
onClick={onClearDraft}
|
||||
disabled={!draft.trim()}
|
||||
disabled={!composerDraft.trim()}
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
|
||||
191
src/app/main/mainChatPanel/ChatDataTablePreview.tsx
Normal file
191
src/app/main/mainChatPanel/ChatDataTablePreview.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Typography } from 'antd';
|
||||
import type { ChatPreviewTarget } from './ChatPreviewBody';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type TableCellValue = string;
|
||||
|
||||
type TabularPreviewModel = {
|
||||
columns: string[];
|
||||
rows: TableCellValue[][];
|
||||
rowCount: number;
|
||||
sourceLabel: string;
|
||||
};
|
||||
|
||||
type ChatDataTablePreviewProps = {
|
||||
model: TabularPreviewModel;
|
||||
};
|
||||
|
||||
function stringifyCellValue(value: unknown): TableCellValue {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeObjectRows(rows: Record<string, unknown>[], sourceLabel: string): TabularPreviewModel | null {
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = Array.from(
|
||||
rows.reduce((set, row) => {
|
||||
Object.keys(row).forEach((key) => set.add(key));
|
||||
return set;
|
||||
}, new Set<string>()),
|
||||
);
|
||||
|
||||
if (!columns.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows: rows.map((row) => columns.map((column) => stringifyCellValue(row[column]))),
|
||||
rowCount: rows.length,
|
||||
sourceLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveJsonRows(value: unknown): Record<string, unknown>[] | null {
|
||||
if (Array.isArray(value) && value.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
|
||||
return value as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const entries = Object.values(value as Record<string, unknown>);
|
||||
for (const entry of entries) {
|
||||
const resolved = resolveJsonRows(entry);
|
||||
if (resolved?.length) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCsvLine(line: string) {
|
||||
const cells: string[] = [];
|
||||
let current = '';
|
||||
let quoted = false;
|
||||
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index] ?? '';
|
||||
const nextChar = line[index + 1] ?? '';
|
||||
|
||||
if (char === '"') {
|
||||
if (quoted && nextChar === '"') {
|
||||
current += '"';
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ',' && !quoted) {
|
||||
cells.push(current.trim());
|
||||
current = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
cells.push(current.trim());
|
||||
return cells;
|
||||
}
|
||||
|
||||
function parseCsvTable(previewText: string, sourceLabel: string): TabularPreviewModel | null {
|
||||
const lines = previewText
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (lines.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = parseCsvLine(lines[0] ?? '');
|
||||
if (!header.length || header.every((column) => !column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = lines.slice(1).map((line) => {
|
||||
const parsed = parseCsvLine(line);
|
||||
return header.map((_, index) => parsed[index] ?? '');
|
||||
});
|
||||
|
||||
return {
|
||||
columns: header,
|
||||
rows,
|
||||
rowCount: rows.length,
|
||||
sourceLabel,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTabularPreviewModel(target: ChatPreviewTarget, previewText: string): TabularPreviewModel | null {
|
||||
const pathname = target.url.toLowerCase().split('?')[0] ?? '';
|
||||
|
||||
if (pathname.endsWith('.json')) {
|
||||
try {
|
||||
const parsed = JSON.parse(previewText) as unknown;
|
||||
const rows = resolveJsonRows(parsed);
|
||||
return rows ? normalizeObjectRows(rows, target.label) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname.endsWith('.csv')) {
|
||||
return parseCsvTable(previewText, target.label);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ChatDataTablePreview({ model }: ChatDataTablePreviewProps) {
|
||||
return (
|
||||
<div className="app-chat-panel__preview-table">
|
||||
<div className="app-chat-panel__preview-table-meta">
|
||||
<Text strong>{model.sourceLabel}</Text>
|
||||
<Text type="secondary">{`행 ${model.rowCount}개 · 열 ${model.columns.length}개`}</Text>
|
||||
</div>
|
||||
<div className="app-chat-panel__preview-table-scroll">
|
||||
<table className="app-chat-panel__preview-table-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{model.columns.map((column) => (
|
||||
<th key={column}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{model.rows.map((row, rowIndex) => (
|
||||
<tr key={`${model.sourceLabel}-${rowIndex}`}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={`${model.columns[cellIndex] ?? cellIndex}-${rowIndex}`}>{cell || '-'}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { MarkdownPreviewContent } from '../../../components/markdownPreview/MarkdownPreviewContent';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import { inferCodeLanguage, renderEditorBlock } from '../../../components/previewer/renderers';
|
||||
import { ChatDataTablePreview, resolveTabularPreviewModel } from './ChatDataTablePreview';
|
||||
import { triggerResourceDownload } from './downloadUtils';
|
||||
import '../../../components/previewer/PreviewerUI.css';
|
||||
|
||||
@@ -359,6 +360,12 @@ export function ChatPreviewBody({
|
||||
}
|
||||
|
||||
if (target.kind === 'code' || target.kind === 'diff' || target.kind === 'document') {
|
||||
const tabularModel = resolveTabularPreviewModel(target, previewText);
|
||||
|
||||
if (tabularModel) {
|
||||
return <ChatDataTablePreview model={tabularModel} />;
|
||||
}
|
||||
|
||||
const resolvedLanguage = resolveCodeLanguage(target, previewText);
|
||||
|
||||
if (renderHtmlAsFrame && resolvedLanguage === 'html' && canRenderFramePreview(target.url)) {
|
||||
|
||||
32
src/app/main/mainChatPanel/chatResourceUrl.js
Normal file
32
src/app/main/mainChatPanel/chatResourceUrl.js
Normal file
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.normalizeChatResourceUrl = normalizeChatResourceUrl;
|
||||
var CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
||||
var CHAT_PUBLIC_RESOURCE_MARKER = '/.codex_chat/';
|
||||
function extractEmbeddedResourcePath(value) {
|
||||
var normalized = String(value !== null && value !== void 0 ? value : '').trim();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
var apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
||||
if (apiMarkerIndex >= 0) {
|
||||
return normalized.slice(apiMarkerIndex);
|
||||
}
|
||||
var publicMarkerIndex = normalized.lastIndexOf(CHAT_PUBLIC_RESOURCE_MARKER);
|
||||
if (publicMarkerIndex >= 0) {
|
||||
return normalized.slice(publicMarkerIndex);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function normalizeChatResourceUrl(value) {
|
||||
var normalized = extractEmbeddedResourcePath(value);
|
||||
if (typeof window === 'undefined') {
|
||||
return normalized;
|
||||
}
|
||||
try {
|
||||
return new URL(normalized, window.location.href).toString();
|
||||
}
|
||||
catch (_a) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
1693
src/app/main/mainChatPanel/chatUtils.js
Normal file
1693
src/app/main/mainChatPanel/chatUtils.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@ const CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX = 'main-chat-panel:notify-offline:';
|
||||
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 CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
||||
const KST_TIME_ZONE = 'Asia/Seoul';
|
||||
const chatSessionLastTypeMemory = new Map<string, string>();
|
||||
@@ -46,18 +48,23 @@ function toConversationSortTime(value: string | null | undefined) {
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function getConversationLastMessageSortTime(item: ChatConversationSummary) {
|
||||
const lastMessageTime = toConversationSortTime(item.lastMessageAt);
|
||||
|
||||
if (lastMessageTime > 0) {
|
||||
return lastMessageTime;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
toConversationSortTime(item.createdAt),
|
||||
toConversationSortTime(item.updatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
const leftTime = getConversationLastMessageSortTime(left);
|
||||
const rightTime = getConversationLastMessageSortTime(right);
|
||||
|
||||
if (rightTime !== leftTime) {
|
||||
return rightTime - leftTime;
|
||||
@@ -289,18 +296,29 @@ function createLocalMessageId() {
|
||||
return Date.now() * 1_000 + localMessageSequence;
|
||||
}
|
||||
|
||||
function createRecoveredMessageId(requestId: string, variant: 'user' | 'codex' | 'activity') {
|
||||
function createRecoveredMessageId(
|
||||
requestId: string,
|
||||
variant: 'user' | 'codex' | 'activity' | 'missing-request' | 'execution-failure',
|
||||
) {
|
||||
const baseId = hashRequestId(requestId) * 10;
|
||||
|
||||
if (variant === 'user') {
|
||||
return -(baseId + 3);
|
||||
}
|
||||
|
||||
if (variant === 'activity') {
|
||||
if (variant === 'missing-request') {
|
||||
return -(baseId + 2);
|
||||
}
|
||||
|
||||
return -(baseId + 1);
|
||||
if (variant === 'activity') {
|
||||
return -(baseId + 1);
|
||||
}
|
||||
|
||||
if (variant === 'execution-failure') {
|
||||
return -(baseId + 4);
|
||||
}
|
||||
|
||||
return -(baseId + 5);
|
||||
}
|
||||
|
||||
function hashRequestId(value: string) {
|
||||
@@ -355,6 +373,170 @@ function isActivityLogMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
export function isMissingRequestMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
export function isExecutionFailureMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function isEmptyCodexExecutionResponse(text: string) {
|
||||
const normalized = text.replace(/\s+/g, ' ').trim();
|
||||
return normalized === 'Codex 실행 결과가 비어 있습니다.';
|
||||
}
|
||||
|
||||
function extractActivityLogFailureReason(lines?: string[] | null) {
|
||||
const normalizedLines = (lines ?? []).map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
for (let index = normalizedLines.length - 1; index >= 0; index -= 1) {
|
||||
const line = normalizedLines[index];
|
||||
|
||||
if (!line.startsWith('# 오류:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = line.slice('# 오류:'.length).trim();
|
||||
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { message?: unknown };
|
||||
const message = typeof parsed.message === 'string' ? parsed.message.trim() : '';
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildExecutionFailureMessage(reason: string) {
|
||||
const normalizedReason = reason.trim();
|
||||
|
||||
if (!normalizedReason) {
|
||||
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n실행 중 오류가 발생했습니다.`;
|
||||
}
|
||||
|
||||
const simplifiedReason = normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')
|
||||
? `세션 리소스 업로드 폴더를 만들 권한이 없어 응답 생성이 중단되었습니다.\n\n원인: ${normalizedReason}`
|
||||
: `실행 중 오류가 발생했습니다.\n\n원인: ${normalizedReason}`;
|
||||
|
||||
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n${simplifiedReason}`;
|
||||
}
|
||||
|
||||
function buildFailurePreviewText(reason: string) {
|
||||
const normalizedReason = reason.trim();
|
||||
|
||||
if (!normalizedReason) {
|
||||
return '실행 실패';
|
||||
}
|
||||
|
||||
if (normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')) {
|
||||
return '실행 실패: 세션 리소스 업로드 폴더 권한 오류';
|
||||
}
|
||||
|
||||
return `실행 실패: ${normalizedReason}`;
|
||||
}
|
||||
|
||||
function enrichFailedRequestsWithActivityLogs(
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
) {
|
||||
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
|
||||
|
||||
return requests.map((request) => {
|
||||
if (request.status !== 'failed') {
|
||||
return request;
|
||||
}
|
||||
|
||||
const activityLog = activityLogMap.get(request.requestId.trim());
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
const normalizedStatusMessage = String(request.statusMessage ?? '').trim();
|
||||
|
||||
if (!failureReason) {
|
||||
return request;
|
||||
}
|
||||
|
||||
if (normalizedStatusMessage && normalizedStatusMessage !== '요청 처리 실패') {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
statusMessage: failureReason,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function replaceGenericFailureMessages(
|
||||
messages: ChatMessage[],
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
): ChatMessage[] {
|
||||
const requestMap = new Map(requests.map((item) => [item.requestId.trim(), item]));
|
||||
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
|
||||
|
||||
return messages.map((message) => {
|
||||
const requestId = message.clientRequestId?.trim() ?? '';
|
||||
|
||||
if (!requestId || message.author !== 'codex' || !isEmptyCodexExecutionResponse(message.text)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const request = requestMap.get(requestId);
|
||||
|
||||
if (request?.status !== 'failed') {
|
||||
return message;
|
||||
}
|
||||
|
||||
const failureReason = extractActivityLogFailureReason(activityLogMap.get(requestId)?.lines);
|
||||
|
||||
if (!failureReason) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
author: 'system' as const,
|
||||
text: buildExecutionFailureMessage(failureReason),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConversationFailurePreview(
|
||||
currentPreview: string,
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
) {
|
||||
if (!isEmptyCodexExecutionResponse(currentPreview)) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
const latestFailedRequest = [...requests]
|
||||
.reverse()
|
||||
.find((request) => request.status === 'failed' && isEmptyCodexExecutionResponse(String(request.responseText ?? '').trim()));
|
||||
|
||||
if (!latestFailedRequest) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
const activityLog = activityLogs.find((item) => item.requestId.trim() === latestFailedRequest.requestId.trim());
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
|
||||
if (!failureReason) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
return buildFailurePreviewText(failureReason);
|
||||
}
|
||||
|
||||
function extractActivityLogLines(text: string) {
|
||||
return text
|
||||
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
|
||||
@@ -934,7 +1116,9 @@ export async function fetchChatConversations() {
|
||||
}
|
||||
|
||||
const clientId = getOrCreateClientId();
|
||||
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>('/conversations')
|
||||
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>(
|
||||
'/conversations?limit=200',
|
||||
)
|
||||
.then((response) => {
|
||||
return sortChatConversationSummaries(
|
||||
response.items.map((item) => ({
|
||||
@@ -971,17 +1155,24 @@ 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 normalizedRequests = enrichFailedRequestsWithActivityLogs(
|
||||
response.requests.map((item) => normalizeChatConversationRequest(item)),
|
||||
response.activityLogs,
|
||||
);
|
||||
const visibleRequestIds = new Set(
|
||||
response.messages
|
||||
.map((message) => message.clientRequestId?.trim() ?? '')
|
||||
.filter(Boolean),
|
||||
);
|
||||
const hydratedMessages = hydrateActivityLogMessages(
|
||||
response.messages,
|
||||
replaceGenericFailureMessages(response.messages, normalizedRequests, response.activityLogs),
|
||||
response.activityLogs.filter((item) => visibleRequestIds.has(item.requestId?.trim() ?? '')),
|
||||
).filter(
|
||||
(message) => message.author !== 'system' || isActivityLogMessage(message),
|
||||
(message) =>
|
||||
message.author !== 'system' ||
|
||||
isActivityLogMessage(message) ||
|
||||
isMissingRequestMessage(message) ||
|
||||
isExecutionFailureMessage(message),
|
||||
);
|
||||
const recoveredMessages = buildRecoveredMessagesFromConversationDetail(normalizedRequests, response.activityLogs);
|
||||
|
||||
@@ -990,6 +1181,11 @@ export async function fetchChatConversationDetail(
|
||||
messages: mergeRecoveredChatMessages(recoveredMessages, hydratedMessages),
|
||||
item: {
|
||||
...response.item,
|
||||
lastMessagePreview: resolveConversationFailurePreview(
|
||||
response.item.lastMessagePreview,
|
||||
normalizedRequests,
|
||||
response.activityLogs,
|
||||
),
|
||||
notifyOffline: resolveSyncedChatOfflineNotificationSetting(
|
||||
response.item.sessionId,
|
||||
response.item.notifyOffline,
|
||||
@@ -1123,6 +1319,7 @@ export async function createChatConversationRoom(args: {
|
||||
title?: string;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
contextLabel?: string;
|
||||
contextDescription?: string;
|
||||
notifyOffline?: boolean;
|
||||
@@ -1136,6 +1333,7 @@ export async function createChatConversationRoom(args: {
|
||||
title: args.title ?? '새 대화',
|
||||
chatTypeId: args.chatTypeId ?? null,
|
||||
lastChatTypeId: args.lastChatTypeId ?? args.chatTypeId ?? null,
|
||||
generalSectionName: args.generalSectionName ?? null,
|
||||
contextLabel: args.contextLabel ?? null,
|
||||
contextDescription: args.contextDescription ?? null,
|
||||
notifyOffline,
|
||||
@@ -1173,6 +1371,7 @@ export async function updateChatConversationRoom(
|
||||
title?: string;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
contextLabel?: string | null;
|
||||
contextDescription?: string | null;
|
||||
notifyOffline?: boolean;
|
||||
@@ -1307,6 +1506,14 @@ function isSameChatMessage(left: ChatMessage, right: ChatMessage) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(left) && isMissingRequestMessage(right)) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(left) && isExecutionFailureMessage(right)) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
(left.author === 'user' || left.author === 'codex') &&
|
||||
left.author === right.author &&
|
||||
@@ -1321,6 +1528,14 @@ function buildComparableChatMessageKey(message: ChatMessage) {
|
||||
return `activity:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(message) && message.clientRequestId) {
|
||||
return `missing-request:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(message) && message.clientRequestId) {
|
||||
return `execution-failure:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (message.author === 'user' && message.clientRequestId) {
|
||||
return `user-request:${message.clientRequestId}`;
|
||||
}
|
||||
@@ -1337,6 +1552,123 @@ function getComparableChatMessageTime(message: ChatMessage) {
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function getChatMessageRequestId(message: ChatMessage) {
|
||||
return message.clientRequestId?.trim() || '';
|
||||
}
|
||||
|
||||
function getChatMessageOrderRank(message: ChatMessage) {
|
||||
if (message.author === 'user') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(message)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(message)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (message.author === 'codex') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
function sortConversationMessages(messages: ChatMessage[]) {
|
||||
if (messages.length <= 1) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const messageIndexMap = new Map(messages.map((message, index) => [message, index]));
|
||||
const requestOrder = new Map<
|
||||
string,
|
||||
{
|
||||
time: number;
|
||||
firstIndex: number;
|
||||
}
|
||||
>();
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
const requestId = getChatMessageRequestId(message);
|
||||
|
||||
if (!requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const time = getComparableChatMessageTime(message);
|
||||
const existing = requestOrder.get(requestId);
|
||||
|
||||
if (!existing) {
|
||||
requestOrder.set(requestId, {
|
||||
time,
|
||||
firstIndex: index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
requestOrder.set(requestId, {
|
||||
time:
|
||||
existing.time > 0 && time > 0
|
||||
? Math.min(existing.time, time)
|
||||
: existing.time > 0
|
||||
? existing.time
|
||||
: time,
|
||||
firstIndex: Math.min(existing.firstIndex, index),
|
||||
});
|
||||
});
|
||||
|
||||
return [...messages].sort((left, right) => {
|
||||
const leftRequestId = getChatMessageRequestId(left);
|
||||
const rightRequestId = getChatMessageRequestId(right);
|
||||
|
||||
if (leftRequestId && rightRequestId && leftRequestId === rightRequestId) {
|
||||
const rankDiff = getChatMessageOrderRank(left) - getChatMessageOrderRank(right);
|
||||
|
||||
if (rankDiff !== 0) {
|
||||
return rankDiff;
|
||||
}
|
||||
}
|
||||
|
||||
const leftOrder = leftRequestId ? requestOrder.get(leftRequestId) : null;
|
||||
const rightOrder = rightRequestId ? requestOrder.get(rightRequestId) : null;
|
||||
const leftTime = leftOrder?.time ?? getComparableChatMessageTime(left);
|
||||
const rightTime = rightOrder?.time ?? getComparableChatMessageTime(right);
|
||||
|
||||
if (leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
|
||||
const leftIndex = leftOrder?.firstIndex ?? messageIndexMap.get(left) ?? 0;
|
||||
const rightIndex = rightOrder?.firstIndex ?? messageIndexMap.get(right) ?? 0;
|
||||
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
if (leftRequestId && rightRequestId && leftRequestId !== rightRequestId) {
|
||||
const requestDiff = leftRequestId.localeCompare(rightRequestId, 'ko-KR');
|
||||
|
||||
if (requestDiff !== 0) {
|
||||
return requestDiff;
|
||||
}
|
||||
}
|
||||
|
||||
const messageTimeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
|
||||
|
||||
if (messageTimeDiff !== 0) {
|
||||
return messageTimeDiff;
|
||||
}
|
||||
|
||||
return left.id - right.id;
|
||||
});
|
||||
}
|
||||
|
||||
function buildRecoveredMessagesFromConversationDetail(
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
@@ -1354,6 +1686,9 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
const userText = String(request.userText ?? '').trim();
|
||||
const responseText = String(request.responseText ?? '').trim();
|
||||
const activityLog = activityLogMap.get(requestId);
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
const shouldReplaceEmptyFailureResponse =
|
||||
request.status === 'failed' && isEmptyCodexExecutionResponse(responseText) && Boolean(failureReason);
|
||||
|
||||
if (userText) {
|
||||
nextMessages.push({
|
||||
@@ -1363,9 +1698,17 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
timestamp: request.createdAt || request.updatedAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
} else if (responseText || activityLog?.lines.length) {
|
||||
nextMessages.push({
|
||||
id: createRecoveredMessageId(requestId, 'missing-request'),
|
||||
author: 'system',
|
||||
text: `${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n이 요청은 저장된 원문이 없어 실제 요청 문장을 표시할 수 없습니다.`,
|
||||
timestamp: request.createdAt || request.updatedAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
}
|
||||
|
||||
if (responseText) {
|
||||
if (responseText && !shouldReplaceEmptyFailureResponse) {
|
||||
nextMessages.push({
|
||||
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'codex'),
|
||||
author: 'codex',
|
||||
@@ -1375,6 +1718,16 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldReplaceEmptyFailureResponse) {
|
||||
nextMessages.push({
|
||||
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'execution-failure'),
|
||||
author: 'system',
|
||||
text: buildExecutionFailureMessage(failureReason),
|
||||
timestamp: request.answeredAt || request.updatedAt || request.createdAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
}
|
||||
|
||||
if (activityLog && activityLog.lines.length > 0) {
|
||||
nextMessages.push({
|
||||
id: createRecoveredMessageId(requestId, 'activity'),
|
||||
@@ -1386,15 +1739,7 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
}
|
||||
});
|
||||
|
||||
return nextMessages.sort((left, right) => {
|
||||
const timeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
|
||||
|
||||
if (timeDiff !== 0) {
|
||||
return timeDiff;
|
||||
}
|
||||
|
||||
return left.id - right.id;
|
||||
});
|
||||
return sortConversationMessages(nextMessages);
|
||||
}
|
||||
|
||||
export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: ChatMessage[]) {
|
||||
@@ -1459,7 +1804,7 @@ export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: Ch
|
||||
});
|
||||
|
||||
const unmatchedLocalMessages = Array.from(previousBuckets.values()).flat();
|
||||
const nextMessages = [...mergedServerMessages, ...unmatchedLocalMessages];
|
||||
const nextMessages = sortConversationMessages([...mergedServerMessages, ...unmatchedLocalMessages]);
|
||||
|
||||
return areChatMessagesEquivalent(previous, nextMessages) ? previous : nextMessages;
|
||||
}
|
||||
|
||||
49
src/app/main/mainChatPanel/index.js
Normal file
49
src/app/main/mainChatPanel/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useErrorLogs = exports.useChatConnection = exports.subscribeChatConnection = exports.setSharedChatRuntimeSnapshot = exports.getSharedChatRuntimeSnapshot = exports.updateChatConversationRoom = exports.upsertChatMessage = exports.uploadChatComposerFile = exports.sortChatConversationSummaries = exports.setChatClientSessionId = exports.setStoredChatSessionLastTypeId = exports.resetLastReceivedChatEventId = exports.removeChatRuntimeJob = exports.renameChatConversationRoom = exports.mergeRecoveredChatMessages = exports.markChatConversationResponsesRead = exports.getChatClientSessionId = exports.isPreparingChatReplyText = exports.isMissingRequestMessage = exports.getStoredChatSessionLastTypeId = exports.fetchChatRuntimeSnapshot = exports.fetchChatRuntimeJobDetail = exports.fetchChatConversations = exports.fetchChatConversationDetail = exports.deleteChatConversationRoom = exports.deleteChatConversationRequest = exports.cancelChatRuntimeJob = exports.createLocalMessage = exports.createIntroMessage = exports.createChatMessage = exports.createChatConversationRoom = exports.createActivityLogPlaceholder = exports.resolvePreviewBodyForCopy = exports.copyText = exports.copyPreviewContent = exports.clearStoredChatClientConversationState = exports.buildOfflineReply = exports.ErrorLogViewer = exports.ChatRuntimeDashboard = exports.ChatConversationView = void 0;
|
||||
var ChatConversationView_1 = require("./ChatConversationView");
|
||||
Object.defineProperty(exports, "ChatConversationView", { enumerable: true, get: function () { return ChatConversationView_1.ChatConversationView; } });
|
||||
var ChatRuntimeDashboard_1 = require("./ChatRuntimeDashboard");
|
||||
Object.defineProperty(exports, "ChatRuntimeDashboard", { enumerable: true, get: function () { return ChatRuntimeDashboard_1.ChatRuntimeDashboard; } });
|
||||
var ErrorLogViewer_1 = require("./ErrorLogViewer");
|
||||
Object.defineProperty(exports, "ErrorLogViewer", { enumerable: true, get: function () { return ErrorLogViewer_1.ErrorLogViewer; } });
|
||||
var chatUtils_1 = require("./chatUtils");
|
||||
Object.defineProperty(exports, "buildOfflineReply", { enumerable: true, get: function () { return chatUtils_1.buildOfflineReply; } });
|
||||
Object.defineProperty(exports, "clearStoredChatClientConversationState", { enumerable: true, get: function () { return chatUtils_1.clearStoredChatClientConversationState; } });
|
||||
Object.defineProperty(exports, "copyPreviewContent", { enumerable: true, get: function () { return chatUtils_1.copyPreviewContent; } });
|
||||
Object.defineProperty(exports, "copyText", { enumerable: true, get: function () { return chatUtils_1.copyText; } });
|
||||
Object.defineProperty(exports, "resolvePreviewBodyForCopy", { enumerable: true, get: function () { return chatUtils_1.resolvePreviewBodyForCopy; } });
|
||||
Object.defineProperty(exports, "createActivityLogPlaceholder", { enumerable: true, get: function () { return chatUtils_1.createActivityLogPlaceholder; } });
|
||||
Object.defineProperty(exports, "createChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.createChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "createChatMessage", { enumerable: true, get: function () { return chatUtils_1.createChatMessage; } });
|
||||
Object.defineProperty(exports, "createIntroMessage", { enumerable: true, get: function () { return chatUtils_1.createIntroMessage; } });
|
||||
Object.defineProperty(exports, "createLocalMessage", { enumerable: true, get: function () { return chatUtils_1.createLocalMessage; } });
|
||||
Object.defineProperty(exports, "cancelChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.cancelChatRuntimeJob; } });
|
||||
Object.defineProperty(exports, "deleteChatConversationRequest", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRequest; } });
|
||||
Object.defineProperty(exports, "deleteChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "fetchChatConversationDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversationDetail; } });
|
||||
Object.defineProperty(exports, "fetchChatConversations", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversations; } });
|
||||
Object.defineProperty(exports, "fetchChatRuntimeJobDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeJobDetail; } });
|
||||
Object.defineProperty(exports, "fetchChatRuntimeSnapshot", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "getStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.getStoredChatSessionLastTypeId; } });
|
||||
Object.defineProperty(exports, "isMissingRequestMessage", { enumerable: true, get: function () { return chatUtils_1.isMissingRequestMessage; } });
|
||||
Object.defineProperty(exports, "isPreparingChatReplyText", { enumerable: true, get: function () { return chatUtils_1.isPreparingChatReplyText; } });
|
||||
Object.defineProperty(exports, "getChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.getChatClientSessionId; } });
|
||||
Object.defineProperty(exports, "markChatConversationResponsesRead", { enumerable: true, get: function () { return chatUtils_1.markChatConversationResponsesRead; } });
|
||||
Object.defineProperty(exports, "mergeRecoveredChatMessages", { enumerable: true, get: function () { return chatUtils_1.mergeRecoveredChatMessages; } });
|
||||
Object.defineProperty(exports, "renameChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.renameChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "removeChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.removeChatRuntimeJob; } });
|
||||
Object.defineProperty(exports, "resetLastReceivedChatEventId", { enumerable: true, get: function () { return chatUtils_1.resetLastReceivedChatEventId; } });
|
||||
Object.defineProperty(exports, "setStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.setStoredChatSessionLastTypeId; } });
|
||||
Object.defineProperty(exports, "setChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.setChatClientSessionId; } });
|
||||
Object.defineProperty(exports, "sortChatConversationSummaries", { enumerable: true, get: function () { return chatUtils_1.sortChatConversationSummaries; } });
|
||||
Object.defineProperty(exports, "uploadChatComposerFile", { enumerable: true, get: function () { return chatUtils_1.uploadChatComposerFile; } });
|
||||
Object.defineProperty(exports, "upsertChatMessage", { enumerable: true, get: function () { return chatUtils_1.upsertChatMessage; } });
|
||||
Object.defineProperty(exports, "updateChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.updateChatConversationRoom; } });
|
||||
var useChatConnection_1 = require("./useChatConnection");
|
||||
Object.defineProperty(exports, "getSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.getSharedChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "setSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.setSharedChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "subscribeChatConnection", { enumerable: true, get: function () { return useChatConnection_1.subscribeChatConnection; } });
|
||||
Object.defineProperty(exports, "useChatConnection", { enumerable: true, get: function () { return useChatConnection_1.useChatConnection; } });
|
||||
var useErrorLogs_1 = require("./useErrorLogs");
|
||||
Object.defineProperty(exports, "useErrorLogs", { enumerable: true, get: function () { return useErrorLogs_1.useErrorLogs; } });
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
fetchChatRuntimeJobDetail,
|
||||
fetchChatRuntimeSnapshot,
|
||||
getStoredChatSessionLastTypeId,
|
||||
isMissingRequestMessage,
|
||||
isPreparingChatReplyText,
|
||||
getChatClientSessionId,
|
||||
markChatConversationResponsesRead,
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
const AUTO_DETECTED_PREVIEW_URL_PATTERN = /(https?:\/\/[^\s<>)\]]+|\/[A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]+)/g;
|
||||
const AUTO_DETECTED_PREVIEW_URL_PATTERN =
|
||||
/(https?:\/\/[^\s<>)\]]+|\/(?:[A-Za-z0-9._~%-][A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]*))/g;
|
||||
const LOCAL_RESOURCE_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
|
||||
const PREVIEWABLE_FILE_EXTENSION_PATTERN =
|
||||
/\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i;
|
||||
|
||||
function stripCodeFenceBlocks(text: string) {
|
||||
return String(text ?? '').replace(/```[\s\S]*?```/g, '');
|
||||
}
|
||||
|
||||
function trimAutoDetectedUrl(value: string) {
|
||||
return String(value ?? '').trim().replace(/[`\])}>.,;!?]+$/g, '');
|
||||
}
|
||||
|
||||
function isLikelyLocalPreviewUrl(value: string) {
|
||||
if (LOCAL_RESOURCE_PREFIXES.some((prefix) => value.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathname = value.split(/[?#]/, 1)[0] ?? '';
|
||||
return PREVIEWABLE_FILE_EXTENSION_PATTERN.test(pathname);
|
||||
}
|
||||
|
||||
export function extractAutoDetectedPreviewUrls(text: string) {
|
||||
const normalized = String(text ?? '');
|
||||
const normalized = stripCodeFenceBlocks(text);
|
||||
const urls: string[] = [];
|
||||
|
||||
for (const match of normalized.matchAll(AUTO_DETECTED_PREVIEW_URL_PATTERN)) {
|
||||
const value = match[0]?.trim();
|
||||
const value = trimAutoDetectedUrl(match[0] ?? '');
|
||||
|
||||
if (!value) {
|
||||
continue;
|
||||
@@ -19,6 +40,10 @@ export function extractAutoDetectedPreviewUrls(text: string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith('/') && !isLikelyLocalPreviewUrl(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.push(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ function normalizeUrl(value: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const malformedResourceMatch = normalized.match(/^https?:\/(api\/chat\/resources\/.+)$/i);
|
||||
if (malformedResourceMatch?.[1]) {
|
||||
return `/${malformedResourceMatch[1]}`;
|
||||
}
|
||||
|
||||
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
@@ -23,11 +28,43 @@ function normalizeUrl(value: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function decodeUrlComponentSafely(value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLinkCardUrlAndActionLabel(rawUrl: string, rawActionLabel?: string) {
|
||||
let resolvedUrl = normalizeText(rawUrl);
|
||||
let resolvedActionLabel = normalizeText(rawActionLabel);
|
||||
|
||||
if (!resolvedActionLabel) {
|
||||
const decodedUrl = decodeUrlComponentSafely(resolvedUrl);
|
||||
const dividerIndex = decodedUrl.lastIndexOf('|');
|
||||
|
||||
if (dividerIndex > 0 && dividerIndex < decodedUrl.length - 1) {
|
||||
resolvedUrl = decodedUrl.slice(0, dividerIndex).trim();
|
||||
resolvedActionLabel = decodedUrl.slice(dividerIndex + 1).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: normalizeUrl(resolvedUrl),
|
||||
actionLabel: resolvedActionLabel || null,
|
||||
};
|
||||
}
|
||||
|
||||
function hasKnownFileExtension(url: string) {
|
||||
const pathname = url.split('?')[0] ?? '';
|
||||
return /\.[a-z0-9]{1,8}$/i.test(pathname);
|
||||
}
|
||||
|
||||
function isInternalResourceUrl(url: string) {
|
||||
return RESOURCE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isStructuredLinkCardCandidate(url: string) {
|
||||
const normalized = normalizeUrl(url);
|
||||
|
||||
@@ -35,15 +72,11 @@ function isStructuredLinkCardCandidate(url: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RESOURCE_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
if (isInternalResourceUrl(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
return !hasKnownFileExtension(normalized);
|
||||
}
|
||||
|
||||
return !hasKnownFileExtension(normalized);
|
||||
return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
|
||||
}
|
||||
|
||||
function buildFallbackLinkTitle(url: string) {
|
||||
@@ -88,8 +121,7 @@ function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
|
||||
|
||||
const [rawTitle, rawUrl, rawActionLabel] = segments;
|
||||
const title = normalizeText(rawTitle);
|
||||
const url = normalizeUrl(rawUrl);
|
||||
const actionLabel = normalizeText(rawActionLabel) || null;
|
||||
const { url, actionLabel } = resolveLinkCardUrlAndActionLabel(rawUrl, rawActionLabel);
|
||||
|
||||
if (!title || !url) {
|
||||
return null;
|
||||
@@ -154,6 +186,14 @@ export function extractChatMessageParts(text: string) {
|
||||
|
||||
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
|
||||
keptLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestPart = parts.at(-1);
|
||||
if (latestPart && isInternalResourceUrl(latestPart.url)) {
|
||||
parts.pop();
|
||||
seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
|
||||
keptLines.push(latestPart.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,117 @@ export type PreviewItem = {
|
||||
source: 'message' | 'context';
|
||||
};
|
||||
|
||||
const CHAT_RESOURCE_INTERNAL_SEGMENTS = new Set(['resource', 'uploads', 'source', 'src']);
|
||||
const CHAT_RESOURCE_HIDDEN_FILE_NAMES = new Set(['.env']);
|
||||
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
||||
const RESOURCE_STRIP_ALLOWED_KINDS = new Set<PreviewKind>([
|
||||
'image',
|
||||
'video',
|
||||
'markdown',
|
||||
'code',
|
||||
'diff',
|
||||
'document',
|
||||
'pdf',
|
||||
'file',
|
||||
]);
|
||||
|
||||
function normalizePreviewUrl(value: string) {
|
||||
return normalizeChatResourceUrl(value);
|
||||
}
|
||||
|
||||
function parsePreviewUrl(url: string) {
|
||||
try {
|
||||
return new URL(url, typeof window !== 'undefined' ? window.location.origin : 'https://local.invalid');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractInternalChatResourcePath(pathname: string) {
|
||||
const normalizedPathname = String(pathname ?? '').trim();
|
||||
|
||||
if (!normalizedPathname) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (normalizedPathname.includes('/.codex_chat/')) {
|
||||
const markerIndex = normalizedPathname.lastIndexOf('/.codex_chat/');
|
||||
return normalizedPathname.slice(markerIndex + 1);
|
||||
}
|
||||
|
||||
const apiMarkerIndex = normalizedPathname.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
||||
if (apiMarkerIndex >= 0) {
|
||||
return normalizedPathname.slice(apiMarkerIndex + CHAT_API_RESOURCE_MARKER.length).replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function hasVisibleFileExtension(fileName: string) {
|
||||
return /\.[a-z0-9]{1,16}$/i.test(fileName);
|
||||
}
|
||||
|
||||
function hasSupportedPreviewFileExtension(fileName: string) {
|
||||
return /\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i.test(
|
||||
fileName,
|
||||
);
|
||||
}
|
||||
|
||||
function isMarkdownResourceFile(fileName: string) {
|
||||
return /\.(md|markdown)$/i.test(fileName);
|
||||
}
|
||||
|
||||
function shouldHideInternalChatResource(url: string) {
|
||||
const parsed = parsePreviewUrl(url);
|
||||
const pathname = parsed?.pathname ?? '';
|
||||
const internalResourcePath = extractInternalChatResourcePath(pathname);
|
||||
const normalizedInternalResourcePath = internalResourcePath.toLowerCase();
|
||||
|
||||
if (!normalizedInternalResourcePath.startsWith('.codex_chat/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segments = internalResourcePath.split('/').filter(Boolean);
|
||||
const lastSegment = segments.at(-1)?.trim() ?? '';
|
||||
const normalizedLastSegment = lastSegment.toLowerCase();
|
||||
const resourceSegmentIndex = segments.findIndex((segment) => segment === 'resource');
|
||||
const nextSegment = resourceSegmentIndex >= 0 ? segments[resourceSegmentIndex + 1]?.toLowerCase() ?? '' : '';
|
||||
|
||||
if (!lastSegment || pathname.endsWith('/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(normalizedLastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasVisibleFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(nextSegment) && !hasVisibleFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedLastSegment.startsWith('.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_HIDDEN_FILE_NAMES.has(normalizedLastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resourceSegmentIndex >= 0 && nextSegment === 'src' && !isMarkdownResourceFile(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasSupportedPreviewFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPreviewRouteUrl(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
@@ -118,15 +225,23 @@ export function extractPreviewItems(messages: ChatMessage[]) {
|
||||
)
|
||||
.map((part) => part.url);
|
||||
const matches = [
|
||||
...extractAutoDetectedPreviewUrls(message.text),
|
||||
...extractHiddenPreviewUrls(message.text),
|
||||
...structuredLinkUrls,
|
||||
...extractAutoDetectedPreviewUrls(message.text),
|
||||
];
|
||||
|
||||
matches.forEach((matchedUrl) => {
|
||||
const normalizedUrl = normalizePreviewUrl(matchedUrl);
|
||||
const kind = classifyPreviewKind(normalizedUrl);
|
||||
|
||||
if (shouldHideInternalChatResource(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RESOURCE_STRIP_ALLOWED_KINDS.has(kind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
2
src/app/main/mainChatPanel/types.js
Normal file
2
src/app/main/mainChatPanel/types.js
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -44,9 +44,11 @@ export type ChatViewContext = {
|
||||
export type ChatConversationSummary = {
|
||||
sessionId: string;
|
||||
clientId: string | null;
|
||||
isDraftOnly?: boolean;
|
||||
title: string;
|
||||
chatTypeId: string | null;
|
||||
lastChatTypeId: string | null;
|
||||
generalSectionName: string | null;
|
||||
contextLabel: string | null;
|
||||
contextDescription: string | null;
|
||||
notifyOffline: boolean;
|
||||
@@ -56,7 +58,9 @@ export type ChatConversationSummary = {
|
||||
currentJobMessage: string | null;
|
||||
currentQueueSize: number;
|
||||
currentStatusUpdatedAt: string | null;
|
||||
lastRequestPreview: string;
|
||||
lastMessagePreview: string;
|
||||
lastResponsePreview: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastMessageAt: string | null;
|
||||
|
||||
495
src/app/main/mainChatPanel/useChatConnection.js
Normal file
495
src/app/main/mainChatPanel/useChatConnection.js
Normal file
@@ -0,0 +1,495 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getChatConnectionSnapshot = getChatConnectionSnapshot;
|
||||
exports.subscribeChatConnection = subscribeChatConnection;
|
||||
exports.getSharedChatRuntimeSnapshot = getSharedChatRuntimeSnapshot;
|
||||
exports.setSharedChatRuntimeSnapshot = setSharedChatRuntimeSnapshot;
|
||||
exports.useChatConnection = useChatConnection;
|
||||
var react_1 = require("react");
|
||||
var chatUtils_1 = require("./chatUtils");
|
||||
var tokenAccess_1 = require("../tokenAccess");
|
||||
var DISCONNECT_UI_DELAY_MS = 1500;
|
||||
var PRESENCE_PING_INTERVAL_MS = 20000;
|
||||
var sharedChatConnection = {
|
||||
connectionState: 'connecting',
|
||||
connectionErrorDetail: '',
|
||||
runtimeSnapshot: null,
|
||||
socketRef: { current: null },
|
||||
reconnectTimerId: null,
|
||||
disconnectUiTimerId: null,
|
||||
connectTimeoutId: null,
|
||||
sessionId: '',
|
||||
currentContext: null,
|
||||
setMessages: null,
|
||||
onMessageEvent: undefined,
|
||||
onJobEvent: undefined,
|
||||
onRuntimeEvent: undefined,
|
||||
onRuntimeDetailEvent: undefined,
|
||||
onActivityEvent: undefined,
|
||||
lastEventId: 0,
|
||||
websocketUrl: '',
|
||||
subscribers: new Set(),
|
||||
pingSubscriberCount: 0,
|
||||
consumerCount: 0,
|
||||
pingIntervalId: null,
|
||||
visibilityHandlerInstalled: false,
|
||||
pageShowHandlerInstalled: false,
|
||||
focusHandlerInstalled: false,
|
||||
onlineHandlerInstalled: false,
|
||||
hasConnectedOnce: false,
|
||||
suppressDisconnectNotification: false,
|
||||
lastBackgroundAt: null,
|
||||
};
|
||||
function emitSharedState() {
|
||||
sharedChatConnection.subscribers.forEach(function (listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
function getSnapshot() {
|
||||
return {
|
||||
connectionState: sharedChatConnection.connectionState,
|
||||
connectionErrorDetail: sharedChatConnection.connectionErrorDetail,
|
||||
runtimeSnapshot: sharedChatConnection.runtimeSnapshot,
|
||||
};
|
||||
}
|
||||
function getChatConnectionSnapshot() {
|
||||
return getSnapshot();
|
||||
}
|
||||
function subscribeChatConnection(listener) {
|
||||
sharedChatConnection.subscribers.add(listener);
|
||||
return function () {
|
||||
sharedChatConnection.subscribers.delete(listener);
|
||||
};
|
||||
}
|
||||
function getSharedChatRuntimeSnapshot() {
|
||||
return sharedChatConnection.runtimeSnapshot;
|
||||
}
|
||||
function setSharedChatRuntimeSnapshot(snapshot) {
|
||||
if (sharedChatConnection.runtimeSnapshot === snapshot) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.runtimeSnapshot = snapshot;
|
||||
emitSharedState();
|
||||
}
|
||||
function setSharedConnectionState(nextState) {
|
||||
if (sharedChatConnection.connectionState === nextState) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.connectionState = nextState;
|
||||
emitSharedState();
|
||||
}
|
||||
function setSharedConnectionError(detail) {
|
||||
if (sharedChatConnection.connectionErrorDetail === detail) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.connectionErrorDetail = detail;
|
||||
emitSharedState();
|
||||
}
|
||||
function clearReconnectTimer() {
|
||||
if (sharedChatConnection.reconnectTimerId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.reconnectTimerId);
|
||||
sharedChatConnection.reconnectTimerId = null;
|
||||
}
|
||||
}
|
||||
function clearDisconnectUiTimer() {
|
||||
if (sharedChatConnection.disconnectUiTimerId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.disconnectUiTimerId);
|
||||
sharedChatConnection.disconnectUiTimerId = null;
|
||||
}
|
||||
}
|
||||
function clearConnectTimeout() {
|
||||
if (sharedChatConnection.connectTimeoutId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.connectTimeoutId);
|
||||
sharedChatConnection.connectTimeoutId = null;
|
||||
}
|
||||
}
|
||||
function sendContextUpdate(context) {
|
||||
if (context === void 0) { context = sharedChatConnection.currentContext; }
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN || !context) {
|
||||
return;
|
||||
}
|
||||
var liveVisibilityState = typeof document !== 'undefined' && document.visibilityState === 'hidden' ? 'hidden' : 'visible';
|
||||
var livePageUrl = typeof window !== 'undefined' ? window.location.href : context.pageUrl;
|
||||
socket.send(JSON.stringify({
|
||||
type: 'context:update',
|
||||
payload: {
|
||||
pageId: context.pageId,
|
||||
pageTitle: context.pageTitle,
|
||||
topMenu: context.topMenu,
|
||||
focusedComponentId: context.focusedComponentId,
|
||||
pageUrl: livePageUrl,
|
||||
isStandaloneMode: context.isStandaloneMode,
|
||||
pageVisibilityState: liveVisibilityState,
|
||||
chatTypeId: context.chatTypeId,
|
||||
chatTypeLabel: context.chatTypeLabel,
|
||||
chatTypeDescription: context.chatTypeDescription,
|
||||
},
|
||||
}));
|
||||
}
|
||||
function sendPresencePing() {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
type: 'presence:ping',
|
||||
payload: {
|
||||
at: Date.now(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
function ensureSharedSocket() {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
connectSharedSocket();
|
||||
}
|
||||
function sendEventReceived(eventId) {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN || !Number.isFinite(eventId) || eventId <= 0) {
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
type: 'event:received',
|
||||
payload: {
|
||||
eventId: eventId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
function stopPresenceMonitoring() {
|
||||
if (sharedChatConnection.pingIntervalId !== null) {
|
||||
window.clearInterval(sharedChatConnection.pingIntervalId);
|
||||
sharedChatConnection.pingIntervalId = null;
|
||||
}
|
||||
if (sharedChatConnection.visibilityHandlerInstalled) {
|
||||
window.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
sharedChatConnection.visibilityHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.pageShowHandlerInstalled) {
|
||||
window.removeEventListener('pageshow', handlePageShow);
|
||||
sharedChatConnection.pageShowHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.focusHandlerInstalled) {
|
||||
window.removeEventListener('focus', handleWindowFocus);
|
||||
sharedChatConnection.focusHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.onlineHandlerInstalled) {
|
||||
window.removeEventListener('online', handleWindowOnline);
|
||||
sharedChatConnection.onlineHandlerInstalled = false;
|
||||
}
|
||||
}
|
||||
function handleVisibilityChange() {
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
||||
sharedChatConnection.lastBackgroundAt = Date.now();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
sendPresencePing();
|
||||
return;
|
||||
}
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
}
|
||||
function handlePageShow() {
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
}
|
||||
function handleWindowFocus() {
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
}
|
||||
function handleWindowOnline() {
|
||||
ensureSharedSocket();
|
||||
}
|
||||
function startPresenceMonitoring() {
|
||||
if (sharedChatConnection.pingSubscriberCount <= 0 || sharedChatConnection.connectionState !== 'connected') {
|
||||
stopPresenceMonitoring();
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.lastBackgroundAt = null;
|
||||
sendPresencePing();
|
||||
if (sharedChatConnection.pingIntervalId === null) {
|
||||
sharedChatConnection.pingIntervalId = window.setInterval(function () {
|
||||
sendPresencePing();
|
||||
}, PRESENCE_PING_INTERVAL_MS);
|
||||
}
|
||||
if (!sharedChatConnection.visibilityHandlerInstalled) {
|
||||
window.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
sharedChatConnection.visibilityHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.pageShowHandlerInstalled) {
|
||||
window.addEventListener('pageshow', handlePageShow);
|
||||
sharedChatConnection.pageShowHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.focusHandlerInstalled) {
|
||||
window.addEventListener('focus', handleWindowFocus);
|
||||
sharedChatConnection.focusHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.onlineHandlerInstalled) {
|
||||
window.addEventListener('online', handleWindowOnline);
|
||||
sharedChatConnection.onlineHandlerInstalled = true;
|
||||
}
|
||||
}
|
||||
function scheduleReconnect() {
|
||||
if (sharedChatConnection.reconnectTimerId !== null || !sharedChatConnection.sessionId) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.reconnectTimerId = window.setTimeout(function () {
|
||||
sharedChatConnection.reconnectTimerId = null;
|
||||
connectSharedSocket();
|
||||
}, chatUtils_1.CHAT_CONNECTION.reconnectDelayMs);
|
||||
}
|
||||
function handleSharedDisconnect(message, detail) {
|
||||
setSharedConnectionError(detail !== null && detail !== void 0 ? detail : '');
|
||||
clearDisconnectUiTimer();
|
||||
if (sharedChatConnection.connectionState !== 'connected') {
|
||||
setSharedConnectionState('disconnected');
|
||||
}
|
||||
else {
|
||||
sharedChatConnection.disconnectUiTimerId = window.setTimeout(function () {
|
||||
var _a;
|
||||
sharedChatConnection.disconnectUiTimerId = null;
|
||||
if (((_a = sharedChatConnection.socketRef.current) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
setSharedConnectionState('disconnected');
|
||||
}, DISCONNECT_UI_DELAY_MS);
|
||||
}
|
||||
if (message) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
function disconnectSharedSocket() {
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
stopPresenceMonitoring();
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
sharedChatConnection.suppressDisconnectNotification = true;
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
socket === null || socket === void 0 ? void 0 : socket.close();
|
||||
}
|
||||
function releaseSharedConnectionConsumer() {
|
||||
sharedChatConnection.consumerCount = Math.max(0, sharedChatConnection.consumerCount - 1);
|
||||
if (sharedChatConnection.consumerCount > 0) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.currentContext = null;
|
||||
sharedChatConnection.setMessages = null;
|
||||
sharedChatConnection.onMessageEvent = undefined;
|
||||
sharedChatConnection.onJobEvent = undefined;
|
||||
sharedChatConnection.onRuntimeEvent = undefined;
|
||||
sharedChatConnection.onRuntimeDetailEvent = undefined;
|
||||
setSharedChatRuntimeSnapshot(null);
|
||||
disconnectSharedSocket();
|
||||
setSharedConnectionError('');
|
||||
setSharedConnectionState('disconnected');
|
||||
}
|
||||
function connectSharedSocket() {
|
||||
if (!sharedChatConnection.sessionId || !sharedChatConnection.setMessages) {
|
||||
return;
|
||||
}
|
||||
if (!(0, tokenAccess_1.hasRegisteredAccessTokenAccess)()) {
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
stopPresenceMonitoring();
|
||||
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결을 시작하지 않았습니다.');
|
||||
setSharedConnectionState('disconnected');
|
||||
return;
|
||||
}
|
||||
var currentSocket = sharedChatConnection.socketRef.current;
|
||||
if (currentSocket && (currentSocket.readyState === WebSocket.OPEN || currentSocket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
if (sharedChatConnection.connectionState !== 'connected') {
|
||||
setSharedConnectionState('connecting');
|
||||
}
|
||||
sharedChatConnection.websocketUrl = (0, chatUtils_1.resolveChatWebSocketUrl)(sharedChatConnection.sessionId, sharedChatConnection.lastEventId);
|
||||
var socket;
|
||||
try {
|
||||
socket = new WebSocket(sharedChatConnection.websocketUrl);
|
||||
}
|
||||
catch (_a) {
|
||||
handleSharedDisconnect("\uC6CC\uD06C\uC11C\uBC84 WebSocket \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."), 'WebSocket 객체를 생성하지 못했습니다. 대상 주소 형식과 환경변수를 확인해 주세요.');
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.socketRef.current = socket;
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
var disconnectHandled = false;
|
||||
var reportDisconnect = function (message, closeEvent) {
|
||||
if (disconnectHandled) {
|
||||
return;
|
||||
}
|
||||
disconnectHandled = true;
|
||||
var wasSuppressed = sharedChatConnection.suppressDisconnectNotification;
|
||||
if (sharedChatConnection.socketRef.current === socket) {
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
}
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
if (wasSuppressed) {
|
||||
setSharedConnectionError('');
|
||||
return;
|
||||
}
|
||||
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1008) {
|
||||
clearReconnectTimer();
|
||||
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결이 차단되었습니다.');
|
||||
setSharedConnectionState('disconnected');
|
||||
return;
|
||||
}
|
||||
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1000 && !message) {
|
||||
setSharedConnectionError('');
|
||||
handleSharedDisconnect();
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
void (0, chatUtils_1.diagnoseConnectionFailure)(sharedChatConnection.websocketUrl, closeEvent).then(function (detail) {
|
||||
handleSharedDisconnect(message, detail);
|
||||
});
|
||||
};
|
||||
sharedChatConnection.connectTimeoutId = window.setTimeout(function () {
|
||||
if (sharedChatConnection.socketRef.current !== socket || socket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
socket.close();
|
||||
reportDisconnect("\uC6CC\uD06C\uC11C\uBC84 \uC5F0\uACB0 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."));
|
||||
}, chatUtils_1.CHAT_CONNECTION.connectTimeoutMs);
|
||||
socket.addEventListener('open', function () {
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
sharedChatConnection.hasConnectedOnce = true;
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
setSharedConnectionState('connected');
|
||||
setSharedConnectionError('');
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
startPresenceMonitoring();
|
||||
});
|
||||
socket.addEventListener('message', function (event) {
|
||||
var _a, _b;
|
||||
var setMessages = sharedChatConnection.setMessages;
|
||||
if (!setMessages) {
|
||||
return;
|
||||
}
|
||||
void (0, chatUtils_1.handleChatServerEvent)({
|
||||
eventData: String(event.data),
|
||||
currentPageUrl: (_b = (_a = sharedChatConnection.currentContext) === null || _a === void 0 ? void 0 : _a.pageUrl) !== null && _b !== void 0 ? _b : '',
|
||||
expectedSessionId: sharedChatConnection.sessionId,
|
||||
setMessages: setMessages,
|
||||
onMessageEvent: sharedChatConnection.onMessageEvent,
|
||||
onJobEvent: sharedChatConnection.onJobEvent,
|
||||
onRuntimeEvent: sharedChatConnection.onRuntimeEvent,
|
||||
onRuntimeDetailEvent: sharedChatConnection.onRuntimeDetailEvent,
|
||||
onActivityEvent: sharedChatConnection.onActivityEvent,
|
||||
onEventReceived: function (eventId) {
|
||||
sharedChatConnection.lastEventId = eventId;
|
||||
(0, chatUtils_1.persistLastReceivedChatEventId)(sharedChatConnection.sessionId, eventId);
|
||||
sendEventReceived(eventId);
|
||||
},
|
||||
});
|
||||
try {
|
||||
var parsedEvent = JSON.parse(String(event.data));
|
||||
if ((parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.type) === 'chat:runtime') {
|
||||
setSharedChatRuntimeSnapshot(parsedEvent.payload);
|
||||
}
|
||||
}
|
||||
catch (_c) {
|
||||
// ignore malformed payloads here; detailed parsing is already handled downstream
|
||||
}
|
||||
});
|
||||
socket.addEventListener('close', function (event) {
|
||||
clearConnectTimeout();
|
||||
stopPresenceMonitoring();
|
||||
reportDisconnect(event.code === 1000 ? undefined : '워크서버 연결이 끊어졌습니다. 자동으로 다시 연결합니다.', event);
|
||||
});
|
||||
socket.addEventListener('error', function () {
|
||||
clearConnectTimeout();
|
||||
stopPresenceMonitoring();
|
||||
reportDisconnect('워크서버 WebSocket 연결에 실패했습니다. 자동으로 다시 연결합니다.');
|
||||
});
|
||||
}
|
||||
function ensureSharedConnection(options) {
|
||||
var sessionChanged = sharedChatConnection.sessionId !== options.sessionId;
|
||||
sharedChatConnection.currentContext = options.currentContext;
|
||||
sharedChatConnection.setMessages = options.setMessages;
|
||||
sharedChatConnection.onMessageEvent = options.onMessageEvent;
|
||||
sharedChatConnection.onJobEvent = options.onJobEvent;
|
||||
sharedChatConnection.onRuntimeEvent = options.onRuntimeEvent;
|
||||
sharedChatConnection.onRuntimeDetailEvent = options.onRuntimeDetailEvent;
|
||||
sharedChatConnection.onActivityEvent = options.onActivityEvent;
|
||||
if (sessionChanged) {
|
||||
sharedChatConnection.sessionId = options.sessionId;
|
||||
sharedChatConnection.lastEventId = (0, chatUtils_1.getLastReceivedChatEventId)(options.sessionId);
|
||||
sharedChatConnection.hasConnectedOnce = false;
|
||||
disconnectSharedSocket();
|
||||
}
|
||||
connectSharedSocket();
|
||||
}
|
||||
function useChatConnection(_a) {
|
||||
var sessionId = _a.sessionId, currentContext = _a.currentContext, setMessages = _a.setMessages, onMessageEvent = _a.onMessageEvent, onJobEvent = _a.onJobEvent, onRuntimeEvent = _a.onRuntimeEvent, onRuntimeDetailEvent = _a.onRuntimeDetailEvent, onActivityEvent = _a.onActivityEvent;
|
||||
var _b = (0, react_1.useState)(function () { return getSnapshot(); }), snapshot = _b[0], setSnapshot = _b[1];
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.consumerCount += 1;
|
||||
return function () {
|
||||
releaseSharedConnectionConsumer();
|
||||
};
|
||||
}, []);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var handleSnapshotChange = function () {
|
||||
setSnapshot(getSnapshot());
|
||||
};
|
||||
var unsubscribe = subscribeChatConnection(handleSnapshotChange);
|
||||
ensureSharedConnection({
|
||||
sessionId: sessionId,
|
||||
currentContext: currentContext,
|
||||
setMessages: setMessages,
|
||||
onMessageEvent: onMessageEvent,
|
||||
onJobEvent: onJobEvent,
|
||||
onRuntimeEvent: onRuntimeEvent,
|
||||
onRuntimeDetailEvent: onRuntimeDetailEvent,
|
||||
onActivityEvent: onActivityEvent,
|
||||
});
|
||||
handleSnapshotChange();
|
||||
return function () {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [sessionId, setMessages]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.currentContext = currentContext;
|
||||
sharedChatConnection.setMessages = setMessages;
|
||||
sharedChatConnection.onMessageEvent = onMessageEvent;
|
||||
sharedChatConnection.onJobEvent = onJobEvent;
|
||||
sharedChatConnection.onRuntimeEvent = onRuntimeEvent;
|
||||
sharedChatConnection.onRuntimeDetailEvent = onRuntimeDetailEvent;
|
||||
sharedChatConnection.onActivityEvent = onActivityEvent;
|
||||
sendContextUpdate(currentContext);
|
||||
}, [
|
||||
currentContext,
|
||||
onMessageEvent,
|
||||
onJobEvent,
|
||||
onRuntimeEvent,
|
||||
onRuntimeDetailEvent,
|
||||
onActivityEvent,
|
||||
setMessages,
|
||||
]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.pingSubscriberCount += 1;
|
||||
startPresenceMonitoring();
|
||||
return function () {
|
||||
sharedChatConnection.pingSubscriberCount = Math.max(0, sharedChatConnection.pingSubscriberCount - 1);
|
||||
if (sharedChatConnection.pingSubscriberCount === 0) {
|
||||
stopPresenceMonitoring();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return {
|
||||
connectionState: snapshot.connectionState,
|
||||
connectionErrorDetail: snapshot.connectionErrorDetail,
|
||||
socketRef: sharedChatConnection.socketRef,
|
||||
};
|
||||
}
|
||||
@@ -483,6 +483,8 @@ function connectSharedSocket() {
|
||||
|
||||
if (closeEvent?.code === 1000 && !message) {
|
||||
setSharedConnectionError('');
|
||||
handleSharedDisconnect();
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
116
src/app/main/mainChatPanel/useErrorLogs.js
Normal file
116
src/app/main/mainChatPanel/useErrorLogs.js
Normal file
@@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useErrorLogs = useErrorLogs;
|
||||
var react_1 = require("react");
|
||||
var errorLogApi_1 = require("../errorLogApi");
|
||||
var errorLogUtils_1 = require("./errorLogUtils");
|
||||
function useErrorLogs(_a) {
|
||||
var _this = this;
|
||||
var activeView = _a.activeView, hasAccess = _a.hasAccess;
|
||||
var _b = (0, react_1.useState)([]), errorLogs = _b[0], setErrorLogs = _b[1];
|
||||
var _c = (0, react_1.useState)(null), selectedErrorLogId = _c[0], setSelectedErrorLogId = _c[1];
|
||||
var _d = (0, react_1.useState)(false), isLoadingErrorLogs = _d[0], setIsLoadingErrorLogs = _d[1];
|
||||
var _e = (0, react_1.useState)(''), errorLogLoadError = _e[0], setErrorLogLoadError = _e[1];
|
||||
var _f = (0, react_1.useState)(''), activeErrorResourceUrl = _f[0], setActiveErrorResourceUrl = _f[1];
|
||||
var _g = (0, react_1.useState)(false), isErrorDetailExpanded = _g[0], setIsErrorDetailExpanded = _g[1];
|
||||
var loadErrorLogs = function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var items_1, error_1;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!hasAccess || isLoadingErrorLogs) {
|
||||
return [2 /*return*/];
|
||||
}
|
||||
setIsLoadingErrorLogs(true);
|
||||
setErrorLogLoadError('');
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 3, 4, 5]);
|
||||
return [4 /*yield*/, (0, errorLogApi_1.fetchErrorLogs)(50)];
|
||||
case 2:
|
||||
items_1 = _a.sent();
|
||||
setErrorLogs(items_1);
|
||||
setSelectedErrorLogId(function (current) { var _a, _b; return (_b = current !== null && current !== void 0 ? current : (_a = items_1[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null; });
|
||||
return [3 /*break*/, 5];
|
||||
case 3:
|
||||
error_1 = _a.sent();
|
||||
setErrorLogLoadError(error_1 instanceof Error ? error_1.message : '에러 로그를 불러오지 못했습니다.');
|
||||
return [3 /*break*/, 5];
|
||||
case 4:
|
||||
setIsLoadingErrorLogs(false);
|
||||
return [7 /*endfinally*/];
|
||||
case 5: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
(0, react_1.useEffect)(function () {
|
||||
if (activeView !== 'errors' || !hasAccess) {
|
||||
return;
|
||||
}
|
||||
void loadErrorLogs();
|
||||
}, [activeView, hasAccess]);
|
||||
var selectedErrorLog = (0, react_1.useMemo)(function () { var _a, _b; return (_b = (_a = errorLogs.find(function (item) { return item.id === selectedErrorLogId; })) !== null && _a !== void 0 ? _a : errorLogs[0]) !== null && _b !== void 0 ? _b : null; }, [errorLogs, selectedErrorLogId]);
|
||||
var selectedErrorLogReferenceSummary = (0, react_1.useMemo)(function () { return (selectedErrorLog ? (0, errorLogUtils_1.buildErrorReferenceSummary)(selectedErrorLog) : null); }, [selectedErrorLog]);
|
||||
var activeErrorResource = (0, react_1.useMemo)(function () {
|
||||
var _a, _b;
|
||||
return (_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources.find(function (resource) { return resource.url === activeErrorResourceUrl; })) !== null && _a !== void 0 ? _a : (0, errorLogUtils_1.getDefaultErrorResource)((_b = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _b !== void 0 ? _b : []);
|
||||
}, [activeErrorResourceUrl, selectedErrorLogReferenceSummary]);
|
||||
var errorSourceSummary = (0, react_1.useMemo)(function () { return (0, errorLogUtils_1.buildErrorSourceSummary)(errorLogs); }, [errorLogs]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var _a, _b, _c;
|
||||
var nextUrl = (_c = (_b = (0, errorLogUtils_1.getDefaultErrorResource)((_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _a !== void 0 ? _a : [])) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : '';
|
||||
setActiveErrorResourceUrl(nextUrl);
|
||||
}, [selectedErrorLog === null || selectedErrorLog === void 0 ? void 0 : selectedErrorLog.id, selectedErrorLogReferenceSummary]);
|
||||
return {
|
||||
errorLogs: errorLogs,
|
||||
selectedErrorLog: selectedErrorLog,
|
||||
selectedErrorLogId: selectedErrorLogId,
|
||||
selectedErrorLogReferenceSummary: selectedErrorLogReferenceSummary,
|
||||
activeErrorResource: activeErrorResource,
|
||||
errorSourceSummary: errorSourceSummary,
|
||||
isLoadingErrorLogs: isLoadingErrorLogs,
|
||||
errorLogLoadError: errorLogLoadError,
|
||||
activeErrorResourceUrl: activeErrorResourceUrl,
|
||||
isErrorDetailExpanded: isErrorDetailExpanded,
|
||||
setSelectedErrorLogId: setSelectedErrorLogId,
|
||||
setActiveErrorResourceUrl: setActiveErrorResourceUrl,
|
||||
setIsErrorDetailExpanded: setIsErrorDetailExpanded,
|
||||
loadErrorLogs: loadErrorLogs,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user