Skip to content
Merged
4 changes: 2 additions & 2 deletions src/__tests__/StatusHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ describe("StatusHeader", () => {
// Status text "Verified" is now shown instead of echoing anchor text
const { container } = render(<StatusHeader status="found" foundPage={5} anchorText="increased by 15%" />);

expect(container.textContent).toContain("Verified");
expect(container.textContent).toContain("DeepCitation Verified");
expect(container.textContent).toContain("p.\u202f5");
});

it("renders status text for not found citations", () => {
// Status text "Not found" is now shown
const { container } = render(<StatusHeader status="not_found" expectedPage={5} anchorText="increased by 15%" />);

expect(container.textContent).toContain("Not Found");
expect(container.textContent).toContain("DeepCitation Not Found");
expect(container.textContent).toContain("p.\u202f5");
});

Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import {
describe("createTranslator", () => {
it("returns default English messages when no overrides provided", () => {
const t = createTranslator();
expect(t("status.verified")).toBe("Verified");
expect(t("status.notFound")).toBe("Not Found");
expect(t("status.verified")).toBe("DeepCitation Verified");
expect(t("status.notFound")).toBe("DeepCitation Not Found");
});

it("overrides specific messages while falling back to defaults", () => {
const t = createTranslator({ "status.verified": "Vérifié" });
expect(t("status.verified")).toBe("Vérifié");
expect(t("status.notFound")).toBe("Not Found"); // fallback
expect(t("status.notFound")).toBe("DeepCitation Not Found"); // fallback
});

it("interpolates {placeholder} values", () => {
Expand Down Expand Up @@ -60,7 +60,7 @@ describe("createTranslator", () => {

it("is a no-op when values are passed but template has no placeholders", () => {
const t = createTranslator();
expect(t("status.verified", { foo: "bar" })).toBe("Verified");
expect(t("status.verified", { foo: "bar" })).toBe("DeepCitation Verified");
});
});

Expand All @@ -70,7 +70,7 @@ describe("createTranslator", () => {

describe("defaultTranslator", () => {
it("is a pre-built translator using default messages", () => {
expect(defaultTranslator("status.verified")).toBe("Verified");
expect(defaultTranslator("status.verified")).toBe("DeepCitation Verified");
expect(defaultTranslator("status.verifying")).toBe("Verifying\u2026");
});
});
Expand Down Expand Up @@ -144,7 +144,7 @@ describe("DeepCitationI18nProvider", () => {

it("provides default translations when no provider is present", () => {
render(<TestConsumer msgKey="status.verified" />);
expect(screen.getByTestId("output").textContent).toBe("Verified");
expect(screen.getByTestId("output").textContent).toBe("DeepCitation Verified");
});

it("provides custom translations via provider", () => {
Expand All @@ -163,7 +163,7 @@ describe("DeepCitationI18nProvider", () => {
<TestConsumer msgKey="status.notFound" />
</DeepCitationI18nProvider>,
);
expect(screen.getByTestId("output").textContent).toBe("Not Found");
expect(screen.getByTestId("output").textContent).toBe("DeepCitation Not Found");
});

it("supports interpolation through the provider", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/i18nLocales.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe("locale message dictionaries", () => {
});

it("overrides a core status label for each locale", () => {
expect(frMessages["status.verified"]).toBe("Vérifié");
expect(esMessages["status.verified"]).toBe("Verificado");
expect(viMessages["status.verified"]).toBe("Đã xác minh");
expect(frMessages["status.verified"]).toBe("DeepCitation Vérifié");
expect(esMessages["status.verified"]).toBe("DeepCitation Verificado");
expect(viMessages["status.verified"]).toBe("DeepCitation Đã xác minh");
});
});
33 changes: 33 additions & 0 deletions src/__tests__/reactUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getCitationAnchorText,
getCitationDisplayText,
getCitationNumber,
truncateMiddle,
} from "../react/utils.js";
import type { Citation } from "../types/citation.js";
import { isUrlCitation } from "../types/citation.js";
Expand Down Expand Up @@ -132,4 +133,36 @@ describe("react utils", () => {
expect(getCitationKey(urlCitation)).toBe(getCitationKey(urlCitation));
});
});

describe("truncateMiddle", () => {
it("returns the original string when within maxLength", () => {
expect(truncateMiddle("abcde", 5)).toBe("abcde");
expect(truncateMiddle("abcde", 10)).toBe("abcde");
});

it("truncates in the middle with ellipsis", () => {
expect(truncateMiddle("abcdefgh", 5)).toBe("ab…gh");
expect(truncateMiddle("abcdefgh", 4)).toBe("a…gh");
});

it("handles maxLength=2", () => {
expect(truncateMiddle("abcde", 2)).toBe("…e");
});

it("handles maxLength=1", () => {
expect(truncateMiddle("abcde", 1)).toBe("…");
});

it("handles maxLength=0", () => {
expect(truncateMiddle("abcde", 0)).toBe("");
});

it("handles negative maxLength", () => {
expect(truncateMiddle("abcde", -1)).toBe("");
});

it("handles empty string", () => {
expect(truncateMiddle("", 5)).toBe("");
});
});
});
6 changes: 3 additions & 3 deletions src/react/CitationContentDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import { CheckIcon, ChevronDownIcon, XIcon } from "./icons.js";
import { handleImageError } from "./imageUtils.js";
import type { CitationContent, CitationRenderProps, CitationVariant } from "./types.js";
import { cn } from "./utils.js";
import { cn, truncateMiddle } from "./utils.js";

// =============================================================================
// CITATION CONTENT DISPLAY COMPONENT
Expand Down Expand Up @@ -107,7 +107,7 @@ export const CitationContentDisplay = ({
isMiss && !shouldShowSpinner && "opacity-70",
)}
>
{displayText}
{truncateMiddle(displayText, 30)}
</span>
{/* aria-hidden: the button's aria-label already describes status; the live region
(statusDescId) announces changes. Including role="img" aria-labels here as
Expand Down Expand Up @@ -221,7 +221,7 @@ export const CitationContentDisplay = ({
)}
style={isMiss && !shouldShowSpinner ? MISS_WAVY_UNDERLINE_STYLE : undefined}
>
{displayText}
{truncateMiddle(displayText, 30)}
</span>
{additionalCount !== undefined && additionalCount > 0 && (
<span className="text-dc-subtle-foreground">+{additionalCount}</span>
Expand Down
10 changes: 4 additions & 6 deletions src/react/UrlCitationComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { handleImageError } from "./imageUtils.js";
import type { IndicatorVariant, UrlCitationProps, UrlFetchStatus } from "./types.js";
import { isBlockedStatus, isErrorStatus } from "./urlStatus.js";
import { getUrlPath, safeWindowOpen, truncateString } from "./urlUtils.js";
import { cn, generateCitationInstanceId } from "./utils.js";
import { cn, generateCitationInstanceId, truncateMiddle } from "./utils.js";

function getUrlStatusLabel(fetchStatus: UrlFetchStatus, t: TranslateFunction): string {
const KEY_MAP: Record<UrlFetchStatus, MessageKey> = {
Expand Down Expand Up @@ -358,7 +358,7 @@ export const UrlCitationComponent = forwardRef<HTMLSpanElement, UrlCitationProps

const displayText = useMemo(() => {
if (showTitle && title) {
return truncateString(title, maxDisplayLength);
return truncateMiddle(title, maxDisplayLength);
}
// Show domain + truncated path
const pathPart = path ? truncateString(path, maxDisplayLength - domain.length - 1) : "";
Expand Down Expand Up @@ -521,7 +521,7 @@ export const UrlCitationComponent = forwardRef<HTMLSpanElement, UrlCitationProps
data-fetch-status={fetchStatus}
data-variant="chip"
className={cn(
"group inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-sm cursor-pointer transition-colors no-underline mr-0.5",
"group inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-sm cursor-pointer transition-colors no-underline mr-0.5 min-w-0",
"bg-dc-muted text-dc-foreground",
"hover:bg-dc-muted",
isBroken && "opacity-60",
Expand All @@ -537,9 +537,7 @@ export const UrlCitationComponent = forwardRef<HTMLSpanElement, UrlCitationProps
aria-label={t("aria.linkToDomainStatus", { domain: displayText || domain, status: statusLabel })}
>
{showFavicon && <DefaultFavicon url={url} faviconUrl={faviconUrl} />}
<span className="max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap text-dc-foreground">
{displayText}
</span>
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-dc-foreground">{displayText}</span>
{showStatusIndicator && statusIndicatorElement}
{externalLinkButtonElement}
</span>
Expand Down
5 changes: 1 addition & 4 deletions src/react/VerificationLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ const MAX_QUOTE_BOX_LENGTH = 150;
/** Maximum length for anchor text preview in headers */
const MAX_ANCHOR_TEXT_PREVIEW_LENGTH = 50;

/** Maximum length for URL display in popover header */
const MAX_URL_DISPLAY_LENGTH = 45;

/** Icon color classes by status - defined outside component to avoid recreation on every render */
const ICON_COLOR_CLASSES = {
green: "text-dc-verified",
Expand Down Expand Up @@ -485,7 +482,7 @@ export function SourceContextHeader({
fetchStatus: mapSearchStatusToUrlFetchStatus(status),
}}
variant="chip"
maxDisplayLength={MAX_URL_DISPLAY_LENGTH}
maxDisplayLength={45}
preventTooltips={true}
showStatusIndicator={false}
showTitle={!!sourceLabel}
Expand Down
8 changes: 1 addition & 7 deletions src/react/evidence/EvidenceTray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,7 @@ export function EvidenceTray({
const borderClass = isMiss ? EVIDENCE_TRAY_BORDER_DASHED : EVIDENCE_TRAY_BORDER_SOLID;
const prefersReducedMotion = usePrefersReducedMotion();

// Detect verified citations where the input citation lacked precise location data
// (no page number or no line IDs). The text was found, but we can't point the user
// to the exact expected location the way a fully-specified citation can.
const citation = verification?.citation;
const hasPageNumber = citation?.pageNumber != null && citation.pageNumber > 0;
const hasLineIds = citation?.lineIds != null && citation.lineIds.length > 0;
const isImpreciseLocation = status.isVerified && !isMiss && !isPartialMatch && (!hasPageNumber || !hasLineIds);
const isImpreciseLocation = verification?.isImpreciseLocation === true;

// Tray-level click: keyhole click if available, else page expansion
const trayAction = onImageClick ?? onExpand;
Expand Down
14 changes: 7 additions & 7 deletions src/react/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ export function useLocale(): string | undefined {
*/
export const defaultMessages = {
// ── Status labels ──────────────────────────────────────────────
"status.verified": "Verified",
"status.partialMatch": "Partial Match",
"status.notFound": "Not Found",
"status.verified": "DeepCitation Verified",
"status.partialMatch": "DeepCitation Partial Match",
"status.notFound": "DeepCitation Not Found",
"status.verifying": "Verifying\u2026",

// ── Outcome labels ─────────────────────────────────────────────
Expand Down Expand Up @@ -177,9 +177,9 @@ export const defaultMessages = {
"aria.citationWithStatus": "Citation: {displayText} ({status})",
"aria.citationNumber": "Citation {number}",
"aria.footnoteSymbol": "Footnote {symbol}",
"aria.statusSuffix.notFound": "not found",
"aria.statusSuffix.partialMatch": "partial match",
"aria.statusSuffix.verified": "verified",
"aria.statusSuffix.notFound": "DeepCitation not found",
"aria.statusSuffix.partialMatch": "DeepCitation partial match",
"aria.statusSuffix.verified": "DeepCitation verified",
"aria.statusSuffix.pendingVerification": "pending verification",
"aria.linkToDomainStatus": "Link to {domain}: {status}",
"aria.viewProofForSource": "View proof for {sourceName}",
Expand Down Expand Up @@ -393,7 +393,7 @@ export type TranslateFunction = (key: MessageKey, values?: MessageValues) => str
* ```ts
* const t = createTranslator({ "status.verified": "Vérifié" });
* t("status.verified"); // "Vérifié"
* t("status.notFound"); // "Not Found" (English fallback)
* t("status.notFound"); // "DeepCitation Not Found" (English fallback)
* ```
*/
export function createTranslator(messages: Partial<DeepCitationMessages> = {}): TranslateFunction {
Expand Down
1 change: 1 addition & 0 deletions src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export {
getCitationAnchorText,
getCitationDisplayText,
getCitationNumber,
truncateMiddle,
} from "./utils.js";
// Verification Log Components (Search attempt timeline display)
export {
Expand Down
12 changes: 6 additions & 6 deletions src/react/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { type DeepCitationMessages, defaultMessages } from "../i18n.js";
* It is used by the i18n parity test to verify all keys are covered.
*/
export const esOverrides = {
"status.verified": "Verificado",
"status.partialMatch": "Coincidencia parcial",
"status.notFound": "No encontrado",
"status.verified": "DeepCitation Verificado",
"status.partialMatch": "DeepCitation Coincidencia parcial",
"status.notFound": "DeepCitation No encontrado",
"status.verifying": "Verificando…",
"outcome.exactMatch": "Coincidencia exacta",
"outcome.normalizedMatch": "Coincidencia normalizada",
Expand Down Expand Up @@ -95,9 +95,9 @@ export const esOverrides = {
"aria.citationWithStatus": "Cita: {displayText} ({status})",
"aria.citationNumber": "Cita {number}",
"aria.footnoteSymbol": "Nota {symbol}",
"aria.statusSuffix.notFound": "no encontrado",
"aria.statusSuffix.partialMatch": "coincidencia parcial",
"aria.statusSuffix.verified": "verificado",
"aria.statusSuffix.notFound": "DeepCitation no encontrado",
"aria.statusSuffix.partialMatch": "DeepCitation coincidencia parcial",
"aria.statusSuffix.verified": "DeepCitation verificado",
"aria.statusSuffix.pendingVerification": "verificación pendiente",
"aria.linkToDomainStatus": "Enlace a {domain}: {status}",
"aria.viewProofForSource": "Ver prueba para {sourceName}",
Expand Down
12 changes: 6 additions & 6 deletions src/react/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { type DeepCitationMessages, defaultMessages } from "../i18n.js";
* It is used by the i18n parity test to verify all keys are covered.
*/
export const frOverrides = {
"status.verified": "Vérifié",
"status.partialMatch": "Correspondance partielle",
"status.notFound": "Introuvable",
"status.verified": "DeepCitation Vérifié",
"status.partialMatch": "DeepCitation Correspondance partielle",
"status.notFound": "DeepCitation Introuvable",
"status.verifying": "Vérification…",
"outcome.exactMatch": "Correspondance exacte",
"outcome.normalizedMatch": "Correspondance normalisée",
Expand Down Expand Up @@ -95,9 +95,9 @@ export const frOverrides = {
"aria.citationWithStatus": "Citation : {displayText} ({status})",
"aria.citationNumber": "Citation {number}",
"aria.footnoteSymbol": "Note {symbol}",
"aria.statusSuffix.notFound": "introuvable",
"aria.statusSuffix.partialMatch": "correspondance partielle",
"aria.statusSuffix.verified": "vérifiée",
"aria.statusSuffix.notFound": "DeepCitation introuvable",
"aria.statusSuffix.partialMatch": "DeepCitation correspondance partielle",
"aria.statusSuffix.verified": "DeepCitation vérifiée",
"aria.statusSuffix.pendingVerification": "vérification en attente",
"aria.linkToDomainStatus": "Lien vers {domain} : {status}",
"aria.viewProofForSource": "Voir la preuve pour {sourceName}",
Expand Down
12 changes: 6 additions & 6 deletions src/react/locales/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { type DeepCitationMessages, defaultMessages } from "../i18n.js";
* It is used by the i18n parity test to verify all keys are covered.
*/
export const viOverrides = {
"status.verified": "Đã xác minh",
"status.partialMatch": "Khớp một phần",
"status.notFound": "Không tìm thấy",
"status.verified": "DeepCitation Đã xác minh",
"status.partialMatch": "DeepCitation Khớp một phần",
"status.notFound": "DeepCitation Không tìm thấy",
"status.verifying": "Đang xác minh…",
"outcome.exactMatch": "Khớp chính xác",
"outcome.normalizedMatch": "Khớp đã chuẩn hóa",
Expand Down Expand Up @@ -97,9 +97,9 @@ export const viOverrides = {
"aria.citationWithStatus": "Trích dẫn: {displayText} ({status})",
"aria.citationNumber": "Trích dẫn {number}",
"aria.footnoteSymbol": "Chú thích {symbol}",
"aria.statusSuffix.notFound": "không tìm thấy",
"aria.statusSuffix.partialMatch": "khớp một phần",
"aria.statusSuffix.verified": "đã xác minh",
"aria.statusSuffix.notFound": "DeepCitation không tìm thấy",
"aria.statusSuffix.partialMatch": "DeepCitation khớp một phần",
"aria.statusSuffix.verified": "DeepCitation đã xác minh",
"aria.statusSuffix.pendingVerification": "đang chờ xác minh",
"aria.linkToDomainStatus": "Liên kết đến {domain}: {status}",
"aria.viewProofForSource": "Xem bằng chứng cho {sourceName}",
Expand Down
12 changes: 12 additions & 0 deletions src/react/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ function applyReferenceSpacing(garbled: string, reference: string): string {
return result;
}

/**
* Truncates a string in the middle, preserving the start and end.
* Useful for API keys, IDs, and hashes where both prefix and suffix matter.
*/
export function truncateMiddle(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
if (maxLength <= 1) return maxLength === 1 ? "…" : "";
const half = Math.floor((maxLength - 1) / 2);
const endLength = maxLength - 1 - half;
return `${str.slice(0, half)}…${str.slice(-endLength)}`;
}

/** Returns true when the verification source is a raster image (not a PDF). */
export function isImageSource(verification: Verification | null | undefined): boolean {
const mt = verification?.document?.mimeType;
Expand Down
5 changes: 5 additions & 0 deletions src/types/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ export interface Verification {
/** Converted artifact download (PDF rendition, transcript, …). Present for URL/Office inputs. */
convertedDownload?: FileDownload;

// ========== Location Precision ==========
/** True when the citation text was found but the input citation lacked precise
* location data (no page number or no line IDs). Set by the verification engine. */
isImpreciseLocation?: boolean;

// ========== Ambiguity Detection ==========
/** Ambiguity information when multiple occurrences of the text exist */
ambiguity?: {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading