diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index ba2753da8..1418cde98 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -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 ( + <> + + + + + +
+ Sources +
+
+ {citations.map((citation) => ( + + ))} +
+
+
+ + ); +}; + export const MessageError: FC = () => { return ( @@ -82,6 +233,8 @@ export const MessageError: FC = () => { }; const AssistantMessageInner: FC = () => { + const isMobile = !useMediaQuery("(min-width: 768px)"); + return (
@@ -133,6 +286,12 @@ const AssistantMessageInner: FC = () => {
+ {isMobile && ( +
+ +
+ )} +
diff --git a/surfsense_web/components/assistant-ui/citation-metadata-context.tsx b/surfsense_web/components/assistant-ui/citation-metadata-context.tsx index b14d9ba4f..0bf5dd946 100644 --- a/surfsense_web/components/assistant-ui/citation-metadata-context.tsx +++ b/surfsense_web/components/assistant-ui/citation-metadata-context.tsx @@ -63,3 +63,7 @@ export function useCitationMetadata(url: string): CitationMeta | undefined { const map = useContext(CitationMetadataContext); return map.get(url); } + +export function useAllCitationMetadata(): CitationMetadataMap { + return useContext(CitationMetadataContext); +} diff --git a/surfsense_web/components/ui/drawer.tsx b/surfsense_web/components/ui/drawer.tsx index 67d4189e4..e5ab33c17 100644 --- a/surfsense_web/components/ui/drawer.tsx +++ b/surfsense_web/components/ui/drawer.tsx @@ -93,7 +93,7 @@ DrawerDescription.displayName = DrawerPrimitive.Description.displayName; function DrawerHandle({ className, ...props }: React.HTMLAttributes) { return (
);