diff --git a/apps/admin-web/src/views/TrackingQuestionDetailView.vue b/apps/admin-web/src/views/TrackingQuestionDetailView.vue index dc7ee14..c8853da 100644 --- a/apps/admin-web/src/views/TrackingQuestionDetailView.vue +++ b/apps/admin-web/src/views/TrackingQuestionDetailView.vue @@ -740,6 +740,172 @@ function sanitizeAnswerBody(answerText: string | null | undefined): string | nul return sanitized || null } +function normalizeLooseMarkdownLine(line: string): string { + const headingNormalizedLine = line + .replace(/^[\u200b-\u200f\ufeff]+/, '') + .replace(/^([ \t]{0,12})(?:\\#|#|#|#)/i, '$1#') + const answerScopedHeading = headingNormalizedLine.replace( + /^([ \t]{0,12})#{1,2}(?!#)\s*(?=\S)/, + '$1### ', + ) + + return answerScopedHeading + .replace(/^([ \t]{0,12})(#{1,6})(?=\S)/, '$1$2 ') + .replace(/^([ \t]{0,12}#{1,6}\s+)#{1,6}\s*/, '$1') + .replace(/^([ \t]{0,12}#{1,6}\s+\d+[.)、])(?=\S)/, '$1 ') + .replace(/^(\s*)([-*+])(?=\S)/, '$1$2 ') + .replace(/^(\s*)(\d{1,3})([.)])(?=\S)/, '$1$2$3 ') +} + +function countMarkdownTablePipes(line: string): number { + return (line.match(/\|/g) ?? []).length +} + +function splitMarkdownTableCells(line: string): string[] { + return line + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => cell.trim()) +} + +function isMarkdownTableSeparatorLine(line: string): boolean { + const normalized = line.trim() + if (!normalized.includes('|')) { + return false + } + + const cells = splitMarkdownTableCells(normalized) + return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell)) +} + +function countMarkdownTableCells(line: string): number { + return splitMarkdownTableCells(line).length +} + +function normalizeMarkdownTableLine(line: string, targetCellCount?: number): string { + let normalized = line.trim() + if (!normalized.startsWith('|')) { + normalized = `| ${normalized}` + } + if (!normalized.endsWith('|')) { + normalized = `${normalized} |` + } + + if (targetCellCount && targetCellCount > 0) { + const cells = splitMarkdownTableCells(normalized) + if (isMarkdownTableSeparatorLine(normalized)) { + while (cells.length < targetCellCount) { + cells.push('--------') + } + } else { + while (cells.length < targetCellCount) { + cells.push('') + } + } + if (cells.length > targetCellCount) { + cells.length = targetCellCount + } + normalized = `| ${cells.join(' | ')} |` + } + + return normalized.replace(/\s*\|\s*/g, ' | ').replace(/\s{2,}/g, ' ').trim() +} + +function normalizeLooseMarkdownTables(content: string): string { + const detachedTableBlocks = content + .replace(/\|\|(?=\s*:?-{3,}:?\s*\|)/g, '|\n|') + .replace( + /(^|\n)(\s{0,3}#{1,6}\s+[^|\n]{1,80})\|(?=[^|\n]+\|[^\n]*\n\s*\|?\s*:?-{3,}:?\s*\|)/g, + '$1$2\n|', + ) + + const lines = detachedTableBlocks.split('\n').flatMap((line: string) => { + const separatorWithFirstRow = line + .trim() + .match(/^((?:\|\s*:?-{3,}:?\s*){2,}\|?)(\S.*)$/) + if ( + !separatorWithFirstRow?.[1] || + !separatorWithFirstRow[2] || + /^\|*$/.test(separatorWithFirstRow[2].trim()) + ) { + return [line] + } + return [separatorWithFirstRow[1], separatorWithFirstRow[2]] + }) + const normalizedLines: string[] = [] + let inTable = false + let tableCellCount = 0 + + for (const line of lines) { + const previousIndex = normalizedLines.length - 1 + if (isMarkdownTableSeparatorLine(line)) { + tableCellCount = + previousIndex >= 0 + ? Math.max( + countMarkdownTableCells(normalizedLines[previousIndex] ?? ''), + countMarkdownTableCells(line), + ) + : countMarkdownTableCells(line) + if ( + previousIndex >= 0 && + countMarkdownTablePipes(normalizedLines[previousIndex] ?? '') >= 2 + ) { + normalizedLines[previousIndex] = normalizeMarkdownTableLine( + normalizedLines[previousIndex] ?? '', + tableCellCount, + ) + } + normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount)) + inTable = true + continue + } + + if (inTable && countMarkdownTablePipes(line) >= 2) { + normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount)) + continue + } + + inTable = false + tableCellCount = 0 + normalizedLines.push(line) + } + + return normalizedLines.join('\n') +} + +function normalizeAnswerMarkdown(content: string): string { + const headingMarkerNormalized = content + .replace(/(^|\n)[\u200b-\u200f\ufeff]*([ \t]{0,12})(?:\\#|#|#|#)/gi, '$1$2#') + .replace(/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}(?:\\#|#|#|#|#){1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/gi, '$1\n\n') + + const detachedHeadings = normalizeLooseMarkdownTables(headingMarkerNormalized) + .replace( + /([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}#{1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/g, + '$1\n\n', + ) + .replace(/([^\n])(?=#{2,6}\s*\S)/g, '$1\n\n') + const lines = detachedHeadings.split('\n') + let inFence = false + + const normalizedLines = lines.map((line) => { + if (/^\s*(?:```|~~~)/.test(line)) { + inFence = !inFence + return line + } + if (inFence) { + return line + } + return normalizeLooseMarkdownLine(line) + }) + + return normalizedLines + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim() +} + function buildInlineCitationMarker( sourceGroupIndex: number, platform: MonitoringQuestionDetailPlatform | null, @@ -854,12 +1020,14 @@ function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null): }) : sanitized - return withInlineMarkers + const normalizedMarkdown = withInlineMarkers .replace(createQwenSourceGroupStripPattern(), '') .replace(createYuanbaoCitationStripPattern(), '') .replace(/[ \t]{2,}/g, ' ') .replace(/\n{3,}/g, '\n\n') .trim() + + return normalizeAnswerMarkdown(normalizedMarkdown) } function collectDeepSeekInlineCitationOccurrences( @@ -1721,6 +1889,76 @@ const contentCitationColumns = computed(() => [ margin-bottom: 18px; } +.tracking-question-answer__content :deep(.markdown-preview) { + color: #111827; + font-size: 15px; + line-height: 1.85; + overflow-wrap: anywhere; +} + +.tracking-question-answer__content :deep(.markdown-preview h1), +.tracking-question-answer__content :deep(.markdown-preview h2), +.tracking-question-answer__content :deep(.markdown-preview h3), +.tracking-question-answer__content :deep(.markdown-preview h4), +.tracking-question-answer__content :deep(.markdown-preview h5), +.tracking-question-answer__content :deep(.markdown-preview h6) { + margin: 20px 0 10px; + color: #0f172a; + line-height: 1.35; + letter-spacing: 0; +} + +.tracking-question-answer__content :deep(.markdown-preview h1) { + font-size: 24px; +} + +.tracking-question-answer__content :deep(.markdown-preview h2) { + font-size: 21px; +} + +.tracking-question-answer__content :deep(.markdown-preview h3) { + font-size: 18px; +} + +.tracking-question-answer__content :deep(.markdown-preview h4), +.tracking-question-answer__content :deep(.markdown-preview h5), +.tracking-question-answer__content :deep(.markdown-preview h6) { + font-size: 16px; +} + +.tracking-question-answer__content :deep(.markdown-preview p), +.tracking-question-answer__content :deep(.markdown-preview ul), +.tracking-question-answer__content :deep(.markdown-preview ol), +.tracking-question-answer__content :deep(.markdown-preview blockquote), +.tracking-question-answer__content :deep(.markdown-preview table) { + margin: 0 0 14px; +} + +.tracking-question-answer__content :deep(.markdown-preview ul), +.tracking-question-answer__content :deep(.markdown-preview ol) { + padding-left: 22px; +} + +.tracking-question-answer__content :deep(.markdown-preview li + li) { + margin-top: 6px; +} + +.tracking-question-answer__content :deep(.markdown-preview strong) { + color: #0f172a; + font-weight: 700; +} + +.tracking-question-answer__content :deep(.markdown-preview hr) { + margin: 22px 0; +} + +.tracking-question-answer__content :deep(.markdown-preview table) { + display: block; + max-width: 100%; + overflow-x: auto; + border-radius: 8px; +} + .custom-scrollbar::-webkit-scrollbar { width: 6px; }