Files
ai-code-app/src/app/main/mainChatPanel/messageParts.ts

165 lines
4.2 KiB
TypeScript

import type { ChatMessagePart } from './types';
const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\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 RESOURCE_PATH_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
function normalizeText(value: unknown) {
return String(value ?? '').trim();
}
function normalizeUrl(value: string) {
const normalized = normalizeText(value);
if (!normalized) {
return '';
}
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
return normalized;
}
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 (RESOURCE_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
return false;
}
if (/^https?:\/\//i.test(normalized)) {
return !hasKnownFileExtension(normalized);
}
return !hasKnownFileExtension(normalized);
}
function buildFallbackLinkTitle(url: string) {
try {
const parsed = new URL(url, typeof window !== 'undefined' ? window.location.origin : '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 buildLinkCardPart(rawBody: string): ChatMessagePart | null {
const segments = rawBody
.split('|')
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length < 2) {
return null;
}
const [rawTitle, rawUrl, rawActionLabel] = segments;
const title = normalizeText(rawTitle);
const url = normalizeUrl(rawUrl);
const actionLabel = normalizeText(rawActionLabel) || null;
if (!title || !url) {
return null;
}
return {
type: 'link_card',
title,
url,
actionLabel,
};
}
export function extractChatMessageParts(text: string) {
const lines = String(text ?? '').split('\n');
const keptLines: string[] = [];
const parts: ChatMessagePart[] = [];
const seenLinkKeys = new Set<string>();
const pushPart = (nextPart: ChatMessagePart | null) => {
if (!nextPart) {
return false;
}
const dedupeKey = `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`;
if (seenLinkKeys.has(dedupeKey)) {
return true;
}
seenLinkKeys.add(dedupeKey);
parts.push(nextPart);
return true;
};
for (const line of lines) {
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;
}
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
keptLines.push(line);
}
}
return {
strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
parts,
};
}