diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 3f6893169..f0de33826 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -33,8 +33,8 @@ import { closeReportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { closeEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { membersAtom } from "@/atoms/members/members-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { Thread } from "@/components/assistant-ui/thread"; import { ThinkingStepsDataUI } from "@/components/assistant-ui/thinking-steps"; +import { Thread } from "@/components/assistant-ui/thread"; import { MobileEditorPanel } from "@/components/editor-panel/editor-panel"; import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel"; import { MobileReportPanel } from "@/components/report-panel/report-panel"; diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index 781bc8e3a..9fefecb1c 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -16,19 +16,47 @@ 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 { CreateConfluencePageToolUI, DeleteConfluencePageToolUI, UpdateConfluencePageToolUI } from "@/components/tool-ui/confluence"; +import { + CreateConfluencePageToolUI, + DeleteConfluencePageToolUI, + UpdateConfluencePageToolUI, +} from "@/components/tool-ui/confluence"; import { GenerateImageToolUI } from "@/components/tool-ui/generate-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { GenerateReportToolUI } from "@/components/tool-ui/generate-report"; -import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; -import { CreateGmailDraftToolUI, SendGmailEmailToolUI, TrashGmailEmailToolUI, UpdateGmailDraftToolUI } from "@/components/tool-ui/gmail"; -import { CreateCalendarEventToolUI, DeleteCalendarEventToolUI, UpdateCalendarEventToolUI } from "@/components/tool-ui/google-calendar"; -import { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "@/components/tool-ui/google-drive"; -import { CreateJiraIssueToolUI, DeleteJiraIssueToolUI, UpdateJiraIssueToolUI } from "@/components/tool-ui/jira"; -import { CreateLinearIssueToolUI, DeleteLinearIssueToolUI, UpdateLinearIssueToolUI } from "@/components/tool-ui/linear"; -import { CreateNotionPageToolUI, DeleteNotionPageToolUI, UpdateNotionPageToolUI } from "@/components/tool-ui/notion"; +import { + CreateGmailDraftToolUI, + SendGmailEmailToolUI, + TrashGmailEmailToolUI, + UpdateGmailDraftToolUI, +} from "@/components/tool-ui/gmail"; +import { + CreateCalendarEventToolUI, + DeleteCalendarEventToolUI, + UpdateCalendarEventToolUI, +} from "@/components/tool-ui/google-calendar"; +import { + CreateGoogleDriveFileToolUI, + DeleteGoogleDriveFileToolUI, +} from "@/components/tool-ui/google-drive"; +import { + CreateJiraIssueToolUI, + DeleteJiraIssueToolUI, + UpdateJiraIssueToolUI, +} from "@/components/tool-ui/jira"; +import { + CreateLinearIssueToolUI, + DeleteLinearIssueToolUI, + UpdateLinearIssueToolUI, +} from "@/components/tool-ui/linear"; +import { + CreateNotionPageToolUI, + DeleteNotionPageToolUI, + UpdateNotionPageToolUI, +} from "@/components/tool-ui/notion"; import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute"; import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory"; +import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation"; import { useComments } from "@/hooks/use-comments"; import { useMediaQuery } from "@/hooks/use-media-query"; import { cn } from "@/lib/utils"; @@ -246,13 +274,13 @@ const AssistantActionBar: FC = () => { const isLast = useAuiState((s) => s.message.isLast); return ( - - + message.isCopied}> @@ -262,19 +290,19 @@ const AssistantActionBar: FC = () => { - + - {/* Only allow regenerating the last assistant message */} - {isLast && ( + {/* Only allow regenerating the last assistant message */} + {isLast && ( )} - - ); + + ); }; diff --git a/surfsense_web/components/assistant-ui/image.tsx b/surfsense_web/components/assistant-ui/image.tsx index e610d70aa..65059bcdc 100644 --- a/surfsense_web/components/assistant-ui/image.tsx +++ b/surfsense_web/components/assistant-ui/image.tsx @@ -1,255 +1,221 @@ "use client"; -import { - memo, - useState, - useEffect, - useRef, - type PropsWithChildren, -} from "react"; -import { createPortal } from "react-dom"; +import type { ImageMessagePartComponent } from "@assistant-ui/react"; import { cva, type VariantProps } from "class-variance-authority"; import { ImageIcon, ImageOffIcon } from "lucide-react"; -import type { ImageMessagePartComponent } from "@assistant-ui/react"; +import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { cn } from "@/lib/utils"; -const imageVariants = cva( - "aui-image-root relative overflow-hidden rounded-lg", - { - variants: { - variant: { - outline: "border border-border", - ghost: "", - muted: "bg-muted/50", - }, - size: { - sm: "max-w-64", - default: "max-w-96", - lg: "max-w-[512px]", - full: "w-full", - }, - }, - defaultVariants: { - variant: "outline", - size: "default", - }, - }, -); +const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", { + variants: { + variant: { + outline: "border border-border", + ghost: "", + muted: "bg-muted/50", + }, + size: { + sm: "max-w-64", + default: "max-w-96", + lg: "max-w-[512px]", + full: "w-full", + }, + }, + defaultVariants: { + variant: "outline", + size: "default", + }, +}); -export type ImageRootProps = React.ComponentProps<"div"> & - VariantProps; +export type ImageRootProps = React.ComponentProps<"div"> & VariantProps; -function ImageRoot({ - className, - variant, - size, - children, - ...props -}: ImageRootProps) { - return ( -
- {children} -
- ); +function ImageRoot({ className, variant, size, children, ...props }: ImageRootProps) { + return ( +
+ {children} +
+ ); } type ImagePreviewProps = Omit, "children"> & { - containerClassName?: string; + containerClassName?: string; }; function ImagePreview({ - className, - containerClassName, - onLoad, - onError, - alt = "Image content", - src, - ...props + className, + containerClassName, + onLoad, + onError, + alt = "Image content", + src, + ...props }: ImagePreviewProps) { - const imgRef = useRef(null); - const [loadedSrc, setLoadedSrc] = useState(undefined); - const [errorSrc, setErrorSrc] = useState(undefined); + const imgRef = useRef(null); + const [loadedSrc, setLoadedSrc] = useState(undefined); + const [errorSrc, setErrorSrc] = useState(undefined); - const loaded = loadedSrc === src; - const error = errorSrc === src; + const loaded = loadedSrc === src; + const error = errorSrc === src; - useEffect(() => { - if ( - typeof src === "string" && - imgRef.current?.complete && - imgRef.current.naturalWidth > 0 - ) { - setLoadedSrc(src); - } - }, [src]); + useEffect(() => { + if (typeof src === "string" && imgRef.current?.complete && imgRef.current.naturalWidth > 0) { + setLoadedSrc(src); + } + }, [src]); - return ( -
- {!loaded && !error && ( -
- -
- )} - {error ? ( -
- -
- ) : ( - // biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs - {alt} { - if (typeof src === "string") setLoadedSrc(src); - onLoad?.(e); - }} - onError={(e) => { - if (typeof src === "string") setErrorSrc(src); - onError?.(e); - }} - {...props} - /> - )} -
- ); + return ( +
+ {!loaded && !error && ( +
+ +
+ )} + {error ? ( +
+ +
+ ) : ( + // biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs + {alt} { + if (typeof src === "string") setLoadedSrc(src); + onLoad?.(e); + }} + onError={(e) => { + if (typeof src === "string") setErrorSrc(src); + onError?.(e); + }} + {...props} + /> + )} +
+ ); } -function ImageFilename({ - className, - children, - ...props -}: React.ComponentProps<"span">) { - if (!children) return null; +function ImageFilename({ className, children, ...props }: React.ComponentProps<"span">) { + if (!children) return null; - return ( - - {children} - - ); + return ( + + {children} + + ); } type ImageZoomProps = PropsWithChildren<{ - src: string; - alt?: string; + src: string; + alt?: string; }>; function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) { - const [isMounted, setIsMounted] = useState(false); - const [isOpen, setIsOpen] = useState(false); + const [isMounted, setIsMounted] = useState(false); + const [isOpen, setIsOpen] = useState(false); - useEffect(() => { - setIsMounted(true); - }, []); + useEffect(() => { + setIsMounted(true); + }, []); - const handleOpen = () => setIsOpen(true); - const handleClose = () => setIsOpen(false); + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); - useEffect(() => { - if (!isOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") setIsOpen(false); - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsOpen(false); + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen]); - useEffect(() => { - if (!isOpen) return; - const originalOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - return () => { - document.body.style.overflow = originalOverflow; - }; - }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, [isOpen]); - return ( - <> - - {isMounted && - isOpen && - createPortal( - , - document.body, - )} - - ); + return ( + <> + + {isMounted && + isOpen && + createPortal( + , + document.body + )} + + ); } const ImageImpl: ImageMessagePartComponent = ({ image, filename }) => { - return ( - - - - - {filename} - - ); + return ( + + + + + {filename} + + ); }; const Image = memo(ImageImpl) as unknown as ImageMessagePartComponent & { - Root: typeof ImageRoot; - Preview: typeof ImagePreview; - Filename: typeof ImageFilename; - Zoom: typeof ImageZoom; + Root: typeof ImageRoot; + Preview: typeof ImagePreview; + Filename: typeof ImageFilename; + Zoom: typeof ImageZoom; }; Image.displayName = "Image"; @@ -258,11 +224,4 @@ Image.Preview = ImagePreview; Image.Filename = ImageFilename; Image.Zoom = ImageZoom; -export { - Image, - ImageRoot, - ImagePreview, - ImageFilename, - ImageZoom, - imageVariants, -}; +export { Image, ImageRoot, ImagePreview, ImageFilename, ImageZoom, imageVariants }; diff --git a/surfsense_web/components/assistant-ui/markdown-text.tsx b/surfsense_web/components/assistant-ui/markdown-text.tsx index 651aa0cd5..3d33463b2 100644 --- a/surfsense_web/components/assistant-ui/markdown-text.tsx +++ b/surfsense_web/components/assistant-ui/markdown-text.tsx @@ -8,23 +8,30 @@ import { useIsMarkdownCodeBlock, } from "@assistant-ui/react-markdown"; import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react"; -import { type FC, memo, type ReactNode, useState } from "react"; import { useTheme } from "next-themes"; +import type { CSSProperties } from "react"; +import { type FC, memo, type ReactNode, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism"; -import type { CSSProperties } from "react"; -import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; +import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image"; import "katex/dist/katex.min.css"; import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { cn } from "@/lib/utils"; function stripThemeBackgrounds( - theme: Record, + theme: Record ): Record { const cleaned: Record = {}; for (const key of Object.keys(theme)) { @@ -261,9 +268,7 @@ function MarkdownImage({ src, alt }: { src?: string; alt?: string }) { {alt && alt !== "Image" && (

{alt}

)} - {domain && ( -

{domain}

- )} + {domain &&

{domain}

} ), - tr: ({ className, ...props }) => ( - - ), + tr: ({ className, ...props }) => , sup: ({ className, ...props }) => ( a]:text-xs [&>a]:no-underline", className)} {...props} /> ), @@ -448,6 +451,8 @@ const defaultComponents = memoizeMarkdownComponents({ {processChildrenWithCitations(children)} ), - img: ({ src, alt }) => , + img: ({ src, alt }) => ( + + ), CodeHeader: () => null, }); diff --git a/surfsense_web/components/assistant-ui/thinking-steps.tsx b/surfsense_web/components/assistant-ui/thinking-steps.tsx index cf0c4ce52..900fc7b09 100644 --- a/surfsense_web/components/assistant-ui/thinking-steps.tsx +++ b/surfsense_web/components/assistant-ui/thinking-steps.tsx @@ -126,8 +126,8 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: {step.items && step.items.length > 0 && (
- {step.items.map((item) => ( - + {step.items.map((item) => ( + {item} ))} @@ -169,4 +169,3 @@ export const ThinkingStepsDataUI = makeAssistantDataUI({ name: "thinking-steps", render: ThinkingStepsDataRenderer, }); - diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index f160114ba..89869dc7e 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -101,13 +101,13 @@ export const Thread: FC = () => { const ThreadContent: FC = () => { return ( - - @@ -135,8 +135,8 @@ const ThreadContent: FC = () => { - - ); + + ); }; const ThreadScrollToBottom: FC = () => { @@ -678,8 +678,8 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser; return ( -
-
+
+
{!isDesktop ? ( <> @@ -983,13 +983,13 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false )}
- {!hasModelConfigured && ( + {!hasModelConfigured && (
Select a model
)} -
+
!thread.isRunning}> = ({ isBlockedByOtherUser = false
-
- ); +
+ ); }; /** Convert snake_case tool names to human-readable labels */ diff --git a/surfsense_web/components/assistant-ui/tool-fallback.tsx b/surfsense_web/components/assistant-ui/tool-fallback.tsx index d12ffb5d6..89498fbca 100644 --- a/surfsense_web/components/assistant-ui/tool-fallback.tsx +++ b/surfsense_web/components/assistant-ui/tool-fallback.tsx @@ -1,13 +1,11 @@ import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon } from "lucide-react"; import { useState } from "react"; -import { cn } from "@/lib/utils"; import { getToolIcon } from "@/contracts/enums/toolIcons"; +import { cn } from "@/lib/utils"; function formatToolName(name: string): string { - return name - .replace(/_/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase()); + return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } export const ToolFallback: ToolCallMessagePartComponent = ({ @@ -42,7 +40,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({ className={cn( "my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none", isCancelled && "opacity-60", - isError && "border-destructive/20 bg-destructive/5", + isError && "border-destructive/20 bg-destructive/5" )} >