From 1458ab1171f24f1082bcc10780e7d8d352346b8f Mon Sep 17 00:00:00 2001 From: Ritwik Shanker Date: Wed, 9 Sep 2026 12:01:33 +0200 Subject: [PATCH 1/3] feat: update citation handling and add copy message functionality --- CHANGELOG.md | 19 ++++ src/components/CitedDatasets.test.tsx | 13 +++ src/components/CitedDatasets.tsx | 45 +++++++-- src/components/CopyMessageButton.test.tsx | 31 ++++++ src/components/CopyMessageButton.tsx | 52 ++++++++++ src/components/DatasetReference.test.tsx | 53 +++++++++- src/components/DatasetReference.tsx | 116 +++++++++++++--------- src/components/MessageMarkdown.test.tsx | 13 +-- src/pages/ChatPage.test.tsx | 83 +++++++++++++++- src/pages/ChatPage.tsx | 21 +++- 10 files changed, 380 insertions(+), 66 deletions(-) create mode 100644 src/components/CopyMessageButton.test.tsx create mode 100644 src/components/CopyMessageButton.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e33b56..609e4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [0.10.4] - 08/09/2026 + +- A dataset cited in an AI answer is now a single control instead of two. The bracketed number and the dataset name + sit in one pill, and clicking anywhere on it takes you to that dataset in the "Datasets cited in this answer" list, + which now opens with its description already showing. Previously the number and the name did two different things. +- Opening the dataset's page at the repository still works the way it does for any link: Ctrl+click (Cmd+click on a + Mac), middle click, or right click and "Open link in new tab". Several sources can still be opened in a row that + way, and the list entry keeps its Source button for opening one directly. +- Matomo now records a plain citation click as `citation_clicked` under the Chat category. Opening a source from a + citation is still `citation_source_clicked` under the Dataset category, and `citation_marker_clicked` is gone, + since the number is no longer a separate control. +- Fixed an open citation following you into the next conversation. Switching threads reused the previous thread's + messages, so the dataset you had opened stayed open, and the list you had hidden stayed hidden. +- Added a copy button beside your own messages, for reusing a question or pasting it somewhere else. It is always + visible on touch screens and appears on hover elsewhere. +- Opening a conversation now starts at its most recent message. Switching threads used to keep the scroll position + of the one you left, dropping you part-way up the new conversation, and an answer streaming into the new thread + would not follow if you had scrolled up in the previous one. + ## [0.10.3] - 08/09/2026 Datasets in an AI answer are now cited the way a paper cites its references: diff --git a/src/components/CitedDatasets.test.tsx b/src/components/CitedDatasets.test.tsx index 8989d2c..938398c 100644 --- a/src/components/CitedDatasets.test.tsx +++ b/src/components/CitedDatasets.test.tsx @@ -73,6 +73,19 @@ describe("CitedDatasets", () => { expect(items[1].className).toContain("bg-blue-100"); }); + it("opens the row it jumps to, and lets the reader close it again", async () => { + const user = userEvent.setup(); + const {rerender} = renderList(); + Element.prototype.scrollIntoView = vi.fn(); + expect(screen.queryByText(/^x{300}\.\.\.$/)).not.toBeInTheDocument(); + + rerender(); + expect(screen.getByText(/^x{300}\.\.\.$/)).toBeInTheDocument(); + + await user.click(screen.getByRole("button", {name: "Hide details of Soil Moisture 2021"})); + expect(screen.queryByText(/^x{300}\.\.\.$/)).not.toBeInTheDocument(); + }); + it("renders nothing without citations", () => { const {container} = renderList(); expect(container).toBeEmptyDOMElement(); diff --git a/src/components/CitedDatasets.tsx b/src/components/CitedDatasets.tsx index f9a00d6..bf3215f 100644 --- a/src/components/CitedDatasets.tsx +++ b/src/components/CitedDatasets.tsx @@ -56,12 +56,17 @@ interface CitedDatasetRowProps { citation: DatasetCitation; isLoggedIn: boolean; highlighted: boolean; + expanded: boolean; + onToggle: (number: number) => void; register: (number: number, element: HTMLLIElement | null) => void; } -/** A compact reference entry; the chevron expands it into the full card details. */ -const CitedDatasetRow = ({citation, isLoggedIn, highlighted, register}: CitedDatasetRowProps) => { - const [expanded, setExpanded] = useState(false); +/** + * A compact reference entry; the chevron expands it into the full card details. + * Whether it is expanded is the list's business, not the row's: arriving here from + * a citation opens the row, so the description is one click away rather than two. + */ +const CitedDatasetRow = ({citation, isLoggedIn, highlighted, expanded, onToggle, register}: CitedDatasetRowProps) => { const {number, dataset} = citation; const creators: SearchHitSrcCreator[] = dataset._source.creators ?? []; @@ -91,7 +96,7 @@ const CitedDatasetRow = ({citation, isLoggedIn, highlighted, register}: CitedDat + ); +}; diff --git a/src/components/DatasetReference.test.tsx b/src/components/DatasetReference.test.tsx index ca462de..7add42c 100644 --- a/src/components/DatasetReference.test.tsx +++ b/src/components/DatasetReference.test.tsx @@ -1,5 +1,6 @@ -import {describe, it, expect} from "vitest"; -import {render, screen} from "@testing-library/react"; +import {describe, it, expect, vi} from "vitest"; +import {fireEvent, render, screen} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import {makeDataset} from "@/test/fixtures/datasets"; import {DatasetReference} from "./DatasetReference"; @@ -34,8 +35,50 @@ describe("DatasetReference", () => { expect(screen.getByTitle("Test Dataset Title")).toBeInTheDocument(); }); - it("keeps the [n] marker working when the pill cannot be linked", () => { - render(); - expect(screen.getByRole("button", {name: /^Reference 2:/})).toHaveTextContent("[2]"); + it("still leads to the reference when the pill cannot be linked", async () => { + const onJump = vi.fn(); + render( + , + ); + const pill = screen.getByRole("button", {name: /Test Dataset Title/}); + expect(pill).toHaveTextContent("[2]"); + await userEvent.click(pill); + expect(onJump).toHaveBeenCalledWith(2); + }); + + // The pill is one control with one apparent action, but it stays a real link so the + // browser's own ways of opening a source keep working. + const clickWith = (init: MouseEventInit) => { + const pill = screen.getByRole("link", {name: /Ocean Temperatures 2023/}); + return fireEvent(pill, new MouseEvent("click", {bubbles: true, cancelable: true, ...init})); + }; + + it("jumps to the reference on a plain click rather than following the link", () => { + const onJump = vi.fn(); + render( + , + ); + // dispatchEvent reports false once preventDefault has stopped the navigation. + expect(clickWith({})).toBe(false); + expect(onJump).toHaveBeenCalledWith(1); + }); + + it("leaves a modifier-click to the browser, so sources can be opened in background tabs", () => { + const onJump = vi.fn(); + render( + , + ); + for (const modifier of [{ctrlKey: true}, {metaKey: true}, {shiftKey: true}]) { + expect(clickWith(modifier)).toBe(true); + } + expect(onJump).not.toHaveBeenCalled(); }); }); diff --git a/src/components/DatasetReference.tsx b/src/components/DatasetReference.tsx index 50df85b..f97977d 100644 --- a/src/components/DatasetReference.tsx +++ b/src/components/DatasetReference.tsx @@ -1,3 +1,4 @@ +import type {MouseEvent} from "react"; import {FileText} from "lucide-react"; import type {BackendDataset} from "../types/commons.ts"; import {getProvenanceSource} from "../lib/repoProvenance.ts"; @@ -11,18 +12,21 @@ interface DatasetReferenceProps { label?: string; // Position in the message's reference list. Omitted when the message has no list. number?: number; - // Called with the reference number when the [n] marker is activated. + // Called with the reference number when the citation is activated. onJump?: (number: number) => void; } /** * Inline citation of a dataset from the search results, in the style of a numbered - * reference. The dataset name is a pill that links straight to the source, so a plain - * click opens it and a modifier-click opens it in a background tab; the pill names the - * repository the data comes from. The bracketed [n] in front of it jumps to the - * dataset's entry in the message's reference list, where the card details and - * actions live; leading with the number is what pairs the pill with that entry. - * Nothing opens on hover. + * reference: one pill carrying [n], the model's label, and the repository the data + * comes from. + * + * The pill is a real link to the dataset's page, so a modifier-click, a middle click + * and the right-click menu open the source exactly as they would on any link, and + * several sources can still be opened in a row. A plain click instead cancels that + * navigation and jumps to the dataset's entry in the message's reference list, which + * opens expanded — the description, the details and the Source button all live there, + * so the citation leads to one place rather than two. Nothing opens on hover. */ export const DatasetReference = ({dataset, label, number, onJump}: DatasetReferenceProps) => { const {trackEvent} = useMatomo(); @@ -31,16 +35,22 @@ export const DatasetReference = ({dataset, label, number, onJump}: DatasetRefere // Both the URL and the id are backend data, so neither is trusted as a link target. const href = sanitizeLinkHref(dataset.dataset_url || dataset._id); const source = getProvenanceSource(dataset); - const reference = number === undefined - ? null + // Without a number there is no reference list to lead to, so the citation stays + // an ordinary link to the source. + const jumpToReference = number === undefined ? null : () => { + trackEvent('Chat', 'citation_clicked'); + onJump?.(number); + }; + + const description = number === undefined + ? title : `Reference ${number}: ${title}${source ? ` (${source.name})` : ''}`; - // Inline (not inline-flex) so a long title wraps across lines with the text; - // box-decoration-clone keeps the pill background/border on every wrapped line. - const pillClass = 'mx-0.5 rounded bg-blue-50 px-1.5 py-px text-xs font-medium text-blue-700 ' - + 'border border-blue-200 [box-decoration-break:clone]'; - const pill = ( + // No aria-label: the accessible name is the visible text, which is what a reader + // has been told to click. The full title is on the tooltip instead. + const content = ( <> + {number !== undefined && [{number}]} {label || title} {source && ( @@ -51,38 +61,52 @@ export const DatasetReference = ({dataset, label, number, onJump}: DatasetRefere ); + // Inline (not inline-flex) so a long title wraps across lines with the text; + // box-decoration-clone keeps the pill background/border on every wrapped line. + const pillClass = 'mx-0.5 rounded bg-blue-50 px-1.5 py-px text-xs font-medium text-blue-700 ' + + 'border border-blue-200 [box-decoration-break:clone]'; + const hoverClass = ' hover:bg-blue-100 hover:border-blue-300 transition-colors cursor-pointer'; + + if (!href) { + // No usable source URL. The citation can still lead to its reference entry; + // with nothing to lead to either, it just names the dataset. + return jumpToReference ? ( + + ) : ( + {content} + ); + } + + const handleClick = (event: MouseEvent) => { + // A modifier or non-primary click is the reader asking the browser for the + // source itself (background tab, new window, saved link); leave those alone. + const opensElsewhere = event.metaKey || event.ctrlKey || event.shiftKey + || event.altKey || event.button !== 0; + if (!jumpToReference || opensElsewhere) { + trackEvent('Dataset', 'citation_source_clicked', title); + return; + } + event.preventDefault(); + jumpToReference(); + }; + return ( - <> - {reference && ( - - )} - {href ? ( - trackEvent('Dataset', 'citation_source_clicked', title)} - className={`${pillClass} hover:bg-blue-100 hover:border-blue-300 transition-colors`} - > - {pill} - - ) : ( - // No usable source URL: the citation still names the dataset, it just does - // not pretend to be clickable. - {pill} - )} - + + {content} + ); }; diff --git a/src/components/MessageMarkdown.test.tsx b/src/components/MessageMarkdown.test.tsx index 6944ad3..7c7b32c 100644 --- a/src/components/MessageMarkdown.test.tsx +++ b/src/components/MessageMarkdown.test.tsx @@ -27,16 +27,17 @@ describe("MessageMarkdown", () => { expect(pill).toHaveAttribute("target", "_blank"); expect(pill).toHaveAttribute("rel", "noopener noreferrer"); expect(pill).toHaveTextContent("Zenodo"); - // Without the message's citations there is no reference list to point at. - expect(screen.queryByRole("button")).not.toBeInTheDocument(); + // Without the message's citations there is no number and nothing to jump to. + expect(pill).not.toHaveTextContent("["); }); - it("follows a cited dataset with a [n] marker that reports its reference number", async () => { + it("carries the reference number in the pill and jumps to it when clicked", async () => { const onCite = vi.fn(); renderMarkdown(`See [Ocean temps](${DATASET_URL}).`, false, [{number: 3, dataset: hit}], onCite); - const marker = screen.getByRole("button", {name: "Reference 3: Ocean Temperatures 2023 (Zenodo)"}); - expect(marker).toHaveTextContent("[3]"); - await userEvent.click(marker); + const pill = screen.getByRole("link", {name: /Ocean temps/}); + expect(pill).toHaveTextContent("[3]"); + expect(pill).toHaveAttribute("title", "Reference 3: Ocean Temperatures 2023 (Zenodo)"); + await userEvent.click(pill); expect(onCite).toHaveBeenCalledWith(3); }); diff --git a/src/pages/ChatPage.test.tsx b/src/pages/ChatPage.test.tsx index 1d9d3c7..f339c2b 100644 --- a/src/pages/ChatPage.test.tsx +++ b/src/pages/ChatPage.test.tsx @@ -96,10 +96,10 @@ describe("ChatPage", () => { expect(external).toHaveAttribute("target", "_blank"); expect(external).toHaveAttribute("rel", "noopener noreferrer"); - // Matched link: a pill linking straight to the source, followed by its [1] marker. + // Matched link: one pill carrying its [1] and linking to the source. const pill = await screen.findByRole("link", {name: /Ocean temps/}); expect(pill).toHaveAttribute("href", DATASET_URL); - expect(screen.getByRole("button", {name: /Reference 1: Ocean Temperatures 2023/})).toHaveTextContent("[1]"); + expect(pill).toHaveTextContent("[1]"); // The reference list under the answer carries the same number and the card's actions. const references = screen.getByRole("region", {name: "Datasets cited in this answer"}); @@ -108,4 +108,83 @@ describe("ChatPage", () => { expect(within(references).getByRole("link", {name: /source of dataset Ocean Temperatures 2023/})) .toHaveAttribute("href", hit._id); }); + + it("copies a user message to the clipboard", async () => { + const user = userEvent.setup(); + renderChat(); + + await user.type(await screen.findByRole("textbox"), "ocean data"); + await user.click(screen.getByRole("button", {name: /send/i})); + + await user.click(await screen.findByRole("button", {name: "Copy message"})); + expect(await navigator.clipboard.readText()).toBe("ocean data"); + }); + + it("does not carry one conversation's open citation into another", async () => { + server.use( + http.get("/api/search/conversations", () => HttpResponse.json([ + {thread_id: "t-9", label: "Earlier chat"}, + ])), + http.get("/api/search/conversation/t-9", () => HttpResponse.json({ + thread_id: "t-9", + label: "Earlier chat", + items: [ + {type: "message", role: "user", content: "earlier question"}, + {type: "tool_call", id: "c9", name: "search_data", arguments: {}}, + {type: "tool_result", call_id: "c9", content: JSON.stringify({hits: [hit]})}, + {type: "message", role: "assistant", content: `See [Ocean temps](${DATASET_URL}).`}, + ], + })), + ); + const user = userEvent.setup(); + renderChat(); + + await user.type(await screen.findByRole("textbox"), "ocean data"); + await user.click(screen.getByRole("button", {name: /send/i})); + + // Open the reference from this answer's citation. + await user.click(await screen.findByRole("link", {name: /Ocean temps/})); + expect(await screen.findByRole("button", {name: /Hide details of Ocean Temperatures 2023/})) + .toBeInTheDocument(); + + // The other conversation cites the same dataset, and starts with it closed. + await user.click(await screen.findByText("Earlier chat")); + expect(await screen.findByText("earlier question")).toBeInTheDocument(); + expect(screen.queryByRole("button", {name: /Hide details of/})).not.toBeInTheDocument(); + expect(screen.getByRole("button", {name: /Show details of Ocean Temperatures 2023/})).toBeInTheDocument(); + }); + + it("opens a conversation at its most recent message", async () => { + // jsdom lays nothing out, so the container has to be told it overflows. + Object.defineProperty(HTMLElement.prototype, "scrollHeight", {configurable: true, value: 900}); + try { + server.use( + http.get("/api/search/conversations", () => HttpResponse.json([ + {thread_id: "t-9", label: "Earlier chat"}, + ])), + http.get("/api/search/conversation/t-9", () => HttpResponse.json({ + thread_id: "t-9", + label: "Earlier chat", + items: [{type: "message", role: "user", content: "earlier question"}], + })), + ); + const user = userEvent.setup(); + renderChat(); + + await user.type(await screen.findByRole("textbox"), "ocean data"); + await user.click(screen.getByRole("button", {name: /send/i})); + await screen.findByRole("link", {name: /Ocean temps/}); + + // The messages scroll in their own container, the scrollable ancestor of + // every bubble. Leave it part-way up the thread, as a reader would. + const container = screen.getByText("ocean data").closest(".overflow-y-auto") as HTMLElement; + container.scrollTop = 120; + + await user.click(await screen.findByText("Earlier chat")); + await screen.findByText("earlier question"); + expect(container.scrollTop).toBe(900); + } finally { + Reflect.deleteProperty(HTMLElement.prototype, "scrollHeight"); + } + }); }); diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 530ca79..cb1a268 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -13,6 +13,7 @@ import {BotMessageBody} from "@/components/BotMessageBody.tsx"; import {SearchInput} from "@/components/SearchInput.tsx"; import {DeleteConversationDialog} from "@/components/DeleteConversationDialog.tsx"; import {ConversationSidebarItem} from "@/components/ConversationSidebarItem.tsx"; +import {CopyMessageButton} from "@/components/CopyMessageButton.tsx"; import {SearchFeedback} from "@/components/SearchFeedback.tsx"; import useMatomo from "@/hooks/useMatomo.ts"; import {errorKind} from "@/lib/analytics.ts"; @@ -192,6 +193,20 @@ const ChatPage: FC = () => { } }, [urlId, handleSelectConversation]); + // Opening a conversation starts at its most recent message. Every thread shares + // one scroll container, so without this it keeps the offset of the thread just + // left — clamped to the new content, which lands the reader mid-conversation — + // along with whether that thread's bottom was being followed. + useEffect(() => { + const container = messagesContainerRef.current; + if (!container) return; + followingRef.current = true; + answerFollowedRef.current = false; + // Set rather than animated: a smooth scroll would travel from the previous + // thread's offset, which is not a position in this one. + container.scrollTop = container.scrollHeight; + }, [selectedConversation?.id]); + useEffect(() => { const state = location.state; if ( @@ -548,7 +563,10 @@ const ChatPage: FC = () => { ) : ( selectedConversation.messages.map((msg, index) => ( -
@@ -611,6 +629,7 @@ const ChatPage: FC = () => {
)}
+ {msg.sender === 'user' && } )) From 28c657c4ea32bed70d1e07e9e92e44a97872c046 Mon Sep 17 00:00:00 2001 From: Ritwik Shanker Date: Wed, 9 Sep 2026 15:51:51 +0200 Subject: [PATCH 2/3] chore: update vitest and related dependencies to version 4.1.11 --- package-lock.json | 173 ++++++++++++++++++++++------------------------ package.json | 4 +- 2 files changed, 85 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdcb5ae..001ae5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.3", - "@vitest/coverage-v8": "^4.1.9", + "@vitest/coverage-v8": "^4.1.11", "autoprefixer": "^10.4.24", "esbuild": "^0.28.1", "eslint": "^9.19.0", @@ -70,7 +70,7 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.36.0", "vite": "^7.3.2", - "vitest": "^4.1.9" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -2147,12 +2147,6 @@ } } }, - "node_modules/@react-router/dev/node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "license": "MIT" - }, "node_modules/@react-router/express": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/@react-router/express/-/express-8.3.0.tgz", @@ -4482,14 +4476,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", - "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -4503,8 +4497,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.9", - "vitest": "4.1.9" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4513,16 +4507,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -4531,13 +4525,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4558,9 +4552,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -4571,13 +4565,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -4585,14 +4579,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4601,9 +4595,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -4611,13 +4605,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -5779,6 +5773,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -7016,9 +7016,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -7671,9 +7671,9 @@ } }, "node_modules/morgan": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", - "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.0.tgz", + "integrity": "sha512-OHpTRQwn2ezasILW8iKe+Yww1XsfWsZIpUOLF7RDb2g5GwO3trPaRwi7+8BDiJ7HFx2Kg2mfUdCBcVhwYlOz2g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -7898,9 +7898,9 @@ } }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -8142,9 +8142,9 @@ } }, "node_modules/pm2": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/pm2/-/pm2-7.0.3.tgz", - "integrity": "sha512-zRJOdburpb9OEPB0uqoNT8C1Gp7hPJPVy4Kr67XJNuT9UlMQcOt1WXrYQUmwqKPHk8FyauvP1CPhqoCrCaPw0Q==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-7.0.4.tgz", + "integrity": "sha512-rMzoZmZlmJw7hoTbO9cOqcwj407TzUVc84JRaIjmnK8+808zldQT2Sd4rVcqVvXUcI2LoXEf1irMevWfOx5SEQ==", "license": "AGPL-3.0", "dependencies": { "@pm2/blessed": "0.1.81", @@ -8162,7 +8162,7 @@ "debug": "4.4.3", "eventemitter2": "6.4.9", "fast-json-patch": "3.1.1", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", "pidusage": "4.0.1", "pm2-deploy": "1.0.2", "proxy-agent": "6.5.0", @@ -9049,9 +9049,9 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -9218,9 +9218,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -9273,9 +9273,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -9765,19 +9765,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -9805,12 +9805,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -9854,17 +9854,10 @@ } } }, - "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", - "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 1f33f0d..def7629 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.3", - "@vitest/coverage-v8": "^4.1.9", + "@vitest/coverage-v8": "^4.1.11", "autoprefixer": "^10.4.24", "esbuild": "^0.28.1", "eslint": "^9.19.0", @@ -83,7 +83,7 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.36.0", "vite": "^7.3.2", - "vitest": "^4.1.9" + "vitest": "^4.1.11" }, "overrides": { "minimatch": "^10.2.1", From 46ad7b802756188fd68cacef3a883725da33f1c1 Mon Sep 17 00:00:00 2001 From: Ritwik Shanker Date: Wed, 9 Sep 2026 15:56:58 +0200 Subject: [PATCH 3/3] chore: update CHANGELOG for version 0.10.4 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 609e4b4..f25f052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [0.10.4] - 08/09/2026 +## [0.10.4] - 09/09/2026 - A dataset cited in an AI answer is now a single control instead of two. The bracketed number and the dataset name sit in one pill, and clicking anywhere on it takes you to that dataset in the "Datasets cited in this answer" list,