diff --git a/frontend/src/components/ExtractedQuestionsDisplay.tsx b/frontend/src/components/ExtractedQuestionsDisplay.tsx index ed7730f..ec9788f 100644 --- a/frontend/src/components/ExtractedQuestionsDisplay.tsx +++ b/frontend/src/components/ExtractedQuestionsDisplay.tsx @@ -1,11 +1,17 @@ import React from 'react' +import type { SubQuestionSources } from '../types' export interface ExtractedQuestionsDisplayProps { extractedQuestions?: string[] | null + subQuestionSources?: SubQuestionSources[] | null isLoading: boolean } -export const ExtractedQuestionsDisplay: React.FC = ({ extractedQuestions, isLoading }) => { +export const ExtractedQuestionsDisplay: React.FC = ({ + extractedQuestions, + subQuestionSources, + isLoading, +}) => { if (!isLoading && (!extractedQuestions || extractedQuestions.length === 0)) { return null } @@ -27,20 +33,41 @@ export const ExtractedQuestionsDisplay: React.FC ) } + const handleScrollToSubq = (index: number) => { + const element = document.getElementById(`subq-${index}`) + element?.scrollIntoView({ behavior: 'smooth' }) + } + return (
Extracted Questions:
    - {extractedQuestions?.map((question, index) => ( -
  1. - {question} -
  2. - ))} + {extractedQuestions?.map((question, index) => { + const displayText = subQuestionSources?.[index]?.sub_question_text ?? question + return ( +
  3. + {subQuestionSources ? ( + { + e.preventDefault() + handleScrollToSubq(index) + }} + className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer" + > + {displayText} + + ) : ( + displayText + )} +
  4. + ) + })}
) diff --git a/frontend/src/components/ResponsePanel.tsx b/frontend/src/components/ResponsePanel.tsx index 1712d4d..45da4a1 100644 --- a/frontend/src/components/ResponsePanel.tsx +++ b/frontend/src/components/ResponsePanel.tsx @@ -1,29 +1,240 @@ import React, { useState } from 'react' import { MessageSquare, AlertCircle, Copy, ChevronDown, ChevronRight } from 'lucide-react' import ReactMarkdown from 'react-markdown' -import type { SourceMetadata } from '../types' +import type { SourceMetadata, SubQuestionSources } from '../types' import { getPdfViewerUrl } from '../lib/api' -import { processCitations } from '../utils/citationParser' +import { processCitations, processCitationsForSubq } from '../utils/citationParser' interface ResponsePanelProps { answer: string | null - sources: SourceMetadata[] - isLoading: boolean + sources?: SourceMetadata[] + subQuestionSources?: SubQuestionSources[] | null + isLoading?: boolean phase?: string - error: string | null + error?: string | null } -export const ResponsePanel: React.FC = ({ +const CitationLink = ({ href, children }: { href?: string; children?: React.ReactNode }) => ( + + {children} + +) + +function parseAnswerSections(answer: string): string[] { + const sections = answer.split(/## Sub-question \d+:[^\n]*\n/) + if (sections.length <= 1) return [answer] + return sections.filter((s) => s.trim().length > 0).map((s) => s.trim()) +} + +function SubQuestionSourceCard({ source, index }: { source: SourceMetadata; index: number }) { + return ( +
+
+
{source.filename}
+ {source.page_number !== null && ( + + Page {source.page_number} + + )} +
+
{source.upload_date}
+
{source.content_summary}
+
+
Chunk {source.chunk_index}
+ {source.chunk_file_path && ( + + View PDF + + )} +
+
+ ) +} + +function SubQuestionSection({ + index, + subQuestion, + answerSection, +}: { + index: number + subQuestion: SubQuestionSources + answerSection: string +}) { + const [expanded, setExpanded] = useState(true) + const processedAnswer = processCitationsForSubq(answerSection, [subQuestion], 0) + + return ( +
+
+ Sub-question {index + 1}: + {subQuestion.sub_question_text} +
+ +
+ + {processedAnswer} + +
+ + {subQuestion.sources.length > 0 && ( +
+ + {expanded && ( +
+ {subQuestion.sources.map((source, idx) => ( + + ))} +
+ )} +
+ )} +
+ ) +} + +function SubQuestionSections({ + answer, + subQuestionSources, + isLoading, +}: { + answer: string | null + subQuestionSources: SubQuestionSources[] + isLoading?: boolean +}) { + const [copied, setCopied] = useState(false) + const sections = answer ? parseAnswerSections(answer) : [] + + const handleCopyAnswer = async (): Promise => { + if (answer) { + await navigator.clipboard.writeText(answer) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + if (isLoading) { + return ( +
+
+
+ Generating answers... +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) + } + + return ( +
+
+
+ +
+ +
+ {subQuestionSources.map((subQuestion, index) => ( + + ))} +
+
+ ) +} + +function FlatResponse({ answer, sources, isLoading, phase, error, -}) => { - const [sourcesExpanded, setSourcesExpanded] = useState(true) - const [copied, setCopied] = useState(false) +}: { + answer: string | null + sources?: SourceMetadata[] + isLoading?: boolean + phase?: string + error?: string | null +}) { + const [sourcesExpanded, setSourcesExpanded] = useState(true) + const [copied, setCopied] = useState(false) + const safeSources = sources ?? [] - const processedAnswer = answer ? processCitations(answer, sources) : answer ?? '' + const processedAnswer = answer ? processCitations(answer, safeSources) : answer ?? '' const handleCopyAnswer = async (): Promise => { if (answer) { @@ -108,17 +319,6 @@ export const ResponsePanel: React.FC = ({ ) } - const CitationLink = ({ href, children }: { href?: string; children?: React.ReactNode }) => ( - - {children} - - ) - return (
@@ -143,7 +343,7 @@ export const ResponsePanel: React.FC = ({
- {sources.length > 0 && ( + {safeSources.length > 0 && (
{sourcesExpanded && (
- {sources.map((source, index) => ( + {safeSources.map((source, index) => (
= ({
) } + +export const ResponsePanel: React.FC = ({ + answer, + sources, + subQuestionSources, + isLoading, + phase, + error, +}) => { + if (subQuestionSources && subQuestionSources.length > 0) { + return ( + + ) + } + + return ( + + ) +} diff --git a/frontend/src/pages/LTTPage.tsx b/frontend/src/pages/LTTPage.tsx index 8b4e7cb..02b99df 100644 --- a/frontend/src/pages/LTTPage.tsx +++ b/frontend/src/pages/LTTPage.tsx @@ -43,6 +43,7 @@ export const LTTPage: React.FC = () => {
@@ -58,6 +59,7 @@ export const LTTPage: React.FC = () => { { @@ -13,23 +13,27 @@ function buildCitationLookup(sources: SourceMetadata[]): Map { + const sources = subQuestionSources[subqIndex]?.sources ?? [] + return buildCitationLookup(sources) +} - const lookup = buildCitationLookup(sources) +export function processCitationsForSubq( + answerSection: string, + subQuestionSources: SubQuestionSources[], + subqIndex: number +): string { + const lookup = buildCitationLookupForSubq(subQuestionSources, subqIndex) + return replaceCitationPatterns(answerSection, lookup) +} - // Match [content] that is NOT part of markdown image ![...] or link [...](...) +function replaceCitationPatterns( + text: string, + lookup: Map +): string { const citationPattern = /(? { @@ -57,3 +61,21 @@ export function processCitations(text: string, sources: SourceMetadata[]): strin return fullMatch }) } + +/** + * Parse citation patterns in answer text and replace with markdown links. + * + * Citation format: [filename, page N] or [filename] + * Only replaces citations that match an actual source in the sources array. + * Unmatched citations remain as plain text. + * + * @param text - The LLM answer text containing citations + * @param sources - Array of source metadata for cross-referencing + * @returns Modified text with matched citations converted to markdown links + */ +export function processCitations(text: string, sources: SourceMetadata[]): string { + if (!sources.length) return text + + const lookup = buildCitationLookup(sources) + return replaceCitationPatterns(text, lookup) +}