Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions extension/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,40 @@ function Button({
}

export { Button, buttonVariants };

interface ToggleButtonProps extends React.ComponentProps<typeof Button> {
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 (
<Button
variant={checked ? "default" : "secondary"} // Green for on, gray for off
size={size}
onClick={handleClick}
className={cn("min-w-[60px] justify-center", className)} // Ensure minimum width
aria-pressed={checked}
{...props}
>
{checked ? onLabel : offLabel}
</Button>
);
}

export { ToggleButton };
51 changes: 51 additions & 0 deletions extension/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 ---

Expand Down Expand Up @@ -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) {
Expand All @@ -549,6 +598,8 @@ export default defineBackground(() => {
console.log(
"Background script loaded. Initial recording status:",
isRecordingEnabled,
"Highlighting status:",
isHighlighting,
"(EventType:",
EventType,
", IncrementalSource:",
Expand Down
96 changes: 96 additions & 0 deletions extension/src/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setTimeout> | null = null;
let lastScrollY: number | null = null;
let lastDirection: 'up' | 'down' | null = null;
Expand Down Expand Up @@ -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.');
}

Expand All @@ -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.');
}
Expand Down Expand Up @@ -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<HTMLElement>(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: ['<all_urls>'],
main(ctx) {
Expand All @@ -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
});
Expand Down Expand Up @@ -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
});

Expand Down
26 changes: 24 additions & 2 deletions extension/src/entrypoints/sidepanel/components/recording-view.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-border">
Expand All @@ -19,6 +34,13 @@ export const RecordingView: React.FC = () => {
Recording ({stepCount} steps)
</span>
</div>
<ToggleButton
checked={highlightingStatus === "highlighting_enabled"}
onCheckedChange={toggleHighlighting}
onLabel="Highlight On"
offLabel="Highlight Off"
aria-label="Toggle highlighting"
/>
<Button variant="destructive" size="sm" onClick={stopRecording}>
Stop Recording
</Button>
Expand Down
49 changes: 49 additions & 0 deletions extension/src/entrypoints/sidepanel/context/workflow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WorkflowContextType | undefined>(
Expand All @@ -39,6 +42,9 @@ export const WorkflowProvider: React.FC<WorkflowProviderProps> = ({
}) => {
const [workflow, setWorkflow] = useState<Workflow | null>(null);
const [recordingStatus, setRecordingStatus] = useState<string>("idle"); // 'idle', 'recording', 'stopped', 'error'
const [highlightingStatus, setHighlightingStatus] = useState<string>(
"highlighting_disabled"
); // 'highlighting_enabled', 'highlighting_disabled'
const [currentEventIndex, setCurrentEventIndex] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -130,6 +136,16 @@ export const WorkflowProvider: React.FC<WorkflowProviderProps> = ({
}
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);
Expand Down Expand Up @@ -200,6 +216,36 @@ export const WorkflowProvider: React.FC<WorkflowProviderProps> = ({
});
}, []); // 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]);
Expand All @@ -211,6 +257,7 @@ export const WorkflowProvider: React.FC<WorkflowProviderProps> = ({
const value = {
workflow,
recordingStatus,
highlightingStatus,
currentEventIndex,
isLoading,
error,
Expand All @@ -219,6 +266,8 @@ export const WorkflowProvider: React.FC<WorkflowProviderProps> = ({
discardAndStartNew,
selectEvent,
fetchWorkflowData,
startHighlighting,
stopHighlighting,
};

return (
Expand Down