diff --git a/apps/web/src/components/chat/AssistantCitationChip.test.tsx b/apps/web/src/components/chat/AssistantCitationChip.test.tsx
index 7d664f2379834b7e72628621293185aaead34a3c..91948cb587b9c84d7e107f994b0ac5c4bfff7e8c 100644
--- a/apps/web/src/components/chat/AssistantCitationChip.test.tsx
+++ b/apps/web/src/components/chat/AssistantCitationChip.test.tsx
@@ -19,11 +19,6 @@ vi.mock("@tanstack/react-router", () => ({
Link: ({ children }: { children: ReactNode }) => {children},
}));
// Keep the real chip/editor lifecycle while replacing DOM positioning and floating layers.
-vi.mock("../ui/tooltip", () => ({
- Tooltip: ({ children }: { children: ReactNode }) => <>{children}>,
- TooltipTrigger: ({ render }: { render: ReactNode }) => render,
- TooltipPopup: ({ children }: { children: ReactNode }) => <>{children}>,
-}));
vi.mock("../ui/popover", () => ({
Popover: ({ children }: { children: ReactNode }) => <>{children}>,
PopoverTrigger: ({ children }: { children: ReactNode }) => ,
@@ -62,6 +57,7 @@ function mount(onSave = vi.fn(() => true)) {
return (
);
diff --git a/apps/web/src/components/chat/AssistantCitationChip.tsx b/apps/web/src/components/chat/AssistantCitationChip.tsx
index 9f5d3a2e1e55dd949f5415571aecad8fe73240c9..0b4d6a962e61803f93715688d68c7ea79efe994b 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 { PencilIcon, QuoteIcon } from "lucide-react";
+import { ArrowUpRightIcon, PencilIcon, QuoteIcon } from "lucide-react";
import {
useEffect,
useEffectEvent,
@@ -20,9 +20,12 @@ import {
assistantCitationHash,
assistantCitationNavigation,
} from "../../lib/assistantCitationNavigation";
+import { cn } from "~/lib/utils";
import { ContextChip, ContextChipAction, ContextChipLabel } from "../ContextChip";
-import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
-import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
+import { ContextChipPopover } from "../contextChipParts";
+import { Button } from "../ui/button";
+import { Popover, PopoverClose, PopoverPopup, PopoverTrigger } from "../ui/popover";
+import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";
import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor";
import { resolveAssistantCitationCommentDismissal } from "./assistantCitationCommentDismissal";
import { observeAssistantCitationCommentSource } from "./AssistantCitationSource";
@@ -122,16 +125,27 @@ export function AssistantCitationChip({
{label}
);
- const chatSourceLink = (
-
-
- {label}
-
- );
+ if (!composer) {
+ return (
+ }
+ label={label}
+ accessibleLabel={`Quoted assistant text: ${label}`}
+ copyMarkdown={serializeAssistantCitation(citation)}
+ >
+
+
+
} />}
+ >
+
+ Go to source
+
+
+
+ );
+ }
return (
- {composer ? (
- composerSourceLink
- ) : (
-
-
- View source
-
- )}
+ {composerSourceLink}
{commentEditor ? (
);
}
+
+function AssistantCitationQuote({ citation }: { citation: AssistantCitation }) {
+ const [fade, setFade] = useState({ top: false, bottom: false });
+ const updateFade = (element: HTMLElement) => {
+ const top = element.scrollTop > 1;
+ const bottom = element.scrollHeight - element.clientHeight - element.scrollTop > 1;
+ setFade((current) =>
+ current.top === top && current.bottom === bottom ? current : { top, bottom },
+ );
+ };
+ return (
+ {
+ if (!element) return;
+ const observer = new ResizeObserver(() => updateFade(element));
+ observer.observe(element);
+ return () => observer.disconnect();
+ }}
+ onScroll={(event) => updateFade(event.currentTarget)}
+ className={cn(
+ "max-h-64 min-h-0 space-y-3 self-stretch overflow-y-auto whitespace-pre-wrap wrap-break-word",
+ getVirtualizedScrollFadeClassName(fade),
+ )}
+ >
+
+ {citation.text}
+
+ {citation.comment ?
{citation.comment}
: null}
+
+ );
+}