mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 17:26:23 +02:00
feat: add mobile citation drawer and enhance citation metadata context for improved citation management
This commit is contained in:
parent
04691d572b
commit
cbcaa7aed8
3 changed files with 166 additions and 3 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue