From 4474b73f299a1d27c6c04cd042f39ee3da7fb298 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:50:14 +0530 Subject: [PATCH 001/103] feat: remove WIP status HITL notion, linear and google drive tools --- .../app/agents/new_chat/chat_deepagent.py | 12 +++++++++ .../app/agents/new_chat/tools/registry.py | 25 +++++-------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py index c247ada61..713545b0b 100644 --- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py +++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py @@ -292,6 +292,18 @@ async def create_surfsense_deep_agent( ] modified_disabled_tools.extend(linear_tools) + # Disable Google Drive action tools if no Google Drive connector is configured + has_google_drive_connector = ( + available_connectors is not None + and "GOOGLE_DRIVE_FILE" in available_connectors + ) + if not has_google_drive_connector: + google_drive_tools = [ + "create_google_drive_file", + "delete_google_drive_file", + ] + modified_disabled_tools.extend(google_drive_tools) + # Build tools using the async registry (includes MCP tools) _t0 = time.perf_counter() tools = await build_tools_async( diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py index 6f2e36b08..c2592207a 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -245,7 +245,8 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ requires=["user_id", "search_space_id", "db_session", "thread_visibility"], ), # ========================================================================= - # LINEAR TOOLS - create, update, delete issues (WIP - hidden from UI) + # LINEAR TOOLS - create, update, delete issues + # Auto-disabled when no Linear connector is configured (see chat_deepagent.py) # ========================================================================= ToolDefinition( name="create_linear_issue", @@ -256,8 +257,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ToolDefinition( name="update_linear_issue", @@ -268,8 +267,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ToolDefinition( name="delete_linear_issue", @@ -280,11 +277,10 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), # ========================================================================= - # NOTION TOOLS - create, update, delete pages (WIP - hidden from UI) + # NOTION TOOLS - create, update, delete pages + # Auto-disabled when no Notion connector is configured (see chat_deepagent.py) # ========================================================================= ToolDefinition( name="create_notion_page", @@ -295,8 +291,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ToolDefinition( name="update_notion_page", @@ -307,8 +301,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ToolDefinition( name="delete_notion_page", @@ -319,11 +311,10 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), # ========================================================================= - # GOOGLE DRIVE TOOLS - create files, delete files (WIP - hidden from UI) + # GOOGLE DRIVE TOOLS - create files, delete files + # Auto-disabled when no Google Drive connector is configured (see chat_deepagent.py) # ========================================================================= ToolDefinition( name="create_google_drive_file", @@ -334,8 +325,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ToolDefinition( name="delete_google_drive_file", @@ -346,8 +335,6 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ user_id=deps["user_id"], ), requires=["db_session", "search_space_id", "user_id"], - enabled_by_default=False, - hidden=True, ), ] From 20c444f83c802294fcd87fb2417938502050b1c7 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:16:37 +0530 Subject: [PATCH 002/103] refactor: edit HITL prompts to be more expressive --- .../new_chat/tools/google_drive/create_file.py | 14 ++++++++------ .../agents/new_chat/tools/linear/create_issue.py | 14 ++++++++------ .../agents/new_chat/tools/notion/create_page.py | 14 +++++++++----- .../agents/new_chat/tools/notion/update_page.py | 11 +++++++---- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py index af93ddc8f..6e0bfd28a 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py @@ -32,13 +32,15 @@ def create_create_google_drive_file_tool( """Create a new Google Doc or Google Sheet in Google Drive. Use this tool when the user explicitly asks to create a new document - or spreadsheet in Google Drive. + or spreadsheet in Google Drive. The user MUST specify a topic before + you call this tool. If the request is vague, ask what the file should + contain. Never call this tool without a clear topic from the user. Args: - name: The file name (without extension). + name: The file name (without extension). Infer from the user's request. file_type: Either "google_doc" or "google_sheet". - content: Optional initial content. For google_doc, provide markdown text. - For google_sheet, provide CSV-formatted text. + content: Optional initial content. Generate from the user's topic. + For google_doc, provide markdown text. For google_sheet, provide CSV-formatted text. Returns: Dictionary with: @@ -55,8 +57,8 @@ def create_create_google_drive_file_tool( Inform the user they need to re-authenticate and do NOT retry the action. Examples: - - "Create a Google Doc called 'Meeting Notes'" - - "Create a spreadsheet named 'Budget 2026' with some sample data" + - "Create a Google Doc with today's meeting notes" + - "Create a spreadsheet for the 2026 budget" """ logger.info( f"create_google_drive_file called: name='{name}', type='{file_type}'" diff --git a/surfsense_backend/app/agents/new_chat/tools/linear/create_issue.py b/surfsense_backend/app/agents/new_chat/tools/linear/create_issue.py index a213fe6fa..c4c72f3ac 100644 --- a/surfsense_backend/app/agents/new_chat/tools/linear/create_issue.py +++ b/surfsense_backend/app/agents/new_chat/tools/linear/create_issue.py @@ -38,11 +38,13 @@ def create_create_linear_issue_tool( """Create a new issue in Linear. Use this tool when the user explicitly asks to create, add, or file - a new issue / ticket / task in Linear. + a new issue / ticket / task in Linear. The user MUST describe the issue + before you call this tool. If the request is vague, ask what the issue + should be about. Never call this tool without a clear topic from the user. Args: - title: Short, descriptive issue title. - description: Optional markdown body for the issue. + title: Short, descriptive issue title. Infer from the user's request. + description: Optional markdown body for the issue. Generate from context. Returns: Dictionary with: @@ -57,9 +59,9 @@ def create_create_linear_issue_tool( and move on. Do NOT retry, troubleshoot, or suggest alternatives. Examples: - - "Create a Linear issue titled 'Fix login bug'" - - "Add a ticket for the payment timeout problem" - - "File an issue about the broken search feature" + - "Create a Linear issue for the login bug" + - "File a ticket about the payment timeout problem" + - "Add an issue for the broken search feature" """ logger.info(f"create_linear_issue called: title='{title}'") diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py index f5ccc5b19..0ed773f3a 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py @@ -33,17 +33,21 @@ def create_create_notion_page_tool( @tool async def create_notion_page( title: str, - content: str, + content: str | None = None, ) -> dict[str, Any]: """Create a new page in Notion with the given title and content. Use this tool when the user asks you to create, save, or publish something to Notion. The page will be created in the user's - configured Notion workspace. + configured Notion workspace. The user MUST specify a topic before you + call this tool. If the request does not contain a topic (e.g. "create a + notion page"), ask what the page should be about. Never call this tool + without a clear topic from the user. Args: title: The title of the Notion page. - content: The markdown content for the page body (supports headings, lists, paragraphs). + content: Optional markdown content for the page body (supports headings, lists, paragraphs). + Generate this yourself based on the user's topic. Returns: Dictionary with: @@ -58,8 +62,8 @@ def create_create_notion_page_tool( and move on. Do NOT troubleshoot or suggest alternatives. Examples: - - "Create a Notion page titled 'Meeting Notes' with content 'Discussed project timeline'" - - "Save this to Notion with title 'Research Summary'" + - "Create a Notion page about our Q2 roadmap" + - "Save a summary of today's discussion to Notion" """ logger.info(f"create_notion_page called: title='{title}'") diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py index b194dea50..ecf6fcd47 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py @@ -33,16 +33,19 @@ def create_update_notion_page_tool( @tool async def update_notion_page( page_title: str, - content: str, + content: str | None = None, ) -> dict[str, Any]: """Update an existing Notion page by appending new content. Use this tool when the user asks you to add content to, modify, or update a Notion page. The new content will be appended to the existing page content. + The user MUST specify what to add before you call this tool. If the + request is vague, ask what content they want added. Args: page_title: The title of the Notion page to update. - content: The markdown content to append to the page body (supports headings, lists, paragraphs). + content: Optional markdown content to append to the page body (supports headings, lists, paragraphs). + Generate this yourself based on the user's request. Returns: Dictionary with: @@ -62,8 +65,8 @@ def create_update_notion_page_tool( ask the user to verify the page title or check if it's been indexed. Examples: - - "Add 'New meeting notes from today' to the 'Meeting Notes' Notion page" - - "Append the following to the 'Project Plan' Notion page: '# Status Update\n\nCompleted phase 1'" + - "Add today's meeting notes to the 'Meeting Notes' Notion page" + - "Update the 'Project Plan' page with a status update on phase 1" """ logger.info( f"update_notion_page called: page_title='{page_title}', content_length={len(content) if content else 0}" From 39ce5979075433860ed505ccbb995d4948731625 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:33:22 +0530 Subject: [PATCH 003/103] feat: add HITL edit panel functionality and integrate with existing components - Introduced HITL edit panel atom for managing state. - Implemented HITL edit panel component for both desktop and mobile views. - Updated right panel to include HITL edit tab and handle its visibility. - Enhanced NewChatPage to incorporate the HITL edit panel for improved user interaction. --- .../new-chat/[[...chat_id]]/page.tsx | 2 + .../atoms/chat/hitl-edit-panel.atom.ts | 59 +++ .../atoms/layout/right-panel.atom.ts | 2 +- .../hitl-edit-panel/hitl-edit-panel.tsx | 154 ++++++ .../layout/ui/right-panel/RightPanel.tsx | 37 +- .../tool-ui/notion/create-notion-page.tsx | 469 ++++++++---------- 6 files changed, 453 insertions(+), 270 deletions(-) create mode 100644 surfsense_web/atoms/chat/hitl-edit-panel.atom.ts create mode 100644 surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx 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 a96725619..a17d7362b 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 @@ -35,6 +35,7 @@ import { membersAtom } from "@/atoms/members/members-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; 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"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; @@ -1683,6 +1684,7 @@ export default function NewChatPage() { + ); diff --git a/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts new file mode 100644 index 000000000..eec3879ab --- /dev/null +++ b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts @@ -0,0 +1,59 @@ +import { atom } from "jotai"; +import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom"; + +interface HitlEditPanelState { + isOpen: boolean; + title: string; + content: string; + toolName: string; + onSave: ((title: string, content: string) => void) | null; +} + +const initialState: HitlEditPanelState = { + isOpen: false, + title: "", + content: "", + toolName: "", + onSave: null, +}; + +export const hitlEditPanelAtom = atom(initialState); + +const preHitlCollapsedAtom = atom(null); + +export const openHitlEditPanelAtom = atom( + null, + ( + get, + set, + payload: { + title: string; + content: string; + toolName: string; + onSave: (title: string, content: string) => void; + } + ) => { + if (!get(hitlEditPanelAtom).isOpen) { + set(preHitlCollapsedAtom, get(rightPanelCollapsedAtom)); + } + set(hitlEditPanelAtom, { + isOpen: true, + title: payload.title, + content: payload.content, + toolName: payload.toolName, + onSave: payload.onSave, + }); + set(rightPanelTabAtom, "hitl-edit"); + set(rightPanelCollapsedAtom, false); + } +); + +export const closeHitlEditPanelAtom = atom(null, (get, set) => { + set(hitlEditPanelAtom, initialState); + set(rightPanelTabAtom, "sources"); + const prev = get(preHitlCollapsedAtom); + if (prev !== null) { + set(rightPanelCollapsedAtom, prev); + set(preHitlCollapsedAtom, null); + } +}); diff --git a/surfsense_web/atoms/layout/right-panel.atom.ts b/surfsense_web/atoms/layout/right-panel.atom.ts index fa1b80613..e06500113 100644 --- a/surfsense_web/atoms/layout/right-panel.atom.ts +++ b/surfsense_web/atoms/layout/right-panel.atom.ts @@ -1,6 +1,6 @@ import { atom } from "jotai"; -export type RightPanelTab = "sources" | "report" | "editor"; +export type RightPanelTab = "sources" | "report" | "editor" | "hitl-edit"; export const rightPanelTabAtom = atom("sources"); diff --git a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx new file mode 100644 index 000000000..c397fcf3e --- /dev/null +++ b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useAtomValue, useSetAtom } from "jotai"; +import { XIcon } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; +import { + closeHitlEditPanelAtom, + hitlEditPanelAtom, +} from "@/atoms/chat/hitl-edit-panel.atom"; +import { PlateEditor } from "@/components/editor/plate-editor"; +import { Button } from "@/components/ui/button"; +import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +export function HitlEditPanelContent({ + title: initialTitle, + content: initialContent, + onSave, + onClose, +}: { + title: string; + content: string; + toolName: string; + onSave: (title: string, content: string) => void; + onClose?: () => void; +}) { + const [editedTitle, setEditedTitle] = useState(initialTitle); + const markdownRef = useRef(initialContent); + const [isSaving, setIsSaving] = useState(false); + + const handleMarkdownChange = useCallback((md: string) => { + markdownRef.current = md; + }, []); + + const handleSave = useCallback(() => { + if (!editedTitle.trim()) return; + setIsSaving(true); + onSave(editedTitle, markdownRef.current); + onClose?.(); + }, [editedTitle, onSave, onClose]); + + return ( + <> +
+ setEditedTitle(e.target.value)} + placeholder="Untitled" + className="flex-1 min-w-0 bg-transparent text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground" + aria-label="Page title" + /> + {onClose && ( + + )} +
+ +
+ +
+ + ); +} + +function DesktopHitlEditPanel() { + const panelState = useAtomValue(hitlEditPanelAtom); + const closePanel = useSetAtom(closeHitlEditPanelAtom); + + if (!panelState.isOpen || !panelState.onSave) return null; + + return ( +
+ +
+ ); +} + +function MobileHitlEditDrawer() { + const panelState = useAtomValue(hitlEditPanelAtom); + const closePanel = useSetAtom(closeHitlEditPanelAtom); + + if (!panelState.onSave) return null; + + return ( + { + if (!open) closePanel(); + }} + shouldScaleBackground={false} + > + + + + Edit {panelState.toolName} + +
+ +
+
+
+ ); +} + +export function HitlEditPanel() { + const panelState = useAtomValue(hitlEditPanelAtom); + const isDesktop = useMediaQuery("(min-width: 1024px)"); + + if (!panelState.isOpen) return null; + + if (isDesktop) { + return ; + } + + return ; +} + +export function MobileHitlEditPanel() { + const panelState = useAtomValue(hitlEditPanelAtom); + const isDesktop = useMediaQuery("(min-width: 1024px)"); + + if (isDesktop || !panelState.isOpen) return null; + + return ; +} diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx index e94982bc5..2489d2a64 100644 --- a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx +++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx @@ -3,11 +3,13 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { PanelRight, PanelRightClose } from "lucide-react"; import { startTransition, useEffect } from "react"; +import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms"; import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom"; import { EditorPanelContent } from "@/components/editor-panel/editor-panel"; +import { HitlEditPanelContent } from "@/components/hitl-edit-panel/hitl-edit-panel"; import { ReportPanelContent } from "@/components/report-panel/report-panel"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -44,9 +46,11 @@ export function RightPanelExpandButton() { const documentsOpen = useAtomValue(documentsSidebarOpenAtom); const reportState = useAtomValue(reportPanelAtom); const editorState = useAtomValue(editorPanelAtom); + const hitlEditState = useAtomValue(hitlEditPanelAtom); const reportOpen = reportState.isOpen && !!reportState.reportId; const editorOpen = editorState.isOpen && !!editorState.documentId; - const hasContent = documentsOpen || reportOpen || editorOpen; + const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; + const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen; if (!collapsed || !hasContent) return null; @@ -70,7 +74,7 @@ export function RightPanelExpandButton() { ); } -const PANEL_WIDTHS = { sources: 420, report: 640, editor: 640 } as const; +const PANEL_WIDTHS = { sources: 420, report: 640, editor: 640, "hitl-edit": 640 } as const; export function RightPanel({ documentsPanel }: RightPanelProps) { const [activeTab] = useAtom(rightPanelTabAtom); @@ -78,33 +82,39 @@ export function RightPanel({ documentsPanel }: RightPanelProps) { const closeReport = useSetAtom(closeReportPanelAtom); const editorState = useAtomValue(editorPanelAtom); const closeEditor = useSetAtom(closeEditorPanelAtom); + const hitlEditState = useAtomValue(hitlEditPanelAtom); + const closeHitlEdit = useSetAtom(closeHitlEditPanelAtom); const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom); const documentsOpen = documentsPanel?.open ?? false; const reportOpen = reportState.isOpen && !!reportState.reportId; const editorOpen = editorState.isOpen && !!editorState.documentId; + const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; useEffect(() => { - if (!reportOpen && !editorOpen) return; + if (!reportOpen && !editorOpen && !hitlEditOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { - if (editorOpen) closeEditor(); + if (hitlEditOpen) closeHitlEdit(); + else if (editorOpen) closeEditor(); else if (reportOpen) closeReport(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [reportOpen, editorOpen, closeReport, closeEditor]); + }, [reportOpen, editorOpen, hitlEditOpen, closeReport, closeEditor, closeHitlEdit]); - const isVisible = (documentsOpen || reportOpen || editorOpen) && !collapsed; + const isVisible = (documentsOpen || reportOpen || editorOpen || hitlEditOpen) && !collapsed; let effectiveTab = activeTab; - if (effectiveTab === "editor" && !editorOpen) { + if (effectiveTab === "hitl-edit" && !hitlEditOpen) { + effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources"; + } else if (effectiveTab === "editor" && !editorOpen) { effectiveTab = reportOpen ? "report" : "sources"; } else if (effectiveTab === "report" && !reportOpen) { effectiveTab = editorOpen ? "editor" : "sources"; } else if (effectiveTab === "sources" && !documentsOpen) { - effectiveTab = editorOpen ? "editor" : reportOpen ? "report" : "sources"; + effectiveTab = hitlEditOpen ? "hitl-edit" : editorOpen ? "editor" : reportOpen ? "report" : "sources"; } const targetWidth = PANEL_WIDTHS[effectiveTab]; @@ -148,6 +158,17 @@ export function RightPanel({ documentsPanel }: RightPanelProps) { /> )} + {effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && ( +
+ +
+ )} ); diff --git a/surfsense_web/components/tool-ui/notion/create-notion-page.tsx b/surfsense_web/components/tool-ui/notion/create-notion-page.tsx index 46e092649..f057ff143 100644 --- a/surfsense_web/components/tool-ui/notion/create-notion-page.tsx +++ b/surfsense_web/components/tool-ui/notion/create-notion-page.tsx @@ -1,10 +1,9 @@ "use client"; import { makeAssistantToolUI } from "@assistant-ui/react"; -import { AlertTriangleIcon, CheckIcon, Loader2Icon, Pen, XIcon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { CornerDownLeftIcon, Loader2Icon, Pen } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -12,7 +11,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; +import { PlateEditor } from "@/components/editor/plate-editor"; +import { useSetAtom } from "jotai"; +import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; interface InterruptResult { __interrupt__: true; @@ -99,8 +100,8 @@ function ApprovalCard({ const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>( interruptData.__decided__ ?? null ); - const [isEditing, setIsEditing] = useState(false); - const [editedArgs, setEditedArgs] = useState>(args); + const [isPanelOpen, setIsPanelOpen] = useState(false); + const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom); const accounts = interruptData.context?.accounts ?? []; const parentPages = interruptData.context?.parent_pages ?? {}; @@ -122,293 +123,244 @@ function ApprovalCard({ }, [selectedAccountId, parentPages]); const isTitleValid = useMemo(() => { - const currentTitle = isEditing ? editedArgs.title : args.title; - return currentTitle && typeof currentTitle === "string" && currentTitle.trim().length > 0; - }, [isEditing, editedArgs.title, args.title]); + return args.title && typeof args.title === "string" && (args.title as string).trim().length > 0; + }, [args.title]); const reviewConfig = interruptData.review_configs[0]; const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; const canEdit = allowedDecisions.includes("edit"); + const handleApprove = useCallback(() => { + if (decided || isPanelOpen || !selectedAccountId || !isTitleValid) return; + if (!allowedDecisions.includes("approve")) return; + setDecided("approve"); + onDecision({ + type: "approve", + edited_action: { + name: interruptData.action_requests[0].name, + args: { + ...args, + connector_id: selectedAccountId ? Number(selectedAccountId) : null, + parent_page_id: + selectedParentPageId === "__none__" ? null : selectedParentPageId, + }, + }, + }); + }, [decided, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, onDecision, interruptData, args, selectedParentPageId]); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + handleApprove(); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [handleApprove]); + return (
-
-
- -
-
-

- Create Notion Page + {/* Header */} +

+
+

+ {decided === "reject" + ? "Notion Page Rejected" + : decided === "approve" || decided === "edit" + ? "Notion Page Approved" + : "Create Notion Page"}

-

- {isEditing ? "You can edit the arguments below" : "Requires your approval to proceed"} +

+ {decided === "reject" + ? "Page creation was cancelled" + : decided === "edit" + ? "Page creation is in progress with your changes" + : decided === "approve" + ? "Page creation is in progress" + : "Requires your approval to proceed"}

-
- - {/* Context section - account and parent page selection */} - {!decided && interruptData.context && ( -
- {interruptData.context.error ? ( -

{interruptData.context.error}

- ) : ( - <> - {accounts.length > 0 && ( -
-
- Notion Account * -
- -
- )} - - {selectedAccountId && ( -
-
- Parent Page (optional) -
- - {availableParentPages.length === 0 && selectedAccountId && ( -

- No pages available. Page will be created at workspace root. -

- )} -
- )} - - )} -
- )} - - {/* Display mode - show args as read-only */} - {!isEditing && ( -
- {args.title != null && ( -
-

Title

-

{String(args.title)}

-
- )} - {args.content != null && ( -
-

Content

-

- {String(args.content)} -

-
- )} -
- )} - - {/* Edit mode - show editable form fields */} - {isEditing && !decided && ( -
-
- - setEditedArgs({ ...editedArgs, title: e.target.value })} - placeholder="Enter page title" - className={!isTitleValid ? "border-destructive" : ""} - /> - {!isTitleValid && ( -

Title is required and cannot be empty

- )} -
-
- -