@deepcitation/deepcitation-js 1.1.22 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/index.d.ts +3 -4
- package/lib/index.js +2 -3
- package/lib/parsing/parseCitation.js +10 -12
- package/lib/react/CitationComponent.d.ts +41 -135
- package/lib/react/CitationComponent.js +196 -338
- package/lib/react/CitationVariants.d.ts +0 -3
- package/lib/react/Popover.d.ts +15 -0
- package/lib/react/Popover.js +20 -0
- package/lib/react/primitives.d.ts +0 -3
- package/lib/react/primitives.js +1 -3
- package/lib/react/types.d.ts +3 -4
- package/lib/react/utils.d.ts +9 -1
- package/lib/react/utils.js +22 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/search.d.ts +0 -16
- package/lib/types/verification.d.ts +9 -7
- package/lib/types/verification.js +3 -9
- package/package.json +19 -11
- package/lib/react/styles.css +0 -814
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, memo, useCallback, useEffect, useMemo,
|
|
2
|
+
import { forwardRef, memo, useCallback, useEffect, useMemo, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { CheckIcon, WarningIcon } from "./icons.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import "./
|
|
8
|
-
const TWO_DOTS_THINKING_CONTENT = "..";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./Popover.js";
|
|
6
|
+
import { generateCitationInstanceId, generateCitationKey, getCitationDisplayText, } from "./utils.js";
|
|
7
|
+
import { useSmartDiff } from "./useSmartDiff.js";
|
|
9
8
|
// =============================================================================
|
|
10
|
-
//
|
|
9
|
+
// UTILITY FUNCTIONS
|
|
11
10
|
// =============================================================================
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
const DefaultVerifiedIndicator = () => (_jsx("span", { className: "dc-indicator dc-indicator--verified", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
17
|
-
/**
|
|
18
|
-
* Default indicator for partial match citations.
|
|
19
|
-
* Shows an orange/warning checkmark.
|
|
20
|
-
*/
|
|
21
|
-
const DefaultPartialIndicator = () => (_jsx("span", { className: "dc-indicator dc-indicator--partial", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
22
|
-
// =============================================================================
|
|
23
|
-
// HELPER FUNCTIONS
|
|
24
|
-
// =============================================================================
|
|
25
|
-
/**
|
|
26
|
-
* Get status label for display in popover.
|
|
27
|
-
*/
|
|
11
|
+
function cn(...classes) {
|
|
12
|
+
return classes.filter(Boolean).join(" ");
|
|
13
|
+
}
|
|
28
14
|
function getStatusLabel(status) {
|
|
29
15
|
if (status.isVerified && !status.isPartialMatch)
|
|
30
16
|
return "Verified";
|
|
@@ -37,258 +23,172 @@ function getStatusLabel(status) {
|
|
|
37
23
|
return "";
|
|
38
24
|
}
|
|
39
25
|
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
function getPopoverStatusClass(status) {
|
|
43
|
-
if (status.isVerified && !status.isPartialMatch)
|
|
44
|
-
return "dc-popover-status--verified";
|
|
45
|
-
if (status.isPartialMatch)
|
|
46
|
-
return "dc-popover-status--partial";
|
|
47
|
-
if (status.isMiss)
|
|
48
|
-
return "dc-popover-status--miss";
|
|
49
|
-
if (status.isPending)
|
|
50
|
-
return "dc-popover-status--pending";
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Get the "found status" class for text styling.
|
|
55
|
-
* This determines if the text appears as found (blue) or not found (gray).
|
|
56
|
-
*
|
|
57
|
-
* Key insight: Partial matches ARE found - they just don't match exactly.
|
|
58
|
-
* So partial matches get "verified" text styling (blue) but with a different indicator.
|
|
59
|
-
*/
|
|
60
|
-
function getFoundStatusClass(status) {
|
|
61
|
-
// Both verified AND partial are "found" - they get the same text styling
|
|
62
|
-
if (status.isVerified || status.isPartialMatch)
|
|
63
|
-
return "dc-citation--verified";
|
|
64
|
-
if (status.isMiss)
|
|
65
|
-
return "dc-citation--miss";
|
|
66
|
-
if (status.isPending)
|
|
67
|
-
return "dc-citation--pending";
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
// =============================================================================
|
|
71
|
-
// SUB-COMPONENTS
|
|
72
|
-
// =============================================================================
|
|
73
|
-
/**
|
|
74
|
-
* Status tooltip content for miss/partial states.
|
|
75
|
-
* Shows explanation when hovering over citations with issues.
|
|
26
|
+
* Derive citation status from a Verification object.
|
|
27
|
+
* The status comes from verification.status.
|
|
76
28
|
*/
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const searchAttempts = verification?.searchState?.searchAttempts;
|
|
83
|
-
const failedAttempts = searchAttempts?.filter((a) => !a.success) || [];
|
|
84
|
-
// Collect all unique phrases tried
|
|
85
|
-
const allPhrases = [];
|
|
86
|
-
const seenPhrases = new Set();
|
|
87
|
-
for (const attempt of failedAttempts) {
|
|
88
|
-
for (const phrase of attempt.searchPhrases || []) {
|
|
89
|
-
if (!seenPhrases.has(phrase)) {
|
|
90
|
-
seenPhrases.add(phrase);
|
|
91
|
-
allPhrases.push(phrase);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Fallback to citation text if no phrases recorded
|
|
96
|
-
if (allPhrases.length === 0) {
|
|
97
|
-
const searchedText = citation.fullPhrase || citation.keySpan?.toString() || "";
|
|
98
|
-
if (searchedText) {
|
|
99
|
-
allPhrases.push(searchedText);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (isMiss) {
|
|
103
|
-
const hiddenCount = allPhrases.length - 1;
|
|
104
|
-
return (_jsxs("span", { className: "dc-status-tooltip", role: "tooltip", children: [_jsxs("span", { className: "dc-status-header dc-status-header--miss", children: [_jsx(WarningIcon, {}), _jsx("span", { children: "Not found in source" })] }), allPhrases.length > 0 && (_jsxs("span", { className: "dc-search-phrases", children: [_jsxs("span", { className: "dc-search-phrases-header", children: [hiddenCount > 0 && (_jsx("button", { type: "button", className: "dc-search-phrases-toggle", onClick: onToggleExpand, children: isExpanded ? "collapse" : `+${hiddenCount} more` })), _jsxs("span", { className: "dc-status-label", children: ["Searched ", allPhrases.length, " phrase", allPhrases.length > 1 ? "s" : ""] })] }), _jsx("span", { className: "dc-search-phrases-list", children: (isExpanded ? allPhrases : allPhrases.slice(0, 1)).map((phrase, idx) => (_jsxs("span", { className: "dc-search-phrase-item", children: ["\"", phrase.length > 80 ? phrase.slice(0, 80) + "…" : phrase, "\""] }, idx))) })] }))] }));
|
|
29
|
+
function getStatusFromVerification(verification) {
|
|
30
|
+
const status = verification?.status;
|
|
31
|
+
// No verification or no status = pending
|
|
32
|
+
if (!verification || !status) {
|
|
33
|
+
return { isVerified: false, isMiss: false, isPartialMatch: false, isPending: true };
|
|
105
34
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
actualLineIds &&
|
|
119
|
-
JSON.stringify(expectedLineIds) !== JSON.stringify(actualLineIds);
|
|
120
|
-
const expectedPage = citation.pageNumber;
|
|
121
|
-
const actualPage = verification?.pageNumber;
|
|
122
|
-
const pageDiffers = expectedPage != null &&
|
|
123
|
-
actualPage != null &&
|
|
124
|
-
expectedPage !== actualPage;
|
|
125
|
-
return (_jsxs("span", { className: "dc-status-tooltip", role: "tooltip", children: [_jsxs("span", { className: "dc-status-header dc-status-header--partial", children: [_jsx(WarningIcon, {}), _jsx("span", { children: "Partial match" })] }), textMatches ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "dc-status-description", children: "Text matches, but location differs." }), truncatedExpected && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Text" }), _jsx("span", { className: "dc-status-text", children: truncatedExpected })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "dc-status-description", children: "Text differs from citation." }), truncatedExpected && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Expected" }), _jsx("span", { className: "dc-status-text dc-status-text--expected", children: truncatedExpected })] })), truncatedActual && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Found" }), _jsx("span", { className: "dc-status-text", children: truncatedActual })] }))] })), pageDiffers && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Page" }), _jsxs("span", { className: "dc-status-text", children: [_jsx("span", { className: "dc-status-text--expected", children: expectedPage }), " → ", actualPage] })] })), lineIdDiffers && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Line" }), _jsxs("span", { className: "dc-status-text", children: [_jsx("span", { className: "dc-status-text--expected", children: expectedLineIds?.join(", ") }), " → ", actualLineIds?.join(", ")] })] }))] }));
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
};
|
|
35
|
+
const isMiss = status === "not_found";
|
|
36
|
+
const isPending = status === "pending" || status === "loading";
|
|
37
|
+
const isPartialMatch = status === "partial_text_found" ||
|
|
38
|
+
status === "found_on_other_page" ||
|
|
39
|
+
status === "found_on_other_line" ||
|
|
40
|
+
status === "first_word_found";
|
|
41
|
+
const isVerified = status === "found" ||
|
|
42
|
+
status === "found_key_span_only" ||
|
|
43
|
+
status === "found_phrase_missed_value" ||
|
|
44
|
+
isPartialMatch;
|
|
45
|
+
return { isVerified, isMiss, isPartialMatch, isPending };
|
|
46
|
+
}
|
|
129
47
|
/**
|
|
130
|
-
* Full-
|
|
131
|
-
*
|
|
48
|
+
* Full-screen image overlay for zoomed verification images.
|
|
49
|
+
* Click anywhere or press Escape to close.
|
|
132
50
|
*/
|
|
133
|
-
|
|
134
|
-
const handleBackdropClick = useCallback((e) => {
|
|
135
|
-
if (e.target === e.currentTarget) {
|
|
136
|
-
onClose();
|
|
137
|
-
}
|
|
138
|
-
}, [onClose]);
|
|
51
|
+
function ImageOverlay({ src, alt, onClose }) {
|
|
139
52
|
useEffect(() => {
|
|
140
53
|
const handleKeyDown = (e) => {
|
|
141
|
-
if (e.key === "Escape")
|
|
54
|
+
if (e.key === "Escape")
|
|
142
55
|
onClose();
|
|
143
|
-
}
|
|
144
56
|
};
|
|
145
57
|
document.addEventListener("keydown", handleKeyDown);
|
|
146
58
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
147
59
|
}, [onClose]);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
60
|
+
return createPortal(_jsx("div", { className: "fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in-0", onClick: onClose, role: "dialog", "aria-modal": "true", "aria-label": "Full size verification image", children: _jsx("div", { className: "relative max-w-[95vw] max-h-[95vh] cursor-zoom-out", children: _jsx("img", { src: src, alt: alt, className: "max-w-full max-h-[95vh] object-contain rounded-lg shadow-2xl", draggable: false }) }) }), document.body);
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// INDICATOR COMPONENTS
|
|
64
|
+
// =============================================================================
|
|
65
|
+
//
|
|
66
|
+
// Status indicators show the verification state visually:
|
|
67
|
+
//
|
|
68
|
+
// | Status | Indicator | Color | searchState.status values |
|
|
69
|
+
// |---------------|--------------------| -------|----------------------------------------------|
|
|
70
|
+
// | Pending | Spinner | Gray | "pending", "loading", null/undefined |
|
|
71
|
+
// | Verified | Checkmark (✓) | Green | "found", "found_key_span_only", etc. |
|
|
72
|
+
// | Partial Match | Checkmark (✓) | Amber | "found_on_other_page", "partial_text_found" |
|
|
73
|
+
// | Not Found | Warning triangle | Red | "not_found" |
|
|
74
|
+
//
|
|
75
|
+
// Use `renderIndicator` prop to customize. Use `variant="indicator"` to show only the icon.
|
|
76
|
+
// =============================================================================
|
|
77
|
+
/** Spinner component for loading/pending state */
|
|
78
|
+
const Spinner = ({ className }) => (_jsxs("svg", { className: cn("animate-spin", className), xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", width: "12", height: "12", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }));
|
|
79
|
+
/** Verified indicator - green checkmark for exact matches */
|
|
80
|
+
const VerifiedIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-green-600 dark:text-green-500", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
81
|
+
/** Partial match indicator - amber checkmark for partial/relocated matches */
|
|
82
|
+
const PartialIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-amber-600 dark:text-amber-500", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
83
|
+
/** Pending indicator - spinner for loading state */
|
|
84
|
+
const PendingIndicator = () => (_jsx("span", { className: "inline-flex ml-1 text-gray-400 dark:text-gray-500", "aria-hidden": "true", children: _jsx(Spinner, {}) }));
|
|
85
|
+
/** Miss indicator - red warning triangle for not found */
|
|
86
|
+
const MissIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-red-500 dark:text-red-400", "aria-hidden": "true", children: _jsx(WarningIcon, {}) }));
|
|
87
|
+
function DefaultPopoverContent({ citation, verification, status, onImageClick, }) {
|
|
88
|
+
const hasImage = verification?.verificationImageBase64;
|
|
89
|
+
const { isMiss, isPartialMatch } = status;
|
|
90
|
+
// Image view
|
|
91
|
+
if (hasImage) {
|
|
92
|
+
return (_jsxs("div", { className: "p-1", children: [_jsx("button", { type: "button", className: "block cursor-zoom-in", onClick: (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
onImageClick?.();
|
|
96
|
+
}, "aria-label": "Click to view full size", children: _jsx("img", { src: verification.verificationImageBase64, alt: "Citation verification", className: "max-w-[700px] max-h-[500px] w-auto h-auto object-contain rounded bg-gray-50 dark:bg-gray-800", loading: "lazy" }) }), (isMiss || isPartialMatch) && (_jsx(DiffDetails, { citation: citation, verification: verification, status: status }))] }));
|
|
97
|
+
}
|
|
98
|
+
// Text-only view
|
|
99
|
+
const statusLabel = getStatusLabel(status);
|
|
100
|
+
const hasSnippet = verification?.verifiedMatchSnippet;
|
|
101
|
+
const pageNumber = verification?.verifiedPageNumber;
|
|
102
|
+
if (!hasSnippet && !statusLabel)
|
|
103
|
+
return null;
|
|
104
|
+
return (_jsxs("div", { className: "p-3 flex flex-col gap-2 min-w-[200px] max-w-[400px]", children: [statusLabel && (_jsx("span", { className: cn("text-xs font-medium", status.isVerified && !status.isPartialMatch && "text-green-600 dark:text-green-500", status.isPartialMatch && "text-amber-600 dark:text-amber-500", status.isMiss && "text-red-600 dark:text-red-500", status.isPending && "text-gray-500 dark:text-gray-400"), children: statusLabel })), hasSnippet && (_jsxs("span", { className: "text-sm text-gray-700 dark:text-gray-300 italic", children: ["\"", verification.verifiedMatchSnippet, "\""] })), pageNumber && pageNumber > 0 && (_jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: ["Page ", pageNumber] })), (isMiss || isPartialMatch) && (_jsx(DiffDetails, { citation: citation, verification: verification, status: status }))] }));
|
|
105
|
+
}
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// DIFF DETAILS COMPONENT
|
|
108
|
+
// =============================================================================
|
|
151
109
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
110
|
+
* Renders diff highlighting between expected citation text and actual found text.
|
|
111
|
+
* Uses the `diff` library via useSmartDiff hook for word-level highlighting.
|
|
154
112
|
*/
|
|
155
|
-
|
|
113
|
+
function DiffDetails({ citation, verification, status, }) {
|
|
156
114
|
const { isMiss, isPartialMatch } = status;
|
|
115
|
+
const expectedText = citation.fullPhrase || citation.keySpan?.toString() || "";
|
|
116
|
+
const actualText = verification?.verifiedMatchSnippet || "";
|
|
117
|
+
// Use the diff library for smart word-level diffing
|
|
118
|
+
const { diffResult, hasDiff, isHighVariance } = useSmartDiff(expectedText, actualText);
|
|
157
119
|
if (!isMiss && !isPartialMatch)
|
|
158
120
|
return null;
|
|
159
|
-
const expectedText = citation.fullPhrase || citation.keySpan?.toString() || "";
|
|
160
|
-
const actualText = verification?.matchSnippet || "";
|
|
161
|
-
const textMatches = expectedText === actualText;
|
|
162
|
-
const truncatedExpected = expectedText.length > 100 ? expectedText.slice(0, 100) + "…" : expectedText;
|
|
163
|
-
const truncatedActual = actualText.length > 100 ? actualText.slice(0, 100) + "…" : actualText;
|
|
164
|
-
// Check for line ID and page number differences
|
|
165
121
|
const expectedLineIds = citation.lineIds;
|
|
166
|
-
const actualLineIds = verification?.
|
|
122
|
+
const actualLineIds = verification?.verifiedLineIds;
|
|
167
123
|
const lineIdDiffers = expectedLineIds &&
|
|
168
124
|
actualLineIds &&
|
|
169
125
|
JSON.stringify(expectedLineIds) !== JSON.stringify(actualLineIds);
|
|
170
126
|
const expectedPage = citation.pageNumber;
|
|
171
|
-
const actualPage = verification?.
|
|
172
|
-
const pageDiffers = expectedPage != null &&
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Text matches but location differs - show text once
|
|
177
|
-
truncatedExpected && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Text" }), _jsx("span", { className: "dc-status-text", children: truncatedExpected })] }))) : (
|
|
178
|
-
// Text differs - show expected and found
|
|
179
|
-
_jsxs(_Fragment, { children: [truncatedExpected && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Expected" }), _jsx("span", { className: "dc-status-text dc-status-text--expected", children: truncatedExpected })] })), isPartialMatch && truncatedActual && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Found" }), _jsx("span", { className: "dc-status-text", children: truncatedActual })] }))] })), isMiss && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Found" }), _jsx("span", { className: "dc-status-text dc-status-text--miss", children: "Not found in source" })] })), pageDiffers && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Page" }), _jsxs("span", { className: "dc-status-text", children: [_jsx("span", { className: "dc-status-text--expected", children: expectedPage }), " → ", actualPage] })] })), lineIdDiffers && (_jsxs("span", { className: "dc-status-searched", children: [_jsx("span", { className: "dc-status-label", children: "Line" }), _jsxs("span", { className: "dc-status-text", children: [_jsx("span", { className: "dc-status-text--expected", children: expectedLineIds?.join(", ") }), " → ", actualLineIds?.join(", ")] })] }))] }));
|
|
180
|
-
};
|
|
181
|
-
/**
|
|
182
|
-
* Default popover content component.
|
|
183
|
-
* Shows verification image if available, otherwise shows text info.
|
|
184
|
-
* For partial/miss states, also displays expected vs found details.
|
|
185
|
-
*/
|
|
186
|
-
const DefaultPopoverContent = ({ citation, verification, status, onImageClick, }) => {
|
|
187
|
-
const hasImage = verification?.verificationImageBase64;
|
|
188
|
-
const { isMiss, isPartialMatch } = status;
|
|
189
|
-
const handleImageClick = useCallback((e) => {
|
|
190
|
-
e.preventDefault();
|
|
191
|
-
e.stopPropagation();
|
|
192
|
-
if (hasImage && onImageClick) {
|
|
193
|
-
onImageClick(verification.verificationImageBase64);
|
|
194
|
-
}
|
|
195
|
-
}, [hasImage, verification?.verificationImageBase64, onImageClick]);
|
|
196
|
-
// If we have a verification image, show image + diff details for partial/miss
|
|
197
|
-
if (hasImage) {
|
|
198
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "dc-popover-image-button", onClick: handleImageClick, "aria-label": "Click to view full size", children: _jsx("img", { src: verification.verificationImageBase64, alt: "Citation verification", className: "dc-popover-image", loading: "lazy" }) }), (isMiss || isPartialMatch) && (_jsx(DiffDetails, { citation: citation, verification: verification, status: status }))] }));
|
|
199
|
-
}
|
|
200
|
-
// No image - show text info
|
|
201
|
-
const statusLabel = getStatusLabel(status);
|
|
202
|
-
const statusClass = getPopoverStatusClass(status);
|
|
203
|
-
const hasSnippet = verification?.matchSnippet;
|
|
204
|
-
const pageNumber = verification?.pageNumber;
|
|
205
|
-
if (!hasSnippet && !statusLabel) {
|
|
206
|
-
return null;
|
|
127
|
+
const actualPage = verification?.verifiedPageNumber;
|
|
128
|
+
const pageDiffers = expectedPage != null && actualPage != null && expectedPage !== actualPage;
|
|
129
|
+
// For "not_found" status, show expected text and "Not found" message
|
|
130
|
+
if (isMiss) {
|
|
131
|
+
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Expected" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-red-600 dark:text-red-400 line-through opacity-70", children: expectedText.length > 100 ? expectedText.slice(0, 100) + "…" : expectedText })] })), _jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Found" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] text-amber-600 dark:text-amber-500 italic", children: "Not found in source" })] })] }));
|
|
207
132
|
}
|
|
208
|
-
|
|
209
|
-
}
|
|
133
|
+
// For partial matches, show word-level diff
|
|
134
|
+
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && actualText && hasDiff ? (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Diff" }), _jsx("div", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: isHighVariance ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 text-[10px]", children: "Expected: " }), _jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedText.length > 100 ? expectedText.slice(0, 100) + "…" : expectedText })] }), _jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 text-[10px]", children: "Found: " }), _jsx("span", { className: "text-green-600 dark:text-green-400", children: actualText.length > 100 ? actualText.slice(0, 100) + "…" : actualText })] })] })) : (
|
|
135
|
+
// Inline word-level diff
|
|
136
|
+
diffResult.map((block, blockIndex) => (_jsx("span", { children: block.parts.map((part, partIndex) => {
|
|
137
|
+
const key = `p-${blockIndex}-${partIndex}`;
|
|
138
|
+
if (part.removed) {
|
|
139
|
+
return (_jsx("span", { className: "bg-red-200 dark:bg-red-900/50 text-red-700 dark:text-red-300 line-through", title: "Expected text", children: part.value }, key));
|
|
140
|
+
}
|
|
141
|
+
if (part.added) {
|
|
142
|
+
return (_jsx("span", { className: "bg-green-200 dark:bg-green-900/50 text-green-700 dark:text-green-300", title: "Actual text found", children: part.value }, key));
|
|
143
|
+
}
|
|
144
|
+
// Unchanged text
|
|
145
|
+
return _jsx("span", { children: part.value }, key);
|
|
146
|
+
}) }, `block-${blockIndex}`)))) })] })) : expectedText && !hasDiff ? (
|
|
147
|
+
// Text matches exactly (partial match is due to location difference)
|
|
148
|
+
_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Text" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: expectedText.length > 100 ? expectedText.slice(0, 100) + "…" : expectedText })] })) : null, pageDiffers && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Page" }), _jsxs("p", { className: "mt-1 font-mono text-[11px] text-gray-700 dark:text-gray-300", children: [_jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedPage }), " → ", actualPage] })] })), lineIdDiffers && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Line" }), _jsxs("p", { className: "mt-1 font-mono text-[11px] text-gray-700 dark:text-gray-300", children: [_jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedLineIds?.join(", ") }), " → ", actualLineIds?.join(", ")] })] }))] }));
|
|
149
|
+
}
|
|
210
150
|
// =============================================================================
|
|
211
151
|
// MAIN COMPONENT
|
|
212
152
|
// =============================================================================
|
|
213
153
|
/**
|
|
214
154
|
* CitationComponent displays a citation with verification status.
|
|
215
155
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
156
|
+
* ## Interaction Pattern
|
|
157
|
+
*
|
|
158
|
+
* - **Hover**: Shows popover with verification image or details
|
|
159
|
+
* - **Click**: Opens full-size image overlay (if image available)
|
|
160
|
+
* - **Escape / Click overlay**: Closes the image overlay
|
|
220
161
|
*
|
|
221
|
-
*
|
|
222
|
-
* - Exact match: green checkmark
|
|
223
|
-
* - Partial match: orange checkmark
|
|
224
|
-
* - Miss: no indicator
|
|
162
|
+
* ## Customization
|
|
225
163
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
164
|
+
* Use `behaviorConfig.onClick` to completely replace the click behavior,
|
|
165
|
+
* or `eventHandlers.onClick` to add side effects (which disables defaults).
|
|
228
166
|
*/
|
|
229
167
|
export const CitationComponent = forwardRef(({ citation, children, className, hideKeySpan = false, hideBrackets = false, fallbackDisplay, verification, variant = "brackets", eventHandlers, behaviorConfig, isMobile = false, renderIndicator, renderContent, popoverPosition = "top", renderPopoverContent, }, ref) => {
|
|
230
|
-
const
|
|
231
|
-
const wrapperRef = useRef(null);
|
|
168
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
232
169
|
const [expandedImageSrc, setExpandedImageSrc] = useState(null);
|
|
233
|
-
const [isTooltipExpanded, setIsTooltipExpanded] = useState(false);
|
|
234
|
-
const [isPhrasesExpanded, setIsPhrasesExpanded] = useState(false);
|
|
235
|
-
const handleImageClick = useCallback((imageSrc) => {
|
|
236
|
-
setExpandedImageSrc(imageSrc);
|
|
237
|
-
}, []);
|
|
238
|
-
const handleCloseOverlay = useCallback(() => {
|
|
239
|
-
setExpandedImageSrc(null);
|
|
240
|
-
}, []);
|
|
241
|
-
const handleTogglePhrases = useCallback((e) => {
|
|
242
|
-
e?.preventDefault();
|
|
243
|
-
e?.stopPropagation();
|
|
244
|
-
setIsPhrasesExpanded((prev) => !prev);
|
|
245
|
-
}, []);
|
|
246
|
-
// Handle click outside to close expanded tooltip
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
if (!isTooltipExpanded)
|
|
249
|
-
return;
|
|
250
|
-
const handleClickOutside = (event) => {
|
|
251
|
-
if (wrapperRef.current &&
|
|
252
|
-
!wrapperRef.current.contains(event.target)) {
|
|
253
|
-
setIsTooltipExpanded(false);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
// Use capture phase to handle clicks before they bubble
|
|
257
|
-
document.addEventListener("mousedown", handleClickOutside, true);
|
|
258
|
-
return () => {
|
|
259
|
-
document.removeEventListener("mousedown", handleClickOutside, true);
|
|
260
|
-
};
|
|
261
|
-
}, [isTooltipExpanded]);
|
|
262
|
-
// Handle escape key to close expanded tooltip
|
|
263
|
-
useEffect(() => {
|
|
264
|
-
if (!isTooltipExpanded)
|
|
265
|
-
return;
|
|
266
|
-
const handleEscape = (event) => {
|
|
267
|
-
if (event.key === "Escape") {
|
|
268
|
-
setIsTooltipExpanded(false);
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
document.addEventListener("keydown", handleEscape);
|
|
272
|
-
return () => {
|
|
273
|
-
document.removeEventListener("keydown", handleEscape);
|
|
274
|
-
};
|
|
275
|
-
}, [isTooltipExpanded]);
|
|
276
170
|
const citationKey = useMemo(() => generateCitationKey(citation), [citation]);
|
|
277
171
|
const citationInstanceId = useMemo(() => generateCitationInstanceId(citationKey), [citationKey]);
|
|
278
|
-
//
|
|
172
|
+
// Derive status from verification object
|
|
173
|
+
const status = useMemo(() => getStatusFromVerification(verification), [verification]);
|
|
174
|
+
const { isMiss, isPartialMatch, isVerified, isPending } = status;
|
|
175
|
+
const displayText = useMemo(() => {
|
|
176
|
+
return getCitationDisplayText(citation, {
|
|
177
|
+
hideKeySpan: variant !== "text" && variant !== "minimal" && hideKeySpan,
|
|
178
|
+
fallbackDisplay,
|
|
179
|
+
});
|
|
180
|
+
}, [citation, variant, hideKeySpan, fallbackDisplay]);
|
|
181
|
+
// Behavior context for custom handlers
|
|
279
182
|
const getBehaviorContext = useCallback(() => ({
|
|
280
183
|
citation,
|
|
281
184
|
citationKey,
|
|
282
185
|
verification: verification ?? null,
|
|
283
|
-
isTooltipExpanded,
|
|
186
|
+
isTooltipExpanded: isHovering,
|
|
284
187
|
isImageExpanded: !!expandedImageSrc,
|
|
285
188
|
hasImage: !!verification?.verificationImageBase64,
|
|
286
|
-
}), [citation, citationKey, verification,
|
|
189
|
+
}), [citation, citationKey, verification, isHovering, expandedImageSrc]);
|
|
287
190
|
// Apply behavior actions from custom handler
|
|
288
191
|
const applyBehaviorActions = useCallback((actions) => {
|
|
289
|
-
if (actions.setTooltipExpanded !== undefined) {
|
|
290
|
-
setIsTooltipExpanded(actions.setTooltipExpanded);
|
|
291
|
-
}
|
|
292
192
|
if (actions.setImageExpanded !== undefined) {
|
|
293
193
|
if (typeof actions.setImageExpanded === "string") {
|
|
294
194
|
setExpandedImageSrc(actions.setImageExpanded);
|
|
@@ -300,94 +200,55 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
300
200
|
setExpandedImageSrc(null);
|
|
301
201
|
}
|
|
302
202
|
}
|
|
303
|
-
if (actions.setPhrasesExpanded !== undefined) {
|
|
304
|
-
setIsPhrasesExpanded(actions.setPhrasesExpanded);
|
|
305
|
-
}
|
|
306
203
|
}, [verification?.verificationImageBase64]);
|
|
307
|
-
|
|
204
|
+
// Click handler
|
|
205
|
+
const handleClick = useCallback((e) => {
|
|
308
206
|
e.preventDefault();
|
|
309
207
|
e.stopPropagation();
|
|
310
208
|
const context = getBehaviorContext();
|
|
311
|
-
//
|
|
209
|
+
// Custom onClick via behaviorConfig replaces default
|
|
312
210
|
if (behaviorConfig?.onClick) {
|
|
313
211
|
const result = behaviorConfig.onClick(context, e);
|
|
314
|
-
// If custom handler returns actions, apply them
|
|
315
212
|
if (result && typeof result === "object") {
|
|
316
213
|
applyBehaviorActions(result);
|
|
317
214
|
}
|
|
318
|
-
// If returns false or void, no state changes
|
|
319
|
-
// Always call eventHandlers.onClick regardless of custom behavior
|
|
320
215
|
eventHandlers?.onClick?.(citation, citationKey, e);
|
|
321
216
|
return;
|
|
322
217
|
}
|
|
323
|
-
//
|
|
324
|
-
// (no popover pinning, no image expansion) - just call the handler
|
|
218
|
+
// Custom eventHandlers.onClick disables default
|
|
325
219
|
if (eventHandlers?.onClick) {
|
|
326
220
|
eventHandlers.onClick(citation, citationKey, e);
|
|
327
221
|
return;
|
|
328
222
|
}
|
|
329
|
-
// Default click
|
|
223
|
+
// Default: click opens image if available
|
|
330
224
|
if (verification?.verificationImageBase64) {
|
|
331
|
-
|
|
332
|
-
// Image is open - close it and unpin
|
|
333
|
-
setExpandedImageSrc(null);
|
|
334
|
-
setIsTooltipExpanded(false);
|
|
335
|
-
}
|
|
336
|
-
else if (isTooltipExpanded) {
|
|
337
|
-
// Already pinned - second click expands image
|
|
338
|
-
setExpandedImageSrc(verification.verificationImageBase64);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
// First click - pin the popover open
|
|
342
|
-
setIsTooltipExpanded(true);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
// No image - toggle phrases expansion for miss/partial tooltips
|
|
347
|
-
setIsTooltipExpanded((prev) => !prev);
|
|
348
|
-
setIsPhrasesExpanded((prev) => !prev);
|
|
225
|
+
setExpandedImageSrc(verification.verificationImageBase64);
|
|
349
226
|
}
|
|
350
227
|
}, [
|
|
351
|
-
eventHandlers,
|
|
352
228
|
behaviorConfig,
|
|
229
|
+
eventHandlers,
|
|
353
230
|
citation,
|
|
354
231
|
citationKey,
|
|
355
232
|
verification?.verificationImageBase64,
|
|
356
|
-
expandedImageSrc,
|
|
357
|
-
isTooltipExpanded,
|
|
358
233
|
getBehaviorContext,
|
|
359
234
|
applyBehaviorActions,
|
|
360
235
|
]);
|
|
361
|
-
|
|
362
|
-
// const { isVerified, isPending } = status;
|
|
363
|
-
const { isMiss, isPartialMatch, isVerified, isPending } = status;
|
|
364
|
-
const displayText = useMemo(() => {
|
|
365
|
-
// For text/minimal variants, always show keySpan (hideKeySpan is ignored)
|
|
366
|
-
// For brackets variant, show keySpan based on hideKeySpan prop
|
|
367
|
-
return getCitationDisplayText(citation, {
|
|
368
|
-
hideKeySpan: variant !== "text" &&
|
|
369
|
-
variant !== "minimal" &&
|
|
370
|
-
hideKeySpan,
|
|
371
|
-
fallbackDisplay,
|
|
372
|
-
});
|
|
373
|
-
}, [citation, variant, hideKeySpan, fallbackDisplay]);
|
|
374
|
-
// Found status class for text styling (blue for found, gray for miss)
|
|
375
|
-
const foundStatusClass = useMemo(() => getFoundStatusClass(status), [status]);
|
|
376
|
-
// Event handlers
|
|
236
|
+
// Hover handlers
|
|
377
237
|
const handleMouseEnter = useCallback(() => {
|
|
378
|
-
|
|
238
|
+
setIsHovering(true);
|
|
379
239
|
if (behaviorConfig?.onHover?.onEnter) {
|
|
380
240
|
behaviorConfig.onHover.onEnter(getBehaviorContext());
|
|
381
241
|
}
|
|
382
242
|
eventHandlers?.onMouseEnter?.(citation, citationKey);
|
|
383
243
|
}, [eventHandlers, behaviorConfig, citation, citationKey, getBehaviorContext]);
|
|
384
244
|
const handleMouseLeave = useCallback(() => {
|
|
385
|
-
|
|
245
|
+
setIsHovering(false);
|
|
386
246
|
if (behaviorConfig?.onHover?.onLeave) {
|
|
387
247
|
behaviorConfig.onHover.onLeave(getBehaviorContext());
|
|
388
248
|
}
|
|
389
249
|
eventHandlers?.onMouseLeave?.(citation, citationKey);
|
|
390
250
|
}, [eventHandlers, behaviorConfig, citation, citationKey, getBehaviorContext]);
|
|
251
|
+
// Touch handler for mobile
|
|
391
252
|
const handleTouchEnd = useCallback((e) => {
|
|
392
253
|
if (isMobile) {
|
|
393
254
|
e.preventDefault();
|
|
@@ -396,35 +257,34 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
396
257
|
}
|
|
397
258
|
}, [eventHandlers, citation, citationKey, isMobile]);
|
|
398
259
|
// Early return for miss with fallback display
|
|
399
|
-
if (fallbackDisplay !== null &&
|
|
400
|
-
|
|
401
|
-
!hideKeySpan &&
|
|
402
|
-
isMiss) {
|
|
403
|
-
return (_jsx("span", { className: classNames("dc-citation-fallback", className), children: fallbackDisplay }));
|
|
260
|
+
if (fallbackDisplay !== null && fallbackDisplay !== undefined && !hideKeySpan && isMiss) {
|
|
261
|
+
return (_jsx("span", { className: cn("text-gray-400 dark:text-gray-500", className), children: fallbackDisplay }));
|
|
404
262
|
}
|
|
405
|
-
//
|
|
263
|
+
// Status classes for text styling
|
|
264
|
+
const statusClasses = cn(
|
|
265
|
+
// Found status (text color) - verified or partial match
|
|
266
|
+
(isVerified || isPartialMatch) && variant === "brackets" && "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline", isMiss && "opacity-70 line-through text-gray-400 dark:text-gray-500", isPending && "text-gray-500 dark:text-gray-400");
|
|
267
|
+
// Render indicator based on status priority:
|
|
268
|
+
// 1. Custom renderIndicator (if provided)
|
|
269
|
+
// 2. Pending → Spinner
|
|
270
|
+
// 3. Miss → Warning triangle
|
|
271
|
+
// 4. Partial match → Amber checkmark
|
|
272
|
+
// 5. Verified → Green checkmark
|
|
406
273
|
const renderStatusIndicator = () => {
|
|
407
|
-
if (renderIndicator)
|
|
274
|
+
if (renderIndicator)
|
|
408
275
|
return renderIndicator(status);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
return _jsx(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
else if (isPending) {
|
|
418
|
-
return (_jsx("span", { className: "dc-indicator dc-indicator--pending", "aria-hidden": "true", children: TWO_DOTS_THINKING_CONTENT }));
|
|
419
|
-
}
|
|
420
|
-
else if (isMiss) {
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
276
|
+
if (isPending)
|
|
277
|
+
return _jsx(PendingIndicator, {});
|
|
278
|
+
if (isMiss)
|
|
279
|
+
return _jsx(MissIndicator, {});
|
|
280
|
+
if (isPartialMatch)
|
|
281
|
+
return _jsx(PartialIndicator, {});
|
|
282
|
+
if (isVerified)
|
|
283
|
+
return _jsx(VerifiedIndicator, {});
|
|
423
284
|
return null;
|
|
424
285
|
};
|
|
425
|
-
// Render
|
|
286
|
+
// Render citation content
|
|
426
287
|
const renderCitationContent = () => {
|
|
427
|
-
// Custom render function takes full control
|
|
428
288
|
if (renderContent) {
|
|
429
289
|
return renderContent({
|
|
430
290
|
citation,
|
|
@@ -434,54 +294,52 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
434
294
|
isMergedDisplay: variant === "text" || variant === "brackets" || !hideKeySpan,
|
|
435
295
|
});
|
|
436
296
|
}
|
|
437
|
-
// Indicator-only variant - just the checkmark/warning
|
|
438
297
|
if (variant === "indicator") {
|
|
439
|
-
return
|
|
298
|
+
return _jsx("span", { children: renderStatusIndicator() });
|
|
440
299
|
}
|
|
441
|
-
// Text variant - no special styling, shows keySpan with indicator
|
|
442
300
|
if (variant === "text") {
|
|
443
|
-
return (_jsxs("span", { className:
|
|
301
|
+
return (_jsxs("span", { className: statusClasses, children: [displayText, renderStatusIndicator()] }));
|
|
444
302
|
}
|
|
445
|
-
// Minimal variant - no brackets, just text with indicator
|
|
446
303
|
if (variant === "minimal") {
|
|
447
|
-
return (_jsxs("span", { className: "
|
|
304
|
+
return (_jsxs("span", { className: cn("max-w-80 overflow-hidden text-ellipsis", statusClasses), children: [displayText, renderStatusIndicator()] }));
|
|
448
305
|
}
|
|
449
|
-
//
|
|
450
|
-
return (_jsxs("span", { className: "
|
|
306
|
+
// brackets variant (default)
|
|
307
|
+
return (_jsxs("span", { className: cn("inline-flex items-baseline gap-0.5 whitespace-nowrap", "font-mono text-xs leading-tight", "text-gray-500 dark:text-gray-400", "transition-colors"), "aria-hidden": "true", children: [!hideBrackets && "[", _jsxs("span", { className: cn("max-w-80 overflow-hidden text-ellipsis", statusClasses), children: [displayText, renderStatusIndicator()] }), !hideBrackets && "]"] }));
|
|
451
308
|
};
|
|
452
|
-
//
|
|
309
|
+
// Popover visibility
|
|
453
310
|
const isPopoverHidden = popoverPosition === "hidden";
|
|
454
311
|
const shouldShowPopover = !isPopoverHidden &&
|
|
455
312
|
verification &&
|
|
456
|
-
(verification.verificationImageBase64 || verification.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
313
|
+
(verification.verificationImageBase64 || verification.verifiedMatchSnippet);
|
|
314
|
+
const hasImage = !!verification?.verificationImageBase64;
|
|
315
|
+
// Image overlay
|
|
316
|
+
const imageOverlay = expandedImageSrc ? (_jsx(ImageOverlay, { src: expandedImageSrc, alt: "Citation verification - full size", onClose: () => setExpandedImageSrc(null) })) : null;
|
|
317
|
+
// Shared trigger element props
|
|
318
|
+
const triggerProps = {
|
|
319
|
+
"data-citation-id": citationKey,
|
|
320
|
+
"data-citation-instance": citationInstanceId,
|
|
321
|
+
className: cn("relative inline-flex items-baseline cursor-pointer", "px-0.5 -mx-0.5 rounded-sm", "transition-all duration-150", "hover:bg-blue-500/10 dark:hover:bg-blue-400/10", hasImage && "hover:cursor-zoom-in", className),
|
|
322
|
+
onMouseEnter: handleMouseEnter,
|
|
323
|
+
onMouseLeave: handleMouseLeave,
|
|
324
|
+
onClick: handleClick,
|
|
325
|
+
onTouchEndCapture: isMobile ? handleTouchEnd : undefined,
|
|
326
|
+
"aria-label": displayText ? `[${displayText}]` : undefined,
|
|
327
|
+
};
|
|
328
|
+
// Render with Radix Popover
|
|
329
|
+
if (shouldShowPopover) {
|
|
330
|
+
const popoverContentElement = renderPopoverContent ? (renderPopoverContent({
|
|
462
331
|
citation,
|
|
463
332
|
verification: verification ?? null,
|
|
464
333
|
status,
|
|
465
|
-
})) : (_jsx(DefaultPopoverContent, { citation: citation, verification: verification ?? null, status: status, onImageClick:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (typeof ref === "function") {
|
|
472
|
-
ref(node);
|
|
473
|
-
}
|
|
474
|
-
else if (ref) {
|
|
475
|
-
ref.current = node;
|
|
476
|
-
}
|
|
477
|
-
}, "data-citation-id": citationKey, "data-citation-instance": citationInstanceId, "data-tooltip-expanded": isTooltipExpanded, "data-has-image": !!verification?.verificationImageBase64, className: classNames("dc-citation", `dc-citation--${variant}`, foundStatusClass, className), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: handleToggleTooltip, onTouchEndCapture: isMobile ? handleTouchEnd : undefined, "aria-label": displayText ? `[${displayText}]` : undefined, "aria-expanded": isTooltipExpanded, children: renderCitationContent() }));
|
|
478
|
-
// Image overlay for full-size view
|
|
479
|
-
const imageOverlay = expandedImageSrc ? (_jsx(ImageOverlay, { src: expandedImageSrc, alt: "Citation verification - full size", onClose: handleCloseOverlay })) : null;
|
|
480
|
-
// Wrap with popover or status tooltip if needed
|
|
481
|
-
if (shouldShowPopover || shouldShowStatusTooltip) {
|
|
482
|
-
return (_jsxs(_Fragment, { children: [children, _jsxs("span", { className: "dc-popover-wrapper", ref: wrapperRef, "data-expanded": isTooltipExpanded, children: [citationTrigger, popoverContent, statusTooltipContent] }), imageOverlay] }));
|
|
334
|
+
})) : (_jsx(DefaultPopoverContent, { citation: citation, verification: verification ?? null, status: status, onImageClick: () => {
|
|
335
|
+
if (verification?.verificationImageBase64) {
|
|
336
|
+
setExpandedImageSrc(verification.verificationImageBase64);
|
|
337
|
+
}
|
|
338
|
+
} }));
|
|
339
|
+
return (_jsxs(_Fragment, { children: [children, _jsxs(Popover, { open: isHovering, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("span", { ref: ref, ...triggerProps, children: renderCitationContent() }) }), _jsx(PopoverContent, { side: popoverPosition === "bottom" ? "bottom" : "top", onPointerDownOutside: (e) => e.preventDefault(), onInteractOutside: (e) => e.preventDefault(), children: popoverContentElement })] }), imageOverlay] }));
|
|
483
340
|
}
|
|
484
|
-
|
|
341
|
+
// Render without popover
|
|
342
|
+
return (_jsxs(_Fragment, { children: [children, _jsx("span", { ref: ref, ...triggerProps, children: renderCitationContent() }), imageOverlay] }));
|
|
485
343
|
});
|
|
486
344
|
CitationComponent.displayName = "CitationComponent";
|
|
487
345
|
export const MemoizedCitationComponent = memo(CitationComponent);
|