fix: inline citations now upgrade to highlighted view (Phase 5.4)

- Added sub_question_text to frontend SourceMetadata type
- SubQuestionSection enriches sources with parent sub-question text
- buildCitationUrl routes to highlight page when sub_question_text present
- processCitations threads highlightReadyKeys through inline citations
This commit is contained in:
Woody 2026-04-29 09:54:40 +08:00
parent c632b9ea3b
commit 1c490ce2fa
3 changed files with 24 additions and 9 deletions

View File

@ -98,10 +98,13 @@ function SubQuestionSection({
highlightReadyKeys: Set<string> highlightReadyKeys: Set<string>
}) { }) {
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
// Enrich sources with sub_question_text so buildCitationUrl can route to highlight page.
// Look up citations across ALL sub-questions' sources because the LLM // Look up citations across ALL sub-questions' sources because the LLM
// may cite chunks from other sub-questions' contexts. // may cite chunks from other sub-questions' contexts.
const allSources = allSubQuestionSources.flatMap(sq => sq.sources) const allSources = allSubQuestionSources.flatMap(sq =>
const processedAnswer = processCitations(answerSection, allSources) sq.sources.map(s => ({ ...s, sub_question_text: sq.sub_question_text }))
)
const processedAnswer = processCitations(answerSection, allSources, highlightReadyKeys)
return ( return (
<div <div

View File

@ -6,6 +6,7 @@ export interface SourceMetadata {
page_number: number | null page_number: number | null
chunk_file_path: string | null chunk_file_path: string | null
document_id: string | null document_id: string | null
sub_question_text?: string
} }
export interface SubQuestionSources { export interface SubQuestionSources {

View File

@ -52,13 +52,17 @@ export function buildCitationLookupForSubq(
export function processCitationsForSubq( export function processCitationsForSubq(
answerSection: string, answerSection: string,
subQuestionSources: SubQuestionSources[], subQuestionSources: SubQuestionSources[],
subqIndex: number subqIndex: number,
highlightKeys?: Set<string>
): string { ): string {
const lookup = buildCitationLookupForSubq(subQuestionSources, subqIndex) const lookup = buildCitationLookupForSubq(subQuestionSources, subqIndex)
return replaceCitationPatterns(answerSection, lookup) return replaceCitationPatterns(answerSection, lookup, highlightKeys)
} }
function buildCitationUrl(source: SourceMetadata): string | null { function buildCitationUrl(source: SourceMetadata, highlightReady?: boolean): string | null {
if (highlightReady && source.document_id && source.sub_question_text) {
return `/api/v1/v2/highlights?document_id=${encodeURIComponent(source.document_id)}&chunk_index=${source.chunk_index}&sub_question=${encodeURIComponent(source.sub_question_text)}`
}
if (source.chunk_file_path) { if (source.chunk_file_path) {
return getPdfViewerUrl( return getPdfViewerUrl(
source.chunk_file_path, source.chunk_file_path,
@ -99,7 +103,8 @@ function findSource(
function replaceCitationPatterns( function replaceCitationPatterns(
text: string, text: string,
lookup: Map<string, SourceMetadata> lookup: Map<string, SourceMetadata>,
highlightKeys?: Set<string>
): string { ): string {
const citationPattern = /(?<!!)\[([^\]]+)\](?!\()/g const citationPattern = /(?<!!)\[([^\]]+)\](?!\()/g
@ -108,7 +113,13 @@ function replaceCitationPatterns(
const source = findSource(trimmed, lookup) const source = findSource(trimmed, lookup)
if (source) { if (source) {
const url = buildCitationUrl(source) let isReady = false
if (highlightKeys && source.document_id && source.sub_question_text) {
isReady = highlightKeys.has(
`${source.document_id}_${source.chunk_index}_${encodeURIComponent(source.sub_question_text)}`
)
}
const url = buildCitationUrl(source, isReady)
if (url) { if (url) {
return `[${trimmed}](${url})` return `[${trimmed}](${url})`
} }
@ -118,11 +129,11 @@ function replaceCitationPatterns(
}) })
} }
export function processCitations(text: string, sources: SourceMetadata[]): string { export function processCitations(text: string, sources: SourceMetadata[], highlightKeys?: Set<string>): string {
if (!sources.length) return text if (!sources.length) return text
const lookup = buildCitationLookup(sources) const lookup = buildCitationLookup(sources)
return replaceCitationPatterns(text, lookup) return replaceCitationPatterns(text, lookup, highlightKeys)
} }
export function extractCitedSources(answerText: string, sources: SourceMetadata[]): SourceMetadata[] { export function extractCitedSources(answerText: string, sources: SourceMetadata[]): SourceMetadata[] {