feat: update codex live chat workflow

This commit is contained in:
2026-04-22 20:00:38 +09:00
parent 9e4b70f1f1
commit b0b9980a6c
70 changed files with 5178 additions and 2401 deletions

View File

@@ -1,4 +1,4 @@
import { CopyOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { CopyOutlined, DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { Button, Empty, Select, message } from 'antd';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
@@ -136,6 +136,22 @@ async function copyText(text: string) {
document.body.removeChild(textarea);
}
function downloadBlob(content: BlobPart, fileName: string, mimeType = 'text/plain;charset=utf-8') {
if (typeof document === 'undefined') {
throw new Error('다운로드를 사용할 수 없습니다.');
}
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 resolveCopyValue({ type, value }: Pick<PreviewerUIProps, 'type' | 'value'>) {
switch (type) {
case 'json':
@@ -147,6 +163,45 @@ function resolveCopyValue({ type, value }: Pick<PreviewerUIProps, 'type' | 'valu
}
}
function resolveDownloadValue({
type,
value,
downloadValue,
}: Pick<PreviewerUIProps, 'type' | 'value' | 'downloadValue'>) {
if (typeof downloadValue === 'string') {
return downloadValue;
}
if (type === 'image') {
return String(value ?? '');
}
return resolveCopyValue({ type, value });
}
function resolveDownloadFileName({
type,
language,
downloadFileName,
}: Pick<PreviewerUIProps, 'type' | 'language' | 'downloadFileName'>) {
if (downloadFileName?.trim()) {
return downloadFileName.trim();
}
switch (type) {
case 'json':
return 'preview.json';
case 'markdown':
return 'preview.md';
case 'code':
return `preview.${language || 'txt'}`;
case 'image':
return 'preview';
default:
return 'preview.txt';
}
}
function renderContent({
type,
value,
@@ -212,6 +267,10 @@ export function PreviewerUI({
value,
copyValue,
copyable = true,
downloadable = true,
downloadValue,
downloadUrl,
downloadFileName,
maximizable = true,
language = 'text',
format = 'auto',
@@ -226,8 +285,11 @@ export function PreviewerUI({
const [isExpanded, setIsExpanded] = useState(false);
const hasLanguageSelector = type === 'code' && languageOptions && languageOptions.length > 0;
const resolvedCopyValue = copyValue ?? resolveCopyValue({ type, value });
const resolvedDownloadValue = resolveDownloadValue({ type, value, downloadValue });
const resolvedDownloadFileName = resolveDownloadFileName({ type, language, downloadFileName });
const canCopy = copyable && resolvedCopyValue.trim().length > 0;
const shouldShowActions = hasLanguageSelector || canCopy || maximizable || Boolean(toolbar);
const canDownload = downloadable && (Boolean(downloadUrl) || resolvedDownloadValue.trim().length > 0);
const shouldShowActions = hasLanguageSelector || canCopy || canDownload || maximizable || Boolean(toolbar);
useEffect(() => {
if (!isExpanded || typeof document === 'undefined') {
@@ -271,6 +333,37 @@ export function PreviewerUI({
setIsExpanded((previous) => !previous);
}
function handleDownload() {
if (!canDownload) {
return;
}
try {
if (downloadUrl) {
if (typeof document === 'undefined') {
throw new Error('다운로드를 사용할 수 없습니다.');
}
const link = document.createElement('a');
link.href = downloadUrl;
link.download = resolvedDownloadFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
const mimeType =
type === 'json'
? 'application/json;charset=utf-8'
: type === 'markdown'
? 'text/markdown;charset=utf-8'
: 'text/plain;charset=utf-8';
downloadBlob(resolvedDownloadValue, resolvedDownloadFileName, mimeType);
}
} catch {
messageApi.error('다운로드에 실패했습니다.');
}
}
const actionContent = (
<>
{hasLanguageSelector ? (
@@ -286,15 +379,27 @@ export function PreviewerUI({
<Button
size="small"
type="text"
className="previewer-ui__action-button"
aria-label="복사"
icon={<CopyOutlined />}
onClick={() => void handleCopy()}
/>
) : null}
{canDownload ? (
<Button
size="small"
type="text"
className="previewer-ui__action-button"
aria-label="다운로드"
icon={<DownloadOutlined />}
onClick={handleDownload}
/>
) : null}
{maximizable ? (
<Button
size="small"
type="text"
className="previewer-ui__action-button"
aria-label={isExpanded ? '최대화 해제' : '최대화'}
icon={isExpanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={() => void toggleFullscreen()}