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 15656813d..117f7f246 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 @@ -38,7 +38,10 @@ import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { GenerateReportToolUI } from "@/components/tool-ui/generate-report"; -import { CreateGoogleDriveFileToolUI } from "@/components/tool-ui/google-drive"; +import { + CreateGoogleDriveFileToolUI, + TrashGoogleDriveFileToolUI, +} from "@/components/tool-ui/google-drive"; import { CreateLinearIssueToolUI, DeleteLinearIssueToolUI, @@ -1666,6 +1669,7 @@ export default function NewChatPage() { + {/* Disabled for now */}
diff --git a/surfsense_web/components/tool-ui/google-drive/index.ts b/surfsense_web/components/tool-ui/google-drive/index.ts index a01c781a7..8a4225469 100644 --- a/surfsense_web/components/tool-ui/google-drive/index.ts +++ b/surfsense_web/components/tool-ui/google-drive/index.ts @@ -1 +1,2 @@ export { CreateGoogleDriveFileToolUI } from "./create-file"; +export { TrashGoogleDriveFileToolUI } from "./trash-file"; diff --git a/surfsense_web/components/tool-ui/google-drive/trash-file.tsx b/surfsense_web/components/tool-ui/google-drive/trash-file.tsx new file mode 100644 index 000000000..39cd9cadd --- /dev/null +++ b/surfsense_web/components/tool-ui/google-drive/trash-file.tsx @@ -0,0 +1,359 @@ +"use client"; + +import { makeAssistantToolUI } from "@assistant-ui/react"; +import { + AlertTriangleIcon, + CheckIcon, + InfoIcon, + Loader2Icon, + Trash2Icon, + XIcon, +} from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +interface GoogleDriveAccount { + id: number; + name: string; +} + +interface GoogleDriveFile { + file_id: string; + name: string; + mime_type: string; + web_view_link: string; +} + +interface InterruptResult { + __interrupt__: true; + __decided__?: "approve" | "reject"; + action_requests: Array<{ + name: string; + args: Record; + }>; + review_configs: Array<{ + action_name: string; + allowed_decisions: Array<"approve" | "reject">; + }>; + context?: { + account?: GoogleDriveAccount; + file?: GoogleDriveFile; + error?: string; + }; +} + +interface SuccessResult { + status: "success"; + file_id: string; + message?: string; +} + +interface ErrorResult { + status: "error"; + message: string; +} + +interface NotFoundResult { + status: "not_found"; + message: string; +} + +type TrashGoogleDriveFileResult = + | InterruptResult + | SuccessResult + | ErrorResult + | NotFoundResult; + +function isInterruptResult(result: unknown): result is InterruptResult { + return ( + typeof result === "object" && + result !== null && + "__interrupt__" in result && + (result as InterruptResult).__interrupt__ === true + ); +} + +function isErrorResult(result: unknown): result is ErrorResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as ErrorResult).status === "error" + ); +} + +function isNotFoundResult(result: unknown): result is NotFoundResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as NotFoundResult).status === "not_found" + ); +} + +const MIME_TYPE_LABELS: Record = { + "application/vnd.google-apps.document": "Google Doc", + "application/vnd.google-apps.spreadsheet": "Google Sheet", + "application/vnd.google-apps.presentation": "Google Slides", +}; + +function ApprovalCard({ + interruptData, + onDecision, +}: { + interruptData: InterruptResult; + onDecision: (decision: { + type: "approve" | "reject"; + message?: string; + edited_action?: { name: string; args: Record }; + }) => void; +}) { + const [decided, setDecided] = useState<"approve" | "reject" | null>( + interruptData.__decided__ ?? null + ); + + const account = interruptData.context?.account; + const file = interruptData.context?.file; + const fileLabel = file?.mime_type + ? (MIME_TYPE_LABELS[file.mime_type] ?? "File") + : "File"; + + return ( +
+ {/* Header */} +
+
+ +
+
+

Trash Google Drive File

+

+ Requires your approval to proceed +

+
+
+ + {/* Context — read-only file details */} + {!decided && interruptData.context && ( +
+ {interruptData.context.error ? ( +

{interruptData.context.error}

+ ) : ( + <> + {account && ( +
+
+ Google Drive Account +
+
+ {account.name} +
+
+ )} + + {file && ( +
+
+ File to Trash +
+
+
{file.name}
+
{fileLabel}
+ {file.web_view_link && ( + + Open in Drive + + )} +
+
+ )} + + )} +
+ )} + + {/* Trash warning */} + {!decided && ( +
+

+ ⚠️ The file will be moved to Google Drive trash. You can restore it from trash within 30 days. +

+
+ )} + + {/* Action buttons */} +
+ {decided ? ( +

+ {decided === "approve" ? ( + <> + + Approved + + ) : ( + <> + + Rejected + + )} +

+ ) : ( + <> + + + + )} +
+
+ ); +} + +function ErrorCard({ result }: { result: ErrorResult }) { + return ( +
+
+
+ +
+
+

Failed to trash file

+
+
+
+

{result.message}

+
+
+ ); +} + +function NotFoundCard({ result }: { result: NotFoundResult }) { + return ( +
+
+
+ +
+
+

{result.message}

+
+
+
+ ); +} + +function SuccessCard({ result }: { result: SuccessResult }) { + return ( +
+
+
+ +
+
+

+ {result.message || "File moved to trash successfully"} +

+
+
+
+ ); +} + +export const TrashGoogleDriveFileToolUI = makeAssistantToolUI< + { file_name: string }, + TrashGoogleDriveFileResult +>({ + toolName: "trash_google_drive_file", + render: function TrashGoogleDriveFileUI({ result, status }) { + if (status.type === "running") { + return ( +
+ +

Looking up file in Google Drive...

+
+ ); + } + + if (!result) return null; + + if (isInterruptResult(result)) { + return ( + { + window.dispatchEvent( + new CustomEvent("hitl-decision", { detail: { decisions: [decision] } }) + ); + }} + /> + ); + } + + if ( + typeof result === "object" && + result !== null && + "status" in result && + (result as { status: string }).status === "rejected" + ) { + return null; + } + + if (isNotFoundResult(result)) return ; + if (isErrorResult(result)) return ; + + return ; + }, +}); diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 8fab309d2..0af026dac 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -32,7 +32,7 @@ export { } from "./display-image"; export { GeneratePodcastToolUI } from "./generate-podcast"; export { GenerateReportToolUI } from "./generate-report"; -export { CreateGoogleDriveFileToolUI } from "./google-drive"; +export { CreateGoogleDriveFileToolUI, TrashGoogleDriveFileToolUI } from "./google-drive"; export { Image, ImageErrorBoundary,