-
{claim.text}
+
+
{claim.text}
+ {claim.source_ids.length > 0 && (
+
+ )}
+
{/* Confidence bar */}
@@ -69,11 +137,19 @@ function ClaimCard({ claim }: { claim: ClaimConfidenceItem }) {
)}
+
+ {popoverCitation && (
+
setPopoverCitation(null)}
+ anchorRect={popoverRect}
+ />
+ )}
);
}
-export function ClaimsPanel({ articleId }: ClaimsPanelProps) {
+export function ClaimsPanel({ articleId, sources }: ClaimsPanelProps) {
const [open, setOpen] = useState(false);
const { data, isLoading, isError } = useQuery({
@@ -192,7 +268,7 @@ export function ClaimsPanel({ articleId }: ClaimsPanelProps) {
{data.claims.length > 0 ? (
{data.claims.map((claim) => (
-
+
))}
) : (
diff --git a/apps/web/src/components/wiki/InlineCitationMarker.tsx b/apps/web/src/components/wiki/InlineCitationMarker.tsx
new file mode 100644
index 00000000..dde1729d
--- /dev/null
+++ b/apps/web/src/components/wiki/InlineCitationMarker.tsx
@@ -0,0 +1,75 @@
+import { useCallback, useRef, useState } from "react";
+import { getSourceSpans } from "../../api/sources";
+import type { ArticleSourceRef, SourceSpanResponse } from "../../types/api";
+import { CitationPopover } from "./CitationPopover";
+import type { CitationTarget } from "./CitationContext";
+import { formatLocator } from "./citationUtils";
+
+interface InlineCitationMarkerProps {
+ /** The article's sources to look up spans from. */
+ sources: ArticleSourceRef[];
+}
+
+export function InlineCitationMarker({ sources }: InlineCitationMarkerProps) {
+ const [popoverCitation, setPopoverCitation] = useState
(
+ null,
+ );
+ const [popoverRect, setPopoverRect] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const markerRef = useRef(null);
+
+ const handleClick = useCallback(async () => {
+ if (popoverCitation) {
+ setPopoverCitation(null);
+ return;
+ }
+ if (sources.length === 0) return;
+
+ setLoading(true);
+ try {
+ // Try each source until we find one with spans
+ for (const source of sources) {
+ const spans: SourceSpanResponse[] = await getSourceSpans(source.id);
+ if (spans.length > 0) {
+ const span = spans[0];
+ const citation: CitationTarget = {
+ sourceId: source.id,
+ spanText: span.text,
+ sourceName: source.title,
+ locatorInfo: formatLocator(span),
+ };
+ setPopoverCitation(citation);
+ if (markerRef.current) {
+ setPopoverRect(markerRef.current.getBoundingClientRect());
+ }
+ return;
+ }
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, [popoverCitation, sources]);
+
+ if (sources.length === 0) return null;
+
+ return (
+ <>
+
+ {popoverCitation && (
+ setPopoverCitation(null)}
+ anchorRect={popoverRect}
+ />
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/components/wiki/SourceHighlightPanel.tsx b/apps/web/src/components/wiki/SourceHighlightPanel.tsx
new file mode 100644
index 00000000..81e9ec99
--- /dev/null
+++ b/apps/web/src/components/wiki/SourceHighlightPanel.tsx
@@ -0,0 +1,153 @@
+import { useEffect, useRef } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { getSourceContent } from "../../api/sources";
+import type { CitationTarget } from "./CitationContext";
+import { useCitation } from "./CitationContext";
+import { Spinner } from "../shared/Spinner";
+
+interface SourceHighlightPanelProps {
+ citation: CitationTarget;
+}
+
+export function SourceHighlightPanel({ citation }: SourceHighlightPanelProps) {
+ const { clearCitation } = useCitation();
+ const highlightRef = useRef(null);
+
+ const contentQuery = useQuery({
+ queryKey: ["source-content", citation.sourceId],
+ queryFn: () => getSourceContent(citation.sourceId),
+ });
+
+ // Auto-scroll to highlighted span once content loads
+ useEffect(() => {
+ if (highlightRef.current) {
+ highlightRef.current.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ }
+ }, [contentQuery.data]);
+
+ return (
+
+ {/* Header */}
+
+
+ Source Preview
+
+
+
+
+ {/* Source info */}
+
+
+ {citation.sourceName || "Unknown source"}
+
+ {citation.locatorInfo && (
+
+ {citation.locatorInfo}
+
+ )}
+
+
+ {/* Content with highlighted span */}
+
+ {contentQuery.isLoading ? (
+
+ Loading source content...
+
+ ) : contentQuery.isError ? (
+
+ Failed to load source content.
+
+ ) : contentQuery.data ? (
+
+ ) : null}
+
+
+ );
+}
+
+interface HighlightedSourceTextProps {
+ content: string;
+ spanText: string;
+ highlightRef: React.RefObject;
+}
+
+function HighlightedSourceText({
+ content,
+ spanText,
+ highlightRef,
+}: HighlightedSourceTextProps) {
+ // Find the span text in the source content
+ const normalizedContent = content;
+ const matchIndex = normalizedContent.indexOf(spanText);
+
+ if (matchIndex === -1) {
+ // Fallback: show the whole content with the quoted span at the top
+ return (
+
+
+
+ Cited passage
+
+
+ {spanText}
+
+
+
+ {content.split(/\n{2,}/).map((para, idx) => (
+
+ {para}
+
+ ))}
+
+
+ );
+ }
+
+ // Split content into before, match, and after
+ const before = content.slice(0, matchIndex);
+ const match = content.slice(matchIndex, matchIndex + spanText.length);
+ const after = content.slice(matchIndex + spanText.length);
+
+ return (
+
+ {before && (
+ {before}
+ )}
+
+ {match}
+
+ {after && (
+ {after}
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/wiki/WikiExplorerView.tsx b/apps/web/src/components/wiki/WikiExplorerView.tsx
index 346ebd9f..93be4d5f 100644
--- a/apps/web/src/components/wiki/WikiExplorerView.tsx
+++ b/apps/web/src/components/wiki/WikiExplorerView.tsx
@@ -9,15 +9,25 @@ import { ArticleCardGrid } from "./ArticleCardGrid";
import { ArticleOutline } from "./ArticleOutline";
import { ArticleReader } from "./ArticleReader";
import { BacklinkPanel } from "./BacklinkPanel";
+import { CitationProvider, useCitation } from "./CitationContext";
import { ConceptTree } from "./ConceptTree";
import { CreateStubModal } from "./CreateStubModal";
import { FiguresPanel } from "./FiguresPanel";
import { InArticleSearch } from "./InArticleSearch";
import { SavedSearches } from "./SavedSearches";
import { SearchBar } from "./SearchBar";
+import { SourceHighlightPanel } from "./SourceHighlightPanel";
import { SourcePanel } from "./SourcePanel";
export function WikiExplorerView() {
+ return (
+
+
+
+ );
+}
+
+function WikiExplorerViewInner() {
const params = useParams<{ slug?: string }>();
const slug = params.slug;
const articleQuery = useArticle(slug);
@@ -26,6 +36,7 @@ export function WikiExplorerView() {
const [figureCount, setFigureCount] = useState(0);
const navigate = useNavigate();
const [showSources, setShowSources] = useState(false);
+ const { activeCitation } = useCitation();
const executeSavedSearchMutation = useMutation({
mutationFn: (searchId: string) => executeSavedSearch(searchId),
onSuccess: () => {
@@ -146,7 +157,7 @@ export function WikiExplorerView() {
{slug ? (