import { CodeOutlined, CopyOutlined, DownloadOutlined, DownOutlined, FullscreenExitOutlined, FullscreenOutlined, UpOutlined, } from '@ant-design/icons'; import { Button, Empty, Spin, Typography, message as antdMessage } from 'antd'; import { useEffect, useState, type ReactNode } from 'react'; import { InlineImage } from '../../../../components/common/InlineImage'; import { CodexDiffBlock } from '../../../../components/previewer'; import { ChatPreviewBody, resolveChatPreviewGlyph, resolveChatPreviewKindLabel, type ChatPreviewKind, type ChatPreviewTarget, } from '../../mainChatPanel/ChatPreviewBody'; import { extractAutoDetectedPreviewUrls } from '../../mainChatPanel/inlinePreviewUrls'; import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from '../../mainChatPanel/previewMarkers'; import { normalizeChatResourceUrl } from '../../mainChatPanel/chatResourceUrl'; import { copyPreviewContent, copyText } from '../../mainChatPanel/chatUtils'; import type { ChatConversationRequest, ChatMessage } from '../../mainChatPanel/types'; const { Text } = Typography; type ConversationRoomPaneProps = { sessionId: string; messages: ChatMessage[]; requests: ChatConversationRequest[]; isLoading: boolean; loadingLabel: string; errorMessage: string; }; const MARKDOWN_IMAGE_LINE_PATTERN = /^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/; const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g; const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g; const COLLAPSIBLE_MESSAGE_LINE_COUNT = 6; const COLLAPSIBLE_MESSAGE_CHAR_COUNT = 280; type MessageRenderPayload = { previewSourceText: string; visibleText: string; diffBlocks: string[]; }; function formatChatTimestamp(timestamp: string) { const normalized = String(timestamp ?? '').trim(); if (!normalized) { return ''; } const parsed = new Date(normalized); if (Number.isNaN(parsed.getTime())) { return normalized; } return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }) .format(parsed) .replace(',', ''); } function classifyInlinePreviewKind(url: string): ChatPreviewKind | 'file' { const pathname = url.toLowerCase().split('?')[0] ?? ''; if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(pathname)) { return 'image'; } if (/\.(mp4|webm|mov|m4v|ogg)$/i.test(pathname)) { return 'video'; } if (/\.(md|markdown)$/i.test(pathname)) { return 'markdown'; } if (/\.(diff|patch)$/i.test(pathname)) { return 'diff'; } if (/\.(ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml)$/i.test(pathname)) { return 'code'; } if (/\.(txt|log|csv)$/i.test(pathname)) { return 'document'; } if (/\.pdf$/i.test(pathname)) { return 'pdf'; } return 'file'; } function buildInlinePreviewLabel(url: string) { try { const parsed = new URL(url); return parsed.pathname.split('/').filter(Boolean).at(-1) || parsed.hostname; } catch { return url; } } function downloadTextFile(content: string, fileName: string, mimeType = 'text/plain;charset=utf-8') { if (typeof document === 'undefined') { return; } const blob = new Blob([content], { type: mimeType }); const objectUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(objectUrl); } function extractInlinePreviewTargets(text: string): ChatPreviewTarget[] { const matches = [...extractAutoDetectedPreviewUrls(text), ...extractHiddenPreviewUrls(text)]; const seen = new Set(); const targets: ChatPreviewTarget[] = []; for (const matchedUrl of matches) { const normalizedUrl = normalizeChatResourceUrl(matchedUrl); const kind = classifyInlinePreviewKind(normalizedUrl); if (kind === 'file' || seen.has(normalizedUrl)) { continue; } seen.add(normalizedUrl); targets.push({ url: normalizedUrl, label: buildInlinePreviewLabel(normalizedUrl), kind, }); } return targets; } function renderMessageInlineParts(line: string): ReactNode[] { const renderedParts: ReactNode[] = []; let cursor = 0; for (const match of line.matchAll(MARKDOWN_LINK_PATTERN)) { const [fullMatch, label, rawHref] = match; const start = match.index ?? 0; if (start > cursor) { renderedParts.push(line.slice(cursor, start)); } const href = normalizeChatResourceUrl(rawHref.trim()); renderedParts.push( {label.trim() || href} , ); cursor = start + fullMatch.length; } if (cursor < line.length) { renderedParts.push(line.slice(cursor)); } return renderedParts.length > 0 ? renderedParts : [line]; } function renderMessageBody(text: string) { return text.split('\n').map((line, index) => { const imageMatch = line.match(MARKDOWN_IMAGE_LINE_PATTERN); if (imageMatch) { const [, alt, rawSrc] = imageMatch; const src = normalizeChatResourceUrl(rawSrc.trim()); return (
); } if (!line.length) { return