Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
import { DownloadOutlined, EyeOutlined } 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 '../../../components/previewer/PreviewerUI.css';
const { Paragraph, Text } = Typography;
export type ChatPreviewKind = 'image' | 'video' | 'markdown' | 'code' | 'document' | 'pdf' | 'file';
export type ChatPreviewTarget = {
label: string;
url: string;
kind: ChatPreviewKind;
};
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;
};
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('<!doctype html') ||
normalizedPreview.startsWith('<html') ||
normalizedPreview.includes('<head') ||
normalizedPreview.includes('<body');
return looksLikeHtml;
}
export function ChatPreviewBody({
target,
previewText,
isPreviewLoading,
previewError,
previewContentType,
}: ChatPreviewBodyProps) {
if (!target) {
return <Empty description="preview 가능한 링크가 아직 없습니다." />;
}
if (isPreviewLoading) {
return (
<div className="app-chat-panel__preview-loading">
<Spin size="small" />
<Text type="secondary">preview를 .</Text>
</div>
);
}
if (previewError) {
return (
<Alert
showIcon
type="warning"
message="preview를 불러오지 못했습니다."
description={resolvePreviewErrorMessage(previewError)}
/>
);
}
if (isHtmlFallbackPreview(target, previewText, previewContentType)) {
return (
<Alert
showIcon
type="info"
message="실제 소스 파일 대신 앱 HTML 화면이 반환되었습니다."
description={`${target.label} 경로가 raw 파일이 아니라 현재 앱의 fallback HTML을 돌려주고 있습니다. 정적 파일 경로 또는 실제 다운로드 경로를 사용해야 코드 preview가 정확하게 표시됩니다.`}
/>
);
}
if (target.kind === 'image') {
return (
<InlineImage
src={target.url}
alt={target.label}
className="app-chat-panel__preview-image"
fallbackText="이미지 preview를 불러오지 못했습니다."
/>
);
}
if (target.kind === 'video') {
return <video src={target.url} className="app-chat-panel__preview-video" controls playsInline />;
}
if (target.kind === 'pdf') {
return <iframe title={target.label} src={target.url} className="app-chat-panel__preview-frame" />;
}
if (target.kind === 'markdown') {
return (
<div className="app-chat-panel__preview-rich app-chat-panel__preview-rich--markdown">
<MarkdownPreviewContent content={previewText || '# Preview\n\n표시할 preview 본문이 없습니다.'} maxBlocks={12} />
</div>
);
}
if (target.kind === 'code' || target.kind === 'document') {
const resolvedLanguage = resolveCodeLanguage(target, previewText);
if (resolvedLanguage === 'diff') {
return (
<div className="app-chat-panel__preview-rich app-chat-panel__preview-rich--code">
<CodexDiffBlock
diffText={previewText || ''}
summary={`${target.label} 기준 raw diff preview입니다.`}
showToolbar={false}
className="app-chat-panel__preview-diff"
/>
</div>
);
}
return (
<div className="app-chat-panel__preview-rich app-chat-panel__preview-rich--code">
{renderEditorBlock(previewText || '표시할 preview 본문이 없습니다.', resolvedLanguage, 'code')}
</div>
);
}
if (isAppRouteUrl(target.url)) {
return (
<Alert
showIcon
type="info"
message="앱 화면 경로는 preview iframe으로 열지 않습니다."
description="현재 화면 문맥은 이미 WebSocket으로 서버에 전달됩니다. 이 경로를 다시 열면 앱만 새로 렌더링되어 preview처럼 보이지 않을 수 있습니다."
/>
);
}
if (canRenderFramePreview(target.url)) {
return <iframe title={target.label} src={target.url} className="app-chat-panel__preview-frame" />;
}
return (
<div className="app-chat-panel__preview-file">
<Paragraph>
. .
</Paragraph>
<Space wrap>
<Button href={target.url} target="_blank" rel="noreferrer" icon={<EyeOutlined />}>
</Button>
<Button href={target.url} download icon={<DownloadOutlined />}>
</Button>
</Space>
</div>
);
}