From 8b0b8f5b7f72cfde0515f8f1f5f079600813c3a0 Mon Sep 17 00:00:00 2001 From: Thien Tran Date: Mon, 13 Nov 2023 22:00:25 +0700 Subject: [PATCH] restore the old code to be able to record --- src/common/App.tsx | 1 + src/common/CTFlowAI.tsx | 1 + src/common/RunTaskButton.tsx | 79 ++++++ src/pages/Common/styles.css | 68 ----- src/pages/Content/ControlBar.tsx | 434 +++++++++++++++++++++++++++---- 5 files changed, 460 insertions(+), 123 deletions(-) create mode 100644 src/common/RunTaskButton.tsx diff --git a/src/common/App.tsx b/src/common/App.tsx index 04f308b..bceb9db 100644 --- a/src/common/App.tsx +++ b/src/common/App.tsx @@ -35,6 +35,7 @@ const App = () => { + {openAIKey ? : } ); diff --git a/src/common/CTFlowAI.tsx b/src/common/CTFlowAI.tsx index 5d41fdb..34dd31d 100644 --- a/src/common/CTFlowAI.tsx +++ b/src/common/CTFlowAI.tsx @@ -28,6 +28,7 @@ const CTFlowAI = () => { + {openAIKey ? : } ); diff --git a/src/common/RunTaskButton.tsx b/src/common/RunTaskButton.tsx new file mode 100644 index 0000000..e1d5bf7 --- /dev/null +++ b/src/common/RunTaskButton.tsx @@ -0,0 +1,79 @@ +import { Button, HStack, Icon } from '@chakra-ui/react'; +import React, { useCallback } from 'react'; +import { useAppState } from '../state/store'; +import { BsPlayFill, BsStopFill } from 'react-icons/bs'; + +export default function RunTaskButton(props: { runTask: () => void }) { + const state = useAppState((state) => ({ + taskState: state.currentTask.status, + instructions: state.ui.instructions, + interruptTask: state.currentTask.actions.interrupt, + })); + + const [lastTriggerAITaskAt, setLastTriggerAITaskAt] = + React.useState(); + const onRunTask = props.runTask; + + console.log('HEY FROM RUN TASK BUTTON'); + + if (chrome.debugger !== undefined) { + console.log('add listener to RUNT TASK BUTTON only'); + chrome.runtime.onMessage.addListener(async function ( + request, + sender, + sendResponse + ) { + console.log( + 'RUN TASK BTN:Message received from RUNT TASK BUTTON index.ts', + request + ); + onRunTask(); + // const [lastTriggerAITaskAt, setLastTriggerAITaskAt] = React.useState(); + }); + } + + React.useEffect(() => { + if (lastTriggerAITaskAt === undefined) { + return; + } + + console.log(lastTriggerAITaskAt, 'RUN TASK BUTTON - CHANGE AND TRIGGER'); + console.log('chrome.debugger', chrome.debugger); + + if (chrome.debugger === undefined) { + console.log('trigger chrome runtime message'); + chrome.runtime.sendMessage({ + source: 'control-bar', + type: 'run-task', + }); + } else if (lastTriggerAITaskAt) { + onRunTask(); + } + }, [lastTriggerAITaskAt]); + + let button = ( + + ); + + if (state.taskState === 'running') { + button = ( + + ); + } + + return {button}; +} diff --git a/src/pages/Common/styles.css b/src/pages/Common/styles.css index a0daa38..6ff0fb1 100644 --- a/src/pages/Common/styles.css +++ b/src/pages/Common/styles.css @@ -125,9 +125,6 @@ .px-4 { padding: 0 1em; } -.pl-4 { - padding-left: 1em; -} /* Margin Utils */ .m-4 { @@ -149,9 +146,6 @@ .mt-12 { margin-top: 3em; } -.ml-4 { - margin-left: 1em; -} .mr-1 { margin-right: 0.25em; } @@ -161,9 +155,6 @@ .mr-4 { margin-right: 1em; } -.mr-4-i { - margin-right: 1em !important; -} .mr-5 { margin-right: 1.5em; } @@ -219,62 +210,3 @@ .cypress-trigger:hover { background: #e9e9e9; } - -.flex { - display: flex; -} - -.justify-between { - justify-content: space-between; -} - -.items-center { - align-items: center; -} - -.block { - display: block; -} - -.static { - position: static; -} - -.fixed { - position: fixed; -} - -.bottom-1 { - bottom: 1rem; -} - -.right-1 { - right: 1rem; -} - -.rounded-md { - border-radius: 0.375rem; -} - -button.no-default { - background: transparent; - border: none; - padding: 0; - margin: 0; - cursor: pointer; - outline: none; -} - -button.bg-common { - background-color: #080a0b; -} - -button.px-4 { - padding-right: 1em; - padding-left: 1em; -} - -button.py-2 { - padding-top: 0.5em; - padding-bottom: 0.5em; -} diff --git a/src/pages/Content/ControlBar.tsx b/src/pages/Content/ControlBar.tsx index da51aa4..af86377 100644 --- a/src/pages/Content/ControlBar.tsx +++ b/src/pages/Content/ControlBar.tsx @@ -1,73 +1,397 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import throttle from 'lodash.throttle'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faCamera, + faCopy, + faCheck, + faCheckCircle, + faTimes, + faChevronUp, + faChevronDown, +} from '@fortawesome/free-solid-svg-icons'; + +import Recorder from './recorder'; +import Highlighter from './Highlighter'; +import ActionList from './ActionList'; +import CodeGen from './CodeGen'; +import genSelectors, { getBestSelectorForAction } from '../builders/selector'; +import { genCode } from '../builders'; +import ScriptTypeSelect from '../Common/ScriptTypeSelect'; +import { usePreferredLibrary, usePreferredBarPosition } from '../Common/hooks'; + +import type { Action } from '../types'; +import { + ActionType, + ActionsMode, + ScriptType, + TagName, + BarPosition, +} from '../types'; import ControlBarStyle from './ControlBar.css'; +import { endRecording } from '../Common/endRecording'; +import CTFlowAI from '../../common/CTFlowAI'; +import { set } from 'lodash'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; +// import ReactTabStyle from 'react-tabs/style/react-tabs.css'; import CustomReactTabStyle from './CustomReactTabs.css'; -import { useAppState } from '../../state/store'; -import { useToast } from '@chakra-ui/react'; + +const ActionButton = ({ + onClick, + children, + label, + testId, +}: { + onClick: () => void; + children: JSX.Element; + label: String; + testId?: String; +}) => ( +
+
+
+ {children} +
+
{label}
+
+
+); + +const AIPanel = () => ( +
+

AI PANEL

+
+
+ +
+
+
+); + +function RenderActionText({ action }: { action: Action }) { + return ( + <> + {action.type === ActionType.Click + ? `Click on ${action.tagName.toLowerCase()} ${getBestSelectorForAction( + action, + ScriptType.Playwright + )}` + : action.type === ActionType.Hover + ? `Hover over ${action.tagName.toLowerCase()} ${getBestSelectorForAction( + action, + ScriptType.Playwright + )}` + : action.type === ActionType.Input + ? `Fill "${ + action.isPassword + ? '*'.repeat(action?.value?.length ?? 0) + : action.value + }" on ${action.tagName.toLowerCase()} ${getBestSelectorForAction( + action, + ScriptType.Playwright + )}` + : action.type === ActionType.Keydown + ? `Press ${action.key} on ${action.tagName.toLowerCase()}` + : action.type === ActionType.Load + ? `Load "${action.url}"` + : action.type === ActionType.Resize + ? `Resize window to ${action.width} x ${action.height}` + : action.type === ActionType.Wheel + ? `Scroll wheel by X:${action.deltaX}, Y:${action.deltaY}` + : action.type === ActionType.FullScreenshot + ? `Take full page screenshot` + : action.type === ActionType.AwaitText + ? `Wait for text "${action.text}"` + : action.type === ActionType.DragAndDrop + ? `Drag n Drop from (${action.sourceX}, ${action.sourceY}) to (${action.targetX}, ${action.targetY})` + : ''} + + ); +} + +function isElementFromOverlay(element: HTMLElement) { + if (element == null) return false; + return element.closest('#overlay-controls') != null; +} export default function ControlBar({ onExit }: { onExit: () => void }) { - const state = useAppState((state) => ({ - taskHistory: state.currentTask.history, - taskStatus: state.currentTask.status, - runTask: state.currentTask.actions.runTask, - instructions: state.ui.instructions, - setInstructions: state.ui.actions.setInstructions, - })); - const [lastTriggerAITaskAt, setLastTriggerAITaskAt] = useState(); - - const toast = useToast(); - - const toastError = useCallback( - (message: string) => { - toast({ - title: 'Error', - description: message, - status: 'error', - duration: 5000, - isClosable: true, - }); - }, - [toast] + const [barPosition, setBarPosition] = usePreferredBarPosition( + BarPosition.Bottom + ); + + const [hoveredElement, setHoveredElement] = useState( + null + ); + const [hoveredElementSelectors, setHoveredElementSelectors] = useState( + {} ); - const onRunTask = () => { - console.log('instructions', state.instructions); - state.instructions && state.runTask(toastError); + const [lastAction, setLastAction] = useState(null); + const [actions, setActions] = useState([]); + + const [showAllActions, setShowAllActions] = useState(false); + + const [showCTFlowAI, setShowCTFlowAI] = useState(true); + + const [showActionsMode, setShowActionsMode] = useState( + ActionsMode.Code + ); + const [preferredLibrary, setPreferredLibrary] = usePreferredLibrary(); + + const [copyCodeConfirm, setCopyCodeConfirm] = useState(false); + const [screenshotConfirm, setScreenshotConfirm] = useState(false); + + const [isFinished, setIsFinished] = useState(false); + + const [isOpen, setIsOpen] = useState(true); + + const handleMouseMoveRef = useRef((_: MouseEvent) => {}); + const recorderRef = useRef(null); + + const onEndRecording = () => { + setIsFinished(true); + + // Show Code + setShowAllActions(true); + + // show AI Panel + setShowCTFlowAI(false); + + // Clear out highlighter + document.removeEventListener('mousemove', handleMouseMoveRef.current, true); + setHoveredElement(null); + + // Turn off recorder + recorderRef.current?.deregister(); + + endRecording(); + }; + + const onClose = () => { + setIsOpen(false); + onExit(); }; useEffect(() => { - if (lastTriggerAITaskAt === undefined) { - return; - } - - console.log(lastTriggerAITaskAt, 'RUN TASK BUTTON - CHANGE AND TRIGGER'); - console.log('chrome.debugger', chrome.debugger); - - if (chrome.debugger === undefined) { - console.log('trigger chrome runtime message'); - chrome.runtime.sendMessage({ - source: 'control-bar', - type: 'run-task', - }); - console.log('onRunTask', onRunTask); - onRunTask(); - } else if (lastTriggerAITaskAt) { - onRunTask(); - } - }, [lastTriggerAITaskAt]); + handleMouseMoveRef.current = throttle((event: MouseEvent) => { + const x = event.clientX, + y = event.clientY, + elementMouseIsOver = document.elementFromPoint(x, y) as HTMLElement; + + if ( + !isElementFromOverlay(elementMouseIsOver) && + elementMouseIsOver != null + ) { + const { parentElement } = elementMouseIsOver; + // Match the logic in recorder.ts for link clicks + const element = + parentElement?.tagName === 'A' ? parentElement : elementMouseIsOver; + setHoveredElement(element || null); + setHoveredElementSelectors(genSelectors(element)); + } + }, 100); + + document.addEventListener('mousemove', handleMouseMoveRef.current, true); + + recorderRef.current = new Recorder({ + onAction: (action: Action, actions: Action[]) => { + setLastAction(action); + setActions(actions); + }, + onInitialized: (lastAction: Action, recording: Action[]) => { + setLastAction( + recording.reduceRight( + (p, v) => (p == null && v.type != 'navigate' ? v : p), + null + ) + ); + setActions(recording); + }, + }); + + // Set recording to be finished if somewhere else (ex. popup) the state has been set to be finished + chrome.storage.onChanged.addListener((changes) => { + if ( + changes.recordingState != null && + changes.recordingState.newValue === 'finished' && + // Firefox will fire change events even if the values are not changed + changes.recordingState.newValue !== changes.recordingState.oldValue + ) { + if (!isFinished) { + onEndRecording(); + } + } + }); + }, []); + + const displayedScriptType = preferredLibrary ?? ScriptType.Cypress; + + const rect = hoveredElement?.getBoundingClientRect(); + const displayedSelector = getBestSelectorForAction( + { + type: ActionType.Click, + tagName: (hoveredElement?.tagName ?? '') as TagName, + inputType: undefined, + value: undefined, + selectors: hoveredElementSelectors || {}, + timestamp: 0, + isPassword: false, + hasOnlyText: + hoveredElement?.children?.length === 0 && + hoveredElement?.innerText?.length > 0, + }, + displayedScriptType + ); + + if (isOpen === false) { + return <> ; + } + + const onTestMessageClick = () => { + console.log('trigger: On test message click: click me'); + chrome.runtime.sendMessage({ from: 'control bar' }); + }; return ( -
+ <> - -
+ + +

Recorder Tab

+
+
+
+ { + setShowActionsMode( + showActionsMode === ActionsMode.Actions + ? ActionsMode.Code + : ActionsMode.Actions + ); + }} + > + Show{' '} + {showActionsMode === ActionsMode.Actions + ? 'Code' + : 'Actions'} + + {!isFinished && ( + { + recorderRef.current?.onFullScreenshot(); + setScreenshotConfirm(true); + setTimeout(() => { + setScreenshotConfirm(false); + }, 2000); + }} + > + {' '} + Record Screenshot + + )} +
+
+ {showActionsMode === ActionsMode.Code && ( + <> + + { + setCopyCodeConfirm(true); + setTimeout(() => { + setCopyCodeConfirm(false); + }, 2000); + }} + > + + {' '} + Copy Code + + + + )} +
+
+ + {showActionsMode === ActionsMode.Code && ( + + )} + {showActionsMode === ActionsMode.Actions && ( + + )} +
+
+ +

AI TAB

+ {showCTFlowAI && ( +
+

PUCK ME HARD

+ +
+ )} +
+ + + 🎥 Recorder + 🤖 CTFlow AI + +
+ + ); }