diff --git a/extension/src/components/ui/button.tsx b/extension/src/components/ui/button.tsx index 66ab90e5..e552629d 100644 --- a/extension/src/components/ui/button.tsx +++ b/extension/src/components/ui/button.tsx @@ -57,3 +57,40 @@ function Button({ } export { Button, buttonVariants }; + +interface ToggleButtonProps extends React.ComponentProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + onLabel?: string; + offLabel?: string; +} + +function ToggleButton({ + checked, + onCheckedChange, + onLabel = "On", + offLabel = "Off", + className, + variant, + size = "sm", + ...props +}: ToggleButtonProps) { + const handleClick = () => { + onCheckedChange(!checked); + }; + + return ( + + ); +} + +export { ToggleButton }; diff --git a/extension/src/entrypoints/background.ts b/extension/src/entrypoints/background.ts index 283a16f4..9875e840 100644 --- a/extension/src/entrypoints/background.ts +++ b/extension/src/entrypoints/background.ts @@ -31,6 +31,7 @@ export default defineBackground(() => { const tabInfo: { [tabId: number]: { url?: string; title?: string } } = {}; let isRecordingEnabled = true; // Default to disabled (OFF) + let isHighlighting = false; let lastWorkflowHash: string | null = null; // Cache for the last logged workflow hash const PYTHON_SERVER_ENDPOINT = "http://127.0.0.1:7331/event"; @@ -135,6 +136,40 @@ export default defineBackground(() => { // console.debug("Could not send status update to sidepanel (might be closed)", err.message); }); } + // Add this function after broadcastRecordingStatus + function broadcastHighlightingStatus() { + const statusString = isHighlighting + ? "highlighting_enabled" + : "highlighting_disabled"; + // Broadcast to Sidepanel (using runtime message) + chrome.tabs.query({}, (tabs) => { + tabs.forEach((tab) => { + if (tab.id) { + chrome.tabs + .sendMessage(tab.id, { + type: "SET_HIGHLIGHTING_STATUS", + payload: isHighlighting, + }) + .catch((err: Error) => { + console.debug( + `Could not send highlighting status to tab ${tab.id}: ${err.message}` + ); + }); + } + }); + }); + chrome.runtime + .sendMessage({ + type: "highlighting_status_updated", + payload: { status: statusString }, + }) + .catch((err) => { + console.debug( + "Could not send highlighting status update to sidepanel (might be closed)", + err.message + ); + }); + } // --- Tab Event Listeners --- @@ -524,6 +559,20 @@ export default defineBackground(() => { sendEventToServer(eventToSend); } sendResponse({ status: "stopped" }); // Send simple confirmation + } else if (message.type === "START_HIGHLIGHTING") { + console.log("Received START_HIGHLIGHTING request."); + if (!isHighlighting) { + isHighlighting = true; + broadcastHighlightingStatus(); + } + sendResponse({ status: "highlighting_enabled" }); + } else if (message.type === "STOP_HIGHLIGHTING") { + console.log("Received STOP_HIGHLIGHTING request."); + if (isHighlighting) { + isHighlighting = false; + broadcastHighlightingStatus(); + } + sendResponse({ status: "highlighting_disabled" }); } // --- Status Request from Content Script --- else if (message.type === "REQUEST_RECORDING_STATUS" && sender.tab?.id) { @@ -549,6 +598,8 @@ export default defineBackground(() => { console.log( "Background script loaded. Initial recording status:", isRecordingEnabled, + "Highlighting status:", + isHighlighting, "(EventType:", EventType, ", IncrementalSource:", diff --git a/extension/src/entrypoints/content.ts b/extension/src/entrypoints/content.ts index 09438784..b54c53f4 100644 --- a/extension/src/entrypoints/content.ts +++ b/extension/src/entrypoints/content.ts @@ -3,6 +3,7 @@ import { EventType, IncrementalSource } from '@rrweb/types'; let stopRecording: (() => void) | undefined = undefined; let isRecordingActive = true; // Content script's local state +let isHighlightingActive = false; // Set to true if you want to highlight by default let scrollTimeout: ReturnType | null = null; let lastScrollY: number | null = null; let lastDirection: 'up' | 'down' | null = null; @@ -206,6 +207,8 @@ function startRecorder() { document.addEventListener('input', handleInput, true); document.addEventListener('change', handleSelectChange, true); document.addEventListener('keydown', handleKeydown, true); + document.addEventListener('mouseover', handleMouseOver, true); + document.addEventListener('mouseout', handleMouseOut, true); console.log('Permanently attached custom event listeners.'); } @@ -221,6 +224,8 @@ function stopRecorder() { document.removeEventListener('input', handleInput, true); document.removeEventListener('change', handleSelectChange, true); // Remove change listener document.removeEventListener('keydown', handleKeydown, true); // Remove keydown listener + document.removeEventListener('mouseover', handleMouseOver, true); + document.removeEventListener('mouseout', handleMouseOut, true); } else { console.log('Recorder not running, cannot stop.'); } @@ -391,6 +396,85 @@ function handleKeydown(event: KeyboardEvent) { } // --- End Custom Keydown Handler --- +// Store the current overlay to manage its lifecycle +let currentOverlay: HTMLDivElement | null = null; + +// Handle mouseover to create overlay +function handleMouseOver(event: MouseEvent) { + if (!isRecordingActive || !isHighlightingActive) return; + const targetElement = event.target as HTMLElement; + if (!targetElement) return; + + // Remove any existing overlay to avoid duplicates + if (currentOverlay) { + // console.log('Removing existing overlay'); + currentOverlay.remove(); + currentOverlay = null; + } + + try { + const xpath = getXPath(targetElement); + // console.log('XPath of target element:', xpath); + let elementToHighlight: HTMLElement | null = document.evaluate( + xpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue as HTMLElement | null; + if (!elementToHighlight) { + const enhancedSelector = getEnhancedCSSSelector(targetElement, xpath); + console.log('CSS Selector:', enhancedSelector); + const elements = document.querySelectorAll(enhancedSelector); + + // Try to find the element under the mouse + for (const el of elements) { + const rect = el.getBoundingClientRect(); + if ( + event.clientX >= rect.left && + event.clientX <= rect.right && + event.clientY >= rect.top && + event.clientY <= rect.bottom + ) { + elementToHighlight = el; + break; + } + } + } + if (elementToHighlight) { + const rect = elementToHighlight.getBoundingClientRect(); + const highlightOverlay = document.createElement('div'); + highlightOverlay.className = 'highlight-overlay'; + Object.assign(highlightOverlay.style, { + position: 'absolute', + top: `${rect.top + window.scrollY}px`, + left: `${rect.left + window.scrollX}px`, + width: `${rect.width}px`, + height: `${rect.height}px`, + border: '2px solid lightgreen', + backgroundColor: 'rgba(144, 238, 144, 0.2)', // lightgreen tint + pointerEvents: 'none', + zIndex: '2147483000', + }); + document.body.appendChild(highlightOverlay); + currentOverlay = highlightOverlay; + } else { + console.warn('No element found to highlight for xpath:', xpath); + } + } catch (error) { + console.error('Error creating highlight overlay:', error); + } +} + +// Handle mouseout to remove overlay +function handleMouseOut(event: MouseEvent) { + if (!isRecordingActive || !isHighlightingActive) return; + if (currentOverlay) { + currentOverlay.remove(); + currentOverlay = null; + } +} + export default defineContentScript({ matches: [''], main(ctx) { @@ -404,6 +488,16 @@ export default defineContentScript({ } else if (!shouldBeRecording && isRecordingActive) { stopRecorder(); } + } else if (message.type === 'SET_HIGHLIGHTING_STATUS') { + const shouldBeHighlighting = message.payload; + console.log( + `Received highlighting status update: ${shouldBeHighlighting}` + ); + isHighlightingActive = shouldBeHighlighting; + if (!isHighlightingActive && currentOverlay) { + currentOverlay.remove(); + currentOverlay = null; + } } // If needed, handle other message types here }); @@ -443,6 +537,8 @@ export default defineContentScript({ document.removeEventListener('input', handleInput, true); document.removeEventListener('change', handleSelectChange, true); document.removeEventListener('keydown', handleKeydown, true); + document.removeEventListener('mouseover', handleMouseOver, true); + document.removeEventListener('mouseout', handleMouseOut, true); stopRecorder(); // Ensure rrweb is stopped }); diff --git a/extension/src/entrypoints/sidepanel/components/recording-view.tsx b/extension/src/entrypoints/sidepanel/components/recording-view.tsx index 0fa3456a..4f28c33d 100644 --- a/extension/src/entrypoints/sidepanel/components/recording-view.tsx +++ b/extension/src/entrypoints/sidepanel/components/recording-view.tsx @@ -1,12 +1,27 @@ -import React from "react"; +import React, { useState } from "react"; import { useWorkflow } from "../context/workflow-provider"; import { Button } from "@/components/ui/button"; +import { ToggleButton } from "@/components/ui/button"; import { EventViewer } from "./event-viewer"; // Import EventViewer export const RecordingView: React.FC = () => { - const { stopRecording, workflow } = useWorkflow(); + const { + stopRecording, + workflow, + stopHighlighting, + startHighlighting, + highlightingStatus, + } = useWorkflow(); const stepCount = workflow?.steps?.length || 0; + const toggleHighlighting = () => { + if (highlightingStatus === "highlighting_enabled") { + stopHighlighting(); + } else { + startHighlighting(); + } + }; + return (
@@ -19,6 +34,13 @@ export const RecordingView: React.FC = () => { Recording ({stepCount} steps)
+ diff --git a/extension/src/entrypoints/sidepanel/context/workflow-provider.tsx b/extension/src/entrypoints/sidepanel/context/workflow-provider.tsx index 418f8b15..9daf372d 100644 --- a/extension/src/entrypoints/sidepanel/context/workflow-provider.tsx +++ b/extension/src/entrypoints/sidepanel/context/workflow-provider.tsx @@ -11,6 +11,7 @@ import { Workflow } from "../../../lib/workflow-types"; // Adjust path as needed type WorkflowState = { workflow: Workflow | null; recordingStatus: string; // e.g., 'idle', 'recording', 'stopped', 'error' + highlightingStatus: string; // e.g., 'highlighting_enabled', 'highlighting_disabled' currentEventIndex: number; isLoading: boolean; error: string | null; @@ -22,6 +23,8 @@ type WorkflowContextType = WorkflowState & { discardAndStartNew: () => void; selectEvent: (index: number) => void; fetchWorkflowData: (isPolling?: boolean) => void; // Add optional flag + startHighlighting: () => void; + stopHighlighting: () => void; }; const WorkflowContext = createContext( @@ -39,6 +42,9 @@ export const WorkflowProvider: React.FC = ({ }) => { const [workflow, setWorkflow] = useState(null); const [recordingStatus, setRecordingStatus] = useState("idle"); // 'idle', 'recording', 'stopped', 'error' + const [highlightingStatus, setHighlightingStatus] = useState( + "highlighting_disabled" + ); // 'highlighting_enabled', 'highlighting_disabled' const [currentEventIndex, setCurrentEventIndex] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -130,6 +136,16 @@ export const WorkflowProvider: React.FC = ({ } return newStatus; // Return the new status to update the state }); + } else if (message.type === "highlighting_status_updated") { + console.log( + "Highlighting status updated message received:", + message.payload + ); + const newStatus = message.payload.status; + // Use functional update to get previous status reliably + setHighlightingStatus((prevStatus) => { + return newStatus; // Return the new status to update the state + }); } }; chrome.runtime.onMessage.addListener(messageListener); @@ -200,6 +216,36 @@ export const WorkflowProvider: React.FC = ({ }); }, []); // No dependencies needed + const startHighlighting = useCallback(() => { + setError(null); + chrome.runtime.sendMessage({ type: "START_HIGHLIGHTING" }, (response) => { + if (chrome.runtime.lastError) { + console.error("Error starting highlighting:", chrome.runtime.lastError); + setError( + `Failed to start highlighting: ${chrome.runtime.lastError.message}` + ); + } else { + console.log("Start highlighting acknowledged by background."); + // State updates happen via broadcast + fetch + } + }); + }, []); + + const stopHighlighting = useCallback(() => { + setError(null); + chrome.runtime.sendMessage({ type: "STOP_HIGHLIGHTING" }, (response) => { + if (chrome.runtime.lastError) { + console.error("Error stopping highlighting:", chrome.runtime.lastError); + setError( + `Failed to stop highlighting: ${chrome.runtime.lastError.message}` + ); + } else { + console.log("Stop highlighting acknowledged by background."); + // State updates happen via broadcast + fetch + } + }); + }, []); + const discardAndStartNew = useCallback(() => { startRecording(); }, [startRecording]); @@ -211,6 +257,7 @@ export const WorkflowProvider: React.FC = ({ const value = { workflow, recordingStatus, + highlightingStatus, currentEventIndex, isLoading, error, @@ -219,6 +266,8 @@ export const WorkflowProvider: React.FC = ({ discardAndStartNew, selectEvent, fetchWorkflowData, + startHighlighting, + stopHighlighting, }; return (