@deepcitation/deepcitation-js 1.0.2 → 1.0.3
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 +71 -1197
- package/lib/client/DeepCitation.d.ts +204 -0
- package/lib/client/DeepCitation.js +473 -0
- package/lib/client/index.d.ts +2 -0
- package/lib/client/index.js +1 -0
- package/lib/client/types.d.ts +157 -0
- package/lib/client/types.js +1 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +22 -0
- package/lib/parsing/normalizeCitation.d.ts +5 -0
- package/lib/parsing/normalizeCitation.js +182 -0
- package/lib/parsing/parseCitation.d.ts +79 -0
- package/lib/parsing/parseCitation.js +371 -0
- package/lib/parsing/parseWorkAround.d.ts +2 -0
- package/lib/parsing/parseWorkAround.js +73 -0
- package/lib/prompts/citationPrompts.d.ts +133 -0
- package/lib/prompts/citationPrompts.js +152 -0
- package/lib/prompts/index.d.ts +3 -0
- package/lib/prompts/index.js +3 -0
- package/lib/prompts/promptCompression.d.ts +14 -0
- package/lib/prompts/promptCompression.js +109 -0
- package/lib/prompts/types.d.ts +4 -0
- package/lib/prompts/types.js +1 -0
- package/lib/react/CitationComponent.d.ts +134 -0
- package/lib/react/CitationComponent.js +376 -0
- package/lib/react/CitationVariants.d.ts +135 -0
- package/lib/react/CitationVariants.js +283 -0
- package/lib/react/DiffDisplay.d.ts +10 -0
- package/lib/react/DiffDisplay.js +33 -0
- package/lib/react/UrlCitationComponent.d.ts +83 -0
- package/lib/react/UrlCitationComponent.js +224 -0
- package/lib/react/VerificationTabs.d.ts +10 -0
- package/lib/react/VerificationTabs.js +36 -0
- package/lib/react/icons.d.ts +8 -0
- package/lib/react/icons.js +9 -0
- package/lib/react/index.d.ts +16 -0
- package/lib/react/index.js +18 -0
- package/lib/react/primitives.d.ts +104 -0
- package/lib/react/primitives.js +190 -0
- package/lib/react/types.d.ts +192 -0
- package/lib/react/types.js +1 -0
- package/lib/react/useSmartDiff.d.ts +16 -0
- package/lib/react/useSmartDiff.js +64 -0
- package/lib/react/utils.d.ts +34 -0
- package/lib/react/utils.js +59 -0
- package/lib/types/boxes.d.ts +11 -0
- package/lib/types/boxes.js +1 -0
- package/lib/types/citation.d.ts +44 -0
- package/lib/types/citation.js +2 -0
- package/lib/types/foundHighlight.d.ts +23 -0
- package/lib/types/foundHighlight.js +22 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/index.js +7 -0
- package/lib/types/search.d.ts +30 -0
- package/lib/types/search.js +1 -0
- package/lib/utils/sha.d.ts +10 -0
- package/lib/utils/sha.js +108 -0
- package/package.json +5 -2
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { CheckIcon, WarningIcon } from "./icons.js";
|
|
5
|
+
import { classNames, generateCitationInstanceId, generateCitationKey, getCitationDisplayText } from "./utils.js";
|
|
6
|
+
import { getCitationStatus } from "../parsing/parseCitation.js";
|
|
7
|
+
import "./styles.css";
|
|
8
|
+
const TWO_DOTS_THINKING_CONTENT = "..";
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// INDICATORS
|
|
11
|
+
// =============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Default indicator for verified citations (exact match).
|
|
14
|
+
* Shows a green checkmark.
|
|
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
|
+
*/
|
|
28
|
+
function getStatusLabel(status) {
|
|
29
|
+
if (status.isVerified && !status.isPartialMatch)
|
|
30
|
+
return "Verified";
|
|
31
|
+
if (status.isPartialMatch)
|
|
32
|
+
return "Partial Match";
|
|
33
|
+
if (status.isMiss)
|
|
34
|
+
return "Not Found";
|
|
35
|
+
if (status.isPending)
|
|
36
|
+
return "Verifying...";
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get popover status CSS class.
|
|
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.
|
|
76
|
+
*/
|
|
77
|
+
const StatusTooltipContent = ({ citation, status, foundCitation, isExpanded, onToggleExpand, }) => {
|
|
78
|
+
const { isMiss, isPartialMatch } = status;
|
|
79
|
+
if (!isMiss && !isPartialMatch)
|
|
80
|
+
return null;
|
|
81
|
+
// Get search attempts from foundCitation
|
|
82
|
+
const searchAttempts = foundCitation?.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.value || "";
|
|
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))) })] }))] }));
|
|
105
|
+
}
|
|
106
|
+
if (isPartialMatch) {
|
|
107
|
+
const expectedText = citation.fullPhrase || citation.value || "";
|
|
108
|
+
const actualText = foundCitation?.matchSnippet || "";
|
|
109
|
+
const truncatedExpected = expectedText.length > 100 ? expectedText.slice(0, 100) + "…" : expectedText;
|
|
110
|
+
const truncatedActual = actualText.length > 100 ? actualText.slice(0, 100) + "…" : actualText;
|
|
111
|
+
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" })] }), _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", 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 })] }))] }));
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Full-size image overlay component.
|
|
117
|
+
* Uses portal to render at document body level.
|
|
118
|
+
*/
|
|
119
|
+
const ImageOverlay = ({ src, alt, onClose }) => {
|
|
120
|
+
const handleBackdropClick = useCallback((e) => {
|
|
121
|
+
if (e.target === e.currentTarget) {
|
|
122
|
+
onClose();
|
|
123
|
+
}
|
|
124
|
+
}, [onClose]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const handleKeyDown = (e) => {
|
|
127
|
+
if (e.key === "Escape") {
|
|
128
|
+
onClose();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
132
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
133
|
+
}, [onClose]);
|
|
134
|
+
// Use portal to render at body level, avoiding any parent positioning issues
|
|
135
|
+
return createPortal(_jsx("div", { className: "dc-overlay", onClick: handleBackdropClick, role: "dialog", "aria-modal": "true", "aria-label": "Full size verification image", children: _jsx("div", { className: "dc-overlay-content", onClick: onClose, children: _jsx("img", { src: src, alt: alt, className: "dc-overlay-image", draggable: false }) }) }), document.body);
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Default popover content component.
|
|
139
|
+
* Shows verification image if available, otherwise shows text info.
|
|
140
|
+
*/
|
|
141
|
+
const DefaultPopoverContent = ({ foundCitation, status, onImageClick, }) => {
|
|
142
|
+
const hasImage = foundCitation?.verificationImageBase64;
|
|
143
|
+
const handleImageClick = useCallback((e) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
if (hasImage && onImageClick) {
|
|
147
|
+
onImageClick(foundCitation.verificationImageBase64);
|
|
148
|
+
}
|
|
149
|
+
}, [hasImage, foundCitation?.verificationImageBase64, onImageClick]);
|
|
150
|
+
// If we have a verification image, show only the image
|
|
151
|
+
if (hasImage) {
|
|
152
|
+
return (_jsx("button", { type: "button", className: "dc-popover-image-button", onClick: handleImageClick, "aria-label": "Click to view full size", children: _jsx("img", { src: foundCitation.verificationImageBase64, alt: "Citation verification", className: "dc-popover-image", loading: "lazy" }) }));
|
|
153
|
+
}
|
|
154
|
+
// No image - show text info
|
|
155
|
+
const statusLabel = getStatusLabel(status);
|
|
156
|
+
const statusClass = getPopoverStatusClass(status);
|
|
157
|
+
const hasSnippet = foundCitation?.matchSnippet;
|
|
158
|
+
const pageNumber = foundCitation?.pageNumber;
|
|
159
|
+
if (!hasSnippet && !statusLabel) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return (_jsxs(_Fragment, { children: [statusLabel && _jsx("span", { className: classNames("dc-popover-status", statusClass), children: statusLabel }), hasSnippet && _jsxs("span", { className: "dc-popover-snippet", children: ["\"", foundCitation.matchSnippet, "\""] }), pageNumber && pageNumber > 0 && _jsxs("span", { className: "dc-popover-page", children: ["Page ", pageNumber] })] }));
|
|
163
|
+
};
|
|
164
|
+
// =============================================================================
|
|
165
|
+
// MAIN COMPONENT
|
|
166
|
+
// =============================================================================
|
|
167
|
+
/**
|
|
168
|
+
* CitationComponent displays a citation with verification status.
|
|
169
|
+
*
|
|
170
|
+
* The component separates two concepts:
|
|
171
|
+
* 1. **Found status** (text styling) - whether the citation was found in the document
|
|
172
|
+
* - Verified & Partial both use "found" styling (blue text)
|
|
173
|
+
* - Miss uses "not found" styling (gray/strikethrough)
|
|
174
|
+
*
|
|
175
|
+
* 2. **Match quality** (indicator styling) - how well the citation matched
|
|
176
|
+
* - Exact match: green checkmark
|
|
177
|
+
* - Partial match: orange checkmark
|
|
178
|
+
* - Miss: no indicator
|
|
179
|
+
*
|
|
180
|
+
* This means partial matches have blue text (because they were found) but
|
|
181
|
+
* an orange indicator (because they didn't match exactly).
|
|
182
|
+
*/
|
|
183
|
+
export const CitationComponent = forwardRef(({ citation, children, className, displayCitationValue = false, fallbackDisplay, foundCitation, variant = "brackets", eventHandlers, isMobile = false, renderIndicator, renderContent, popoverPosition = "top", renderPopoverContent, }, ref) => {
|
|
184
|
+
const containerRef = useRef(null);
|
|
185
|
+
const wrapperRef = useRef(null);
|
|
186
|
+
const [expandedImageSrc, setExpandedImageSrc] = useState(null);
|
|
187
|
+
const [isTooltipExpanded, setIsTooltipExpanded] = useState(false);
|
|
188
|
+
const [isPhrasesExpanded, setIsPhrasesExpanded] = useState(false);
|
|
189
|
+
const handleImageClick = useCallback((imageSrc) => {
|
|
190
|
+
setExpandedImageSrc(imageSrc);
|
|
191
|
+
}, []);
|
|
192
|
+
const handleCloseOverlay = useCallback(() => {
|
|
193
|
+
setExpandedImageSrc(null);
|
|
194
|
+
}, []);
|
|
195
|
+
const handleTogglePhrases = useCallback((e) => {
|
|
196
|
+
e?.preventDefault();
|
|
197
|
+
e?.stopPropagation();
|
|
198
|
+
setIsPhrasesExpanded(prev => !prev);
|
|
199
|
+
}, []);
|
|
200
|
+
// Handle click outside to close expanded tooltip
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!isTooltipExpanded)
|
|
203
|
+
return;
|
|
204
|
+
const handleClickOutside = (event) => {
|
|
205
|
+
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
206
|
+
setIsTooltipExpanded(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
// Use capture phase to handle clicks before they bubble
|
|
210
|
+
document.addEventListener("mousedown", handleClickOutside, true);
|
|
211
|
+
return () => {
|
|
212
|
+
document.removeEventListener("mousedown", handleClickOutside, true);
|
|
213
|
+
};
|
|
214
|
+
}, [isTooltipExpanded]);
|
|
215
|
+
// Handle escape key to close expanded tooltip
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!isTooltipExpanded)
|
|
218
|
+
return;
|
|
219
|
+
const handleEscape = (event) => {
|
|
220
|
+
if (event.key === "Escape") {
|
|
221
|
+
setIsTooltipExpanded(false);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
document.addEventListener("keydown", handleEscape);
|
|
225
|
+
return () => {
|
|
226
|
+
document.removeEventListener("keydown", handleEscape);
|
|
227
|
+
};
|
|
228
|
+
}, [isTooltipExpanded]);
|
|
229
|
+
const citationKey = useMemo(() => generateCitationKey(citation), [citation]);
|
|
230
|
+
const citationInstanceId = useMemo(() => generateCitationInstanceId(citationKey), [citationKey]);
|
|
231
|
+
const handleToggleTooltip = useCallback((e) => {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
e.stopPropagation();
|
|
234
|
+
// If we have a verification image
|
|
235
|
+
if (foundCitation?.verificationImageBase64) {
|
|
236
|
+
if (expandedImageSrc) {
|
|
237
|
+
// Image is open - close it and unpin
|
|
238
|
+
setExpandedImageSrc(null);
|
|
239
|
+
setIsTooltipExpanded(false);
|
|
240
|
+
}
|
|
241
|
+
else if (isTooltipExpanded) {
|
|
242
|
+
// Already pinned - second click expands image
|
|
243
|
+
setExpandedImageSrc(foundCitation.verificationImageBase64);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// First click - just pin the popover open
|
|
247
|
+
setIsTooltipExpanded(true);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// No image - toggle phrases expansion for miss/partial tooltips
|
|
252
|
+
setIsTooltipExpanded(prev => !prev);
|
|
253
|
+
setIsPhrasesExpanded(prev => !prev);
|
|
254
|
+
}
|
|
255
|
+
eventHandlers?.onClick?.(citation, citationKey, e);
|
|
256
|
+
}, [
|
|
257
|
+
eventHandlers,
|
|
258
|
+
citation,
|
|
259
|
+
citationKey,
|
|
260
|
+
foundCitation?.verificationImageBase64,
|
|
261
|
+
expandedImageSrc,
|
|
262
|
+
isTooltipExpanded,
|
|
263
|
+
]);
|
|
264
|
+
const status = getCitationStatus(foundCitation ?? null);
|
|
265
|
+
// const { isVerified, isPending } = status;
|
|
266
|
+
const { isMiss, isPartialMatch, isVerified, isPending } = status;
|
|
267
|
+
const displayText = useMemo(() => {
|
|
268
|
+
// For numeric variant, always show the citation number
|
|
269
|
+
if (variant === "numeric") {
|
|
270
|
+
return citation.citationNumber?.toString() ?? "";
|
|
271
|
+
}
|
|
272
|
+
// For text/minimal/brackets, show the value or fullPhrase
|
|
273
|
+
return getCitationDisplayText(citation, {
|
|
274
|
+
displayCitationValue: variant === "text" || variant === "minimal" || variant === "brackets" || displayCitationValue,
|
|
275
|
+
fallbackDisplay,
|
|
276
|
+
});
|
|
277
|
+
}, [citation, variant, displayCitationValue, fallbackDisplay]);
|
|
278
|
+
// Found status class for text styling (blue for found, gray for miss)
|
|
279
|
+
const foundStatusClass = useMemo(() => getFoundStatusClass(status), [status]);
|
|
280
|
+
// Event handlers
|
|
281
|
+
const handleMouseEnter = useCallback(() => {
|
|
282
|
+
eventHandlers?.onMouseEnter?.(citation, citationKey);
|
|
283
|
+
}, [eventHandlers, citation, citationKey]);
|
|
284
|
+
const handleMouseLeave = useCallback(() => {
|
|
285
|
+
eventHandlers?.onMouseLeave?.(citation, citationKey);
|
|
286
|
+
}, [eventHandlers, citation, citationKey]);
|
|
287
|
+
const handleTouchEnd = useCallback((e) => {
|
|
288
|
+
if (isMobile) {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
eventHandlers?.onTouchEnd?.(citation, citationKey, e);
|
|
292
|
+
}
|
|
293
|
+
}, [eventHandlers, citation, citationKey, isMobile]);
|
|
294
|
+
// Early return for miss with fallback display
|
|
295
|
+
if (fallbackDisplay !== null && fallbackDisplay !== undefined && displayCitationValue && isMiss) {
|
|
296
|
+
return _jsx("span", { className: classNames("dc-citation-fallback", className), children: fallbackDisplay });
|
|
297
|
+
}
|
|
298
|
+
// Render the appropriate indicator based on match quality
|
|
299
|
+
const renderStatusIndicator = () => {
|
|
300
|
+
if (renderIndicator) {
|
|
301
|
+
return renderIndicator(status);
|
|
302
|
+
}
|
|
303
|
+
if (isVerified) {
|
|
304
|
+
return _jsx(DefaultVerifiedIndicator, {});
|
|
305
|
+
}
|
|
306
|
+
else if (isPartialMatch) {
|
|
307
|
+
return _jsx(DefaultPartialIndicator, {});
|
|
308
|
+
}
|
|
309
|
+
else if (isPending) {
|
|
310
|
+
return (_jsx("span", { className: "dc-indicator dc-indicator--pending", "aria-hidden": "true", children: TWO_DOTS_THINKING_CONTENT }));
|
|
311
|
+
}
|
|
312
|
+
else if (isMiss) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
// Render the citation content based on variant
|
|
318
|
+
const renderCitationContent = () => {
|
|
319
|
+
// Custom render function takes full control
|
|
320
|
+
if (renderContent) {
|
|
321
|
+
return renderContent({
|
|
322
|
+
citation,
|
|
323
|
+
status,
|
|
324
|
+
citationKey,
|
|
325
|
+
displayText,
|
|
326
|
+
isMergedDisplay: variant === "text" || variant === "brackets" || displayCitationValue,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// Indicator-only variant - just the checkmark/warning
|
|
330
|
+
if (variant === "indicator") {
|
|
331
|
+
return _jsx("span", { className: "dc-citation-text", children: renderStatusIndicator() });
|
|
332
|
+
}
|
|
333
|
+
// Text variant - no special styling, shows value with indicator
|
|
334
|
+
if (variant === "text") {
|
|
335
|
+
return (_jsxs("span", { className: "dc-citation-text dc-citation-text--plain", children: [displayText, renderStatusIndicator()] }));
|
|
336
|
+
}
|
|
337
|
+
// Minimal variant - no brackets, just text with indicator
|
|
338
|
+
if (variant === "minimal") {
|
|
339
|
+
return (_jsxs("span", { className: "dc-citation-text", children: [displayText, renderStatusIndicator()] }));
|
|
340
|
+
}
|
|
341
|
+
// Numeric variant - shows citation number with indicator, no brackets
|
|
342
|
+
if (variant === "numeric") {
|
|
343
|
+
return (_jsxs("span", { className: "dc-citation-text", children: [displayText, renderStatusIndicator()] }));
|
|
344
|
+
}
|
|
345
|
+
// Brackets variant (default) - value/number in brackets with styling
|
|
346
|
+
return (_jsxs("span", { className: "dc-citation-bracket", "aria-hidden": "true", role: "presentation", children: ["[", _jsxs("span", { className: "dc-citation-text", children: [displayText, renderStatusIndicator()] }), "]"] }));
|
|
347
|
+
};
|
|
348
|
+
// Determine if popover should be shown
|
|
349
|
+
const isPopoverHidden = popoverPosition === "hidden";
|
|
350
|
+
const shouldShowPopover = !isPopoverHidden && foundCitation && (foundCitation.verificationImageBase64 || foundCitation.matchSnippet);
|
|
351
|
+
// Determine if status tooltip should be shown (miss/partial without full verification)
|
|
352
|
+
const shouldShowStatusTooltip = !isPopoverHidden && (isMiss || isPartialMatch) && !shouldShowPopover;
|
|
353
|
+
// Popover content - determine position class (only "top" or "bottom" add classes)
|
|
354
|
+
const popoverPositionClass = popoverPosition === "bottom" ? "dc-popover--bottom" : "";
|
|
355
|
+
const popoverContent = shouldShowPopover ? (_jsx("span", { className: classNames("dc-popover", popoverPositionClass), children: renderPopoverContent ? (renderPopoverContent({ citation, foundCitation: foundCitation ?? null, status })) : (_jsx(DefaultPopoverContent, { citation: citation, foundCitation: foundCitation ?? null, status: status, onImageClick: handleImageClick })) })) : null;
|
|
356
|
+
// Status tooltip for miss/partial explanations
|
|
357
|
+
const statusTooltipContent = shouldShowStatusTooltip ? (_jsx(StatusTooltipContent, { citation: citation, status: status, foundCitation: foundCitation ?? null, isExpanded: isPhrasesExpanded, onToggleExpand: handleTogglePhrases })) : null;
|
|
358
|
+
const citationTrigger = (_jsx("span", { ref: node => {
|
|
359
|
+
containerRef.current = node;
|
|
360
|
+
if (typeof ref === "function") {
|
|
361
|
+
ref(node);
|
|
362
|
+
}
|
|
363
|
+
else if (ref) {
|
|
364
|
+
ref.current = node;
|
|
365
|
+
}
|
|
366
|
+
}, "data-citation-id": citationKey, "data-citation-instance": citationInstanceId, "data-tooltip-expanded": isTooltipExpanded, "data-has-image": !!foundCitation?.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() }));
|
|
367
|
+
// Image overlay for full-size view
|
|
368
|
+
const imageOverlay = expandedImageSrc ? (_jsx(ImageOverlay, { src: expandedImageSrc, alt: "Citation verification - full size", onClose: handleCloseOverlay })) : null;
|
|
369
|
+
// Wrap with popover or status tooltip if needed
|
|
370
|
+
if (shouldShowPopover || shouldShowStatusTooltip) {
|
|
371
|
+
return (_jsxs(_Fragment, { children: [children, _jsxs("span", { className: "dc-popover-wrapper", ref: wrapperRef, "data-expanded": isTooltipExpanded, children: [citationTrigger, popoverContent, statusTooltipContent] }), imageOverlay] }));
|
|
372
|
+
}
|
|
373
|
+
return (_jsxs(_Fragment, { children: [children, citationTrigger, imageOverlay] }));
|
|
374
|
+
});
|
|
375
|
+
CitationComponent.displayName = "CitationComponent";
|
|
376
|
+
export const MemoizedCitationComponent = memo(CitationComponent);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { CitationStatus } from "../types/citation.js";
|
|
3
|
+
import type { FoundHighlightLocation } from "../types/foundHighlight.js";
|
|
4
|
+
import type { SearchState } from "../types/search.js";
|
|
5
|
+
import type { BaseCitationProps, CitationVariant as CitationVariantType, CitationEventHandlers } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Shared props for all citation variant components.
|
|
8
|
+
*/
|
|
9
|
+
export interface CitationVariantProps extends BaseCitationProps {
|
|
10
|
+
/** Found citation highlight location data */
|
|
11
|
+
foundCitation?: FoundHighlightLocation | null;
|
|
12
|
+
/** Current search state for the citation */
|
|
13
|
+
searchState?: SearchState | null;
|
|
14
|
+
/** Event handlers */
|
|
15
|
+
eventHandlers?: CitationEventHandlers;
|
|
16
|
+
/** Whether on mobile device */
|
|
17
|
+
isMobile?: boolean;
|
|
18
|
+
/** Whether tooltips should be prevented */
|
|
19
|
+
preventTooltips?: boolean;
|
|
20
|
+
/** Custom pending text content */
|
|
21
|
+
pendingContent?: ReactNode;
|
|
22
|
+
/** Custom render function for verified indicator */
|
|
23
|
+
renderVerifiedIndicator?: (status: CitationStatus) => ReactNode;
|
|
24
|
+
/** Custom render function for partial match indicator */
|
|
25
|
+
renderPartialIndicator?: (status: CitationStatus) => ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export interface ChipCitationProps extends CitationVariantProps {
|
|
28
|
+
/** Chip size */
|
|
29
|
+
size?: "sm" | "md" | "lg";
|
|
30
|
+
/** Whether to show an icon before the text */
|
|
31
|
+
showIcon?: boolean;
|
|
32
|
+
/** Custom icon to display */
|
|
33
|
+
icon?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Chip/Badge style citation component.
|
|
37
|
+
* Displays citation as a rounded pill/badge.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <ChipCitation citation={citation} foundCitation={found} size="md" />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare const ChipCitation: React.ForwardRefExoticComponent<ChipCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
45
|
+
export interface SuperscriptCitationProps extends CitationVariantProps {
|
|
46
|
+
/** Whether to show brackets around the superscript */
|
|
47
|
+
showBrackets?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Superscript style citation component.
|
|
51
|
+
* Displays citation as a superscript number like academic papers.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <SuperscriptCitation citation={citation} foundCitation={found} />
|
|
56
|
+
* // Renders: Text content¹
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare const SuperscriptCitation: React.ForwardRefExoticComponent<SuperscriptCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
60
|
+
export interface FootnoteCitationProps extends CitationVariantProps {
|
|
61
|
+
/** Footnote symbol style */
|
|
62
|
+
symbolStyle?: "number" | "asterisk" | "dagger" | "custom";
|
|
63
|
+
/** Custom symbol (when symbolStyle is "custom") */
|
|
64
|
+
customSymbol?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Footnote style citation component.
|
|
68
|
+
* Displays citation as a footnote marker.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <FootnoteCitation citation={citation} symbolStyle="asterisk" />
|
|
73
|
+
* // Renders: Text content*
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const FootnoteCitation: React.ForwardRefExoticComponent<FootnoteCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
77
|
+
export interface InlineCitationProps extends CitationVariantProps {
|
|
78
|
+
/** Underline style */
|
|
79
|
+
underlineStyle?: "solid" | "dotted" | "dashed" | "none";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Inline style citation component.
|
|
83
|
+
* Displays citation inline with subtle underline decoration.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <InlineCitation citation={citation} underlineStyle="dotted" />
|
|
88
|
+
* // Renders: "quoted text" with subtle underline
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare const InlineCitation: React.ForwardRefExoticComponent<InlineCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
92
|
+
export interface MinimalCitationProps extends CitationVariantProps {
|
|
93
|
+
/** Whether to show status indicator */
|
|
94
|
+
showStatusIndicator?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Minimal style citation component.
|
|
98
|
+
* Displays just the citation number with minimal decoration.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* <MinimalCitation citation={citation} />
|
|
103
|
+
* // Renders: 1
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare const MinimalCitation: React.ForwardRefExoticComponent<MinimalCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
107
|
+
export interface VariantCitationProps extends CitationVariantProps {
|
|
108
|
+
/** The variant to render */
|
|
109
|
+
variant?: CitationVariantType;
|
|
110
|
+
/** Chip-specific props */
|
|
111
|
+
chipProps?: Partial<ChipCitationProps>;
|
|
112
|
+
/** Superscript-specific props */
|
|
113
|
+
superscriptProps?: Partial<SuperscriptCitationProps>;
|
|
114
|
+
/** Footnote-specific props */
|
|
115
|
+
footnoteProps?: Partial<FootnoteCitationProps>;
|
|
116
|
+
/** Inline-specific props */
|
|
117
|
+
inlineProps?: Partial<InlineCitationProps>;
|
|
118
|
+
/** Minimal-specific props */
|
|
119
|
+
minimalProps?: Partial<MinimalCitationProps>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Factory component that renders the appropriate citation variant.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* <CitationVariantFactory variant="chip" citation={citation} chipProps={{ size: "lg" }} />
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare const CitationVariantFactory: React.ForwardRefExoticComponent<VariantCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
130
|
+
export declare const MemoizedChipCitation: React.NamedExoticComponent<ChipCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
131
|
+
export declare const MemoizedSuperscriptCitation: React.NamedExoticComponent<SuperscriptCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
132
|
+
export declare const MemoizedFootnoteCitation: React.NamedExoticComponent<FootnoteCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
133
|
+
export declare const MemoizedInlineCitation: React.NamedExoticComponent<InlineCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
134
|
+
export declare const MemoizedMinimalCitation: React.NamedExoticComponent<MinimalCitationProps & React.RefAttributes<HTMLSpanElement>>;
|
|
135
|
+
export declare const MemoizedCitationVariantFactory: React.NamedExoticComponent<VariantCitationProps & React.RefAttributes<HTMLSpanElement>>;
|