feat: add mobile citation drawer and enhance citation metadata context for improved citation management

This commit is contained in:
Anish Sarkar 2026-03-30 02:06:57 +05:30
parent 04691d572b
commit cbcaa7aed8
3 changed files with 166 additions and 3 deletions

View file

@ -12,19 +12,25 @@ import {
ClipboardPaste,
CopyIcon,
DownloadIcon,
ExternalLink,
Globe,
MessageSquare,
RefreshCwIcon,
} from "lucide-react";
import type { FC } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { CitationMetadataProvider } from "@/components/assistant-ui/citation-metadata-context";
import {
CitationMetadataProvider,
useAllCitationMetadata,
} from "@/components/assistant-ui/citation-metadata-context";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
import type { SerializableCitation } from "@/components/tool-ui/citation";
import {
CreateConfluencePageToolUI,
DeleteConfluencePageToolUI,
@ -65,12 +71,157 @@ import {
} from "@/components/tool-ui/notion";
import { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "@/components/tool-ui/onedrive";
import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute";
import {
openSafeNavigationHref,
resolveSafeNavigationHref,
} from "@/components/tool-ui/shared/media";
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { useComments } from "@/hooks/use-comments";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
function extractDomain(url: string): string | undefined {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return undefined;
}
}
function useCitationsFromMetadata(): SerializableCitation[] {
const allCitations = useAllCitationMetadata();
return useMemo(() => {
const result: SerializableCitation[] = [];
for (const [url, meta] of allCitations) {
const domain = extractDomain(url);
result.push({
id: `url-cite-${url}`,
href: url,
title: meta.title,
snippet: meta.snippet,
domain,
favicon: domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=32` : undefined,
type: "webpage",
});
}
return result;
}, [allCitations]);
}
const MobileCitationDrawer: FC = () => {
const [open, setOpen] = useState(false);
const citations = useCitationsFromMetadata();
if (citations.length === 0) return null;
const maxIcons = 4;
const visible = citations.slice(0, maxIcons);
const remainingCount = Math.max(0, citations.length - maxIcons);
const handleNavigate = (citation: SerializableCitation) => {
const href = resolveSafeNavigationHref(citation.href);
if (href) openSafeNavigationHref(href);
};
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className={cn(
"isolate inline-flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2",
"bg-muted/40 outline-none",
"transition-colors duration-150",
"hover:bg-muted/70",
"focus-visible:ring-ring focus-visible:ring-2"
)}
>
<div className="flex items-center">
{visible.map((citation, index) => (
<div
key={citation.id}
className={cn(
"border-border bg-background dark:border-foreground/20 relative flex size-6 items-center justify-center rounded-full border shadow-xs",
index > 0 && "-ml-2"
)}
style={{ zIndex: maxIcons - index }}
>
{citation.favicon ? (
// biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain
<img
src={citation.favicon}
alt=""
aria-hidden="true"
width={18}
height={18}
className="size-4.5 rounded-full object-cover"
/>
) : (
<Globe className="text-muted-foreground size-3" aria-hidden="true" />
)}
</div>
))}
{remainingCount > 0 && (
<div
className="border-border bg-background dark:border-foreground/20 relative -ml-2 flex size-6 items-center justify-center rounded-full border shadow-xs"
style={{ zIndex: 0 }}
>
<span className="text-muted-foreground text-[10px] font-medium tracking-tight">
</span>
</div>
)}
</div>
<span className="text-muted-foreground text-sm tabular-nums">
{citations.length} source{citations.length !== 1 && "s"}
</span>
</button>
<Drawer open={open} onOpenChange={setOpen}>
<DrawerContent className="max-h-[70dvh]">
<DrawerHandle />
<div className="flex items-center px-4 py-2">
<DrawerTitle className="text-sm font-medium">Sources</DrawerTitle>
</div>
<div className="overflow-y-auto px-1 pb-6">
{citations.map((citation) => (
<button
key={citation.id}
type="button"
onClick={() => handleNavigate(citation)}
className="group flex w-full items-center gap-2.5 rounded-md px-3 py-2.5 text-left transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none"
>
{citation.favicon ? (
// biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain
<img
src={citation.favicon}
alt=""
aria-hidden="true"
width={16}
height={16}
className="bg-muted size-4 shrink-0 rounded object-cover"
/>
) : (
<Globe className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
)}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2">
{citation.title}
</p>
<p className="text-muted-foreground truncate text-xs">{citation.domain}</p>
</div>
<ExternalLink className="text-muted-foreground size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
))}
</div>
</DrawerContent>
</Drawer>
</>
);
};
export const MessageError: FC = () => {
return (
<MessagePrimitive.Error>
@ -82,6 +233,8 @@ export const MessageError: FC = () => {
};
const AssistantMessageInner: FC = () => {
const isMobile = !useMediaQuery("(min-width: 768px)");
return (
<CitationMetadataProvider>
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
@ -133,6 +286,12 @@ const AssistantMessageInner: FC = () => {
<MessageError />
</div>
{isMobile && (
<div className="ml-2 mt-2">
<MobileCitationDrawer />
</div>
)}
<div className="aui-assistant-message-footer mt-1 mb-5 ml-2 flex">
<AssistantActionBar />
</div>