"use client"; import { useEffect, useRef, useState } from "react"; import { LoadingThreeDot } from "./loading-three-dot"; const IDE_SCHEMES = [ { matches: ["antigravity"], createUrl: (locator: string) => `antigravity://file/${locator}`, }, { matches: ["cursor"], createUrl: (locator: string) => `cursor://file/${locator}`, }, { matches: ["vscode", "code"], createUrl: (locator: string) => `vscode://file/${locator}`, }, { matches: ["webstorm", "intellij"], createUrl: (locator: string) => `webstorm://open?file=${locator}`, }, { matches: ["sublime"], createUrl: (locator: string) => `subl://open?url=file://${locator}`, }, { matches: ["atom", "nova"], createUrl: (locator: string) => `atom://open?url=file://${locator}`, }, ] as const; function parseLocator(locator: string) { const match = locator.match(/^(.*):(\d+|unknown):(\d+|unknown)$/); if (!match) { return { filePath: locator, line: null, column: null }; } const [, filePath, line, column] = match; return { filePath, line: line === "unknown" ? null : Number(line), column: column === "unknown" ? null : Number(column), }; } export function DevClickToComponent() { const [isInspecting, setIsInspecting] = useState(false); const [isMobile, setIsMobile] = useState(false); const [position, setPosition] = useState<{ x: number; y: number } | null>( null, ); const [isDragging, setIsDragging] = useState(false); const dragStart = useRef({ x: 0, y: 0, buttonX: 0, buttonY: 0, hasMoved: false, }); // Detect mobile size useEffect(() => { const checkSize = () => { setIsMobile(window.innerWidth <= 768); }; checkSize(); window.addEventListener("resize", checkSize); return () => window.removeEventListener("resize", checkSize); }, []); // Initialize button position useEffect(() => { if (typeof window !== "undefined") { setPosition({ x: window.innerWidth - 66, // 50px width + 16px margin y: window.innerHeight - 66, // 50px height + 16px margin }); } }, [isMobile]); // Keep button in bounds on resize useEffect(() => { const handleResize = () => { if (!isMobile) return; setPosition((prev) => { if (!prev) return null; return { x: Math.max(16, Math.min(window.innerWidth - 66, prev.x)), y: Math.max(16, Math.min(window.innerHeight - 66, prev.y)), }; }); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [isMobile]); useEffect(() => { const userAgent = navigator.userAgent.toLowerCase(); // Desktop: Alt + Click logic const handleDesktopClick = (event: MouseEvent) => { if (!event.altKey) { return; } const target = event.target; if (!(target instanceof HTMLElement)) { return; } const locator = target .closest("[data-locator]") ?.getAttribute("data-locator"); if (!locator) { return; } event.preventDefault(); event.stopPropagation(); const { filePath, line, column } = parseLocator(locator); const positionSuffix = line === null ? "" : `:${line}${column === null ? "" : `:${column}`}`; const locatorString = `${filePath}${positionSuffix}`; // Use the API route to execute the shell command which reuses the existing window fetch("/api/open-in-ide", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ locator: locatorString, userAgent }), }).catch((err) => { console.error("[DevClickToComponent] Error sending API request:", err); // Fallback to URL scheme const ideUrl = IDE_SCHEMES.find(({ matches }) => matches.some((match) => userAgent.includes(match)), )?.createUrl(locatorString) ?? `antigravity://file/${locatorString}`; try { window.location.href = ideUrl; } catch { window.open(`file://${filePath}`, "_blank", "noopener,noreferrer"); } }); }; document.addEventListener("click", handleDesktopClick, true); return () => { document.removeEventListener("click", handleDesktopClick, true); }; }, []); // Mobile: Inspect Mode click interception useEffect(() => { if (!isMobile || !isInspecting) return; const userAgent = navigator.userAgent.toLowerCase(); const handleInspectClick = async (event: MouseEvent) => { const target = event.target; if (!(target instanceof Element)) return; // Ignore clicks on the inspect button itself if (target.closest(".dev-inspect-btn")) { return; } event.preventDefault(); event.stopPropagation(); const locator = target .closest("[data-locator]") ?.getAttribute("data-locator"); if (locator) { console.log( `[DevClickToComponent] Inspect Mode matched element. Opening in IDE:`, locator, ); // Flash target element outline briefly as visual feedback const element = target.closest("[data-locator]"); if (element) { const originalOutline = element.style.outline; element.style.outline = "3px solid #3b82f6"; setTimeout(() => { element.style.outline = originalOutline; }, 400); } try { await fetch("/api/open-in-ide", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ locator, userAgent }), }); } catch (err) { console.error( "[DevClickToComponent] Error sending API request:", err, ); } } setIsInspecting(false); }; // Use capturing phase to intercept clicks/taps before they trigger other actions document.addEventListener("click", handleInspectClick, true); return () => { document.removeEventListener("click", handleInspectClick, true); }; }, [isMobile, isInspecting]); // Dev mode: Intercept and forward console logs globally useEffect(() => { const originalLog = console.log; const originalWarn = console.warn; const originalError = console.error; let isSending = false; const forwardLog = async (type: "log" | "warn" | "error", args: any[]) => { if (isSending) return; isSending = true; try { await fetch("/api/remote-logs", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ type, args }), }); } catch (err) { originalError.call( console, "[DevClickToComponent] Failed to forward log to server:", err, ); } finally { isSending = false; } }; console.log = (...args: any[]) => { originalLog.apply(console, args); forwardLog("log", args); }; console.warn = (...args: any[]) => { originalWarn.apply(console, args); forwardLog("warn", args); }; console.error = (...args: any[]) => { originalError.apply(console, args); forwardLog("error", args); }; return () => { console.log = originalLog; console.warn = originalWarn; console.error = originalError; }; }, []); // Dev mode: Intercept and forward fetch requests globally useEffect(() => { const originalFetch = window.fetch; window.fetch = async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input instanceof Request ? input.url : ""; // Ignore logging requests to prevent infinite recursion if ( url.includes("/api/remote-logs") || url.includes("/api/remote-network") || url.includes("/api/open-in-ide") ) { return originalFetch(input, init); } const method = init?.method ?? (input instanceof Request ? input.method : "GET"); const startTime = Date.now(); let requestBody: string | null = null; if (init?.body) { if (typeof init.body === "string") { requestBody = init.body; } else { requestBody = "[Non-string payload]"; } } else if (input instanceof Request) { try { const clonedReq = input.clone(); requestBody = await clonedReq.text(); } catch { requestBody = null; } } try { const response = await originalFetch(input, init); const duration = Date.now() - startTime; const status = response.status; let responseBody: string | null = null; try { const clonedRes = response.clone(); responseBody = await clonedRes.text(); } catch { responseBody = "[Unreadable response body]"; } originalFetch("/api/remote-network", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ method, url, status, requestBody, responseBody, duration, }), }).catch(() => {}); return response; } catch (err) { const duration = Date.now() - startTime; originalFetch("/api/remote-network", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ method, url, status: 0, requestBody, responseBody: String(err), duration, }), }).catch(() => {}); throw err; } }; return () => { window.fetch = originalFetch; }; }, []); // Dev mode: Intercept and forward XMLHttpRequest (XHR) requests globally useEffect(() => { const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function ( this: any, method: any, url: any, ...args: any[] ) { this._method = method; this._url = typeof url === "string" ? url : url.toString(); return originalOpen.apply(this, [method, url, ...args] as any); }; XMLHttpRequest.prototype.send = function (this: any, body?: any) { const url = this._url || ""; const method = this._method || "GET"; const startTime = Date.now(); // Avoid infinite recursion on dev logging endpoints if ( url.includes("/api/remote-logs") || url.includes("/api/remote-network") || url.includes("/api/open-in-ide") ) { return originalSend.apply(this, [body]); } this.addEventListener("loadend", () => { const duration = Date.now() - startTime; const status = this.status; let responseBody = ""; try { responseBody = this.responseText; } catch { try { if (typeof this.response === "string") { responseBody = this.response; } else if (this.response) { responseBody = JSON.stringify(this.response); } } catch { responseBody = "[Binary or unreadable response]"; } } let requestBody: string | null = null; if (body) { if (typeof body === "string") { requestBody = body; } else { requestBody = "[Non-string payload]"; } } window .fetch("/api/remote-network", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ method, url, status, requestBody, responseBody, duration, }), }) .catch(() => {}); }); return originalSend.apply(this, [body]); }; return () => { XMLHttpRequest.prototype.open = originalOpen; XMLHttpRequest.prototype.send = originalSend; }; }, []); // Dragging event handlers const handlePointerDown = (e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); setIsDragging(true); const startX = position?.x ?? window.innerWidth - 66; const startY = position?.y ?? window.innerHeight - 126; dragStart.current = { x: e.clientX, y: e.clientY, buttonX: startX, buttonY: startY, hasMoved: false, }; }; const handlePointerMove = (e: React.PointerEvent) => { if (!isDragging) return; const dx = e.clientX - dragStart.current.x; const dy = e.clientY - dragStart.current.y; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { dragStart.current.hasMoved = true; } const newX = Math.max( 16, Math.min(window.innerWidth - 66, dragStart.current.buttonX + dx), ); const newY = Math.max( 16, Math.min(window.innerHeight - 126, dragStart.current.buttonY + dy), ); setPosition({ x: newX, y: newY }); }; const handlePointerUp = (e: React.PointerEvent) => { if (!isDragging) return; e.currentTarget.releasePointerCapture(e.pointerId); setIsDragging(false); }; const handleInspectClick = () => { if (!dragStart.current.hasMoved) { setIsInspecting(!isInspecting); } }; const [isSendingStorage, setIsSendingStorage] = useState(false); const [sendSuccess, setSendSuccess] = useState(false); const handleSendStorageClick = async () => { if (dragStart.current.hasMoved) return; if (isSendingStorage) return; setIsSendingStorage(true); setSendSuccess(false); try { const data: Record = {}; for (let i = 0; i < window.localStorage.length; i++) { const key = window.localStorage.key(i); if (key) { const val = window.localStorage.getItem(key); try { data[key] = JSON.parse(val || ""); } catch { data[key] = val; } } } const response = await fetch("/api/dev-storage", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), }); if (response.ok) { setSendSuccess(true); setTimeout(() => setSendSuccess(false), 2000); } } catch (err) { console.error( "[DevClickToComponent] Failed to send localStorage values:", err, ); } finally { setIsSendingStorage(false); } }; return (
{/* Send LocalStorage Button */} {/* Floating Action Draggable Button (Inspect Mode) */} {isMobile && ( )}
); } export default DevClickToComponent;