diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py index 3b72cabf0..971472876 100644 --- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py +++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py @@ -329,6 +329,7 @@ async def create_surfsense_deep_agent( if not has_gmail_connector: gmail_tools = [ "create_gmail_draft", + "update_gmail_draft", "send_gmail_email", "trash_gmail_email", ] diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 80cdbbb3d..ce811c690 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -26,9 +26,11 @@ import { SquareIcon, Unplug, Upload, + Wrench, X, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; +import Image from "next/image"; import { useParams } from "next/navigation"; import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; @@ -88,7 +90,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { getToolIcon } from "@/contracts/enums/toolIcons"; +import { CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons"; import type { Document } from "@/contracts/types/document.types"; import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useCommentsElectric } from "@/hooks/use-comments-electric"; @@ -598,26 +600,31 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false const { data: agentTools } = useAtomValue(agentToolsAtom); const disabledTools = useAtomValue(disabledToolsAtom); const toggleTool = useSetAtom(toggleToolAtom); + const setDisabledTools = useSetAtom(disabledToolsAtom); const hydrateDisabled = useSetAtom(hydrateDisabledToolsAtom); + const toggleToolGroup = useCallback( + (toolNames: string[]) => { + const allDisabled = toolNames.every((name) => disabledTools.includes(name)); + if (allDisabled) { + setDisabledTools((prev) => prev.filter((t) => !toolNames.includes(t))); + } else { + setDisabledTools((prev) => [...new Set([...prev, ...toolNames])]); + } + }, + [disabledTools, setDisabledTools] + ); + const hasWebSearchTool = agentTools?.some((t) => t.name === "web_search") ?? false; const isWebSearchEnabled = hasWebSearchTool && !disabledTools.includes("web_search"); const filteredTools = useMemo( () => agentTools?.filter((t) => t.name !== "web_search"), [agentTools] ); - const filteredEnabledCount = useMemo(() => { - if (!filteredTools) return 0; - return ( - filteredTools.length - - disabledTools.filter((d) => filteredTools.some((t) => t.name === d)).length - ); - }, [filteredTools, disabledTools]); - const groupedTools = useMemo(() => { if (!filteredTools) return []; const toolsByName = new Map(filteredTools.map((t) => [t.name, t])); - const result: { label: string; tools: typeof filteredTools }[] = []; + const result: { label: string; tools: typeof filteredTools; connectorIcon?: string }[] = []; const placed = new Set(); for (const group of TOOL_GROUPS) { @@ -628,7 +635,7 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false return [tool]; }); if (matched.length > 0) { - result.push({ label: group.label, tools: matched }); + result.push({ label: group.label, tools: matched, connectorIcon: group.connectorIcon }); } } @@ -640,6 +647,24 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false return result; }, [filteredTools]); + const { visibleTotal, visibleEnabled } = useMemo(() => { + let total = 0; + let enabled = 0; + for (const group of groupedTools) { + if (group.connectorIcon) { + total += 1; + const allDisabled = group.tools.every((t) => disabledTools.includes(t.name)); + if (!allDisabled) enabled += 1; + } else { + for (const tool of group.tools) { + total += 1; + if (!disabledTools.includes(tool.name)) enabled += 1; + } + } + } + return { visibleTotal: total, visibleEnabled: enabled }; + }, [groupedTools, disabledTools]); + useEffect(() => { hydrateDisabled(); }, [hydrateDisabled]); @@ -691,11 +716,11 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false
Agent Tools - {filteredEnabledCount}/{filteredTools?.length ?? 0} enabled + {visibleEnabled}/{visibleTotal} enabled
- {groupedTools.map((group) => ( + {groupedTools.filter((g) => !g.connectorIcon).map((group) => (
{group.label} @@ -722,6 +747,46 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false })}
))} + {groupedTools.some((g) => g.connectorIcon) && ( +
+
+ Connector Actions +
+ {groupedTools.filter((g) => g.connectorIcon).map((group) => { + const iconKey = group.connectorIcon ?? ""; + const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; + const toolNames = group.tools.map((t) => t.name); + const allDisabled = toolNames.every((n) => disabledTools.includes(n)); + return ( +
+ {iconInfo ? ( + {iconInfo.alt} + ) : ( + + )} + + {group.label} + + toggleToolGroup(toolNames)} + className="shrink-0" + /> +
+ ); + })} +
+ )} {!filteredTools?.length && (
Loading tools... @@ -766,7 +831,7 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false
Agent Tools - {filteredEnabledCount}/{filteredTools?.length ?? 0} enabled + {visibleEnabled}/{visibleTotal} enabled
= ({ isBlockedByOtherUser = false WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, }} > - {groupedTools.map((group) => ( + {groupedTools.filter((g) => !g.connectorIcon).map((group) => (
{group.label} @@ -809,6 +874,52 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false })}
))} + {groupedTools.some((g) => g.connectorIcon) && ( +
+
+ Connector Actions +
+ {groupedTools.filter((g) => g.connectorIcon).map((group) => { + const iconKey = group.connectorIcon ?? ""; + const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; + const toolNames = group.tools.map((t) => t.name); + const allDisabled = toolNames.every((n) => disabledTools.includes(n)); + const groupDef = TOOL_GROUPS.find((g) => g.label === group.label); + const row = ( +
+ {iconInfo ? ( + {iconInfo.alt} + ) : ( + + )} + + {group.label} + + toggleToolGroup(toolNames)} + className="shrink-0 scale-[0.6] sm:scale-75" + /> +
+ ); + return ( + + {row} + + {groupDef?.tooltip ?? group.tools.map((t) => t.description).join(" ยท ")} + + + ); + })} +
+ )} {!filteredTools?.length && (
Loading tools... @@ -931,7 +1042,14 @@ function formatToolName(name: string): string { .join(" "); } -const TOOL_GROUPS: { label: string; tools: string[] }[] = [ +interface ToolGroup { + label: string; + tools: string[]; + connectorIcon?: string; + tooltip?: string; +} + +const TOOL_GROUPS: ToolGroup[] = [ { label: "Research", tools: ["search_knowledge_base", "search_surfsense_docs", "scrape_webpage", "link_preview"], @@ -944,6 +1062,36 @@ const TOOL_GROUPS: { label: string; tools: string[] }[] = [ label: "Memory", tools: ["save_memory", "recall_memory"], }, + { + label: "Gmail", + tools: ["create_gmail_draft", "update_gmail_draft", "send_gmail_email", "trash_gmail_email"], + connectorIcon: "gmail", + tooltip: "Create drafts, update drafts, send emails, and trash emails in Gmail.", + }, + { + label: "Google Calendar", + tools: ["create_calendar_event", "update_calendar_event", "delete_calendar_event"], + connectorIcon: "google_calendar", + tooltip: "Create, update, and delete events in Google Calendar.", + }, + { + label: "Google Drive", + tools: ["create_google_drive_file", "delete_google_drive_file"], + connectorIcon: "google_drive", + tooltip: "Create and delete files in Google Drive.", + }, + { + label: "Notion", + tools: ["create_notion_page", "update_notion_page", "delete_notion_page"], + connectorIcon: "notion", + tooltip: "Create, update, and delete pages in Notion.", + }, + { + label: "Linear", + tools: ["create_linear_issue", "update_linear_issue", "delete_linear_issue"], + connectorIcon: "linear", + tooltip: "Create, update, and delete issues in Linear.", + }, ]; const MessageError: FC = () => { diff --git a/surfsense_web/contracts/enums/toolIcons.tsx b/surfsense_web/contracts/enums/toolIcons.tsx index 0cf384e49..d8317345c 100644 --- a/surfsense_web/contracts/enums/toolIcons.tsx +++ b/surfsense_web/contracts/enums/toolIcons.tsx @@ -30,3 +30,11 @@ const TOOL_ICONS: Record = { export function getToolIcon(name: string): LucideIcon { return TOOL_ICONS[name] ?? Wrench; } + +export const CONNECTOR_TOOL_ICON_PATHS: Record = { + gmail: { src: "/connectors/google-gmail.svg", alt: "Gmail" }, + google_calendar: { src: "/connectors/google-calendar.svg", alt: "Google Calendar" }, + google_drive: { src: "/connectors/google-drive.svg", alt: "Google Drive" }, + notion: { src: "/connectors/notion.svg", alt: "Notion" }, + linear: { src: "/connectors/linear.svg", alt: "Linear" }, +};