| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import type { Page } from "playwright"; |
| import { pause } from "../human-typing.js"; |
| import { humanMove, resetMouseState } from "./mouse-cursor.js"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function getCurrentPmSelection( |
| page: Page, |
| ): Promise<{ from: number; to: number } | null> { |
| return await page.evaluate(() => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| view: { |
| state: { selection: { from: number; to: number; empty: boolean } }; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| if (!editor) return null; |
| const sel = editor.view.state.selection; |
| if (sel.empty || !(sel.to > sel.from)) return null; |
| return { from: sel.from, to: sel.to }; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function selectSubstringInParagraph( |
| page: Page, |
| paragraphSnippet: string, |
| substring: string, |
| opts: { setAgentFocus?: boolean } = {}, |
| ): Promise<{ from: number; to: number } | null> { |
| const setFocus = opts.setAgentFocus ?? false; |
| return await page.evaluate( |
| ({ paraText, substrText, setFocus: doFocus }) => { |
| const g = globalThis as unknown as { |
| __name?: <T>(fn: T, name?: string) => T; |
| }; |
| if (typeof g.__name !== "function") g.__name = <T,>(fn: T) => fn; |
|
|
| const root = document.querySelector(".ProseMirror") as HTMLElement | null; |
| if (!root) return null; |
| const para = |
| (Array.from(root.querySelectorAll("p")).find((p) => |
| (p.textContent || "").includes(paraText), |
| ) as HTMLElement | undefined) ?? null; |
| if (!para) return null; |
| para.scrollIntoView({ behavior: "smooth", block: "center" }); |
|
|
| const text = para.textContent || ""; |
| const idx = text.indexOf(substrText); |
| if (idx === -1) return null; |
|
|
| |
| |
| const walker = document.createTreeWalker(para, NodeFilter.SHOW_TEXT); |
| let walked = 0; |
| let startNode: Text | null = null; |
| let startOffset = 0; |
| let endNode: Text | null = null; |
| let endOffset = 0; |
| while (walker.nextNode()) { |
| const node = walker.currentNode as Text; |
| const len = node.nodeValue?.length ?? 0; |
| if (!startNode && walked + len > idx) { |
| startNode = node; |
| startOffset = idx - walked; |
| } |
| if (walked + len >= idx + substrText.length) { |
| endNode = node; |
| endOffset = idx + substrText.length - walked; |
| break; |
| } |
| walked += len; |
| } |
| if (!startNode || !endNode) return null; |
|
|
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| view: { |
| posAtDOM: (node: Node, offset: number, bias?: number) => number; |
| }; |
| commands: { |
| setTextSelection: (args: { from: number; to: number }) => boolean; |
| setAgentFocus: (args: { from: number; to: number }) => boolean; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| if (!editor) return null; |
|
|
| let from: number; |
| let to: number; |
| try { |
| from = editor.view.posAtDOM(startNode, startOffset, -1); |
| to = editor.view.posAtDOM(endNode, endOffset, 1); |
| } catch { |
| return null; |
| } |
| if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) { |
| return null; |
| } |
|
|
| editor.commands.setTextSelection({ from, to }); |
| if (doFocus) editor.commands.setAgentFocus({ from, to }); |
| return { from, to }; |
| }, |
| { |
| paraText: paragraphSnippet, |
| substrText: substring, |
| setFocus, |
| }, |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function dragSelectSubstringInParagraph( |
| page: Page, |
| paragraphSnippet: string, |
| substring: string, |
| opts: { setAgentFocus?: boolean } = {}, |
| ): Promise<{ from: number; to: number } | null> { |
| const setFocus = opts.setAgentFocus ?? false; |
|
|
| const rect = await page.evaluate( |
| ({ paraText, substrText }) => { |
| const root = document.querySelector(".ProseMirror") as HTMLElement | null; |
| if (!root) return null; |
| const para = |
| (Array.from(root.querySelectorAll("p")).find((p) => |
| (p.textContent || "").includes(paraText), |
| ) as HTMLElement | undefined) ?? null; |
| if (!para) return null; |
| |
| |
| |
| |
| |
| |
| |
| |
| para.scrollIntoView({ behavior: "auto", block: "center" }); |
|
|
| const text = para.textContent || ""; |
| const idx = text.indexOf(substrText); |
| if (idx === -1) return null; |
|
|
| const walker = document.createTreeWalker(para, NodeFilter.SHOW_TEXT); |
| let walked = 0; |
| let startNode: Text | null = null; |
| let startOffset = 0; |
| let endNode: Text | null = null; |
| let endOffset = 0; |
| while (walker.nextNode()) { |
| const node = walker.currentNode as Text; |
| const len = node.nodeValue?.length ?? 0; |
| if (!startNode && walked + len > idx) { |
| startNode = node; |
| startOffset = idx - walked; |
| } |
| if (walked + len >= idx + substrText.length) { |
| endNode = node; |
| endOffset = idx + substrText.length - walked; |
| break; |
| } |
| walked += len; |
| } |
| if (!startNode || !endNode) return null; |
|
|
| const range = document.createRange(); |
| try { |
| range.setStart(startNode, startOffset); |
| range.setEnd(endNode, endOffset); |
| } catch { |
| return null; |
| } |
| |
| |
| |
| const rects = Array.from(range.getClientRects()); |
| if (rects.length === 0) return null; |
| const first = rects[0]; |
| const last = rects[rects.length - 1]; |
|
|
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| view: { |
| posAtDOM: (node: Node, offset: number, bias?: number) => number; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| if (!editor) return null; |
| let from: number; |
| let to: number; |
| try { |
| from = editor.view.posAtDOM(startNode, startOffset, -1); |
| to = editor.view.posAtDOM(endNode, endOffset, 1); |
| } catch { |
| return null; |
| } |
| if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) { |
| return null; |
| } |
|
|
| return { |
| from, |
| to, |
| startX: first.left + 2, |
| startY: first.top + first.height / 2, |
| endX: last.right - 2, |
| endY: last.top + last.height / 2, |
| }; |
| }, |
| { paraText: paragraphSnippet, substrText: substring }, |
| ); |
|
|
| if (!rect) return null; |
|
|
| |
| |
| |
| await page.waitForTimeout(180); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| await humanMove(page, rect.startX, rect.startY, { |
| steps: 22, |
| jitter: 0, |
| }); |
| await pause(120, 180); |
|
|
| |
| |
| await page.evaluate( |
| ({ from }) => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| commands: { |
| focus: () => boolean; |
| setTextSelection: (args: { from: number; to: number }) => boolean; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| if (!editor) return; |
| editor.commands.focus(); |
| editor.commands.setTextSelection({ from, to: from }); |
| }, |
| { from: rect.from }, |
| ); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const dx = rect.endX - rect.startX; |
| const dy = rect.endY - rect.startY; |
| const distance = Math.hypot(dx, dy); |
| const selLen = rect.to - rect.from; |
| const glideSteps = Math.max(18, Math.min(40, Math.round(distance / 14))); |
| for (let i = 1; i <= glideSteps; i++) { |
| const t = i / glideSteps; |
| const x = rect.startX + dx * t; |
| const y = rect.startY + dy * t; |
| await page.mouse.move(x, y, { steps: 2 }); |
| const midTo = rect.from + Math.round(selLen * t); |
| await page.evaluate( |
| ({ from, to }) => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| commands: { |
| setTextSelection: (args: { |
| from: number; |
| to: number; |
| }) => boolean; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| editor?.commands.setTextSelection({ from, to }); |
| }, |
| { from: rect.from, to: midTo }, |
| ); |
| await page.waitForTimeout(14 + Math.floor(Math.random() * 14)); |
| } |
|
|
| |
| |
| |
| await page.evaluate( |
| ({ from, to }) => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| commands: { |
| setTextSelection: (args: { from: number; to: number }) => boolean; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| editor?.commands.setTextSelection({ from, to }); |
| }, |
| { from: rect.from, to: rect.to }, |
| ); |
|
|
| |
| |
| |
| if (setFocus) { |
| await page.evaluate( |
| ({ from, to }) => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { |
| commands: { |
| setAgentFocus: (args: { from: number; to: number }) => boolean; |
| }; |
| }; |
| } |
| ).__demoEditor; |
| editor?.commands.setAgentFocus({ from, to }); |
| }, |
| { from: rect.from, to: rect.to }, |
| ); |
| } |
|
|
| |
| |
| |
| |
| resetMouseState(rect.endX, rect.endY); |
|
|
| return { from: rect.from, to: rect.to }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function clearAgentFocus(page: Page) { |
| await page.evaluate(() => { |
| const editor = ( |
| window as unknown as { |
| __demoEditor?: { commands: { clearAgentFocus: () => boolean } }; |
| } |
| ).__demoEditor; |
| editor?.commands?.clearAgentFocus?.(); |
| }); |
| } |
|
|