From 88b54f5aec45e89a249f35fa7566c881df288877 Mon Sep 17 00:00:00 2001 From: Elliot Drel Date: Sat, 7 Nov 2026 10:34:35 -0500 Subject: [PATCH 1/9] fix(web): open citation comments from composer chips --- ...ditorTiptap.citationDraft.browser.test.tsx | 20 ++++++++- .../components/ComposerPromptEditorTiptap.tsx | 11 ++++- apps/web/src/components/ContextChip.tsx | 3 +- .../components/chat/AssistantCitationChip.tsx | 41 +++++++++++-------- docs/user/composer.md | 4 +- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.citationDraft.browser.test.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.citationDraft.browser.test.tsx index a1a69a7968ee9..4d61aaaee31da 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.citationDraft.browser.test.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.citationDraft.browser.test.tsx @@ -95,5 +95,23 @@ it("keeps an unsaved citation comment when a question takes the composer", async expect(drafts()?.prompt).toBe( `See ${serializeAssistantCitation(withAssistantCitationComment(citation, "check this"))} `, ); - await expect.element(page.getByLabelText("Edit citation comment")).toBeInTheDocument(); + await expect + .element(page.getByRole("link", { name: "View cited assistant text: check this" })) + .toBeInTheDocument(); }); + +it.each(["click", "Enter", " "])( + "opens the citation comment editor with %s on the chip", + async (activation) => { + const editorRef = createRef(); + await using screen = await render(); + const chip = screen.getByRole("link", { name: "View cited assistant text: hello" }); + if (activation === "click") { + await chip.click(); + } else { + screen.container.querySelector("[data-citation-comment-trigger]")?.focus(); + await userEvent.keyboard(activation === "Enter" ? "{Enter}" : " "); + } + await expect.element(page.getByLabelText("Comment on selected text")).toBeVisible(); + }, +); diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index 8c4698210b1d5..a6d543c7d7bcc 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -121,7 +121,10 @@ import { ComposerContextReferenceChip, ComposerContextRecordsContext, } from "./composerContextPresentation"; -import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection"; +import { + findAssistantCitationSourceAnchor, + type AssistantCitationSourceAnchor, +} from "~/lib/assistantTextSelection"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { ComposerCodeBlockNodeView } from "./chat/ComposerCodeBlockNodeView"; import { resolveAssistantCitationCommentDismissal } from "./chat/assistantCitationCommentDismissal"; @@ -470,7 +473,11 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { composer commentEditor={{ open: commentTarget !== null, - sourceAnchor: commentTarget?.sourceAnchor, + sourceAnchor: commentTarget + ? (commentTarget.sourceAnchor ?? + findAssistantCitationSourceAnchor(document, citation) ?? + undefined) + : undefined, onOpenChange: (open) => { if (open && !editor.isEditable) return; commentContext.onOpenChange(citeKey, open); diff --git a/apps/web/src/components/ContextChip.tsx b/apps/web/src/components/ContextChip.tsx index a536f28ccfb12..6405963175b6a 100644 --- a/apps/web/src/components/ContextChip.tsx +++ b/apps/web/src/components/ContextChip.tsx @@ -37,6 +37,7 @@ const contextChipVariants = cva( }, shape: { inline: "", + pill: "mx-[0.15em] gap-1 rounded-md bg-(--context-chip-accent)/12 px-1.5 py-0.5", bubble: "w-64 gap-2 rounded-2xl bg-message px-3 py-2 text-sm text-message-foreground", }, state: { @@ -53,7 +54,7 @@ type ContextChipKind = NonNullable["kin interface ContextChipProps extends useRender.ComponentProps<"span"> { kind?: ContextChipKind; state?: "unresolved" | "invalid"; - shape?: "inline" | "bubble"; + shape?: "inline" | "pill" | "bubble"; } function ContextChip({ className, kind, state, shape, render, ...props }: ContextChipProps) { diff --git a/apps/web/src/components/chat/AssistantCitationChip.tsx b/apps/web/src/components/chat/AssistantCitationChip.tsx index cf62358069701..ba12595348b11 100644 --- a/apps/web/src/components/chat/AssistantCitationChip.tsx +++ b/apps/web/src/components/chat/AssistantCitationChip.tsx @@ -4,7 +4,7 @@ import { serializeAssistantCitation, } from "@t3tools/shared/assistantCitations"; import { Link, useNavigate } from "@tanstack/react-router"; -import { ArrowUpRightIcon, PencilIcon, QuoteIcon } from "lucide-react"; +import { ArrowUpRightIcon, QuoteIcon } from "lucide-react"; import { useEffect, useEffectEvent, @@ -21,10 +21,10 @@ import { assistantCitationNavigation, } from "../../lib/assistantCitationNavigation"; import { cn } from "~/lib/utils"; -import { ContextChip, ContextChipAction, ContextChipLabel } from "../ContextChip"; +import { ContextChip, ContextChipLabel } from "../ContextChip"; import { ContextChipPopover } from "../contextChipParts"; import { Button } from "../ui/button"; -import { Popover, PopoverClose, PopoverPopup, PopoverTrigger } from "../ui/popover"; +import { Popover, PopoverClose, PopoverPopup } from "../ui/popover"; import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor"; import { resolveAssistantCitationCommentDismissal } from "./assistantCitationCommentDismissal"; @@ -52,6 +52,7 @@ export function AssistantCitationChip({ }; }) { const navigate = useNavigate(); + const chipRef = useRef(null); const commentInputRef = useRef(null); const commentPopupRef = useRef(null); const draftCommentRef = useRef(null); @@ -123,19 +124,31 @@ export function AssistantCitationChip({ } event.preventDefault(); void navigate(assistantCitationNavigation(citation)); + if (composer) commentEditor?.onOpenChange(true); }, }; const composerSourceLink = ( - } + data-citation-comment-trigger={commentEditor ? "true" : undefined} + onKeyDown={(event) => { + if (event.key !== " " || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) + return; + event.preventDefault(); + event.currentTarget.click(); + }} aria-label={`View cited assistant text: ${label}`} >