chore: exclude local resource artifacts from main sync
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
CodeOutlined,
|
||||
CheckCircleOutlined,
|
||||
DownOutlined,
|
||||
EyeOutlined,
|
||||
ExpandOutlined,
|
||||
LeftOutlined,
|
||||
LinkOutlined,
|
||||
@@ -9,12 +11,18 @@ import {
|
||||
RightOutlined,
|
||||
UpOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { App, Button, Input, Modal, Spin, Typography } from 'antd';
|
||||
import { App, Button, Input, Spin, Tabs, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
import { StepperUI } from '../../../components/stepper';
|
||||
import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { MarkdownPreviewContent } from '../../../components/markdownPreview/MarkdownPreviewContent';
|
||||
import { FullscreenPreviewModal, ZoomablePreviewSurface } from '../../../components/previewer';
|
||||
import { renderEditorBlock } from '../../../components/previewer/renderers';
|
||||
import { ChatPreviewBody } from './ChatPreviewBody';
|
||||
import { isMarkdownContentType, isMarkdownResourceUrl, normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { openChatExternalLink } from './linkNavigation';
|
||||
import { classifyPreviewKind } from './previewKind';
|
||||
import { resolvePromptPreviewOptionValue } from './promptPreviewState';
|
||||
import type { ChatMessagePart } from './types';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
@@ -104,6 +112,14 @@ function buildStepSelectionText(step: PromptStep, selectedValues: string[]) {
|
||||
return buildOptionSelectionText(step.options, selectedValues);
|
||||
}
|
||||
|
||||
function getPreviewablePromptOptions(step: PromptStep | null | undefined) {
|
||||
if (!step) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return step.options.filter((option) => option.preview);
|
||||
}
|
||||
|
||||
function replacePromptTemplate(
|
||||
template: string,
|
||||
replacements: Record<string, string>,
|
||||
@@ -114,6 +130,38 @@ function replacePromptTemplate(
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePromptTemplateKey(value: string) {
|
||||
const normalized = value.trim().replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
return normalized || 'step';
|
||||
}
|
||||
|
||||
function buildStepTemplateReplacements(steps: PromptStep[], stepSelections: PromptStepDraftSelection[]) {
|
||||
return stepSelections.reduce<Record<string, string>>((replacements, selection, index) => {
|
||||
const step = steps.find((candidate) => candidate.key === selection.stepKey) ?? null;
|
||||
const stepKey = normalizePromptTemplateKey(selection.stepKey || step?.key || `step_${index + 1}`);
|
||||
const stepTitle = selection.stepTitle || step?.title || `단계 ${index + 1}`;
|
||||
const selectedOptions = step?.options.filter((option) => selection.selectedValues.includes(option.value)) ?? [];
|
||||
const selectedValues = selection.selectedValues.join(', ');
|
||||
const selectedLabels = selectedOptions.map((option) => option.label).join(', ');
|
||||
const selectionText = step ? buildStepSelectionText(step, selection.selectedValues) : selectedValues;
|
||||
const trimmedFreeText = selection.freeText.trim();
|
||||
const summaryText = selection.skipped
|
||||
? `${stepTitle}: 건너뜀`
|
||||
: `${stepTitle}: ${selectionText || '선택 없음'}${trimmedFreeText ? ` / 추가 요청: ${trimmedFreeText.replace(/\n+/g, ' ')}` : ''}`;
|
||||
|
||||
replacements[`${stepKey}_title`] = stepTitle;
|
||||
replacements[`${stepKey}_value`] = selection.selectedValues[0] ?? '';
|
||||
replacements[`${stepKey}_values`] = selectedValues;
|
||||
replacements[`${stepKey}_label`] = selectedOptions[0]?.label ?? '';
|
||||
replacements[`${stepKey}_labels`] = selectedLabels;
|
||||
replacements[`${stepKey}_text`] = selectionText;
|
||||
replacements[`${stepKey}_summary`] = summaryText;
|
||||
replacements[`${stepKey}_free_text`] = trimmedFreeText;
|
||||
replacements[`${stepKey}_free_text_block`] = trimmedFreeText ? `추가 요청:\n${trimmedFreeText}` : '';
|
||||
return replacements;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizePromptDraftSelection(
|
||||
target: PromptTarget,
|
||||
selectionOrSelectedValues: PromptDraftSelection | string[],
|
||||
@@ -243,7 +291,14 @@ export function buildPromptResponseText(
|
||||
return `${index + 1}. ${stepTitle}: ${selectionText || '선택 없음'}${freeTextSummary}`;
|
||||
});
|
||||
const trimmedFreeText = draft.freeText.trim();
|
||||
const lastMeaningfulSelection = [...stepSelections].reverse().find(
|
||||
(selection) => selection.skipped || selection.selectedValues.length > 0 || selection.freeText.trim().length > 0,
|
||||
);
|
||||
const lastMeaningfulStep = lastMeaningfulSelection
|
||||
? steps.find((candidate) => candidate.key === lastMeaningfulSelection.stepKey) ?? null
|
||||
: null;
|
||||
const template =
|
||||
lastMeaningfulStep?.responseTemplate?.trim() ||
|
||||
target.responseTemplate?.trim() ||
|
||||
`프롬프트 "${target.title}" 단계 선택을 정리했습니다.\n{{step_summaries}}\n{{custom_text_block}}`;
|
||||
const resolvedText = replacePromptTemplate(template, {
|
||||
@@ -257,6 +312,7 @@ export function buildPromptResponseText(
|
||||
custom_text_block: trimmedFreeText ? `최종 요청:\n${trimmedFreeText}` : '',
|
||||
step_summaries: stepSummaryLines.join('\n'),
|
||||
step_count: String(stepSelections.length),
|
||||
...buildStepTemplateReplacements(steps, stepSelections),
|
||||
}).trim();
|
||||
|
||||
if (!trimmedFreeText || resolvedText.includes(trimmedFreeText)) {
|
||||
@@ -302,6 +358,26 @@ function isHtmlLikeUrl(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function canShowHtmlPreviewActions(preview: PromptPreview | null | undefined) {
|
||||
if (!preview) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preview.type === 'html') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preview.type !== 'resource') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preview.content?.trim()) {
|
||||
return /<(?:!doctype\s+html|html|head|body|main|section|article|div)\b/i.test(preview.content);
|
||||
}
|
||||
|
||||
return Boolean(preview.url && isHtmlLikeUrl(preview.url));
|
||||
}
|
||||
|
||||
function canRenderFramePreview(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
@@ -385,11 +461,22 @@ function isTextLikeContentType(contentType: string | null) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePromptPreviewUrl(url?: string | null) {
|
||||
const normalized = String(url ?? '').trim();
|
||||
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeChatResourceUrl(normalized);
|
||||
}
|
||||
|
||||
function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
|
||||
const [remoteContent, setRemoteContent] = useState<string | null>(preview?.content ?? null);
|
||||
const [remoteContentType, setRemoteContentType] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const normalizedPreviewUrl = resolvePromptPreviewUrl(preview?.url);
|
||||
|
||||
useEffect(() => {
|
||||
setRemoteContent(preview?.content ?? null);
|
||||
@@ -399,12 +486,12 @@ function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
|
||||
const shouldFetchTextPreview =
|
||||
preview?.type === 'markdown' || preview?.type === 'html';
|
||||
const shouldInspectResourcePreview =
|
||||
preview?.type === 'resource' && preview.url && canRenderFramePreview(preview.url);
|
||||
preview?.type === 'resource' && normalizedPreviewUrl && canRenderFramePreview(normalizedPreviewUrl);
|
||||
|
||||
if (
|
||||
!preview ||
|
||||
preview.content ||
|
||||
!preview.url ||
|
||||
!normalizedPreviewUrl ||
|
||||
(!shouldFetchTextPreview && !shouldInspectResourcePreview)
|
||||
) {
|
||||
setIsLoading(false);
|
||||
@@ -414,7 +501,7 @@ function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
|
||||
const controller = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
void fetch(preview.url, {
|
||||
void fetch(normalizedPreviewUrl, {
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
})
|
||||
@@ -454,7 +541,7 @@ function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [preview]);
|
||||
}, [normalizedPreviewUrl, preview]);
|
||||
|
||||
return { remoteContent, remoteContentType, isLoading, loadError };
|
||||
}
|
||||
@@ -462,23 +549,36 @@ function usePromptPreviewContent(preview: PromptPreview | null | undefined) {
|
||||
function PromptPreviewSurface({
|
||||
preview,
|
||||
compact = false,
|
||||
htmlMode = 'preview',
|
||||
}: {
|
||||
preview: PromptPreview;
|
||||
compact?: boolean;
|
||||
htmlMode?: 'preview' | 'source';
|
||||
}) {
|
||||
const { remoteContent, remoteContentType, isLoading, loadError } = usePromptPreviewContent(preview);
|
||||
const normalizedPreviewUrl = resolvePromptPreviewUrl(preview.url);
|
||||
const shouldRenderAsHtml = shouldRenderAsHtmlDocument(preview, remoteContentType, remoteContent);
|
||||
const htmlDocument = shouldRenderAsHtml ? buildHtmlFrameDocument(remoteContent || '', preview.url) : null;
|
||||
const htmlDocument = shouldRenderAsHtml ? buildHtmlFrameDocument(remoteContent || '', normalizedPreviewUrl || preview.url) : null;
|
||||
|
||||
if (preview.type === 'image' && preview.url) {
|
||||
return (
|
||||
if (preview.type === 'image' && normalizedPreviewUrl) {
|
||||
const imageNode = (
|
||||
<InlineImage
|
||||
src={preview.url}
|
||||
src={normalizedPreviewUrl}
|
||||
alt={preview.alt?.trim() || preview.title?.trim() || 'prompt preview image'}
|
||||
className={`app-chat-prompt-card__preview-image${compact ? ' app-chat-prompt-card__preview-image--compact' : ''}`}
|
||||
fallbackText="이미지 preview를 불러오지 못했습니다."
|
||||
/>
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return imageNode;
|
||||
}
|
||||
|
||||
return (
|
||||
<ZoomablePreviewSurface stageClassName="app-chat-prompt-card__preview-zoom-stage" contentClassName="app-chat-prompt-card__preview-zoom-content">
|
||||
{imageNode}
|
||||
</ZoomablePreviewSurface>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -504,10 +604,10 @@ function PromptPreviewSurface({
|
||||
if (preview.type === 'html') {
|
||||
const htmlContent = remoteContent || '';
|
||||
|
||||
if (preview.url && isAppRouteUrl(preview.url)) {
|
||||
if (normalizedPreviewUrl && isAppRouteUrl(normalizedPreviewUrl)) {
|
||||
return (
|
||||
<div className="app-chat-prompt-card__preview-placeholder">
|
||||
앱 화면 경로는 prompt iframe으로 열지 않습니다. `/api/chat/resources/...html` 같은 실제 HTML 리소스 URL을 사용해 주세요.
|
||||
앱 화면 경로는 prompt iframe으로 열지 않습니다. `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 실제 HTML 리소스 URL을 사용해 주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -515,26 +615,49 @@ function PromptPreviewSurface({
|
||||
if (isHtmlFallbackPreview(preview, htmlContent)) {
|
||||
return (
|
||||
<div className="app-chat-prompt-card__preview-placeholder">
|
||||
현재 앱 fallback HTML이 반환되었습니다. `type:"html"`에는 실제 HTML 본문을 넣거나, 세션 HTML 문서는 `type:"resource"`와 `/api/chat/resources/...html` URL로 전달해 주세요.
|
||||
현재 앱 fallback HTML이 반환되었습니다. `type:"html"`에는 실제 HTML 본문을 넣거나, `type:"resource"`에 `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 정적 HTML 리소스를 연결해 주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
if (htmlMode === 'source') {
|
||||
return (
|
||||
<div className="app-chat-prompt-card__preview-code">
|
||||
{renderEditorBlock(htmlContent || '표시할 HTML 코드가 없습니다.', 'html', 'code')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const frameNode = (
|
||||
<iframe
|
||||
title={preview.title?.trim() || 'prompt html preview'}
|
||||
srcDoc={htmlDocument ?? buildHtmlFrameDocument(htmlContent, preview.url)}
|
||||
srcDoc={htmlDocument ?? buildHtmlFrameDocument(htmlContent, normalizedPreviewUrl || preview.url)}
|
||||
className="app-chat-prompt-card__preview-frame"
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return frameNode;
|
||||
}
|
||||
|
||||
return (
|
||||
<ZoomablePreviewSurface stageClassName="app-chat-prompt-card__preview-zoom-stage" contentClassName="app-chat-prompt-card__preview-zoom-content">
|
||||
{frameNode}
|
||||
</ZoomablePreviewSurface>
|
||||
);
|
||||
}
|
||||
|
||||
if (preview.type === 'resource' && preview.url) {
|
||||
if (isAppRouteUrl(preview.url)) {
|
||||
if (preview.type === 'resource' && normalizedPreviewUrl) {
|
||||
const resourceKind =
|
||||
isMarkdownResourceUrl(normalizedPreviewUrl || preview.url) || isMarkdownContentType(remoteContentType)
|
||||
? 'markdown'
|
||||
: classifyPreviewKind(normalizedPreviewUrl);
|
||||
|
||||
if (isAppRouteUrl(normalizedPreviewUrl)) {
|
||||
return (
|
||||
<div className="app-chat-prompt-card__preview-placeholder">
|
||||
앱 화면 URL은 resource preview로 열지 않습니다. `/api/chat/resources/...html` 같은 정적 리소스 경로만 사용해 주세요.
|
||||
앱 화면 URL은 resource preview로 열지 않습니다. `/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html` 같은 정적 리소스 경로만 사용해 주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -542,33 +665,26 @@ function PromptPreviewSurface({
|
||||
if (remoteContentType?.toLowerCase().includes('text/html') && isHtmlFallbackPreview(preview, remoteContent || '')) {
|
||||
return (
|
||||
<div className="app-chat-prompt-card__preview-placeholder">
|
||||
현재 앱 fallback HTML이 반환되었습니다. `type:"resource"`에는 실제 세션 리소스(`/api/chat/resources/...html`)만 연결해 주세요.
|
||||
현재 앱 fallback HTML이 반환되었습니다. `type:"resource"`에는 실제 리소스(`/api/chat/resources/...html`, `/api/resource-manager/preview/...html`, `resource/...html`)만 연결해 주세요.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldRenderAsHtml && htmlDocument) {
|
||||
return (
|
||||
<iframe
|
||||
title={preview.title?.trim() || 'prompt resource preview'}
|
||||
srcDoc={htmlDocument}
|
||||
className="app-chat-prompt-card__preview-frame"
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (canRenderFramePreview(preview.url)) {
|
||||
return (
|
||||
<iframe
|
||||
title={preview.title?.trim() || 'prompt resource preview'}
|
||||
src={preview.url}
|
||||
className="app-chat-prompt-card__preview-frame"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="app-chat-prompt-card__preview-placeholder">같은 출처 리소스만 inline preview 합니다.</div>;
|
||||
return (
|
||||
<ChatPreviewBody
|
||||
target={{
|
||||
label: preview.title?.trim() || preview.alt?.trim() || 'prompt resource preview',
|
||||
url: normalizedPreviewUrl,
|
||||
kind: resourceKind,
|
||||
}}
|
||||
previewText={remoteContent || ''}
|
||||
isPreviewLoading={isLoading}
|
||||
previewError={loadError}
|
||||
previewContentType={remoteContentType ?? undefined}
|
||||
maxMarkdownBlocks={compact ? 5 : undefined}
|
||||
renderHtmlAsFrame={htmlMode !== 'source'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="app-chat-prompt-card__preview-placeholder">표시할 preview가 없습니다.</div>;
|
||||
@@ -583,6 +699,7 @@ function PromptPreviewCard({
|
||||
}) {
|
||||
const preview = option.preview;
|
||||
const { message } = App.useApp();
|
||||
const normalizedPreviewUrl = resolvePromptPreviewUrl(preview?.url);
|
||||
|
||||
if (!preview) {
|
||||
return null;
|
||||
@@ -614,7 +731,7 @@ function PromptPreviewCard({
|
||||
aria-label={`${option.label} preview 열기`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openChatExternalLink(preview.url ?? '', event);
|
||||
openChatExternalLink(normalizedPreviewUrl || preview.url || '', event);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -636,6 +753,15 @@ function PromptPreviewCard({
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePromptPreviewOption(
|
||||
options: PromptOption[],
|
||||
activePreviewOptionValue: string | null,
|
||||
selectedValues: string[],
|
||||
) {
|
||||
const resolvedValue = resolvePromptPreviewOptionValue(options, activePreviewOptionValue, selectedValues);
|
||||
return options.find((option) => option.value === resolvedValue) ?? null;
|
||||
}
|
||||
|
||||
function buildInitialStepSelectionMap(target: PromptTarget, steps: PromptStep[]) {
|
||||
return Object.fromEntries(
|
||||
steps.map((step) => {
|
||||
@@ -705,14 +831,18 @@ function buildPromptSelectionPayload(target: PromptTarget, stepSelections: Recor
|
||||
}
|
||||
|
||||
const selectedValues = meaningfulSelections.flatMap((selection) => selection.selectedValues);
|
||||
const aggregatedFreeText = meaningfulSelections
|
||||
.map((selection) => selection.freeText.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
return {
|
||||
selectedValues,
|
||||
freeText: '',
|
||||
freeText: aggregatedFreeText,
|
||||
stepSelections: meaningfulSelections,
|
||||
summaryText: buildPromptDraftSummaryText(target, {
|
||||
selectedValues,
|
||||
freeText: '',
|
||||
freeText: aggregatedFreeText,
|
||||
stepSelections: meaningfulSelections,
|
||||
}),
|
||||
} satisfies PromptDraftSelection;
|
||||
@@ -723,12 +853,14 @@ export function ChatPromptCard({
|
||||
onSubmit,
|
||||
readOnly = false,
|
||||
onSelectionChange,
|
||||
onSubmitted,
|
||||
submittedSelection,
|
||||
}: {
|
||||
target: PromptTarget;
|
||||
onSubmit: (payload: { text: string; mode: 'queue' | 'direct' }) => Promise<boolean>;
|
||||
readOnly?: boolean;
|
||||
onSelectionChange?: (selection: PromptDraftSelection | null) => void;
|
||||
onSubmitted?: (selection: PromptDraftSelection) => void;
|
||||
submittedSelection?: PromptDraftSelection | null;
|
||||
}) {
|
||||
const steps = useMemo(() => normalizePromptSteps(target), [target]);
|
||||
@@ -740,7 +872,9 @@ export function ChatPromptCard({
|
||||
const [submittedSummary, setSubmittedSummary] = useState('');
|
||||
const [submittedFreeText, setSubmittedFreeText] = useState('');
|
||||
const [expandedOptionValue, setExpandedOptionValue] = useState<string | null>(null);
|
||||
const [expandedHtmlMode, setExpandedHtmlMode] = useState<'preview' | 'source'>('preview');
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [activePreviewOptionValue, setActivePreviewOptionValue] = useState<string | null>(null);
|
||||
const resolvedSelectedValues = target.selectedValues ?? [];
|
||||
const resolvedSelectionSummary = buildSelectionText(target, resolvedSelectedValues);
|
||||
const isResolved = resolvedSelectedValues.length > 0;
|
||||
@@ -762,8 +896,17 @@ export function ChatPromptCard({
|
||||
const expandedOption = steps
|
||||
.flatMap((step) => step.options)
|
||||
.find((option) => option.value === expandedOptionValue) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedHtmlMode('preview');
|
||||
}, [expandedOptionValue]);
|
||||
const activeStep = steps[Math.min(activeStepIndex, Math.max(steps.length - 1, 0))] ?? steps[0];
|
||||
const activeSelection = activeStep ? stepSelections[activeStep.key] : undefined;
|
||||
const previewableOptions = useMemo(() => getPreviewablePromptOptions(activeStep), [activeStep]);
|
||||
const activePreviewOption = useMemo(
|
||||
() => resolvePromptPreviewOption(previewableOptions, activePreviewOptionValue, activeSelection?.selectedValues ?? []),
|
||||
[activePreviewOptionValue, activeSelection?.selectedValues, previewableOptions],
|
||||
);
|
||||
const selectionSummary = activeStep && activeSelection ? buildStepSelectionText(activeStep, activeSelection.selectedValues) : '';
|
||||
const displayedSelectionSummary = displayedSubmittedSummary || (hasStepper ? buildPromptDraftSummaryText(target, buildPromptSelectionPayload(target, stepSelections) ?? {
|
||||
selectedValues: [],
|
||||
@@ -794,6 +937,21 @@ export function ChatPromptCard({
|
||||
setActiveStepIndex(resolveCurrentStepIndex(steps, initialStepSelections, target.currentStepKey));
|
||||
}, [initialStepSelections, steps, target.currentStepKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewableOptions.length === 0) {
|
||||
setActivePreviewOptionValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setActivePreviewOptionValue((current) => {
|
||||
if (current && previewableOptions.some((option) => option.value === current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, [previewableOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLocked) {
|
||||
emitSelectionChange({});
|
||||
@@ -841,7 +999,6 @@ export function ChatPromptCard({
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedFreeText = submittedSelection?.freeText ?? '';
|
||||
const payload = buildPromptSelectionPayload(target, stepSelections);
|
||||
|
||||
if (!payload) {
|
||||
@@ -850,7 +1007,7 @@ export function ChatPromptCard({
|
||||
|
||||
setIsSubmitting(true);
|
||||
const isSent = await onSubmit({
|
||||
text: buildPromptResponseText(target, payload, trimmedFreeText),
|
||||
text: buildPromptResponseText(target, payload),
|
||||
mode: activeStep.mode === 'direct' ? 'direct' : target.mode === 'direct' ? 'direct' : 'queue',
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
@@ -860,7 +1017,8 @@ export function ChatPromptCard({
|
||||
}
|
||||
|
||||
setSubmittedSummary(payload.summaryText || buildPromptDraftSummaryText(target, payload));
|
||||
setSubmittedFreeText(trimmedFreeText);
|
||||
setSubmittedFreeText(payload.freeText);
|
||||
onSubmitted?.(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -970,6 +1128,26 @@ export function ChatPromptCard({
|
||||
{hasStepper && activeStep.description ? (
|
||||
<Paragraph className="app-chat-prompt-card__description">{activeStep.description}</Paragraph>
|
||||
) : null}
|
||||
{previewableOptions.length > 0 ? (
|
||||
<div className="app-chat-prompt-card__preview-tabs-shell">
|
||||
<Tabs
|
||||
size="small"
|
||||
className="app-chat-prompt-card__preview-tabs"
|
||||
activeKey={activePreviewOption?.value}
|
||||
onChange={(nextValue) => setActivePreviewOptionValue(nextValue)}
|
||||
items={previewableOptions.map((option) => ({
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
}))}
|
||||
/>
|
||||
{activePreviewOption?.preview ? (
|
||||
<PromptPreviewCard
|
||||
option={activePreviewOption}
|
||||
onOpenPreview={(nextOption) => setExpandedOptionValue(nextOption.value)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="app-chat-prompt-card__options" role={activeStep.multiple ? 'group' : 'radiogroup'} aria-label={activeStep.title}>
|
||||
{activeStep.options.map((option) => {
|
||||
const isSelected = activeSelection?.selectedValues.includes(option.value) ?? false;
|
||||
@@ -1041,12 +1219,7 @@ export function ChatPromptCard({
|
||||
{option.description ? (
|
||||
<span className="app-chat-prompt-card__option-description">{option.description}</span>
|
||||
) : null}
|
||||
{option.preview ? (
|
||||
<PromptPreviewCard
|
||||
option={option}
|
||||
onOpenPreview={(nextOption) => setExpandedOptionValue(nextOption.value)}
|
||||
/>
|
||||
) : null}
|
||||
{option.preview ? <span className="app-chat-prompt-card__option-preview-hint">상단 미리보기 탭에서 확인</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1143,18 +1316,36 @@ export function ChatPromptCard({
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
<Modal
|
||||
<FullscreenPreviewModal
|
||||
open={Boolean(expandedOption?.preview)}
|
||||
onCancel={() => setExpandedOptionValue(null)}
|
||||
footer={null}
|
||||
width="100vw"
|
||||
onClose={() => setExpandedOptionValue(null)}
|
||||
title={expandedOption?.preview?.title?.trim() || expandedOption?.label || 'preview'}
|
||||
actions={
|
||||
canShowHtmlPreviewActions(expandedOption?.preview) ? (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="fullscreen-preview-modal__icon-button"
|
||||
aria-label="HTML 실행 미리보기"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => setExpandedHtmlMode('preview')}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
className="fullscreen-preview-modal__icon-button"
|
||||
aria-label="HTML 코드 보기"
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => setExpandedHtmlMode('source')}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
className="app-chat-prompt-card__preview-modal"
|
||||
title={null}
|
||||
>
|
||||
<div className="app-chat-prompt-card__preview-modal-surface">
|
||||
{expandedOption?.preview ? <PromptPreviewSurface preview={expandedOption.preview} /> : null}
|
||||
{expandedOption?.preview ? <PromptPreviewSurface preview={expandedOption.preview} htmlMode={expandedHtmlMode} /> : null}
|
||||
</div>
|
||||
</Modal>
|
||||
</FullscreenPreviewModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user