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 dd11382a8..15656813d 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,6 +38,7 @@ 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 { CreateLinearIssueToolUI, DeleteLinearIssueToolUI, @@ -1664,6 +1665,7 @@ export default function NewChatPage() { + {/* Disabled for now */}
diff --git a/surfsense_web/components/tool-ui/google-drive/create-file.tsx b/surfsense_web/components/tool-ui/google-drive/create-file.tsx new file mode 100644 index 000000000..cf0a01319 --- /dev/null +++ b/surfsense_web/components/tool-ui/google-drive/create-file.tsx @@ -0,0 +1,490 @@ +"use client"; + +import { makeAssistantToolUI } from "@assistant-ui/react"; +import { + AlertTriangleIcon, + CheckIcon, + FileIcon, + Loader2Icon, + PencilIcon, + XIcon, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +interface GoogleDriveAccount { + id: number; + name: string; +} + +interface InterruptResult { + __interrupt__: true; + __decided__?: "approve" | "reject" | "edit"; + action_requests: Array<{ + name: string; + args: Record; + }>; + review_configs: Array<{ + action_name: string; + allowed_decisions: Array<"approve" | "edit" | "reject">; + }>; + context?: { + accounts?: GoogleDriveAccount[]; + supported_types?: string[]; + error?: string; + }; +} + +interface SuccessResult { + status: "success"; + file_id: string; + name: string; + web_view_link?: string; + message?: string; +} + +interface ErrorResult { + status: "error"; + message: string; +} + +type CreateGoogleDriveFileResult = InterruptResult | SuccessResult | ErrorResult; + +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" + ); +} + +const FILE_TYPE_LABELS: Record = { + google_doc: "Google Doc", + google_sheet: "Google Sheet", +}; + +function ApprovalCard({ + args, + interruptData, + onDecision, +}: { + args: { name: string; file_type: string; content?: string }; + interruptData: InterruptResult; + onDecision: (decision: { + type: "approve" | "reject" | "edit"; + message?: string; + edited_action?: { name: string; args: Record }; + }) => void; +}) { + const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>( + interruptData.__decided__ ?? null + ); + const [isEditing, setIsEditing] = useState(false); + const [editedName, setEditedName] = useState(args.name ?? ""); + const [editedContent, setEditedContent] = useState(args.content ?? ""); + + const accounts = interruptData.context?.accounts ?? []; + + const defaultAccountId = useMemo(() => { + if (accounts.length === 1) return String(accounts[0].id); + return ""; + }, [accounts]); + + const [selectedAccountId, setSelectedAccountId] = useState(defaultAccountId); + const [selectedFileType, setSelectedFileType] = useState(args.file_type ?? "google_doc"); + const [parentFolderId, setParentFolderId] = useState(""); + + const isNameValid = useMemo( + () => (isEditing ? editedName.trim().length > 0 : args.name?.trim().length > 0), + [isEditing, editedName, args.name] + ); + + const canApprove = !!selectedAccountId && isNameValid; + + const reviewConfig = interruptData.review_configs[0]; + const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; + const canEdit = allowedDecisions.includes("edit"); + + function buildFinalArgs() { + return { + name: isEditing ? editedName : args.name, + file_type: selectedFileType, + content: isEditing ? editedContent || null : (args.content ?? null), + connector_id: selectedAccountId ? Number(selectedAccountId) : null, + parent_folder_id: parentFolderId.trim() || null, + }; + } + + return ( +
+ {/* Header */} +
+
+ +
+
+

Create Google Drive File

+

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

+
+
+ + {/* Context section */} + {!decided && interruptData.context && ( +
+ {interruptData.context.error ? ( +

{interruptData.context.error}

+ ) : ( + <> + {accounts.length > 0 && ( +
+
+ Google Drive Account * +
+ +
+ )} + +
+
+ File Type * +
+ +
+ +
+
+ Parent Folder ID (optional) +
+ setParentFolderId(e.target.value)} + placeholder="Leave blank to create at Drive root" + /> +

+ Paste a Google Drive folder ID to place the file in a specific folder. +

+
+ + )} +
+ )} + + {/* Display mode */} + {!isEditing && ( +
+
+

Name

+

{args.name}

+
+
+

Type

+

+ {FILE_TYPE_LABELS[args.file_type] ?? args.file_type} +

+
+ {args.content && ( +
+

Content

+

+ {args.content} +

+
+ )} +
+ )} + + {/* Edit mode */} + {isEditing && !decided && ( +
+
+ + setEditedName(e.target.value)} + placeholder="Enter file name" + className={!isNameValid ? "border-destructive" : ""} + /> + {!isNameValid &&

Name is required

} +
+
+ +