feat: update main chat and system chat UI
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user