Skip to content

Commit 2cf6181

Browse files
Integrated citation API on click and implemented handling for the updated response format.
1 parent 8294553 commit 2cf6181

File tree

4 files changed

+186
-54
lines changed

4 files changed

+186
-54
lines changed

src/frontend/src/components/Answer/Answer.tsx

Lines changed: 156 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown'
33
import { Components } from 'react-markdown';
44
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
55
import { nord } from 'react-syntax-highlighter/dist/esm/styles/prism'
6-
import { Checkbox, DefaultButton, Dialog, FontIcon, Stack, Text } from '@fluentui/react'
6+
import { Checkbox, DefaultButton, Dialog, FontIcon, Stack, Text, Spinner, MessageBar, MessageBarType, PrimaryButton } from '@fluentui/react'
77
import { useBoolean } from '@fluentui/react-hooks'
88
import { ThumbDislike20Filled, ThumbLike20Filled } from '@fluentui/react-icons'
99
import DOMPurify from 'dompurify'
@@ -22,6 +22,13 @@ interface Props {
2222
onCitationClicked: (citedDocument: Citation) => void
2323
}
2424

25+
// Add interface for citation content response
26+
interface CitationContentResponse {
27+
content: string
28+
title: string
29+
error?: string
30+
}
31+
2532
export const Answer = ({ answer, onCitationClicked }: Props) => {
2633
const initializeAnswerFeedback = (answer: AskResponse) => {
2734
if (answer.message_id == undefined) return undefined
@@ -40,11 +47,72 @@ export const Answer = ({ answer, onCitationClicked }: Props) => {
4047
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false)
4148
const [showReportInappropriateFeedback, setShowReportInappropriateFeedback] = useState(false)
4249
const [negativeFeedbackList, setNegativeFeedbackList] = useState<Feedback[]>([])
50+
51+
// Add new state for citation content dialog
52+
const [isCitationContentDialogOpen, setIsCitationContentDialogOpen] = useState(false)
53+
const [citationContent, setCitationContent] = useState<CitationContentResponse | null>(null)
54+
const [isLoadingCitationContent, setIsLoadingCitationContent] = useState(false)
55+
const [citationContentError, setCitationContentError] = useState<string | null>(null)
56+
4357
const appStateContext = useContext(AppStateContext)
4458
const FEEDBACK_ENABLED =
4559
appStateContext?.state.frontendSettings?.feedback_enabled && appStateContext?.state.isCosmosDBAvailable?.cosmosDB
4660
const SANITIZE_ANSWER = appStateContext?.state.frontendSettings?.sanitize_answer
4761

62+
// Add function to fetch citation content
63+
const fetchCitationContent = async (citation: Citation) => {
64+
setIsLoadingCitationContent(true)
65+
setCitationContentError(null)
66+
67+
try {
68+
const payload = {
69+
url: citation.url || '',
70+
title: citation.title || 'Citation Content',
71+
}
72+
73+
const response = await fetch('/fetch-azure-search-content', {
74+
method: 'POST',
75+
headers: {
76+
'Content-Type': 'application/json',
77+
},
78+
body: JSON.stringify(payload)
79+
})
80+
81+
if (!response.ok) {
82+
throw new Error(`HTTP error! status: ${response.status}`)
83+
}
84+
85+
const data: CitationContentResponse = await response.json()
86+
// {
87+
// content: "abcd",
88+
// title: 'abcdtile'
89+
// }
90+
setCitationContent(data)
91+
setIsCitationContentDialogOpen(true)
92+
} catch (error) {
93+
console.error('Error fetching citation content:', error)
94+
setCitationContentError(error instanceof Error ? error.message : 'Failed to fetch citation content')
95+
} finally {
96+
setIsLoadingCitationContent(false)
97+
}
98+
}
99+
100+
// Update the onCitationClicked handler
101+
const handleCitationClick = (citation: Citation) => {
102+
// Call the original onCitationClicked prop
103+
onCitationClicked(citation)
104+
105+
// Fetch citation content and show dialog
106+
fetchCitationContent(citation)
107+
}
108+
109+
// Add function to close citation content dialog
110+
const closeCitationContentDialog = () => {
111+
setIsCitationContentDialogOpen(false)
112+
setCitationContent(null)
113+
setCitationContentError(null)
114+
}
115+
48116
const handleChevronClick = () => {
49117
setChevronIsExpanded(!chevronIsExpanded)
50118
toggleIsRefAccordionOpen()
@@ -67,22 +135,22 @@ export const Answer = ({ answer, onCitationClicked }: Props) => {
67135
}, [appStateContext?.state.feedbackState, feedbackState, answer.message_id])
68136

69137
const createCitationFilepath = (citation: Citation, index: number, truncate: boolean = false) => {
70-
let citationFilename = ''
71-
72-
if (citation.filepath) {
73-
const part_i = citation.part_index ?? (citation.chunk_id ? parseInt(citation.chunk_id) + 1 : '')
74-
if (truncate && citation.filepath.length > filePathTruncationLimit) {
75-
const citationLength = citation.filepath.length
76-
citationFilename = `${citation.filepath.substring(0, 20)}...${citation.filepath.substring(citationLength - 20)} - Part ${part_i}`
77-
} else {
78-
citationFilename = `${citation.filepath} - Part ${part_i}`
79-
}
80-
} else if (citation.filepath && citation.reindex_id) {
81-
citationFilename = `${citation.filepath} - Part ${citation.reindex_id}`
82-
} else {
83-
citationFilename = `Citation ${index}`
84-
}
85-
return citationFilename
138+
// let citationFilename = ''
139+
// console.log('createCitationFilepath', citation, index, truncate)
140+
// if (citation.filepath) {
141+
// const part_i = citation.part_index ?? (citation.chunk_id ? parseInt(citation.chunk_id) + 1 : '')
142+
// if (truncate && citation.filepath.length > filePathTruncationLimit) {
143+
// const citationLength = citation.filepath.length
144+
// citationFilename = `${citation.filepath.substring(0, 20)}...${citation.filepath.substring(citationLength - 20)} - Part ${part_i}`
145+
// } else {
146+
// citationFilename = `${citation.filepath} - Part ${part_i}`
147+
// }
148+
// } else if (citation.filepath && citation.reindex_id) {
149+
// citationFilename = `${citation.filepath} - Part ${citation.reindex_id}`
150+
// } else {
151+
// citationFilename = `Citation ${index}`
152+
// }
153+
return citation.title ? citation.title : `Citation ${index + 1}`
86154
}
87155

88156
const onLikeResponseClicked = async () => {
@@ -345,22 +413,24 @@ export const Answer = ({ answer, onCitationClicked }: Props) => {
345413
{parsedAnswer.citations.map((citation, idx) => {
346414
return (
347415
<span
348-
title={createCitationFilepath(citation, ++idx)}
416+
title={citation.title ?? undefined}
349417
tabIndex={0}
350418
role="link"
351419
key={idx}
352-
onClick={() => onCitationClicked(citation)}
353-
onKeyDown={e => (e.key === 'Enter' || e.key === ' ' ? onCitationClicked(citation) : null)}
420+
onClick={() => handleCitationClick(citation)}
421+
onKeyDown={e => (e.key === 'Enter' || e.key === ' ' ? handleCitationClick(citation) : null)}
354422
className={styles.citationContainer}
355423
aria-label={createCitationFilepath(citation, idx)}>
356-
<div className={styles.citation}>{idx}</div>
424+
<div className={styles.citation}>{idx+1}</div>
357425
{createCitationFilepath(citation, idx, true)}
358426
</span>
359427
)
360428
})}
361429
</div>
362430
)}
363431
</Stack>
432+
433+
{/* Existing feedback dialog */}
364434
<Dialog
365435
onDismiss={() => {
366436
resetFeedbackDialog()
@@ -389,16 +459,78 @@ export const Answer = ({ answer, onCitationClicked }: Props) => {
389459
}}>
390460
<Stack tokens={{ childrenGap: 4 }}>
391461
<div>Your feedback will improve this experience.</div>
392-
393462
{!showReportInappropriateFeedback ? <UnhelpfulFeedbackContent /> : <ReportInappropriateFeedbackContent />}
394-
395463
<div>By pressing submit, your feedback will be visible to the application owner.</div>
396-
397464
<DefaultButton disabled={negativeFeedbackList.length < 1} onClick={onSubmitNegativeFeedback}>
398465
Submit
399466
</DefaultButton>
400467
</Stack>
401468
</Dialog>
469+
470+
{/* New citation content dialog */}
471+
<Dialog
472+
onDismiss={closeCitationContentDialog}
473+
hidden={!isCitationContentDialogOpen}
474+
styles={{
475+
main: [
476+
{
477+
selectors: {
478+
['@media (min-width: 480px)']: {
479+
width: '800px',
480+
height: '600px',
481+
maxWidth: '800px',
482+
maxHeight: '600px',
483+
minWidth: '800px',
484+
minHeight: '600px',
485+
background: '#FFFFFF',
486+
boxShadow: '0px 14px 28.8px rgba(0, 0, 0, 0.24), 0px 0px 8px rgba(0, 0, 0, 0.2)',
487+
borderRadius: '8px'
488+
}
489+
}
490+
}
491+
]
492+
}}
493+
dialogContentProps={{
494+
title: citationContent?.title || 'Citation Content',
495+
showCloseButton: true
496+
}}>
497+
<Stack tokens={{ childrenGap: 16 }} styles={{ root: { height: '500px' } }}>
498+
{isLoadingCitationContent && (
499+
<Stack horizontal horizontalAlign="center" tokens={{ childrenGap: 8 }}>
500+
<Spinner label="Loading citation content..." />
501+
</Stack>
502+
)}
503+
504+
{citationContentError && (
505+
<MessageBar messageBarType={MessageBarType.error}>
506+
Error loading citation content: {citationContentError}
507+
</MessageBar>
508+
)}
509+
510+
{citationContent && !isLoadingCitationContent && (
511+
<div style={{
512+
height: '400px',
513+
overflowY: 'auto',
514+
padding: '16px',
515+
border: '1px solid #e1e1e1',
516+
borderRadius: '4px',
517+
backgroundColor: '#fafafa'
518+
}}>
519+
<ReactMarkdown
520+
remarkPlugins={[remarkGfm, supersub]}
521+
children={citationContent.content}
522+
components={components}
523+
/>
524+
</div>
525+
)}
526+
527+
<Stack horizontal horizontalAlign="end">
528+
<PrimaryButton onClick={closeCitationContentDialog}>
529+
Close
530+
</PrimaryButton>
531+
</Stack>
532+
</Stack>
533+
</Dialog>
402534
</>
403535
)
404536
}

src/frontend/src/components/Answer/AnswerParser.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,26 @@ export const enumerateCitations = (citations: Citation[]) => {
2323

2424
export function parseAnswer(answer: AskResponse): ParsedAnswer {
2525
let answerText = answer.answer
26-
const citationLinks = answerText.match(/\[(doc\d\d?\d?)]/g)
27-
28-
const lengthDocN = '[doc'.length
29-
30-
let filteredCitations = [] as Citation[]
31-
let citationReindex = 0
32-
citationLinks?.forEach(link => {
33-
// Replacing the links/citations with number
34-
const citationIndex = link.slice(lengthDocN, link.length - 1)
35-
const citation = cloneDeep(answer.citations[Number(citationIndex) - 1]) as Citation
36-
if (!filteredCitations.find(c => c.id === citationIndex) && citation) {
37-
answerText = answerText.replaceAll(link, ` ^${++citationReindex}^ `)
38-
citation.id = citationIndex // original doc index to de-dupe
39-
citation.reindex_id = citationReindex.toString() // reindex from 1 for display
40-
filteredCitations.push(citation)
41-
}
42-
})
43-
44-
filteredCitations = enumerateCitations(filteredCitations)
45-
26+
// const citationLinks = answerText.match(/\[(doc\d\d?\d?)]/g)
27+
// const lengthDocN = '[doc'.length
28+
// let filteredCitations = [] as Citation[]
29+
// let citationReindex = 0
30+
// citationLinks?.forEach(link => {
31+
// // Replacing the links/citations with number
32+
// const citationIndex = link.slice(lengthDocN, link.length - 1)
33+
// const citation = cloneDeep(answer.citations[Number(citationIndex) - 1]) as Citation
34+
// if (!filteredCitations.find(c => c.id === citationIndex) && citation) {
35+
// answerText = answerText.replaceAll(link, ` ^${++citationReindex}^ `)
36+
// citation.id = citationIndex // original doc index to de-dupe
37+
// citation.reindex_id = citationReindex.toString() // reindex from 1 for display
38+
// filteredCitations.push(citation)
39+
// }
40+
// })
41+
42+
// filteredCitations = enumerateCitations(filteredCitations)
43+
// console.log('filteredCitations', filteredCitations)
4644
return {
47-
citations: filteredCitations,
45+
citations: answer.citations,
4846
markdownFormatText: answerText
4947
}
5048
}

src/frontend/src/helpers/helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Conversation, ChatMessage, ToolMessageContent } from '../api/models'
1+
import { Conversation, ChatMessage, ToolMessageContent, Citation } from '../api/models'
22

33

44
// -------------Chat.tsx-------------
@@ -15,9 +15,10 @@ const enum contentTemplateSections {
1515
export const parseCitationFromMessage = (message: ChatMessage) => {
1616
if (message?.role && message?.role === 'tool') {
1717
try {
18-
const toolMessage = JSON.parse(message.content) as ToolMessageContent
19-
return toolMessage.citations
18+
const toolMessage = JSON.parse(message.content)
19+
return toolMessage
2020
} catch {
21+
console.error('Error parsing tool message content:', message.content)
2122
return []
2223
}
2324
}

src/frontend/src/pages/chat/Chat.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -808,9 +808,9 @@ const Chat = ({ type = ChatType.Browse }: Props) => {
808808
}, [messages])
809809

810810
const onShowCitation = (citation: Citation) => {
811-
const path = `/#/document/${citation.filepath}`
812-
const url = window.location.origin + path
813-
setModalUrl(url)
811+
console.log('onShowCitation url', citation.url)
812+
const url = citation.url
813+
setModalUrl(url ?? '')
814814
setIsModalOpen(true)
815815
}
816816

@@ -1013,14 +1013,15 @@ const Chat = ({ type = ChatType.Browse }: Props) => {
10131013
type === ChatType.Template && <ChatHistoryPanel />}
10141014
</Stack>
10151015
)}
1016-
<Modal isOpen={isModalOpen} onDismiss={onCloseModal} isBlocking={false} styles={modalStyles}>
1016+
1017+
{/* <Modal isOpen={isModalOpen} onDismiss={onCloseModal} isBlocking={false} styles={modalStyles}>
10171018
<Stack tokens={stackTokens} styles={{ root: { padding: 20 } }}>
10181019
<iframe src={modalUrl} className={contentStyles.iframe} title="Citation"></iframe>
10191020
<PrimaryButton onClick={onCloseModal} className={contentStyles.closeButton}>
1020-
Close
1021+
Close modal
10211022
</PrimaryButton>
10221023
</Stack>
1023-
</Modal>
1024+
</Modal> */}
10241025
</div>
10251026
)
10261027
}

0 commit comments

Comments
 (0)