Fix chat type persistence and board flow
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import { ChatPreviewBody, resolveChatPreviewGlyph, resolveChatPreviewKindLabel, type ChatPreviewKind } from './ChatPreviewBody';
|
||||
import { extractAutoDetectedPreviewUrls } from './inlinePreviewUrls';
|
||||
import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from './previewMarkers';
|
||||
import { normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { copyPreviewContent, copyText } from './chatUtils';
|
||||
@@ -83,7 +84,6 @@ type PreviewFetchError = Error & {
|
||||
status?: number;
|
||||
};
|
||||
|
||||
const INLINE_PREVIEW_URL_PATTERN = /(https?:\/\/[^\s)]+|\/[A-Za-z0-9._~:/?#[\]@!$&'()*+,;=%-]+)/g;
|
||||
const MARKDOWN_IMAGE_LINE_PATTERN = /^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/;
|
||||
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
@@ -92,6 +92,7 @@ const COLLAPSIBLE_MESSAGE_CHAR_COUNT = 280;
|
||||
const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g;
|
||||
|
||||
type MessageRenderPayload = {
|
||||
previewSourceText: string;
|
||||
visibleText: string;
|
||||
diffBlocks: string[];
|
||||
};
|
||||
@@ -169,6 +170,21 @@ function buildPreviewFileName(item: PreviewOption) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePreviewOptionKind(kind: string): ChatPreviewKind {
|
||||
switch (kind) {
|
||||
case 'image':
|
||||
case 'video':
|
||||
case 'markdown':
|
||||
case 'code':
|
||||
case 'diff':
|
||||
case 'document':
|
||||
case 'pdf':
|
||||
return kind;
|
||||
default:
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
|
||||
async function createPreviewFetchError(response: Response): Promise<PreviewFetchError> {
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
|
||||
let responseMessage = '';
|
||||
@@ -199,7 +215,7 @@ async function createPreviewFetchError(response: Response): Promise<PreviewFetch
|
||||
}
|
||||
|
||||
function extractInlinePreviewTargets(text: string): InlinePreviewTarget[] {
|
||||
const matches = [...(text.match(INLINE_PREVIEW_URL_PATTERN) ?? []), ...extractHiddenPreviewUrls(text)];
|
||||
const matches = [...extractAutoDetectedPreviewUrls(text), ...extractHiddenPreviewUrls(text)];
|
||||
const seen = new Set<string>();
|
||||
const targets: InlinePreviewTarget[] = [];
|
||||
|
||||
@@ -293,9 +309,11 @@ function extractMessageRenderPayload(text: string): MessageRenderPayload {
|
||||
.map((match) => match[1]?.trim())
|
||||
.filter((value): value is string => Boolean(value));
|
||||
|
||||
const visibleText = stripHiddenPreviewTags(text.replace(DIFF_CODE_BLOCK_PATTERN, ''));
|
||||
const previewSourceText = text.replace(DIFF_CODE_BLOCK_PATTERN, '');
|
||||
const visibleText = stripHiddenPreviewTags(previewSourceText);
|
||||
|
||||
return {
|
||||
previewSourceText,
|
||||
visibleText,
|
||||
diffBlocks,
|
||||
};
|
||||
@@ -688,6 +706,7 @@ type ChatConversationViewProps = {
|
||||
previewItems: PreviewOption[];
|
||||
isResourceStripOpen: boolean;
|
||||
isComposerDisabled: boolean;
|
||||
isChatTypeSelectionLocked: boolean;
|
||||
isComposerAttachmentUploading: boolean;
|
||||
onViewportScroll: () => void;
|
||||
onViewportTouchEnd: () => void;
|
||||
@@ -733,6 +752,7 @@ export function ChatConversationView({
|
||||
previewItems,
|
||||
isResourceStripOpen,
|
||||
isComposerDisabled,
|
||||
isChatTypeSelectionLocked,
|
||||
isComposerAttachmentUploading,
|
||||
onViewportScroll,
|
||||
onViewportTouchEnd,
|
||||
@@ -756,6 +776,7 @@ export function ChatConversationView({
|
||||
}: ChatConversationViewProps) {
|
||||
const [expandedMessageIds, setExpandedMessageIds] = useState<number[]>([]);
|
||||
const [expandedPreviewKey, setExpandedPreviewKey] = useState<string | null>(null);
|
||||
const [expandedResourcePreviewKey, setExpandedResourcePreviewKey] = useState<string | null>(null);
|
||||
const [fullscreenPreviewKey, setFullscreenPreviewKey] = useState<string | null>(null);
|
||||
const [collapsedActivityRequestIds, setCollapsedActivityRequestIds] = useState<string[]>([]);
|
||||
const [collapsibleMessageIds, setCollapsibleMessageIds] = useState<number[]>([]);
|
||||
@@ -1191,17 +1212,22 @@ export function ChatConversationView({
|
||||
</label>
|
||||
<div className="app-chat-panel__resource-strip-list">
|
||||
{visiblePreviewItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className="app-chat-panel__resource-chip"
|
||||
onClick={() => {
|
||||
onOpenPreview(item.id);
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<span>{item.kind}</span>
|
||||
</button>
|
||||
<InlineMessagePreview
|
||||
key={item.id}
|
||||
target={{
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
kind: normalizePreviewOptionKind(item.kind),
|
||||
}}
|
||||
isExpanded={expandedResourcePreviewKey === item.id}
|
||||
hasModalPreview
|
||||
onOpenModalPreview={() => {
|
||||
onOpenPreview(item.id, { fullscreen: true });
|
||||
}}
|
||||
onToggle={() => {
|
||||
setExpandedResourcePreviewKey((current) => (current === item.id ? null : item.id));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
@@ -1248,13 +1274,13 @@ export function ChatConversationView({
|
||||
const isExpandedMessage = expandedMessageIds.includes(message.id);
|
||||
const shouldTruncateMessage = canCollapseMessage && !isExpandedMessage;
|
||||
const messageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}`;
|
||||
const { visibleText, diffBlocks } = extractMessageRenderPayload(message.text);
|
||||
const { previewSourceText, visibleText, diffBlocks } = extractMessageRenderPayload(message.text);
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return renderActivityCard(message);
|
||||
}
|
||||
|
||||
const inlinePreviewTargets = extractInlinePreviewTargets(visibleText);
|
||||
const inlinePreviewTargets = extractInlinePreviewTargets(previewSourceText);
|
||||
const hasPreviewCards = diffBlocks.length > 0 || inlinePreviewTargets.length > 0;
|
||||
const shouldRenderStandalonePreview =
|
||||
hasPreviewCards && !visibleText && (message.author === 'codex' || message.author === 'system');
|
||||
@@ -1498,7 +1524,7 @@ export function ChatConversationView({
|
||||
),
|
||||
}))}
|
||||
getPopupContainer={(triggerNode) => triggerNode.closest('.app-chat-panel') ?? document.body}
|
||||
disabled={chatTypeOptions.length === 0}
|
||||
disabled={chatTypeOptions.length === 0 || isChatTypeSelectionLocked}
|
||||
onChange={onSelectChatType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -203,6 +203,33 @@ function canRenderFramePreview(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildHtmlFrameDocument(html: string, sourceUrl: string) {
|
||||
const trimmed = html.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return '<!doctype html><html><body></body></html>';
|
||||
}
|
||||
|
||||
const baseHref = (() => {
|
||||
try {
|
||||
return new URL('.', sourceUrl).toString();
|
||||
} catch {
|
||||
return sourceUrl;
|
||||
}
|
||||
})();
|
||||
const baseTag = `<base href="${baseHref}">`;
|
||||
|
||||
if (/<head(\s|>)/i.test(trimmed)) {
|
||||
return trimmed.replace(/<head(\s*[^>]*)>/i, (match) => `${match}${baseTag}`);
|
||||
}
|
||||
|
||||
if (/<html(\s|>)/i.test(trimmed)) {
|
||||
return trimmed.replace(/<html(\s*[^>]*)>/i, (match) => `${match}<head>${baseTag}</head>`);
|
||||
}
|
||||
|
||||
return `<!doctype html><html><head>${baseTag}</head><body>${trimmed}</body></html>`;
|
||||
}
|
||||
|
||||
type ChatPreviewBodyProps = {
|
||||
target: ChatPreviewTarget | null;
|
||||
previewText: string;
|
||||
@@ -210,6 +237,7 @@ type ChatPreviewBodyProps = {
|
||||
previewError: string;
|
||||
previewContentType?: string;
|
||||
maxMarkdownBlocks?: number;
|
||||
renderHtmlAsFrame?: boolean;
|
||||
};
|
||||
|
||||
function isHtmlFallbackPreview(target: ChatPreviewTarget, previewText: string, previewContentType: string | undefined) {
|
||||
@@ -238,6 +266,7 @@ export function ChatPreviewBody({
|
||||
previewError,
|
||||
previewContentType,
|
||||
maxMarkdownBlocks,
|
||||
renderHtmlAsFrame = false,
|
||||
}: ChatPreviewBodyProps) {
|
||||
if (!target) {
|
||||
return <Empty description="preview 가능한 링크가 아직 없습니다." />;
|
||||
@@ -307,6 +336,16 @@ export function ChatPreviewBody({
|
||||
if (target.kind === 'code' || target.kind === 'diff' || target.kind === 'document') {
|
||||
const resolvedLanguage = resolveCodeLanguage(target, previewText);
|
||||
|
||||
if (renderHtmlAsFrame && resolvedLanguage === 'html' && canRenderFramePreview(target.url)) {
|
||||
return (
|
||||
<iframe
|
||||
title={target.label}
|
||||
srcDoc={buildHtmlFrameDocument(previewText, target.url)}
|
||||
className="app-chat-panel__preview-frame"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'diff' || resolvedLanguage === 'diff') {
|
||||
return (
|
||||
<div className="app-chat-panel__preview-rich app-chat-panel__preview-rich--code">
|
||||
|
||||
26
src/app/main/mainChatPanel/inlinePreviewUrls.ts
Normal file
26
src/app/main/mainChatPanel/inlinePreviewUrls.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const AUTO_DETECTED_PREVIEW_URL_PATTERN = /(https?:\/\/[^\s<>)\]]+|\/[A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]+)/g;
|
||||
|
||||
export function extractAutoDetectedPreviewUrls(text: string) {
|
||||
const normalized = String(text ?? '');
|
||||
const urls: string[] = [];
|
||||
|
||||
for (const match of normalized.matchAll(AUTO_DETECTED_PREVIEW_URL_PATTERN)) {
|
||||
const value = match[0]?.trim();
|
||||
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startIndex = match.index ?? -1;
|
||||
const previousChar = startIndex > 0 ? normalized[startIndex - 1] : '';
|
||||
|
||||
// Ignore HTML closing tags like </div> that were being misread as same-origin routes.
|
||||
if (previousChar === '<') {
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.push(value);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
Reference in New Issue
Block a user