| ... | ... | @@ -4,7 +4,7 @@ import { |
| 4 | 4 | serializeAssistantCitation, |
| 5 | 5 | } from "@t3tools/shared/assistantCitations"; |
| 6 | 6 | import { Link, useNavigate } from "@tanstack/react-router"; |
| 7 | | import { PencilIcon, QuoteIcon } from "lucide-react"; |
| 7 | import { ArrowUpRightIcon, PencilIcon, QuoteIcon } from "lucide-react"; |
| 8 | 8 | import { |
| 9 | 9 | useEffect, |
| 10 | 10 | useEffectEvent, |
| ... | ... | @@ -20,9 +20,12 @@ import { |
| 20 | 20 | assistantCitationHash, |
| 21 | 21 | assistantCitationNavigation, |
| 22 | 22 | } from "../../lib/assistantCitationNavigation"; |
| 23 | import { cn } from "~/lib/utils"; |
| 23 | 24 | import { ContextChip, ContextChipAction, ContextChipLabel } from "../ContextChip"; |
| 24 | | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 25 | | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 25 | import { ContextChipPopover } from "../contextChipParts"; |
| 26 | import { Button } from "../ui/button"; |
| 27 | import { Popover, PopoverClose, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 28 | import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; |
| 26 | 29 | import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor"; |
| 27 | 30 | import { resolveAssistantCitationCommentDismissal } from "./assistantCitationCommentDismissal"; |
| 28 | 31 | import { observeAssistantCitationCommentSource } from "./AssistantCitationSource"; |
| ... | ... | @@ -122,16 +125,27 @@ export function AssistantCitationChip({ |
| 122 | 125 | <ContextChipLabel className="max-w-[16em]">{label}</ContextChipLabel> |
| 123 | 126 | </Link> |
| 124 | 127 | ); |
| 125 | | const chatSourceLink = ( |
| 126 | | <Link |
| 127 | | {...sourceLinkProps} |
| 128 | | className="inline-flex h-full min-w-0 items-center gap-[0.33em] rounded-sm text-inherit no-underline hover:bg-(--context-chip-accent)/17 focus-visible:outline-2 focus-visible:outline-foreground" |
| 129 | | aria-label={`View cited assistant text: ${label}`} |
| 130 | | > |
| 131 | | <QuoteIcon aria-hidden="true" /> |
| 132 | | <ContextChipLabel className="max-w-[16em]">{label}</ContextChipLabel> |
| 133 | | </Link> |
| 134 | | ); |
| 128 | if (!composer) { |
| 129 | return ( |
| 130 | <ContextChipPopover |
| 131 | kind="citation" |
| 132 | icon={<QuoteIcon />} |
| 133 | label={label} |
| 134 | accessibleLabel={`Quoted assistant text: ${label}`} |
| 135 | copyMarkdown={serializeAssistantCitation(citation)} |
| 136 | > |
| 137 | <div className="flex max-h-[calc(var(--available-height)_-_1rem_-_2px)] flex-col items-start gap-3 p-1 text-sm"> |
| 138 | <AssistantCitationQuote citation={citation} /> |
| 139 | <PopoverClose |
| 140 | render={<Button variant="outline" size="sm" render={<Link {...sourceLinkProps} />} />} |
| 141 | > |
| 142 | <ArrowUpRightIcon aria-hidden="true" /> |
| 143 | Go to source |
| 144 | </PopoverClose> |
| 145 | </div> |
| 146 | </ContextChipPopover> |
| 147 | ); |
| 148 | } |
| 135 | 149 | return ( |
| 136 | 150 | <ContextChip |
| 137 | 151 | kind="citation" |
| ... | ... | @@ -139,14 +153,7 @@ export function AssistantCitationChip({ |
| 139 | 153 | data-assistant-citation-chip="true" |
| 140 | 154 | data-markdown-copy={serializeAssistantCitation(citation)} |
| 141 | 155 | > |
| 142 | | {composer ? ( |
| 143 | | composerSourceLink |
| 144 | | ) : ( |
| 145 | | <Tooltip> |
| 146 | | <TooltipTrigger render={chatSourceLink} /> |
| 147 | | <TooltipPopup side="top">View source</TooltipPopup> |
| 148 | | </Tooltip> |
| 149 | | )} |
| 156 | {composerSourceLink} |
| 150 | 157 | {commentEditor ? ( |
| 151 | 158 | <Popover |
| 152 | 159 | open={commentEditor.open} |
| ... | ... | @@ -232,3 +239,34 @@ export function AssistantCitationChip({ |
| 232 | 239 | </ContextChip> |
| 233 | 240 | ); |
| 234 | 241 | } |
| 242 | |
| 243 | function AssistantCitationQuote({ citation }: { citation: AssistantCitation }) { |
| 244 | const [fade, setFade] = useState({ top: false, bottom: false }); |
| 245 | const updateFade = (element: HTMLElement) => { |
| 246 | const top = element.scrollTop > 1; |
| 247 | const bottom = element.scrollHeight - element.clientHeight - element.scrollTop > 1; |
| 248 | setFade((current) => |
| 249 | current.top === top && current.bottom === bottom ? current : { top, bottom }, |
| 250 | ); |
| 251 | }; |
| 252 | return ( |
| 253 | <div |
| 254 | ref={(element) => { |
| 255 | if (!element) return; |
| 256 | const observer = new ResizeObserver(() => updateFade(element)); |
| 257 | observer.observe(element); |
| 258 | return () => observer.disconnect(); |
| 259 | }} |
| 260 | onScroll={(event) => updateFade(event.currentTarget)} |
| 261 | className={cn( |
| 262 | "max-h-64 min-h-0 space-y-3 self-stretch overflow-y-auto whitespace-pre-wrap wrap-break-word", |
| 263 | getVirtualizedScrollFadeClassName(fade), |
| 264 | )} |
| 265 | > |
| 266 | <blockquote className="border-l-2 border-border pl-3 text-muted-foreground"> |
| 267 | {citation.text} |
| 268 | </blockquote> |
| 269 | {citation.comment ? <p>{citation.comment}</p> : null} |
| 270 | </div> |
| 271 | ); |
| 272 | } |