mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 22:02:39 +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,
|
ClipboardPaste,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
ExternalLink,
|
||||||
|
Globe,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { FC } from "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 { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
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 { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
||||||
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
||||||
|
import type { SerializableCitation } from "@/components/tool-ui/citation";
|
||||||
import {
|
import {
|
||||||
CreateConfluencePageToolUI,
|
CreateConfluencePageToolUI,
|
||||||
DeleteConfluencePageToolUI,
|
DeleteConfluencePageToolUI,
|
||||||
|
|
@ -65,12 +71,157 @@ import {
|
||||||
} from "@/components/tool-ui/notion";
|
} from "@/components/tool-ui/notion";
|
||||||
import { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "@/components/tool-ui/onedrive";
|
import { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "@/components/tool-ui/onedrive";
|
||||||
import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute";
|
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 { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
|
||||||
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
|
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
|
||||||
|
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||||
import { useComments } from "@/hooks/use-comments";
|
import { useComments } from "@/hooks/use-comments";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { cn } from "@/lib/utils";
|
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 = () => {
|
export const MessageError: FC = () => {
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Error>
|
<MessagePrimitive.Error>
|
||||||
|
|
@ -82,6 +233,8 @@ export const MessageError: FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AssistantMessageInner: FC = () => {
|
const AssistantMessageInner: FC = () => {
|
||||||
|
const isMobile = !useMediaQuery("(min-width: 768px)");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CitationMetadataProvider>
|
<CitationMetadataProvider>
|
||||||
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
|
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
|
||||||
|
|
@ -133,6 +286,12 @@ const AssistantMessageInner: FC = () => {
|
||||||
<MessageError />
|
<MessageError />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
|
<div className="ml-2 mt-2">
|
||||||
|
<MobileCitationDrawer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="aui-assistant-message-footer mt-1 mb-5 ml-2 flex">
|
<div className="aui-assistant-message-footer mt-1 mb-5 ml-2 flex">
|
||||||
<AssistantActionBar />
|
<AssistantActionBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,7 @@ export function useCitationMetadata(url: string): CitationMeta | undefined {
|
||||||
const map = useContext(CitationMetadataContext);
|
const map = useContext(CitationMetadataContext);
|
||||||
return map.get(url);
|
return map.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAllCitationMetadata(): CitationMetadataMap {
|
||||||
|
return useContext(CitationMetadataContext);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||||
function DrawerHandle({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
function DrawerHandle({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted-foreground/40", className)}
|
className={cn("mx-auto mt-4 h-4 w-14 rounded-full bg-muted-foreground/40", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue