import { ActionBarPrimitive, AuiIf, ErrorPrimitive, MessagePrimitive, useAui, useAuiState, } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { CheckIcon, ClipboardPaste, CopyIcon, DownloadIcon, ExternalLink, Globe, MessageSquare, RefreshCwIcon, } from "lucide-react"; import dynamic from "next/dynamic"; import type { FC } 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, 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 { Drawer, DrawerContent, DrawerHandle, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { useComments } from "@/hooks/use-comments"; import { useMediaQuery } from "@/hooks/use-media-query"; import { useElectronAPI } from "@/hooks/use-platform"; import { cn } from "@/lib/utils"; // Captured once at module load — survives client-side navigations that strip the query param. const IS_QUICK_ASSIST_WINDOW = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("quickAssist") === "true"; // Dynamically import tool UI components to avoid loading them in main bundle const GenerateReportToolUI = dynamic( () => import("@/components/tool-ui/generate-report").then(m => ({ default: m.GenerateReportToolUI })), { ssr: false } ); const GeneratePodcastToolUI = dynamic( () => import("@/components/tool-ui/generate-podcast").then(m => ({ default: m.GeneratePodcastToolUI })), { ssr: false } ); const GenerateVideoPresentationToolUI = dynamic( () => import("@/components/tool-ui/video-presentation").then(m => ({ default: m.GenerateVideoPresentationToolUI })), { ssr: false } ); const GenerateImageToolUI = dynamic( () => import("@/components/tool-ui/generate-image").then(m => ({ default: m.GenerateImageToolUI })), { ssr: false } ); const SaveMemoryToolUI = dynamic( () => import("@/components/tool-ui/user-memory").then(m => ({ default: m.SaveMemoryToolUI })), { ssr: false } ); const RecallMemoryToolUI = dynamic( () => import("@/components/tool-ui/user-memory").then(m => ({ default: m.RecallMemoryToolUI })), { ssr: false } ); const SandboxExecuteToolUI = dynamic( () => import("@/components/tool-ui/sandbox-execute").then(m => ({ default: m.SandboxExecuteToolUI })), { ssr: false } ); const CreateNotionPageToolUI = dynamic( () => import("@/components/tool-ui/notion").then(m => ({ default: m.CreateNotionPageToolUI })), { ssr: false } ); const UpdateNotionPageToolUI = dynamic( () => import("@/components/tool-ui/notion").then(m => ({ default: m.UpdateNotionPageToolUI })), { ssr: false } ); const DeleteNotionPageToolUI = dynamic( () => import("@/components/tool-ui/notion").then(m => ({ default: m.DeleteNotionPageToolUI })), { ssr: false } ); const CreateLinearIssueToolUI = dynamic( () => import("@/components/tool-ui/linear").then(m => ({ default: m.CreateLinearIssueToolUI })), { ssr: false } ); const UpdateLinearIssueToolUI = dynamic( () => import("@/components/tool-ui/linear").then(m => ({ default: m.UpdateLinearIssueToolUI })), { ssr: false } ); const DeleteLinearIssueToolUI = dynamic( () => import("@/components/tool-ui/linear").then(m => ({ default: m.DeleteLinearIssueToolUI })), { ssr: false } ); const CreateGoogleDriveFileToolUI = dynamic( () => import("@/components/tool-ui/google-drive").then(m => ({ default: m.CreateGoogleDriveFileToolUI })), { ssr: false } ); const DeleteGoogleDriveFileToolUI = dynamic( () => import("@/components/tool-ui/google-drive").then(m => ({ default: m.DeleteGoogleDriveFileToolUI })), { ssr: false } ); const CreateOneDriveFileToolUI = dynamic( () => import("@/components/tool-ui/onedrive").then(m => ({ default: m.CreateOneDriveFileToolUI })), { ssr: false } ); const DeleteOneDriveFileToolUI = dynamic( () => import("@/components/tool-ui/onedrive").then(m => ({ default: m.DeleteOneDriveFileToolUI })), { ssr: false } ); const CreateDropboxFileToolUI = dynamic( () => import("@/components/tool-ui/dropbox").then(m => ({ default: m.CreateDropboxFileToolUI })), { ssr: false } ); const DeleteDropboxFileToolUI = dynamic( () => import("@/components/tool-ui/dropbox").then(m => ({ default: m.DeleteDropboxFileToolUI })), { ssr: false } ); const CreateCalendarEventToolUI = dynamic( () => import("@/components/tool-ui/google-calendar").then(m => ({ default: m.CreateCalendarEventToolUI })), { ssr: false } ); const UpdateCalendarEventToolUI = dynamic( () => import("@/components/tool-ui/google-calendar").then(m => ({ default: m.UpdateCalendarEventToolUI })), { ssr: false } ); const DeleteCalendarEventToolUI = dynamic( () => import("@/components/tool-ui/google-calendar").then(m => ({ default: m.DeleteCalendarEventToolUI })), { ssr: false } ); const CreateGmailDraftToolUI = dynamic( () => import("@/components/tool-ui/gmail").then(m => ({ default: m.CreateGmailDraftToolUI })), { ssr: false } ); const UpdateGmailDraftToolUI = dynamic( () => import("@/components/tool-ui/gmail").then(m => ({ default: m.UpdateGmailDraftToolUI })), { ssr: false } ); const SendGmailEmailToolUI = dynamic( () => import("@/components/tool-ui/gmail").then(m => ({ default: m.SendGmailEmailToolUI })), { ssr: false } ); const TrashGmailEmailToolUI = dynamic( () => import("@/components/tool-ui/gmail").then(m => ({ default: m.TrashGmailEmailToolUI })), { ssr: false } ); const CreateJiraIssueToolUI = dynamic( () => import("@/components/tool-ui/jira").then(m => ({ default: m.CreateJiraIssueToolUI })), { ssr: false } ); const UpdateJiraIssueToolUI = dynamic( () => import("@/components/tool-ui/jira").then(m => ({ default: m.UpdateJiraIssueToolUI })), { ssr: false } ); const DeleteJiraIssueToolUI = dynamic( () => import("@/components/tool-ui/jira").then(m => ({ default: m.DeleteJiraIssueToolUI })), { ssr: false } ); const CreateConfluencePageToolUI = dynamic( () => import("@/components/tool-ui/confluence").then(m => ({ default: m.CreateConfluencePageToolUI })), { ssr: false } ); const UpdateConfluencePageToolUI = dynamic( () => import("@/components/tool-ui/confluence").then(m => ({ default: m.UpdateConfluencePageToolUI })), { ssr: false } ); const DeleteConfluencePageToolUI = dynamic( () => import("@/components/tool-ui/confluence").then(m => ({ default: m.DeleteConfluencePageToolUI })), { ssr: false } ); 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 ( <> 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" )} > {visible.map((citation, index) => ( 0 && "-ml-2" )} style={{ zIndex: maxIcons - index }} > {citation.favicon ? ( // biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain ) : ( )} ))} {remainingCount > 0 && ( ••• )} {citations.length} source{citations.length !== 1 && "s"} Sources {citations.map((citation) => ( 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 ) : ( )} {citation.title} {citation.domain} ))} > ); }; export const MessageError: FC = () => { return ( ); }; const AssistantMessageInner: FC = () => { const isMobile = !useMediaQuery("(min-width: 768px)"); return ( null, link_preview: () => null, multi_link_preview: () => null, scrape_webpage: () => null, }, Fallback: ToolFallback, }, }} /> {isMobile && ( )} ); }; function parseMessageId(assistantUiMessageId: string | undefined): number | null { if (!assistantUiMessageId) return null; const match = assistantUiMessageId.match(/^msg-(\d+)$/); return match ? Number.parseInt(match[1], 10) : null; } export const AssistantMessage: FC = () => { const [isSheetOpen, setIsSheetOpen] = useState(false); const [isInlineOpen, setIsInlineOpen] = useState(false); const messageRef = useRef(null); const commentPanelRef = useRef(null); const commentTriggerRef = useRef(null); const messageId = useAuiState(({ message }) => message?.id); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const dbMessageId = parseMessageId(messageId); const commentsEnabled = useAtomValue(commentsEnabledAtom); // Desktop: >= 1024px (inline expandable), Medium: 768px-1023px (right sheet), Mobile: <768px (bottom sheet) const isMediumScreen = useMediaQuery("(min-width: 768px) and (max-width: 1023px)"); const isDesktop = useMediaQuery("(min-width: 1024px)"); const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); const isLastMessage = useAuiState(({ message }) => message?.isLast ?? false); const isMessageStreaming = isThreadRunning && isLastMessage; const { data: commentsData, isSuccess: commentsLoaded } = useComments({ messageId: dbMessageId ?? 0, enabled: !!dbMessageId, }); const targetCommentId = useAtomValue(targetCommentIdAtom); const hasTargetComment = useMemo(() => { if (!targetCommentId || !commentsData?.comments) return false; return commentsData.comments.some( (c) => c.id === targetCommentId || c.replies?.some((r) => r.id === targetCommentId) ); }, [targetCommentId, commentsData]); const commentCount = commentsData?.total_count ?? 0; const hasComments = commentCount > 0; const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId; // Close floating panel when clicking outside (but not on portaled popover/dropdown content) useEffect(() => { if (!isInlineOpen) return; const handleClickOutside = (e: MouseEvent) => { const target = e.target as Element; if ( commentPanelRef.current?.contains(target) || commentTriggerRef.current?.contains(target) || target.closest?.("[data-radix-popper-content-wrapper]") ) return; setIsInlineOpen(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isInlineOpen]); // Auto-open floating panel on desktop when this message has the target comment useEffect(() => { if (hasTargetComment && isDesktop && commentsLoaded) { setIsInlineOpen(true); const timeoutId = setTimeout(() => { messageRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); return () => clearTimeout(timeoutId); } }, [hasTargetComment, isDesktop, commentsLoaded]); // Auto-open sheet on mobile/tablet when this message has the target comment useEffect(() => { if (hasTargetComment && !isDesktop && commentsLoaded) { setIsSheetOpen(true); } }, [hasTargetComment, isDesktop, commentsLoaded]); const sheetSide = isMediumScreen ? "right" : "bottom"; return ( {/* Comment trigger — right-aligned, just below user query on all screen sizes */} {showCommentTrigger && ( setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true) } className={cn( "flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors", isDesktop && isInlineOpen ? "bg-primary/10 text-primary" : hasComments ? "text-primary hover:bg-primary/10" : "text-muted-foreground hover:text-foreground hover:bg-muted" )} > {hasComments ? ( {commentCount} {commentCount === 1 ? "comment" : "comments"} ) : ( Add comment )} )} {/* Desktop floating comment panel — overlays on top of chat content */} {showCommentTrigger && isDesktop && isInlineOpen && dbMessageId && ( )} {/* Comment sheet — bottom for mobile, right for medium screens */} {showCommentTrigger && !isDesktop && ( )} ); }; const AssistantActionBar: FC = () => { const isLast = useAuiState((s) => s.message.isLast); const aui = useAui(); const api = useElectronAPI(); const isQuickAssist = !!api?.replaceText && IS_QUICK_ASSIST_WINDOW; return ( message.isCopied}> !message.isCopied}> {isLast && ( )} {isQuickAssist && ( { const text = aui.message().getCopyText(); api?.replaceText(text); }} > )} ); };
{citation.title}
{citation.domain}