Files
ai-code-app/src/app/main/mainChatPanel/ErrorLogViewer.tsx

328 lines
13 KiB
TypeScript

import { CloseOutlined, ExpandOutlined, ReloadOutlined } from '@ant-design/icons';
import { Alert, Button, Empty, Space, Spin, Tag, Typography } from 'antd';
import { PreviewerUI } from '../../../components/previewer';
import type { ErrorLogItem } from '../errorLogApi';
import {
buildErrorListPreviewLine,
buildErrorMetaRows,
buildErrorReferenceSummary,
formatErrorLogTime,
formatErrorSummary,
getErrorResourceKindLabel,
getErrorSourceColor,
getErrorSourceLabel,
renderErrorTabs,
} from './errorLogUtils';
import type { ErrorReferenceResource, ErrorReferenceSummary } from './types';
const { Paragraph, Text } = Typography;
type ErrorLogViewerProps = {
errorLogs: ErrorLogItem[];
selectedErrorLog: ErrorLogItem | null;
selectedErrorLogReferenceSummary: ErrorReferenceSummary | null;
activeErrorResource: ErrorReferenceResource | null;
isErrorDetailExpanded: boolean;
isLoadingErrorLogs: boolean;
errorLogLoadError: string;
errorSourceSummary: Record<string, number>;
onRefresh: () => void;
onSelectErrorLog: (id: number) => void;
onSelectResource: (url: string) => void;
onToggleExpanded: (expanded: boolean) => void;
};
function ErrorDetailContent({
selectedErrorLog,
selectedErrorLogReferenceSummary,
activeErrorResource,
onSelectResource,
}: {
selectedErrorLog: ErrorLogItem;
selectedErrorLogReferenceSummary: ErrorReferenceSummary | null;
activeErrorResource: ErrorReferenceResource | null;
onSelectResource: (url: string) => void;
}) {
return (
<>
{selectedErrorLogReferenceSummary ? (
<div className="app-chat-panel__error-summary-strip">
<div className="app-chat-panel__error-summary-pill">
<Text type="secondary">preview</Text>
<Text strong>{selectedErrorLogReferenceSummary.previewCount}</Text>
</div>
<div className="app-chat-panel__error-summary-pill">
<Text type="secondary">image</Text>
<Text strong>{selectedErrorLogReferenceSummary.imageCount}</Text>
</div>
<div className="app-chat-panel__error-summary-pill">
<Text type="secondary">link</Text>
<Text strong>{selectedErrorLogReferenceSummary.linkCount}</Text>
</div>
<div className="app-chat-panel__error-summary-pill">
<Text type="secondary"> </Text>
<Text strong>{selectedErrorLogReferenceSummary.resources.length}</Text>
</div>
</div>
) : null}
{selectedErrorLogReferenceSummary?.resources.length ? (
<div className="app-chat-panel__error-reference-stage">
<div className="app-chat-panel__error-reference-rail">
{selectedErrorLogReferenceSummary.resources.map((resource, index) => (
<button
key={`${selectedErrorLog.id}-${resource.url}`}
type="button"
className={
resource.url === activeErrorResource?.url
? 'app-chat-panel__error-reference-item app-chat-panel__error-reference-item--active'
: 'app-chat-panel__error-reference-item'
}
onClick={() => {
onSelectResource(resource.url);
}}
>
<div className="app-chat-panel__error-reference-item-top">
<Text strong>{index + 1}</Text>
<Tag>{getErrorResourceKindLabel(resource.kind)}</Tag>
</div>
<Text strong>{resource.label}</Text>
<Text type="secondary">{resource.sourcePath}</Text>
<Text type="secondary" className="app-chat-panel__error-reference-item-snippet">
{resource.sourcePreview}
</Text>
</button>
))}
</div>
{activeErrorResource ? (
<div className="app-chat-panel__error-reference-main">
<div className="app-chat-panel__error-reference-main-top">
<div>
<Text strong>{activeErrorResource.label}</Text>
<br />
<Text type="secondary">
{getErrorResourceKindLabel(activeErrorResource.kind)} · {activeErrorResource.sourcePath}
</Text>
</div>
<Button size="small" type="primary" href={activeErrorResource.url} target="_blank" rel="noreferrer">
</Button>
</div>
<div className="app-chat-panel__error-reference-main-meta">
<div className="app-chat-panel__error-reference-main-meta-item">
<Text type="secondary"></Text>
<Text>{getErrorResourceKindLabel(activeErrorResource.kind)}</Text>
</div>
<div className="app-chat-panel__error-reference-main-meta-item">
<Text type="secondary"> </Text>
<Text>{activeErrorResource.sourcePath}</Text>
</div>
<div className="app-chat-panel__error-reference-main-meta-item app-chat-panel__error-reference-main-meta-item--wide">
<Text type="secondary">URL</Text>
<Text>{activeErrorResource.url}</Text>
</div>
</div>
{activeErrorResource.kind === 'image' ? (
<PreviewerUI
type="image"
title={activeErrorResource.label}
description={activeErrorResource.url}
value={activeErrorResource.url}
imageAlt={activeErrorResource.label}
height={420}
/>
) : activeErrorResource.kind === 'preview' ? (
<div className="app-chat-panel__error-preview-card app-chat-panel__error-preview-card--stage">
<div className="app-chat-panel__error-preview-frame-wrap">
<iframe
className="app-chat-panel__error-preview-frame app-chat-panel__error-preview-frame--stage"
src={activeErrorResource.url}
title={activeErrorResource.label}
loading="lazy"
referrerPolicy="no-referrer"
/>
</div>
</div>
) : (
<PreviewerUI
type="text"
title={activeErrorResource.label}
description="외부 링크 참조"
value={activeErrorResource.url}
height="auto"
/>
)}
<Text type="secondary" className="app-chat-panel__error-preview-url">
{activeErrorResource.url}
</Text>
<div className="app-chat-panel__error-source-preview">
<Text strong> </Text>
<Text type="secondary">{activeErrorResource.sourcePath}</Text>
<pre>{activeErrorResource.sourcePreview}</pre>
</div>
</div>
) : null}
</div>
) : null}
<div className="app-chat-panel__error-detail-meta">
{buildErrorMetaRows(selectedErrorLog).map((row) => (
<div key={`${selectedErrorLog.id}-${row.label}`} className="app-chat-panel__error-detail-meta-row">
<Text type="secondary">{row.label}</Text>
<Text>{row.value}</Text>
</div>
))}
</div>
<Paragraph className="app-chat-panel__error-detail-line">
<Text strong> </Text>
<br />
{selectedErrorLog.errorMessage}
</Paragraph>
{renderErrorTabs(selectedErrorLog)}
</>
);
}
export function ErrorLogViewer({
errorLogs,
selectedErrorLog,
selectedErrorLogReferenceSummary,
activeErrorResource,
isErrorDetailExpanded,
isLoadingErrorLogs,
errorLogLoadError,
errorSourceSummary,
onRefresh,
onSelectErrorLog,
onSelectResource,
onToggleExpanded,
}: ErrorLogViewerProps) {
const errorDetailHeader = selectedErrorLog ? (
<div className="app-chat-panel__error-detail-header">
<div className="app-chat-panel__error-detail-header-meta">
<Space size={[8, 8]} wrap>
<Tag color={getErrorSourceColor(selectedErrorLog.source)}>{getErrorSourceLabel(selectedErrorLog)}</Tag>
<Tag>{selectedErrorLog.errorType}</Tag>
{selectedErrorLog.errorName ? <Tag>{selectedErrorLog.errorName}</Tag> : null}
{selectedErrorLog.statusCode ? <Tag>{selectedErrorLog.statusCode}</Tag> : null}
{selectedErrorLog.relatedPlanId ? <Tag>Plan #{selectedErrorLog.relatedPlanId}</Tag> : null}
{selectedErrorLog.relatedWorkId ? <Tag>{selectedErrorLog.relatedWorkId}</Tag> : null}
</Space>
<Text type="secondary"> #{selectedErrorLog.id}</Text>
</div>
<div className="app-chat-panel__error-detail-actions">
{isErrorDetailExpanded ? (
<Button type="text" size="small" icon={<CloseOutlined />} aria-label="상세영역 닫기" onClick={() => onToggleExpanded(false)} />
) : (
<Button type="text" size="small" icon={<ExpandOutlined />} aria-label="상세영역 최대화" onClick={() => onToggleExpanded(true)} />
)}
</div>
</div>
) : null;
return (
<div className="app-chat-panel__error-layout">
{isErrorDetailExpanded && selectedErrorLog ? (
<div className="app-chat-panel__error-detail-screen">
<div className="app-chat-panel__error-detail app-chat-panel__error-detail--expanded">
{errorDetailHeader}
<ErrorDetailContent
selectedErrorLog={selectedErrorLog}
selectedErrorLogReferenceSummary={selectedErrorLogReferenceSummary}
activeErrorResource={activeErrorResource}
onSelectResource={onSelectResource}
/>
</div>
</div>
) : null}
<div className="app-chat-panel__error-toolbar">
<div className="app-chat-panel__error-toolbar-copy">
<Text strong> 50</Text>
<Text type="secondary">, API, .</Text>
</div>
<Space size={[8, 8]} wrap>
{Object.entries(errorSourceSummary).map(([label, count]) => (
<Tag key={label}>{label} {count}</Tag>
))}
<Button size="small" icon={<ReloadOutlined />} loading={isLoadingErrorLogs} onClick={onRefresh}>
</Button>
</Space>
</div>
{errorLogLoadError ? <Alert showIcon type="error" message={errorLogLoadError} /> : null}
{isLoadingErrorLogs ? (
<div className="app-chat-panel__error-loading">
<Spin />
</div>
) : errorLogs.length === 0 ? (
<div className="app-chat-panel__error-empty">
<Empty description="저장된 에러 로그가 없습니다." />
</div>
) : (
<div className="app-chat-panel__error-content">
<div className="app-chat-panel__error-list">
{errorLogs.map((item) => {
const referenceSummary = buildErrorReferenceSummary(item);
return (
<button
key={item.id}
type="button"
className={
item.id === selectedErrorLog?.id
? 'app-chat-panel__error-item app-chat-panel__error-item--active'
: 'app-chat-panel__error-item'
}
onClick={() => {
onSelectErrorLog(item.id);
}}
>
<div className="app-chat-panel__error-item-top">
<Text type="secondary">{formatErrorLogTime(item.createdAt)}</Text>
<Tag bordered={false} color={getErrorSourceColor(item.source)}>
{getErrorSourceLabel(item)}
</Tag>
</div>
<Text strong className="app-chat-panel__error-item-title">{formatErrorSummary(item)}</Text>
<Text type="secondary" className="app-chat-panel__error-item-message">
{buildErrorListPreviewLine(item, referenceSummary)}
</Text>
<div className="app-chat-panel__error-item-badges">
{referenceSummary.previewCount ? <Tag bordered={false}>preview {referenceSummary.previewCount}</Tag> : null}
{referenceSummary.imageCount ? <Tag bordered={false}>image {referenceSummary.imageCount}</Tag> : null}
{referenceSummary.linkCount ? <Tag bordered={false}>link {referenceSummary.linkCount}</Tag> : null}
{item.statusCode ? <Tag bordered={false}>{item.statusCode}</Tag> : null}
{item.requestMethod ? <Tag bordered={false}>{item.requestMethod}</Tag> : null}
</div>
</button>
);
})}
</div>
{selectedErrorLog ? (
<div className="app-chat-panel__error-detail">
{errorDetailHeader}
<ErrorDetailContent
selectedErrorLog={selectedErrorLog}
selectedErrorLogReferenceSummary={selectedErrorLogReferenceSummary}
activeErrorResource={activeErrorResource}
onSelectResource={onSelectResource}
/>
</div>
) : null}
</div>
)}
</div>
);
}