feat: Phase 7.2 — wire highlightTerms into ResponsePanel + mark CSS

- Add HighlightMark component rendering <mark class="bg-yellow-200...">
- Call highlightTerms() in SubQuestionSection and FlatResponse before ReactMarkdown
- Add mark: HighlightMark to ReactMarkdown components in both paths
- Add .prose mark CSS rule (yellow-200 bg, rounded, px-0.5)
- Tests: 56/56 pass (citation + highlight + ResponsePanel)
This commit is contained in:
Woody 2026-05-15 10:51:08 +08:00
parent 534559b2e0
commit e78f53b687
2 changed files with 16 additions and 5 deletions

View File

@ -3,7 +3,7 @@ import { MessageSquare, AlertCircle, Copy, ChevronDown, ChevronRight } from 'luc
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import type { SourceMetadata, SubQuestionSources } from '../types' import type { SourceMetadata, SubQuestionSources } from '../types'
import { getPdfViewerUrl } from '../lib/api' import { getPdfViewerUrl } from '../lib/api'
import { processCitations, processCitationsForSubq, extractCitedSources } from '../utils/citationParser' import { processCitations, processCitationsForSubq, extractCitedSources, highlightTerms } from '../utils/citationParser'
import { bulletizeMarkdown } from '../utils/citationParser' import { bulletizeMarkdown } from '../utils/citationParser'
function getHighlightUrl(document_id: string, chunk_index: number, sub_question: string): string { function getHighlightUrl(document_id: string, chunk_index: number, sub_question: string): string {
@ -32,6 +32,10 @@ const CitationLink = ({ href, children }: { href?: string; children?: React.Reac
</a> </a>
) )
const HighlightMark = ({ children }: { children?: React.ReactNode }) => (
<mark className="bg-yellow-200 rounded px-0.5">{children}</mark>
)
function parseAnswerSections(answer: string): string[] { function parseAnswerSections(answer: string): string[] {
const sections = answer.split(/## Sub-question \d+:[^\n]*\n/) const sections = answer.split(/## Sub-question \d+:[^\n]*\n/)
if (sections.length <= 1) return [bulletizeMarkdown(answer)] if (sections.length <= 1) return [bulletizeMarkdown(answer)]
@ -107,6 +111,7 @@ function SubQuestionSection({
sq.sources.map(s => ({ ...s, sub_question_text: sq.sub_question_text })) sq.sources.map(s => ({ ...s, sub_question_text: sq.sub_question_text }))
) )
const processedAnswer = processCitations(answerSection, allSources, highlightReadyKeys) const processedAnswer = processCitations(answerSection, allSources, highlightReadyKeys)
const highlightedAnswer = highlightTerms(processedAnswer)
return ( return (
<div <div
@ -120,8 +125,8 @@ function SubQuestionSection({
</div> </div>
<div className="prose prose-sm max-w-none text-gray-800"> <div className="prose prose-sm max-w-none text-gray-800">
<ReactMarkdown components={{ a: CitationLink }}> <ReactMarkdown components={{ a: CitationLink, mark: HighlightMark }}>
{processedAnswer} {highlightedAnswer}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
@ -353,6 +358,7 @@ function FlatResponse({
const safeSources = sources ?? [] const safeSources = sources ?? []
const processedAnswer = answer ? processCitations(answer, safeSources) : answer ?? '' const processedAnswer = answer ? processCitations(answer, safeSources) : answer ?? ''
const highlightedAnswer = processedAnswer ? highlightTerms(processedAnswer) : processedAnswer
const handleCopyAnswer = async (): Promise<void> => { const handleCopyAnswer = async (): Promise<void> => {
if (answer) { if (answer) {
@ -454,9 +460,9 @@ function FlatResponse({
</div> </div>
<div className="prose prose-sm max-w-none text-gray-800"> <div className="prose prose-sm max-w-none text-gray-800">
<ReactMarkdown <ReactMarkdown
components={{ a: CitationLink }} components={{ a: CitationLink, mark: HighlightMark }}
> >
{processedAnswer} {highlightedAnswer}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</div> </div>

View File

@ -10,3 +10,8 @@
list-style: decimal !important; list-style: decimal !important;
padding-left: 1.5rem !important; padding-left: 1.5rem !important;
} }
.prose mark {
background-color: #FEF08A;
border-radius: 0.125rem;
padding: 0 0.125rem;
}