import {
CodeOutlined,
DownloadOutlined,
EyeOutlined,
FileMarkdownOutlined,
FilePdfOutlined,
FileTextOutlined,
LinkOutlined,
PictureOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Alert, Button, Empty, Space, Spin, Typography } from 'antd';
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 { triggerResourceDownload } from './downloadUtils';
import '../../../components/previewer/PreviewerUI.css';
const { Paragraph, Text } = Typography;
export type ChatPreviewKind = 'image' | 'video' | 'markdown' | 'code' | 'diff' | 'document' | 'pdf' | 'file';
export type ChatPreviewTarget = {
label: string;
url: string;
kind: ChatPreviewKind;
};
export function resolveChatPreviewGlyph(kind: ChatPreviewKind) {
switch (kind) {
case 'image':
return ;
case 'video':
return ;
case 'markdown':
return ;
case 'code':
case 'diff':
return ;
case 'document':
return ;
case 'pdf':
return ;
default:
return ;
}
}
export function resolveChatPreviewKindLabel(kind: ChatPreviewKind) {
switch (kind) {
case 'image':
return 'image preview';
case 'video':
return 'video preview';
case 'markdown':
return 'markdown preview';
case 'code':
return 'code preview';
case 'diff':
return 'diff preview';
case 'document':
return 'document preview';
case 'pdf':
return 'pdf preview';
default:
return 'resource preview';
}
}
function resolvePreviewErrorMessage(previewError: string) {
const normalized = previewError.trim();
if (!normalized) {
return '';
}
if (/^\s*403\b/.test(normalized) || normalized.includes('권한으로 열 수 없습니다')) {
return '권한이 없거나 허용되지 않은 경로입니다. 세션 리소스 경로와 접근 권한을 확인해 주세요.';
}
if (/^\s*404\b/.test(normalized) || normalized.includes('찾을 수 없습니다')) {
return '파일이 이동되었거나 아직 세션 리소스 경로에 생성되지 않았습니다. 경로를 다시 확인해 주세요.';
}
if (/^\s*401\b/.test(normalized) || normalized.includes('인증이 필요합니다')) {
return '인증 정보가 없어서 문서를 열 수 없습니다.';
}
return normalized;
}
function resolvePreviewExtension(target: ChatPreviewTarget) {
const raw = target.label || target.url;
const normalized = raw.toLowerCase().split('?')[0] ?? '';
const match = normalized.match(/\.([a-z0-9]+)$/i);
return match?.[1] ?? '';
}
function resolveCodeLanguage(target: ChatPreviewTarget, previewText: string) {
const extension = resolvePreviewExtension(target);
if (extension === 'tsx' || extension === 'ts') {
return 'typescript';
}
if (extension === 'jsx' || extension === 'js' || extension === 'mjs' || extension === 'cjs') {
return 'javascript';
}
if (extension === 'json') {
return 'json';
}
if (extension === 'css') {
return 'css';
}
if (extension === 'scss') {
return 'scss';
}
if (extension === 'html' || extension === 'htm') {
return 'html';
}
if (extension === 'md' || extension === 'markdown') {
return 'markdown';
}
if (extension === 'java') {
return 'java';
}
if (extension === 'kt') {
return 'kotlin';
}
if (extension === 'py') {
return 'python';
}
if (extension === 'go') {
return 'go';
}
if (extension === 'rs') {
return 'rust';
}
if (extension === 'sql') {
return 'sql';
}
if (extension === 'sh' || extension === 'bash' || extension === 'zsh') {
return 'bash';
}
if (extension === 'yml' || extension === 'yaml') {
return 'yaml';
}
if (extension === 'xml') {
return 'xml';
}
if (extension === 'diff' || extension === 'patch') {
return 'diff';
}
if (/^(diff --git|@@\s|--- a\/|\+\+\+ b\/)/m.test(previewText)) {
return 'diff';
}
return inferCodeLanguage(extension || 'text');
}
function isAppRouteUrl(url: string) {
if (typeof window === 'undefined') {
return false;
}
try {
const parsed = new URL(url, window.location.href);
const pathname = parsed.pathname.toLowerCase();
const hasKnownFileExtension = /\.[a-z0-9]+$/i.test(pathname);
return parsed.origin === window.location.origin && !hasKnownFileExtension;
} catch {
return false;
}
}
function canRenderFramePreview(url: string) {
if (typeof window === 'undefined') {
return false;
}
try {
const parsed = new URL(url, window.location.href);
return parsed.origin === window.location.origin;
} catch {
return false;
}
}
type ChatPreviewBodyProps = {
target: ChatPreviewTarget | null;
previewText: string;
isPreviewLoading: boolean;
previewError: string;
previewContentType?: string;
maxMarkdownBlocks?: number;
};
function isHtmlFallbackPreview(target: ChatPreviewTarget, previewText: string, previewContentType: string | undefined) {
const extension = resolvePreviewExtension(target);
const normalizedContentType = previewContentType?.toLowerCase() ?? '';
const normalizedPreview = previewText.trimStart().toLowerCase();
if (extension === 'html' || extension === 'htm' || target.kind === 'markdown') {
return false;
}
const looksLikeHtml =
normalizedContentType.includes('text/html') ||
normalizedPreview.startsWith(';
}
if (isPreviewLoading) {
return (
preview를 불러오는 중입니다.
);
}
if (previewError) {
return (
);
}
if (isHtmlFallbackPreview(target, previewText, previewContentType)) {
return (
);
}
if (target.kind === 'image') {
return (
);
}
if (target.kind === 'video') {
return ;
}
if (target.kind === 'pdf') {
return ;
}
if (target.kind === 'markdown') {
return (
);
}
if (target.kind === 'code' || target.kind === 'diff' || target.kind === 'document') {
const resolvedLanguage = resolveCodeLanguage(target, previewText);
if (target.kind === 'diff' || resolvedLanguage === 'diff') {
return (
);
}
return (
{renderEditorBlock(previewText || '표시할 preview 본문이 없습니다.', resolvedLanguage, 'code')}
);
}
if (isAppRouteUrl(target.url)) {
return (
);
}
if (canRenderFramePreview(target.url)) {
return ;
}
return (
브라우저에서 직접 렌더링하지 않는 형식입니다. 아래 버튼으로 새 탭에서 열거나 바로 다운로드할 수 있습니다.
} />
}
onClick={() => {
const fileName = target.url.split('/').pop()?.trim() || target.label;
triggerResourceDownload(target.url, fileName);
}}
/>
);
}