feat: update main chat and system chat UI

This commit is contained in:
2026-05-25 17:26:37 +09:00
parent fb5ec649cd
commit f59522ffc4
120 changed files with 43262 additions and 3325 deletions

View File

@@ -2,10 +2,15 @@ import type { ChatMessagePart } from './types';
const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\s*$/i;
const PROMPT_LINE_PATTERN = /^\s*\[\[prompt:(.+?)\]\]\s*$/i;
const STANDALONE_MARKDOWN_LINK_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?\[([^\]]+)\]\((.+)\)\s*$/;
const STANDALONE_URL_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?((?:https?:\/\/|\/)[^\s<>)\]]+)\s*$/i;
const PROMPT_BLOCK_START_PATTERN = /^\s*\[\[prompt:\s*$/i;
const PROMPT_BLOCK_END_PATTERN = /^\s*\]\]\s*$/;
const PROMPT_CODE_BLOCK_START_PATTERN = /^\s*```(?:json|prompt)(?:\s+prompt)?\s*$/i;
const CODE_BLOCK_END_PATTERN = /^\s*```\s*$/;
const CODE_FENCE_TOGGLE_PATTERN = /^\s*```/;
const ATTACHMENT_SECTION_TITLE_PATTERN = /^\s*첨부\s*파일\s*:?\s*$/i;
const ATTACHMENT_ENTRY_PATTERN = /^\s*-\s+(.+?)\s*:\s*(.+?)\s*$/;
const RESOURCE_PATH_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
const CHAT_DOT_CODEX_MARKER = '/.codex_chat/';
@@ -21,6 +26,17 @@ function normalizeText(value: unknown) {
return String(value ?? '').trim();
}
function unwrapMarkdownLinkTarget(value: string) {
const normalized = normalizeText(value);
if (!normalized) {
return '';
}
const matched = normalized.match(/^<([\s\S]+)>$/);
return matched?.[1]?.trim() ?? normalized;
}
function normalizeResourceManagerPathSegment(segment: string) {
const normalized = normalizeText(segment);
@@ -81,7 +97,7 @@ function extractKnownPreviewPath(value: string) {
}
function normalizeUrl(value: string) {
const normalized = normalizeText(value);
const normalized = unwrapMarkdownLinkTarget(value);
if (!normalized) {
return '';
@@ -132,6 +148,55 @@ function normalizeUrl(value: string) {
return '';
}
function hasKnownFileExtension(url: string) {
const pathname = url.split('?')[0] ?? '';
return /\.[a-z0-9]{1,8}$/i.test(pathname);
}
function isStructuredLinkCardCandidate(url: string) {
const normalized = normalizeUrl(url);
if (!normalized) {
return false;
}
if (isInternalResourceUrl(normalized)) {
return false;
}
return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
}
function buildFallbackLinkTitle(url: string) {
try {
const parsed = new URL(url, 'https://local.invalid');
const lastSegment = parsed.pathname.split('/').filter(Boolean).at(-1)?.trim();
return lastSegment || parsed.hostname || normalizeText(url);
} catch {
return normalizeText(url);
}
}
function normalizeStandaloneTitle(value: string) {
return value
.replace(/^\s*(?:[-*+]\s+|\d+\.\s+)?/, '')
.replace(/[`'"]+/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function resolveStandaloneLinkTitle(keptLines: string[], url: string) {
for (let index = keptLines.length - 1; index >= 0; index -= 1) {
const candidate = normalizeStandaloneTitle(keptLines[index] ?? '');
if (candidate) {
return candidate;
}
}
return buildFallbackLinkTitle(url);
}
function normalizePromptPreview(value: unknown): PromptPreview | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
@@ -328,6 +393,7 @@ function buildPromptPart(rawBody: string): ChatMessagePart | null {
const selectedValues = [
...normalizePromptSelectedValues(record.selectedValues),
...(record.selectedValue != null ? [record.selectedValue] : []),
...steps.flatMap((step) => step.selectedValues ?? []),
]
.map((item) => normalizeText(item))
.filter(Boolean)
@@ -349,7 +415,7 @@ function buildPromptPart(rawBody: string): ChatMessagePart | null {
freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
currentStepKey: normalizeText(record.currentStepKey) || null,
steps: steps.length > 0 ? steps : undefined,
readOnly: record.readOnly === true || selectedValues.length > 0,
readOnly: record.readOnly === true || resolvedBy != null,
selectedValues,
resolvedBy,
resolvedAt: normalizeText(record.resolvedAt) || null,
@@ -369,11 +435,82 @@ function buildPromptPartFromBlock(rawBody: string) {
return buildPromptPart(promptWrapperMatched?.[1] ?? trimmed);
}
export function extractChatMessageParts(text: string) {
function extractAttachmentEntryUrl(rawLine: string) {
const matched = rawLine.match(ATTACHMENT_ENTRY_PATTERN);
if (!matched) {
return '';
}
const resolvedUrl = normalizeUrl(matched[2] ?? '');
return resolvedUrl || '';
}
export function extractAttachmentPreviewUrls(text: string) {
const lines = String(text ?? '').split('\n');
const keptLines: string[] = [];
const urls: string[] = [];
const seen = new Set<string>();
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index] ?? '';
if (!ATTACHMENT_SECTION_TITLE_PATTERN.test(line)) {
keptLines.push(line);
continue;
}
const attachmentUrls: string[] = [];
let cursor = index + 1;
while (cursor < lines.length) {
const nextLine = lines[cursor] ?? '';
if (!nextLine.trim()) {
cursor += 1;
continue;
}
const attachmentUrl = extractAttachmentEntryUrl(nextLine);
if (!attachmentUrl) {
break;
}
attachmentUrls.push(attachmentUrl);
cursor += 1;
}
if (attachmentUrls.length === 0) {
keptLines.push(line);
continue;
}
attachmentUrls.forEach((url) => {
if (seen.has(url)) {
return;
}
seen.add(url);
urls.push(url);
});
index = cursor - 1;
}
return {
strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
urls,
};
}
export function extractChatMessageParts(text: string) {
const attachmentExtraction = extractAttachmentPreviewUrls(text);
const lines = String(attachmentExtraction.strippedText ?? '').split('\n');
const keptLines: string[] = [];
const parts: ChatMessagePart[] = [];
const seenLinkKeys = new Set<string>();
let isInsideCodeFence = false;
const pushPart = (nextPart: ChatMessagePart | null) => {
if (!nextPart) {
return false;
@@ -423,6 +560,46 @@ export function extractChatMessageParts(text: string) {
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const line = lines[lineIndex] ?? '';
if (PROMPT_CODE_BLOCK_START_PATTERN.test(line)) {
const fencedLines = [line];
const jsonBodyLines: string[] = [];
let cursor = lineIndex + 1;
let foundFenceEnd = false;
for (; cursor < lines.length; cursor += 1) {
const nextLine = lines[cursor] ?? '';
fencedLines.push(nextLine);
if (CODE_BLOCK_END_PATTERN.test(nextLine)) {
foundFenceEnd = true;
break;
}
jsonBodyLines.push(nextLine);
}
if (foundFenceEnd && pushPart(buildPromptPartFromBlock(jsonBodyLines.join('\n')))) {
lineIndex = cursor;
continue;
}
keptLines.push(...fencedLines);
lineIndex = foundFenceEnd ? cursor : lines.length;
continue;
}
if (CODE_FENCE_TOGGLE_PATTERN.test(line)) {
keptLines.push(line);
isInsideCodeFence = !isInsideCodeFence;
continue;
}
if (isInsideCodeFence) {
keptLines.push(line);
continue;
}
const promptMatched = line.match(PROMPT_LINE_PATTERN);
if (promptMatched) {
@@ -460,37 +637,29 @@ export function extractChatMessageParts(text: string) {
continue;
}
if (PROMPT_CODE_BLOCK_START_PATTERN.test(line)) {
const fencedLines = [line];
const jsonBodyLines: string[] = [];
let cursor = lineIndex + 1;
let foundFenceEnd = false;
for (; cursor < lines.length; cursor += 1) {
const nextLine = lines[cursor] ?? '';
fencedLines.push(nextLine);
if (CODE_BLOCK_END_PATTERN.test(nextLine)) {
foundFenceEnd = true;
break;
}
jsonBodyLines.push(nextLine);
}
if (foundFenceEnd && pushPart(buildPromptPartFromBlock(jsonBodyLines.join('\n')))) {
lineIndex = cursor;
continue;
}
keptLines.push(...fencedLines);
lineIndex = foundFenceEnd ? cursor : lines.length;
continue;
}
const matched = line.match(LINK_CARD_LINE_PATTERN);
if (!matched) {
const markdownLinkMatch = line.match(STANDALONE_MARKDOWN_LINK_LINE_PATTERN);
if (markdownLinkMatch) {
const [, rawTitle, rawUrl] = markdownLinkMatch;
if (isStructuredLinkCardCandidate(rawUrl ?? '')) {
if (pushPart(buildLinkCardPart(`${rawTitle}|${rawUrl}`))) {
continue;
}
}
}
const standaloneUrlMatch = line.match(STANDALONE_URL_LINE_PATTERN);
if (standaloneUrlMatch) {
const rawUrl = standaloneUrlMatch[1] ?? '';
if (isStructuredLinkCardCandidate(rawUrl)) {
if (pushPart(buildLinkCardPart(`${resolveStandaloneLinkTitle(keptLines, rawUrl)}|${rawUrl}`))) {
continue;
}
}
}
keptLines.push(line);
continue;
}
@@ -508,8 +677,32 @@ export function extractChatMessageParts(text: string) {
}
}
const strippedWithEmbeddedPrompts = (() => {
const nextLines: string[] = [];
let isInsideCodeFence = false;
keptLines.forEach((line) => {
if (CODE_FENCE_TOGGLE_PATTERN.test(line)) {
nextLines.push(line);
isInsideCodeFence = !isInsideCodeFence;
return;
}
if (isInsideCodeFence) {
nextLines.push(line);
return;
}
nextLines.push(
line.replace(/\[\[prompt:(.+?)\]\]/gi, (fullMatch, rawBody) => (pushPart(buildPromptPart(rawBody)) ? '' : fullMatch)),
);
});
return nextLines.join('\n');
})();
return {
strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
strippedText: strippedWithEmbeddedPrompts.replace(/\n{3,}/g, '\n\n').trim(),
parts,
};
}