| import { useState, useRef, useEffect } from "react"; | |
| import { X, Minus, Square } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import type { Tool } from "@/types/tool"; | |
| import api from "@/lib/api"; | |
| import { toast } from "react-toastify"; | |
| interface ToolExecutionWindowProps { | |
| tool: Tool; | |
| onClose: () => void; | |
| } | |
| export function ToolExecutionWindow({ tool, onClose }: ToolExecutionWindowProps) { | |
| const [isMaximized, setIsMaximized] = useState(false); | |
| const [isMinimized, setIsMinimized] = useState(false); | |
| const windowRef = useRef<HTMLDivElement>(null); | |
| // Listen to tool sandbox notifications | |
| useEffect(() => { | |
| const handleMessage = async (e: MessageEvent) => { | |
| if (e.data && e.data.type === "SEND_NOTIFICATION") { | |
| const { title, body, priority } = e.data.payload || {}; | |
| try { | |
| await api.post("/notifications", { | |
| title: title || `${tool.name} Alert`, | |
| body: body || "", | |
| priority: priority || "medium", | |
| type: "system", | |
| source: tool.name, | |
| }); | |
| toast.success(`Notification sent from ${tool.name}`); | |
| } catch (err) { | |
| console.error("Failed to send notification from custom tool:", err); | |
| toast.error("Failed to dispatch application notification"); | |
| } | |
| } | |
| }; | |
| window.addEventListener("message", handleMessage); | |
| return () => window.removeEventListener("message", handleMessage); | |
| }, [tool]); | |
| const injectedCode = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { margin: 0; padding: 16px; font-family: system-ui, -apple-system, sans-serif; } | |
| .tool-container { max-width: 100%; overflow-x: auto; } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| window.sendNotification = function(title, body, priority = 'medium') { | |
| window.parent.postMessage({ | |
| type: "SEND_NOTIFICATION", | |
| payload: { title, body, priority } | |
| }, "*"); | |
| }; | |
| </script> | |
| ${tool.code} | |
| </body> | |
| </html> | |
| `; | |
| if (isMinimized) { | |
| return ( | |
| <div className="fixed bottom-4 left-4 z-50"> | |
| <button | |
| onClick={() => setIsMinimized(false)} | |
| className="flex items-center gap-3 premium-glass px-4 py-2 rounded-full border border-white/20 shadow-xl hover:bg-white/10" | |
| > | |
| <span className="text-xl">{tool.icon || "🔧"}</span> | |
| <span className="font-bold text-sm text-white">{tool.name}</span> | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <AnimatePresence> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.95 }} | |
| className={`fixed z-50 flex flex-col overflow-hidden shadow-2xl premium-glass border border-white/20 dark:border-white/5 backdrop-blur-xl ${ | |
| isMaximized | |
| ? "inset-0 rounded-none" | |
| : "top-[10%] left-[10%] right-[10%] bottom-[10%] rounded-[1.5rem]" | |
| }`} | |
| ref={windowRef} | |
| > | |
| {/* macOS-style Window Title Bar */} | |
| <div | |
| className="flex items-center justify-between px-4 py-3 bg-gradient-to-b from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900 border-b border-slate-300 dark:border-slate-700/50 select-none" | |
| > | |
| <div className="flex flex-1 items-center gap-2"> | |
| {/* Traffic Lights */} | |
| <div className="flex items-center gap-1.5 group"> | |
| <button | |
| onClick={onClose} | |
| className="w-3.5 h-3.5 rounded-full bg-rose-500 flex items-center justify-center hover:bg-rose-600 transition" | |
| > | |
| <X className="w-2.5 h-2.5 text-black/50 opacity-0 group-hover:opacity-100" /> | |
| </button> | |
| <button | |
| onClick={() => setIsMinimized(true)} | |
| className="w-3.5 h-3.5 rounded-full bg-amber-500 flex items-center justify-center hover:bg-amber-600 transition" | |
| > | |
| <Minus className="w-2.5 h-2.5 text-black/50 opacity-0 group-hover:opacity-100" /> | |
| </button> | |
| <button | |
| onClick={() => setIsMaximized(!isMaximized)} | |
| className="w-3.5 h-3.5 rounded-full bg-emerald-500 flex items-center justify-center hover:bg-emerald-600 transition" | |
| > | |
| <Square className="w-2.5 h-2.5 text-black/50 opacity-0 group-hover:opacity-100" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex-1 text-center flex items-center justify-center gap-2"> | |
| <span className="text-sm">{tool.icon || "🔧"}</span> | |
| <span className="font-semibold text-xs text-slate-800 dark:text-slate-200 tracking-wide">{tool.name}</span> | |
| </div> | |
| <div className="flex-1" /> | |
| </div> | |
| {/* Window Content */} | |
| <div className="flex-1 bg-white relative"> | |
| <iframe | |
| srcDoc={injectedCode} | |
| title={tool.name} | |
| className="absolute inset-0 w-full h-full border-none" | |
| sandbox="allow-scripts allow-modals" | |
| /> | |
| </div> | |
| </motion.div> | |
| {/* Dimmed Background Overlay */} | |
| {!isMaximized && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="fixed inset-0 z-40 bg-slate-950/20 backdrop-blur-sm pointer-events-none" | |
| /> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 5.82 kB
- Xet hash:
- b60dba5d229c7a5c60df032b94be0ae6a263e4cd5642f60cf8214ae9e4b63371
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.