@omnivoice/page-agent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +137 -15
- package/dist/esm/SimulatorMask-DIoKZt1z.js +379 -0
- package/dist/esm/SimulatorMask-DIoKZt1z.js.map +1 -0
- package/dist/esm/brain-extension-DDuKTYrJ.js +2192 -0
- package/dist/esm/brain-extension-DDuKTYrJ.js.map +1 -0
- package/dist/esm/debug-extension-D0P_sNkc.js +45 -0
- package/dist/esm/debug-extension-D0P_sNkc.js.map +1 -0
- package/dist/esm/page-agent.d.ts +1200 -0
- package/dist/esm/page-agent.js +4870 -0
- package/dist/esm/page-agent.js.map +1 -0
- package/dist/esm/profiler-CcdRV96d.js +203 -0
- package/dist/esm/profiler-CcdRV96d.js.map +1 -0
- package/dist/iife/page-agent.demo.js +456 -0
- package/dist/voice/index.d.ts +533 -0
- package/dist/voice/index.js +1096 -0
- package/dist/voice/index.js.map +1 -0
- package/package.json +70 -28
- package/server/agent-gateway.mjs +248 -0
- package/src/voice/README.md +0 -62
- package/src/voice/bridge.ts +0 -230
- package/src/voice/config.ts +0 -200
- package/src/voice/engines.ts +0 -161
- package/src/voice/index.ts +0 -17
- package/src/voice/session.ts +0 -309
- package/src/voice/types.ts +0 -98
|
@@ -0,0 +1,4870 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
"use strict";
|
|
3
|
+
try {
|
|
4
|
+
if (typeof document != "undefined") {
|
|
5
|
+
var elementStyle = document.createElement("style");
|
|
6
|
+
elementStyle.appendChild(document.createTextNode("._wrapper_1rq3n_1 {\n position: fixed;\n bottom: 100px;\n left: 50%;\n transform: translateX(-50%) translateY(20px);\n opacity: 0;\n z-index: 2147483642; /* 比 SimulatorMask 高一层 */\n box-sizing: border-box;\n\n overflow: visible;\n\n * {\n box-sizing: border-box;\n }\n\n --width: 360px;\n --height: 40px;\n --border-radius: 12px;\n\n --side-space: 12px; /* 控制栏两侧的间距 */\n --history-width: calc(var(--width) - var(--side-space) * 2);\n\n --color-1: rgb(57, 182, 255);\n --color-2: rgb(189, 69, 251);\n --color-3: rgb(255, 87, 51);\n --color-4: rgb(255, 214, 0);\n\n width: var(--width);\n height: var(--height);\n\n transition: all 0.3s ease-in-out;\n\n /* FORK: responsivo. O upstream só encolhia a largura; no celular o painel ainda ficava a\n 100px do rodapé (sobre a área do polegar e sobre a barra do navegador), com as mesmas\n margens laterais de desktop. Aqui ele encosta no rodapé respeitando a safe area, usa a\n largura quase inteira e limita a altura do histórico para não cobrir a página. */\n @media (max-width: 640px) {\n --width: calc(100vw - 16px);\n --side-space: 8px;\n width: var(--width);\n bottom: calc(12px + env(safe-area-inset-bottom, 0px));\n --history-max: min(60vh, calc(100dvh - 120px - var(--height)));\n }\n\n /* Quem prefere menos animação não deve receber transição de layout. */\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n\n * {\n animation: none !important;\n transition: none !important;\n }\n }\n\n ._background_1rq3n_55 {\n position: absolute;\n inset: -2px -8px;\n border-radius: calc(var(--border-radius) + 4px);\n filter: blur(16px);\n overflow: hidden;\n /* mix-blend-mode: lighten; */\n /* display: none; */\n\n &::before {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n /* left: -100%; */\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-1),\n var(--color-2),\n var(--color-1)\n );\n animation: _mask-running_1rq3n_1 2s linear infinite;\n }\n &::after {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-2),\n var(--color-1),\n var(--color-2)\n );\n animation: _mask-running_1rq3n_1 2s linear infinite;\n animation-delay: 1s;\n }\n }\n}\n\n@keyframes _mask-running_1rq3n_1 {\n from {\n transform: translateX(-100%);\n }\n to {\n transform: translateX(100%);\n }\n}\n\n/* 控制栏 */\n._header_1rq3n_115 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n user-select: none;\n\n position: absolute;\n inset: 0;\n\n cursor: pointer;\n flex-shrink: 0; /* 防止 header 被压缩 */\n\n background: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(10px);\n border-radius: var(--border-radius);\n background-clip: padding-box;\n\n box-shadow:\n 0 0 0px 2px rgba(255, 255, 255, 0.4),\n 0 0 5px 1px rgba(255, 255, 255, 0.3);\n\n ._statusSection_1rq3n_137 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-height: 24px; /* 确保垂直居中 */\n\n ._indicator_1rq3n_144 {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.5);\n flex-shrink: 0;\n animation: none; /* 默认无动画 */\n\n /* 运行状态 - 有动画 */\n &._thinking_1rq3n_153 {\n background: rgb(57, 182, 255);\n animation: _pulse_1rq3n_1 0.8s ease-in-out infinite;\n }\n\n &._tool_executing_1rq3n_158 {\n background: rgb(189, 69, 251);\n animation: _pulse_1rq3n_1 0.6s ease-in-out infinite;\n }\n\n &._retry_1rq3n_163 {\n background: rgb(255, 214, 0);\n animation: _retryPulse_1rq3n_1 1s ease-in-out infinite;\n }\n\n /* 静止状态 - 无动画 */\n &._completed_1rq3n_169,\n &._input_1rq3n_170,\n &._output_1rq3n_171 {\n background: rgb(34, 197, 94);\n animation: none;\n }\n\n &._error_1rq3n_176 {\n background: rgb(239, 68, 68);\n animation: none;\n }\n }\n\n ._statusText_1rq3n_182 {\n color: white;\n font-size: 12px;\n line-height: 1;\n font-weight: 500;\n transition: all 0.3s ease-in-out;\n position: relative;\n overflow: hidden;\n display: flex;\n align-items: center;\n min-height: 24px; /* 确保垂直居中 */\n\n &._fadeOut_1rq3n_194 {\n animation: _statusTextFadeOut_1rq3n_1 0.3s ease forwards;\n }\n\n &._fadeIn_1rq3n_198 {\n animation: _statusTextFadeIn_1rq3n_1 0.3s ease forwards;\n }\n }\n }\n\n ._controls_1rq3n_204 {\n display: flex;\n align-items: center;\n gap: 4px;\n\n ._controlButton_1rq3n_209 {\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 4px;\n background: rgba(255, 255, 255, 0.1);\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n line-height: 1;\n\n &:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n }\n\n ._stopButton_1rq3n_228 {\n background: rgba(239, 68, 68, 0.2);\n color: rgb(255, 41, 41);\n font-weight: 600;\n\n &:hover {\n background: rgba(239, 68, 68, 0.3);\n }\n }\n }\n}\n\n@keyframes _statusTextFadeIn_1rq3n_1 {\n 0% {\n opacity: 0;\n transform: translateY(5px);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes _statusTextFadeOut_1rq3n_1 {\n 0% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-5px);\n }\n}\n\n._historySectionWrapper_1rq3n_262 {\n position: absolute;\n width: var(--history-width);\n bottom: var(--height);\n left: var(--side-space);\n z-index: -2;\n\n padding-top: 0px;\n visibility: collapse;\n overflow: hidden;\n\n transition: all 0.2s;\n\n background: rgba(2, 0, 20, 0.5);\n /* background: rgba(186, 186, 186, 0.2); */\n backdrop-filter: blur(10px);\n\n text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);\n\n border-top-left-radius: calc(var(--border-radius) + 4px);\n border-top-right-radius: calc(var(--border-radius) + 4px);\n\n /* border: 2px solid rgba(255, 255, 255, 0.8); */\n border: 2px solid rgba(255, 255, 255, 0.4);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n\n /* @media (prefers-color-scheme: dark) {\n box-shadow:\n 0 8px 32px 0 rgba(0, 0, 0, 0.85),\n 0 2px 12px 0 rgba(57, 182, 255, 0.1);\n } */\n\n ._expanded_1rq3n_294 & {\n padding-top: 8px;\n visibility: visible;\n }\n\n ._historySection_1rq3n_262 {\n position: relative;\n overflow-y: auto;\n overscroll-behavior: contain;\n scrollbar-width: none;\n max-height: 0;\n padding-inline: 8px;\n\n transition: max-height 0.2s;\n\n ._expanded_1rq3n_294 & {\n /* FORK: vira variável para o celular poder reaproveitar o espaço que o painel deixou de\n ocupar no rodapé; a reserva fixa de 200px era do layout de desktop. */\n max-height: var(--history-max, min(500px, calc(100vh - 200px - var(--height))));\n }\n\n /* FORK: componentes de mensagem redesenhados.\n O upstream usava lajota translúcida sobre a página do usuário, então o contraste do texto\n dependia do que estivesse atrás — sobre página clara o texto branco sumia. Aqui a bolha é\n OPACA, com par de cores fixo e contraste medido (o teste mede). O formato segue o de um\n aplicativo de mensagens: a fala da pessoa de um lado, a do agente do outro. */\n ._historyRow_1rq3n_320 {\n display: flex;\n margin-bottom: 8px;\n\n &:last-child {\n margin-bottom: 12px;\n }\n\n &._fromPessoa_1rq3n_328 {\n justify-content: flex-end;\n }\n\n &._fromAgente_1rq3n_332,\n &._fromSistema_1rq3n_333 {\n justify-content: flex-start;\n }\n }\n\n ._historyItem_1rq3n_338 {\n --bolha: #22222e;\n --bolha-texto: #ececf2;\n --bolha-meta: #a9a9bd;\n --bolha-realce: #7a8cff;\n\n max-width: 88%;\n min-width: 0;\n padding: 7px 10px 8px;\n border-radius: 12px;\n background: var(--bolha);\n color: var(--bolha-texto);\n font-size: 13px;\n line-height: 1.45;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);\n\n /* A bolha só precisa de um canto reto do lado de quem fala — é o que dá a leitura de\n conversa sem desenhar rabicho. */\n ._fromPessoa_1rq3n_328 & {\n --bolha: #2f4f74;\n --bolha-texto: #f2f6fb;\n --bolha-meta: #bcd0e6;\n border-bottom-right-radius: 4px;\n }\n\n ._fromAgente_1rq3n_332 &,\n ._fromSistema_1rq3n_333 & {\n border-bottom-left-radius: 4px;\n }\n\n /* O tipo não pinta o fundo inteiro: pintar derruba o contraste e some com a hierarquia.\n Ele colore só o ícone e o nome do autor, que é onde a informação faz falta. */\n &._output_1rq3n_171 {\n --bolha-realce: #58d68d;\n }\n\n &._observation_1rq3n_374 {\n --bolha-realce: #c49bff;\n }\n\n &._question_1rq3n_378 {\n --bolha-realce: #ffb067;\n }\n\n &._doneSuccess_1rq3n_382 {\n --bolha: #1d3b2a;\n --bolha-texto: #e3f8ec;\n --bolha-meta: #a9d8bd;\n --bolha-realce: #58d68d;\n font-weight: 500;\n }\n\n &._doneFailure_1rq3n_390,\n &._error_1rq3n_176 {\n --bolha: #412128;\n --bolha-texto: #fde8ec;\n --bolha-meta: #e3aeb8;\n --bolha-realce: #ff8095;\n }\n\n &._retry_1rq3n_163 {\n --bolha-realce: #ffd166;\n }\n\n ._historyHeader_1rq3n_402 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 3px;\n color: var(--bolha-realce);\n }\n\n ._historyAuthor_1rq3n_410 {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.01em;\n }\n\n ._historyMeta_1rq3n_416 {\n margin-left: auto;\n font-size: 11px;\n color: var(--bolha-meta);\n white-space: nowrap;\n }\n\n /* O ícone é SVG, então o tamanho vem de uma caixa fixa e não do font-size — emoji herdava\n a métrica da fonte do sistema e saía desproporcional. É decorativo: o autor vai em texto\n ao lado, e o SVG carrega aria-hidden. */\n ._statusIcon_1rq3n_426 {\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n line-height: 1;\n\n svg {\n width: 100%;\n height: 100%;\n display: block;\n }\n }\n\n ._historyContent_1rq3n_439 {\n /* FORK: `break-all` partia palavra no meio (\"corretamen/te\"). `anywhere` só quebra\n quando o token não cabe de jeito nenhum — uma URL longa, não uma palavra comum. */\n word-break: normal;\n overflow-wrap: anywhere;\n\n > span,\n ._reflectionLine_1rq3n_446 {\n white-space: pre-wrap;\n }\n }\n\n ._reflectionLines_1rq3n_451 {\n display: flex;\n flex-direction: column;\n gap: 7px;\n }\n\n ._reflectionLine_1rq3n_446 {\n display: block;\n }\n\n /* O rótulo substitui o emoji que antes dava o sentido da linha. */\n ._reflectionLabel_1rq3n_462 {\n display: block;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.07em;\n text-transform: uppercase;\n color: var(--bolha-meta);\n margin-bottom: 1px;\n }\n }\n\n /* Em telas de alto contraste, a borda devolve o limite da bolha que a cor sozinha dá. */\n @media (prefers-contrast: more) {\n ._historyItem_1rq3n_338 {\n --bolha: #000000;\n --bolha-texto: #ffffff;\n --bolha-meta: #e0e0e0;\n border: 1px solid #ffffff;\n }\n }\n }\n}\n\n/* 动画关键帧 - 更快的闪烁 */\n@keyframes _pulse_1rq3n_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.4;\n transform: scale(1.3);\n }\n}\n\n/* 重试动画 - 旋转脉冲 */\n@keyframes _retryPulse_1rq3n_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n }\n 25% {\n opacity: 0.6;\n transform: scale(1.2) rotate(90deg);\n }\n 50% {\n opacity: 0.8;\n transform: scale(1.1) rotate(180deg);\n }\n 75% {\n opacity: 0.6;\n transform: scale(1.2) rotate(270deg);\n }\n}\n\n/* 庆祝动画 */\n@keyframes _celebrate_1rq3n_1 {\n 0%,\n 100% {\n transform: scale(1);\n }\n 25% {\n transform: scale(1.2) rotate(-5deg);\n }\n 75% {\n transform: scale(1.2) rotate(5deg);\n }\n}\n\n/* done 卡片的光泽效果 */\n@keyframes _shimmer_1rq3n_1 {\n 0% {\n left: -100%;\n }\n 100% {\n left: 100%;\n }\n}\n\n/* 输入区域样式 */\n._inputSectionWrapper_1rq3n_544 {\n position: absolute;\n width: var(--history-width);\n top: var(--height);\n left: var(--side-space);\n z-index: -1;\n\n visibility: visible;\n overflow: hidden;\n\n height: 48px;\n\n transition: all 0.2s;\n\n background: rgba(186, 186, 186, 0.2);\n backdrop-filter: blur(10px);\n\n border-bottom-left-radius: calc(var(--border-radius) + 4px);\n border-bottom-right-radius: calc(var(--border-radius) + 4px);\n\n border: 2px solid rgba(255, 255, 255, 0.3);\n box-shadow: 0 1px 16px rgba(0, 0, 0, 0.4);\n\n &._hidden_1rq3n_567 {\n visibility: collapse;\n height: 0;\n }\n\n ._inputSection_1rq3n_544 {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 8px 8px;\n\n ._taskInput_1rq3n_578 {\n flex: 1;\n background: rgba(255, 255, 255, 0.4);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 10px;\n padding-inline: 10px;\n color: rgb(20, 20, 20);\n font-size: 12px;\n height: 28px;\n line-height: 1;\n outline: none;\n transition: all 0.2s ease;\n\n /* text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); */\n\n /* border-color: rgba(57, 182, 255, 0.3); */\n\n &::placeholder {\n color: rgb(53, 53, 53);\n }\n\n &:focus {\n background: rgba(255, 255, 255, 0.8);\n border-color: rgba(57, 182, 255, 0.6);\n box-shadow: 0 0 0 2px rgba(57, 182, 255, 0.2);\n }\n }\n }\n}\n/* FORK: estilos do widget central (launcher + microfone). Injetados via CSS-in-JS — sem\n request de CSS separada, sem fontes externas. Pensado para mobile (alvo de toque 48px,\n safe-area) e para não brigar com a página host (z-index alto, cor própria). */\n\n.page-agent-widget-launcher {\n position: fixed;\n z-index: 2147483630;\n width: 48px;\n height: 48px;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: #111827;\n color: #f9fafb;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);\n cursor: pointer;\n transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n/* Safe-area em telas com notch/barras: o botão não fica preso na borda. */\n.page-agent-widget-launcher {\n right: calc(20px + env(safe-area-inset-right, 0px));\n bottom: calc(20px + env(safe-area-inset-bottom, 0px));\n}\n\n.page-agent-widget-launcher:hover {\n transform: translateY(-1px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.34);\n background: #1f2937;\n}\n\n.page-agent-widget-launcher:active {\n transform: translateY(0) scale(0.97);\n}\n\n.page-agent-widget-launcher:focus-visible {\n outline: 3px solid #60a5fa;\n outline-offset: 2px;\n}\n\n.page-agent-widget-launcher svg {\n display: block;\n}\n\n/* Botão de microfone, injetado ao lado do input de tarefa do painel. */\n.page-agent-voice-mic {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n flex: 0 0 auto;\n margin-right: 8px;\n padding: 0;\n border: 1px solid rgba(148, 163, 184, 0.4);\n border-radius: 8px;\n background: rgba(148, 163, 184, 0.12);\n color: inherit;\n cursor: pointer;\n transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n.page-agent-voice-mic:hover {\n background: rgba(148, 163, 184, 0.22);\n border-color: rgba(148, 163, 184, 0.6);\n}\n\n.page-agent-voice-mic:active {\n transform: scale(0.96);\n}\n\n.page-agent-voice-mic:focus-visible {\n outline: 2px solid #60a5fa;\n outline-offset: 1px;\n}\n\n.page-agent-voice-mic.is-listening {\n background: rgba(239, 68, 68, 0.16);\n border-color: rgba(239, 68, 68, 0.6);\n animation: page-agent-voice-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes page-agent-voice-pulse {\n 0%,\n 100% {\n box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.35);\n }\n 50% {\n box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .page-agent-voice-mic.is-listening {\n animation: none;\n }\n}"));
|
|
7
|
+
document.head.appendChild(elementStyle);
|
|
8
|
+
}
|
|
9
|
+
} catch (e) {
|
|
10
|
+
console.error("vite-plugin-css-injected-by-js", e);
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
import * as z from "zod/v4";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
const InvokeErrorTypes = {
|
|
16
|
+
// Retryable
|
|
17
|
+
NETWORK_ERROR: "network_error",
|
|
18
|
+
// Network error, retry
|
|
19
|
+
RATE_LIMIT: "rate_limit",
|
|
20
|
+
// Rate limit, retry
|
|
21
|
+
SERVER_ERROR: "server_error",
|
|
22
|
+
// 5xx, retry
|
|
23
|
+
NO_TOOL_CALL: "no_tool_call",
|
|
24
|
+
// Model did not call tool
|
|
25
|
+
INVALID_TOOL_ARGS: "invalid_tool_args",
|
|
26
|
+
// Tool args don't match schema
|
|
27
|
+
TOOL_EXECUTION_ERROR: "tool_execution_error",
|
|
28
|
+
// Tool execution error
|
|
29
|
+
INVALID_RESPONSE: "invalid_response",
|
|
30
|
+
// Response body is not valid JSON
|
|
31
|
+
INVALID_SCHEMA: "invalid_schema",
|
|
32
|
+
// Response is valid JSON but doesn't match expected shape
|
|
33
|
+
UNKNOWN: "unknown",
|
|
34
|
+
// Non-retryable
|
|
35
|
+
CONFIG_ERROR: "config_error",
|
|
36
|
+
// Invalid local configuration or hook
|
|
37
|
+
AUTH_ERROR: "auth_error",
|
|
38
|
+
// Authentication failed
|
|
39
|
+
CONTEXT_LENGTH: "context_length",
|
|
40
|
+
// Prompt too long
|
|
41
|
+
CONTENT_FILTER: "content_filter"
|
|
42
|
+
// Content filtered
|
|
43
|
+
};
|
|
44
|
+
const RETRYABLE_TYPES = [
|
|
45
|
+
InvokeErrorTypes.NETWORK_ERROR,
|
|
46
|
+
InvokeErrorTypes.RATE_LIMIT,
|
|
47
|
+
InvokeErrorTypes.SERVER_ERROR,
|
|
48
|
+
InvokeErrorTypes.NO_TOOL_CALL,
|
|
49
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
50
|
+
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
|
51
|
+
InvokeErrorTypes.INVALID_RESPONSE,
|
|
52
|
+
InvokeErrorTypes.INVALID_SCHEMA,
|
|
53
|
+
InvokeErrorTypes.UNKNOWN
|
|
54
|
+
];
|
|
55
|
+
class InvokeError extends Error {
|
|
56
|
+
type;
|
|
57
|
+
retryable;
|
|
58
|
+
statusCode;
|
|
59
|
+
/* raw error (provided if this error is caused by another error) */
|
|
60
|
+
rawError;
|
|
61
|
+
/* raw response from the API (provided if this error is caused by an API calling) */
|
|
62
|
+
rawResponse;
|
|
63
|
+
constructor(type, message, rawError, rawResponse) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "InvokeError";
|
|
66
|
+
this.type = type;
|
|
67
|
+
this.retryable = RETRYABLE_TYPES.includes(type);
|
|
68
|
+
this.rawError = rawError;
|
|
69
|
+
this.rawResponse = rawResponse;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function debug(...args) {
|
|
73
|
+
console.debug(chalk.gray("[LLM]"), ...args);
|
|
74
|
+
}
|
|
75
|
+
function zodToOpenAITool(name, tool2) {
|
|
76
|
+
return {
|
|
77
|
+
type: "function",
|
|
78
|
+
function: {
|
|
79
|
+
name,
|
|
80
|
+
description: tool2.description,
|
|
81
|
+
parameters: z.toJSONSchema(tool2.inputSchema, { target: "openapi-3.0" })
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function modelPatch(body, baseURL) {
|
|
86
|
+
const model = body.model || "";
|
|
87
|
+
if (!model) return body;
|
|
88
|
+
const provider = getProvider(baseURL);
|
|
89
|
+
const modelName = normalizeModelName(model);
|
|
90
|
+
if (modelName.startsWith("qwen")) {
|
|
91
|
+
if (provider === "openrouter" && modelName.startsWith("qwen38-max")) {
|
|
92
|
+
debug("Patch Qwen3.8-max on OpenRouter: reasoning_effort=low, remove tool_choice");
|
|
93
|
+
body.reasoning_effort = "low";
|
|
94
|
+
delete body.tool_choice;
|
|
95
|
+
} else {
|
|
96
|
+
debug("Patch Qwen: disable thinking");
|
|
97
|
+
body.enable_thinking = false;
|
|
98
|
+
}
|
|
99
|
+
if (body.temperature === void 0 && !/max|plus/.test(modelName)) {
|
|
100
|
+
debug("Patch Qwen: raise temperature to 1.0");
|
|
101
|
+
body.temperature = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (modelName.startsWith("deepseek")) {
|
|
105
|
+
debug("Patch DeepSeek: disable thinking, remove tool_choice");
|
|
106
|
+
body.thinking = { type: "disabled" };
|
|
107
|
+
delete body.tool_choice;
|
|
108
|
+
}
|
|
109
|
+
if (modelName.startsWith("gpt")) {
|
|
110
|
+
if (modelName.startsWith("gpt-5")) {
|
|
111
|
+
body.verbosity = "low";
|
|
112
|
+
}
|
|
113
|
+
if (modelName.includes("chat-latest")) {
|
|
114
|
+
debug("Patch chat-latest: omit reasoning_effort and temperature");
|
|
115
|
+
delete body.reasoning_effort;
|
|
116
|
+
delete body.temperature;
|
|
117
|
+
} else if (/^gpt-5[12](-|$)/.test(modelName)) {
|
|
118
|
+
debug("Patch GPT-5.1/5.2: reasoning_effort=none");
|
|
119
|
+
body.reasoning_effort = "none";
|
|
120
|
+
} else if (/^gpt-5(-|$)/.test(modelName)) {
|
|
121
|
+
debug("Patch GPT-5: reasoning_effort=minimal");
|
|
122
|
+
body.reasoning_effort = "minimal";
|
|
123
|
+
} else {
|
|
124
|
+
debug("Patch GPT: omit reasoning_effort");
|
|
125
|
+
delete body.reasoning_effort;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (modelName.startsWith("claude")) {
|
|
129
|
+
if (/opus|sonnet|haiku/.test(modelName)) {
|
|
130
|
+
debug("Patch Claude: disable thinking");
|
|
131
|
+
body.thinking = { type: "disabled" };
|
|
132
|
+
if (provider !== "openrouter") {
|
|
133
|
+
if (body.tool_choice === "required") {
|
|
134
|
+
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }');
|
|
135
|
+
body.tool_choice = { type: "any" };
|
|
136
|
+
} else if (body.tool_choice?.function?.name) {
|
|
137
|
+
debug("Applying Claude patch: convert tool_choice format");
|
|
138
|
+
body.tool_choice = { type: "tool", name: body.tool_choice.function.name };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
debug("Patch Claude: reasoning_effort=low");
|
|
143
|
+
body.reasoning_effort = "low";
|
|
144
|
+
delete body.tool_choice;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (modelName.startsWith("gemini")) {
|
|
148
|
+
debug("Patch Gemini: reasoning_effort=low");
|
|
149
|
+
body.reasoning_effort = "low";
|
|
150
|
+
if (/^gemini-25(?!.*pro)/.test(modelName)) {
|
|
151
|
+
debug("Patch Gemini 2.5 non-Pro: reasoning_effort=none");
|
|
152
|
+
body.reasoning_effort = "none";
|
|
153
|
+
} else if (modelName.startsWith("gemini-35-flash") || modelName.startsWith("gemini-31-flash-lite") || modelName.startsWith("gemini-3-flash")) {
|
|
154
|
+
debug("Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal");
|
|
155
|
+
body.reasoning_effort = "minimal";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (modelName.startsWith("glm")) {
|
|
159
|
+
if (/^glm-5[3-9]/.test(modelName)) {
|
|
160
|
+
debug("Patch GLM 5.3+: reasoning_effort=low");
|
|
161
|
+
body.reasoning_effort = "low";
|
|
162
|
+
} else {
|
|
163
|
+
debug("Patch GLM: disable thinking");
|
|
164
|
+
body.thinking = { type: "disabled" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (modelName.startsWith("hy")) {
|
|
168
|
+
debug("Patch Hunyuan: disable thinking, reasoning_effort=low");
|
|
169
|
+
body.thinking = { type: "disabled" };
|
|
170
|
+
body.reasoning_effort = "low";
|
|
171
|
+
}
|
|
172
|
+
if (modelName.startsWith("grok")) {
|
|
173
|
+
if (/^grok-4-?3/.test(modelName)) {
|
|
174
|
+
debug("Patch Grok 4.3: reasoning_effort=none");
|
|
175
|
+
body.reasoning_effort = "none";
|
|
176
|
+
} else if (modelName.startsWith("grok-3-mini") || modelName.startsWith("grok-code-fast")) {
|
|
177
|
+
debug("Patch Grok mini/code: reasoning_effort=low");
|
|
178
|
+
body.reasoning_effort = "low";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (modelName.startsWith("kimi")) {
|
|
182
|
+
if (modelName.startsWith("kimi-k3")) {
|
|
183
|
+
debug("Patch Kimi K3: use required tool choice, remove parallel tool calls");
|
|
184
|
+
delete body.parallel_tool_calls;
|
|
185
|
+
if (body.tool_choice?.function?.name) body.tool_choice = "required";
|
|
186
|
+
} else if (!modelName.includes("code")) {
|
|
187
|
+
debug("Patch Kimi: disable thinking");
|
|
188
|
+
body.thinking = { type: "disabled" };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (modelName.startsWith("minimax")) {
|
|
192
|
+
debug("Patch MiniMax: remove parallel_tool_calls");
|
|
193
|
+
delete body.parallel_tool_calls;
|
|
194
|
+
if (modelName.includes("m3")) {
|
|
195
|
+
debug("Patch MiniMax: disable thinking");
|
|
196
|
+
body.thinking = { type: "disabled" };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (provider === "openrouter") {
|
|
200
|
+
const reasoningEffort = body.reasoning_effort;
|
|
201
|
+
const reasoningDisabled = body.thinking?.type === "disabled" || body.enable_thinking === false || reasoningEffort === "none";
|
|
202
|
+
if (reasoningDisabled) {
|
|
203
|
+
body.reasoning = { enabled: false };
|
|
204
|
+
} else if (reasoningEffort) {
|
|
205
|
+
body.reasoning = { enabled: true, effort: reasoningEffort };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return body;
|
|
209
|
+
}
|
|
210
|
+
function normalizeModelName(modelName) {
|
|
211
|
+
let normalizedName = modelName.toLowerCase();
|
|
212
|
+
if (normalizedName.includes("/")) {
|
|
213
|
+
normalizedName = normalizedName.split("/")[1];
|
|
214
|
+
}
|
|
215
|
+
normalizedName = normalizedName.replace(/_/g, "");
|
|
216
|
+
normalizedName = normalizedName.replace(/\./g, "");
|
|
217
|
+
return normalizedName;
|
|
218
|
+
}
|
|
219
|
+
function getProvider(baseURL) {
|
|
220
|
+
if (!baseURL) return void 0;
|
|
221
|
+
try {
|
|
222
|
+
const url = new URL(baseURL);
|
|
223
|
+
const hostname = url.hostname;
|
|
224
|
+
if (hostname === "openrouter.ai") return "openrouter";
|
|
225
|
+
return void 0;
|
|
226
|
+
} catch (e) {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
class OpenAIClient {
|
|
231
|
+
config;
|
|
232
|
+
fetch;
|
|
233
|
+
constructor(config) {
|
|
234
|
+
this.config = config;
|
|
235
|
+
this.fetch = config.customFetch;
|
|
236
|
+
}
|
|
237
|
+
async invoke(messages, tools2, abortSignal, options) {
|
|
238
|
+
abortSignal?.throwIfAborted();
|
|
239
|
+
const openaiTools = Object.entries(tools2).map(([name, t]) => zodToOpenAITool(name, t));
|
|
240
|
+
let toolChoice = "required";
|
|
241
|
+
if (options?.toolChoiceName && !this.config.disableNamedToolChoice) {
|
|
242
|
+
toolChoice = { type: "function", function: { name: options.toolChoiceName } };
|
|
243
|
+
}
|
|
244
|
+
const requestBody = {
|
|
245
|
+
model: this.config.model,
|
|
246
|
+
messages,
|
|
247
|
+
tools: openaiTools,
|
|
248
|
+
parallel_tool_calls: false,
|
|
249
|
+
tool_choice: toolChoice
|
|
250
|
+
};
|
|
251
|
+
if (this.config.temperature !== void 0) {
|
|
252
|
+
requestBody.temperature = this.config.temperature;
|
|
253
|
+
}
|
|
254
|
+
modelPatch(requestBody, this.config.baseURL);
|
|
255
|
+
let transformedBody;
|
|
256
|
+
try {
|
|
257
|
+
transformedBody = this.config.transformRequestBody(requestBody);
|
|
258
|
+
} catch (error2) {
|
|
259
|
+
throw new InvokeError(
|
|
260
|
+
InvokeErrorTypes.CONFIG_ERROR,
|
|
261
|
+
`transformRequestBody failed: ${error2.message}`,
|
|
262
|
+
error2
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const finalRequestBody = transformedBody ?? requestBody;
|
|
266
|
+
let response;
|
|
267
|
+
try {
|
|
268
|
+
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: {
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
...this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify(finalRequestBody),
|
|
275
|
+
signal: abortSignal
|
|
276
|
+
});
|
|
277
|
+
} catch (error2) {
|
|
278
|
+
if (error2?.name === "AbortError") throw error2;
|
|
279
|
+
console.error(error2);
|
|
280
|
+
throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, "Network request failed", error2);
|
|
281
|
+
}
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
let errorData;
|
|
284
|
+
try {
|
|
285
|
+
errorData = await response.json();
|
|
286
|
+
} catch (error2) {
|
|
287
|
+
if (error2?.name === "AbortError") throw error2;
|
|
288
|
+
}
|
|
289
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
290
|
+
if (response.status === 401 || response.status === 403) {
|
|
291
|
+
throw new InvokeError(
|
|
292
|
+
InvokeErrorTypes.AUTH_ERROR,
|
|
293
|
+
`Authentication failed: ${errorMessage}`,
|
|
294
|
+
errorData
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (response.status === 429) {
|
|
298
|
+
throw new InvokeError(
|
|
299
|
+
InvokeErrorTypes.RATE_LIMIT,
|
|
300
|
+
`Rate limit exceeded: ${errorMessage}`,
|
|
301
|
+
errorData
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (response.status >= 500) {
|
|
305
|
+
throw new InvokeError(
|
|
306
|
+
InvokeErrorTypes.SERVER_ERROR,
|
|
307
|
+
`Server error: ${errorMessage}`,
|
|
308
|
+
errorData
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
throw new InvokeError(
|
|
312
|
+
InvokeErrorTypes.UNKNOWN,
|
|
313
|
+
`HTTP ${response.status}: ${errorMessage}`,
|
|
314
|
+
errorData
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
let data;
|
|
318
|
+
try {
|
|
319
|
+
data = await response.json();
|
|
320
|
+
} catch (error2) {
|
|
321
|
+
if (error2?.name === "AbortError") throw error2;
|
|
322
|
+
throw new InvokeError(
|
|
323
|
+
InvokeErrorTypes.INVALID_RESPONSE,
|
|
324
|
+
"Response body is not valid JSON",
|
|
325
|
+
error2
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const choice = data.choices?.[0];
|
|
329
|
+
if (!choice) {
|
|
330
|
+
throw new InvokeError(InvokeErrorTypes.INVALID_SCHEMA, "No choices in response", data);
|
|
331
|
+
}
|
|
332
|
+
switch (choice.finish_reason) {
|
|
333
|
+
case "tool_calls":
|
|
334
|
+
case "function_call":
|
|
335
|
+
// gemini
|
|
336
|
+
case "stop":
|
|
337
|
+
break;
|
|
338
|
+
case "length":
|
|
339
|
+
throw new InvokeError(
|
|
340
|
+
InvokeErrorTypes.CONTEXT_LENGTH,
|
|
341
|
+
"Response truncated: max tokens reached",
|
|
342
|
+
void 0,
|
|
343
|
+
data
|
|
344
|
+
);
|
|
345
|
+
case "content_filter":
|
|
346
|
+
throw new InvokeError(
|
|
347
|
+
InvokeErrorTypes.CONTENT_FILTER,
|
|
348
|
+
"Content filtered by safety system",
|
|
349
|
+
void 0,
|
|
350
|
+
data
|
|
351
|
+
);
|
|
352
|
+
default:
|
|
353
|
+
throw new InvokeError(
|
|
354
|
+
InvokeErrorTypes.INVALID_SCHEMA,
|
|
355
|
+
`Unexpected finish_reason: ${choice.finish_reason}`,
|
|
356
|
+
void 0,
|
|
357
|
+
data
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data;
|
|
361
|
+
const normalizedChoice = normalizedData.choices?.[0];
|
|
362
|
+
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name;
|
|
363
|
+
if (!toolCallName) {
|
|
364
|
+
throw new InvokeError(
|
|
365
|
+
InvokeErrorTypes.NO_TOOL_CALL,
|
|
366
|
+
"No tool call found in response",
|
|
367
|
+
void 0,
|
|
368
|
+
data
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const tool2 = tools2[toolCallName];
|
|
372
|
+
if (!tool2) {
|
|
373
|
+
throw new InvokeError(
|
|
374
|
+
InvokeErrorTypes.UNKNOWN,
|
|
375
|
+
`Tool "${toolCallName}" not found in tools`,
|
|
376
|
+
void 0,
|
|
377
|
+
data
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments;
|
|
381
|
+
if (!argString) {
|
|
382
|
+
throw new InvokeError(
|
|
383
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
384
|
+
"No tool call arguments found",
|
|
385
|
+
void 0,
|
|
386
|
+
data
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
let parsedArgs;
|
|
390
|
+
try {
|
|
391
|
+
parsedArgs = JSON.parse(argString);
|
|
392
|
+
} catch (error2) {
|
|
393
|
+
throw new InvokeError(
|
|
394
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
395
|
+
"Failed to parse tool arguments as JSON",
|
|
396
|
+
error2,
|
|
397
|
+
data
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const validation = tool2.inputSchema.safeParse(parsedArgs);
|
|
401
|
+
if (!validation.success) {
|
|
402
|
+
console.error(z.prettifyError(validation.error));
|
|
403
|
+
throw new InvokeError(
|
|
404
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
405
|
+
"Tool arguments validation failed",
|
|
406
|
+
validation.error,
|
|
407
|
+
data
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const toolInput = validation.data;
|
|
411
|
+
let toolResult;
|
|
412
|
+
try {
|
|
413
|
+
toolResult = await tool2.execute(toolInput);
|
|
414
|
+
} catch (error2) {
|
|
415
|
+
if (error2?.name === "AbortError") throw error2;
|
|
416
|
+
throw new InvokeError(
|
|
417
|
+
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
|
418
|
+
`Tool execution failed: ${error2?.message}`,
|
|
419
|
+
error2,
|
|
420
|
+
data
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
toolCall: {
|
|
425
|
+
name: toolCallName,
|
|
426
|
+
args: toolInput
|
|
427
|
+
},
|
|
428
|
+
toolResult,
|
|
429
|
+
usage: {
|
|
430
|
+
promptTokens: data.usage?.prompt_tokens ?? 0,
|
|
431
|
+
completionTokens: data.usage?.completion_tokens ?? 0,
|
|
432
|
+
totalTokens: data.usage?.total_tokens ?? 0,
|
|
433
|
+
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
|
|
434
|
+
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens
|
|
435
|
+
},
|
|
436
|
+
rawResponse: data,
|
|
437
|
+
rawRequest: finalRequestBody
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
class LLM extends EventTarget {
|
|
442
|
+
config;
|
|
443
|
+
client;
|
|
444
|
+
constructor(config) {
|
|
445
|
+
super();
|
|
446
|
+
this.config = parseLLMConfig(config);
|
|
447
|
+
this.client = new OpenAIClient(this.config);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* - call llm api *once*
|
|
451
|
+
* - invoke tool call *once*
|
|
452
|
+
* - return the result of the tool
|
|
453
|
+
*/
|
|
454
|
+
async invoke(messages, tools2, abortSignal, options) {
|
|
455
|
+
return await withRetry(async () => this.client.invoke(messages, tools2, abortSignal, options), {
|
|
456
|
+
maxRetries: this.config.maxRetries,
|
|
457
|
+
onRetry: (attempt, lastError) => {
|
|
458
|
+
this.dispatchEvent(
|
|
459
|
+
new CustomEvent("retry", {
|
|
460
|
+
detail: { attempt, maxAttempts: this.config.maxRetries, lastError }
|
|
461
|
+
})
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function withRetry(fn, settings) {
|
|
468
|
+
let attempt = 0;
|
|
469
|
+
while (true) {
|
|
470
|
+
try {
|
|
471
|
+
return await fn();
|
|
472
|
+
} catch (error2) {
|
|
473
|
+
if (error2?.name === "AbortError") throw error2;
|
|
474
|
+
if (error2 instanceof InvokeError && !error2.retryable) throw error2;
|
|
475
|
+
attempt++;
|
|
476
|
+
if (attempt > settings.maxRetries) throw error2;
|
|
477
|
+
console.debug("[LLM] retryable failure, will retry:", error2);
|
|
478
|
+
settings.onRetry(attempt, error2);
|
|
479
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function parseLLMConfig(config) {
|
|
484
|
+
if (!config.baseURL || !config.model) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
"[PageAgent] LLM configuration required. Please provide: baseURL, model. See: https://alibaba.github.io/page-agent/docs/features/models"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (config.temperature !== void 0) {
|
|
490
|
+
console.warn(
|
|
491
|
+
"[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. Use transformRequestBody to set it only for models you have verified accept it."
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
baseURL: config.baseURL,
|
|
496
|
+
model: config.model,
|
|
497
|
+
apiKey: config.apiKey || "",
|
|
498
|
+
temperature: config.temperature,
|
|
499
|
+
maxRetries: config.maxRetries ?? 2,
|
|
500
|
+
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
|
501
|
+
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
|
502
|
+
customFetch: (config.customFetch ?? fetch).bind(globalThis)
|
|
503
|
+
// fetch will be illegal unless bound
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const SYSTEM_PROMPT = 'You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating complex websites and extracting precise information\n2. Automating form submissions and interactive web actions\n3. Gathering and saving information \n4. Operate effectively in an agent loop\n5. Efficiently performing diverse web tasks\n</intro>\n\n<language_settings>\n- Default working language: **English**\n- Use the language that user is using. Return in user\'s language.\n</language_settings>\n\n<input>\nAt every step, your input will consist of: \n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> and <step_info>.\n3. <browser_state>: Current URL, interactive elements indexed for actions, and visible page content.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is your ultimate objective and always remains visible.\n- This has the highest priority. Make the user happy.\n- If the user request is very specific - then carefully follow each step and don\'t skip or hallucinate steps.\n- If the task is open ended you can plan yourself how to get it done.\n</user_request>\n\n<browser_state>\n1. Browser State will be given as:\n\nCurrent URL: URL of the page you are currently viewing.\nInteractive Elements: All interactive elements will be provided in format as [index]<type>text</type> where\n- index: Numeric identifier for interaction\n- type: HTML element type (button, input, etc.)\n- text: Element description\n\nExamples:\n[33]<div>User form</div>\n\\t*[35]<button aria-label=\'Submit form\'>Submit</button>\n\nNote that:\n- Only elements with numeric indexes in [] are interactive\n- (stacked) indentation (with \\t) is important and means that the element is a (html) child of the element above (with a lower index)\n- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.\n- Pure text elements without [] are not interactive.\n</browser_state>\n\n<browser_rules>\nStrictly follow these rules while using the browser and navigating the web:\n- Only interact with elements that have a numeric [index] assigned.\n- Only use indexes that are explicitly provided.\n- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.\n- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.\n- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).\n- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.\n- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat one action for more than 3 times unless some conditions changed.\n- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.\n- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.\n- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.\n- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.\n- Don\'t login into a page if you don\'t have to. Don\'t login if you don\'t have the credentials. \n- There are 2 types of tasks always first think which type of request you are dealing with:\n1. Very specific step by step instructions:\n- Follow them as very precise and don\'t skip steps. Try to complete everything as requested.\n2. Open ended tasks. Plan yourself, be creative in achieving them.\n- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.\n</browser_rules>\n\n<capability>\n- You can only handle single page app. Do not jump out of current page.\n- Do not click on link if it will open in a new page (e.g., <a target="_blank">)\n- It is ok to fail the task.\n - User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.\n - Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It\'s encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.\n - Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.\n- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.\n</capability>\n\n<task_completion_rules>\nYou must call the `done` action in one of three cases:\n- When you have fully completed the USER REQUEST.\n- When you reach the final allowed step (`max_steps`), even if the task is incomplete.\n- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.\n- If it is ABSOLUTELY IMPOSSIBLE to continue.\n\nThe `done` action is your opportunity to terminate and share your findings with the user.\n- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.\n- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.\n- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.\n- You are ONLY ALLOWED to call `done` as a single action. Don\'t call it together with other actions.\n- If the user asks for specified format, such as "return JSON with following structure", "return a list of format...", MAKE sure to use the right format in your answer.\n- If the user asks for a structured output, your `done` action\'s schema may be modified. Take this schema into account when solving the task!\n</task_completion_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns to successfully achieve the <user_request>:\n\n- Reason about <agent_history> to track progress and context toward <user_request>.\n- Analyze the most recent "Next Goal" and "Action Result" in <agent_history> and clearly state what you previously tried to achieve.\n- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.\n- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.\n- Ask user for help if you have any difficulty. Keep user in the loop.\n- If you see information relevant to <user_request>, plan saving the information to memory.\n- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns. Use them as reference but never copy them directly.\n\n<evaluation_examples>\n"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"\n"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"\n</evaluation_examples>\n\n<memory_examples>\n"memory": "Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports."\n</memory_examples>\n\n<next_goal_examples>\n"next_goal": "Click on the \'Add to Cart\' button to proceed with the purchase flow."\n</next_goal_examples>\n</examples>\n\n<output>\n{\n "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",\n "memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",\n "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",\n "action":{\n "Action name": {// Action parameters}\n }\n}\n</output>\n';
|
|
507
|
+
const log = console.log.bind(console, chalk.yellow("[autoFixer]"));
|
|
508
|
+
function normalizeResponse(response, tools2) {
|
|
509
|
+
let resolvedArguments;
|
|
510
|
+
const choice = response.choices?.[0];
|
|
511
|
+
if (!choice) throw new Error("No choices in response");
|
|
512
|
+
const message = choice.message;
|
|
513
|
+
if (!message) throw new Error("No message in choice");
|
|
514
|
+
const toolCall = message.tool_calls?.[0];
|
|
515
|
+
if (toolCall?.function?.arguments) {
|
|
516
|
+
resolvedArguments = safeJsonParse(toolCall.function.arguments);
|
|
517
|
+
if (toolCall.function.name && toolCall.function.name !== "AgentOutput") {
|
|
518
|
+
log(`#1: fixing tool_call`);
|
|
519
|
+
resolvedArguments = { action: safeJsonParse(resolvedArguments) };
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
if (message.content) {
|
|
523
|
+
const content = message.content.trim();
|
|
524
|
+
const jsonInContent = retrieveJsonFromString(content);
|
|
525
|
+
if (jsonInContent) {
|
|
526
|
+
resolvedArguments = safeJsonParse(jsonInContent);
|
|
527
|
+
if (resolvedArguments?.name === "AgentOutput") {
|
|
528
|
+
log(`#2: fixing tool_call`);
|
|
529
|
+
resolvedArguments = safeJsonParse(resolvedArguments.arguments);
|
|
530
|
+
}
|
|
531
|
+
if (resolvedArguments?.type === "function") {
|
|
532
|
+
log(`#3: fixing tool_call`);
|
|
533
|
+
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments);
|
|
534
|
+
}
|
|
535
|
+
if (!resolvedArguments?.action && !resolvedArguments?.evaluation_previous_goal && !resolvedArguments?.memory && !resolvedArguments?.next_goal && !resolvedArguments?.thinking) {
|
|
536
|
+
log(`#4: fixing tool_call`);
|
|
537
|
+
resolvedArguments = { action: safeJsonParse(resolvedArguments) };
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
throw new Error("No tool_call and the message content does not contain valid JSON");
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
throw new Error("No tool_call nor message content is present");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
resolvedArguments = safeJsonParse(resolvedArguments);
|
|
547
|
+
if (resolvedArguments.action) {
|
|
548
|
+
resolvedArguments.action = safeJsonParse(resolvedArguments.action);
|
|
549
|
+
}
|
|
550
|
+
if (resolvedArguments.action && tools2) {
|
|
551
|
+
resolvedArguments.action = validateAction(resolvedArguments.action, tools2);
|
|
552
|
+
}
|
|
553
|
+
if (!resolvedArguments.action) {
|
|
554
|
+
log(`#5: fixing tool_call`);
|
|
555
|
+
resolvedArguments.action = { wait: { seconds: 1 } };
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
...response,
|
|
559
|
+
choices: [
|
|
560
|
+
{
|
|
561
|
+
...choice,
|
|
562
|
+
message: {
|
|
563
|
+
...message,
|
|
564
|
+
tool_calls: [
|
|
565
|
+
{
|
|
566
|
+
...toolCall || {},
|
|
567
|
+
function: {
|
|
568
|
+
...toolCall?.function || {},
|
|
569
|
+
name: "AgentOutput",
|
|
570
|
+
arguments: JSON.stringify(resolvedArguments)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function validateAction(action, tools2) {
|
|
580
|
+
if (typeof action !== "object" || action === null) return action;
|
|
581
|
+
const toolName = Object.keys(action)[0];
|
|
582
|
+
if (!toolName) return action;
|
|
583
|
+
const tool2 = tools2.get(toolName);
|
|
584
|
+
if (!tool2) {
|
|
585
|
+
const available = Array.from(tools2.keys()).join(", ");
|
|
586
|
+
throw new InvokeError(
|
|
587
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
588
|
+
`Unknown action "${toolName}". Available: ${available}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
let value = action[toolName];
|
|
592
|
+
const schema = tool2.inputSchema;
|
|
593
|
+
if (schema instanceof z.ZodObject && value !== null && typeof value !== "object") {
|
|
594
|
+
const requiredKey = Object.keys(schema.shape).find(
|
|
595
|
+
(k) => !schema.shape[k].safeParse(void 0).success
|
|
596
|
+
);
|
|
597
|
+
if (requiredKey) {
|
|
598
|
+
log(`coercing primitive action input for "${toolName}"`);
|
|
599
|
+
value = { [requiredKey]: value };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const result2 = schema.safeParse(value);
|
|
603
|
+
if (!result2.success) {
|
|
604
|
+
throw new InvokeError(
|
|
605
|
+
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
|
606
|
+
`Invalid input for action "${toolName}": ${z.prettifyError(result2.error)}`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return { [toolName]: result2.data };
|
|
610
|
+
}
|
|
611
|
+
function safeJsonParse(input2) {
|
|
612
|
+
if (typeof input2 === "string") {
|
|
613
|
+
try {
|
|
614
|
+
return JSON.parse(input2.trim());
|
|
615
|
+
} catch {
|
|
616
|
+
return input2;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return input2;
|
|
620
|
+
}
|
|
621
|
+
function retrieveJsonFromString(str) {
|
|
622
|
+
try {
|
|
623
|
+
const json = /({[\s\S]*})/.exec(str) ?? [];
|
|
624
|
+
if (json.length === 0) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return JSON.parse(json[0]);
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function waitFor$1(seconds, signal2) {
|
|
633
|
+
if (!signal2) {
|
|
634
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
signal2.throwIfAborted();
|
|
638
|
+
await new Promise((resolve, reject) => {
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
signal2.removeEventListener("abort", onAbort);
|
|
641
|
+
resolve();
|
|
642
|
+
}, seconds * 1e3);
|
|
643
|
+
const onAbort = () => {
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
reject(signal2.reason);
|
|
646
|
+
};
|
|
647
|
+
signal2.addEventListener("abort", onAbort, { once: true });
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function truncate$1(text, maxLength) {
|
|
651
|
+
if (text.length > maxLength) {
|
|
652
|
+
return text.substring(0, maxLength) + "...";
|
|
653
|
+
}
|
|
654
|
+
return text;
|
|
655
|
+
}
|
|
656
|
+
function randomID(existingIDs) {
|
|
657
|
+
let id = Math.random().toString(36).substring(2, 11);
|
|
658
|
+
if (!existingIDs) {
|
|
659
|
+
return id;
|
|
660
|
+
}
|
|
661
|
+
const MAX_TRY = 1e3;
|
|
662
|
+
let tryCount = 0;
|
|
663
|
+
while (existingIDs.includes(id)) {
|
|
664
|
+
id = Math.random().toString(36).substring(2, 11);
|
|
665
|
+
tryCount++;
|
|
666
|
+
if (tryCount > MAX_TRY) {
|
|
667
|
+
throw new Error("randomID: too many tries");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return id;
|
|
671
|
+
}
|
|
672
|
+
const _global = globalThis;
|
|
673
|
+
if (!_global.__PAGE_AGENT_IDS__) {
|
|
674
|
+
_global.__PAGE_AGENT_IDS__ = [];
|
|
675
|
+
}
|
|
676
|
+
const ids = _global.__PAGE_AGENT_IDS__;
|
|
677
|
+
function uid() {
|
|
678
|
+
const id = randomID(ids);
|
|
679
|
+
ids.push(id);
|
|
680
|
+
return id;
|
|
681
|
+
}
|
|
682
|
+
const llmsTxtCache = /* @__PURE__ */ new Map();
|
|
683
|
+
async function fetchLlmsTxt(url) {
|
|
684
|
+
let origin;
|
|
685
|
+
try {
|
|
686
|
+
origin = new URL(url).origin;
|
|
687
|
+
} catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
if (origin === "null") return null;
|
|
691
|
+
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin);
|
|
692
|
+
const endpoint = `${origin}/llms.txt`;
|
|
693
|
+
let result2 = null;
|
|
694
|
+
try {
|
|
695
|
+
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`));
|
|
696
|
+
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3e3) });
|
|
697
|
+
if (res.ok) {
|
|
698
|
+
result2 = await res.text();
|
|
699
|
+
console.log(chalk.green(`[llms.txt] Found (${result2.length} chars)`));
|
|
700
|
+
if (result2.length > 1e3) {
|
|
701
|
+
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`));
|
|
702
|
+
result2 = truncate$1(result2, 1e3);
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`));
|
|
706
|
+
}
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e);
|
|
709
|
+
}
|
|
710
|
+
llmsTxtCache.set(origin, result2);
|
|
711
|
+
return result2;
|
|
712
|
+
}
|
|
713
|
+
function assert(condition, message, silent) {
|
|
714
|
+
if (!condition) {
|
|
715
|
+
const errorMessage = message ?? "Assertion failed";
|
|
716
|
+
console.error(chalk.red(`❌ assert: ${errorMessage}`));
|
|
717
|
+
throw new Error(errorMessage);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function suppress(fn) {
|
|
721
|
+
try {
|
|
722
|
+
return await fn();
|
|
723
|
+
} catch (error2) {
|
|
724
|
+
console.error(error2);
|
|
725
|
+
return void 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function tool(options) {
|
|
729
|
+
return options;
|
|
730
|
+
}
|
|
731
|
+
const tools = /* @__PURE__ */ new Map();
|
|
732
|
+
tools.set(
|
|
733
|
+
"done",
|
|
734
|
+
tool({
|
|
735
|
+
description: "Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.",
|
|
736
|
+
inputSchema: z.object({
|
|
737
|
+
text: z.string(),
|
|
738
|
+
success: z.boolean().default(true)
|
|
739
|
+
}),
|
|
740
|
+
execute: async function(input2) {
|
|
741
|
+
return Promise.resolve("Task completed");
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
tools.set(
|
|
746
|
+
"wait",
|
|
747
|
+
tool({
|
|
748
|
+
description: "Wait for x seconds. Can be used to wait until the page or data is fully loaded.",
|
|
749
|
+
inputSchema: z.object({
|
|
750
|
+
seconds: z.number().min(1).max(10).default(1)
|
|
751
|
+
}),
|
|
752
|
+
execute: async function(input2, { signal: signal2 }) {
|
|
753
|
+
const lastTimeUpdate = await this.pageController.getLastUpdateTime();
|
|
754
|
+
const secondsSinceLastUpdate = (Date.now() - lastTimeUpdate) / 1e3;
|
|
755
|
+
const actualWaitTime = Math.max(0, input2.seconds - secondsSinceLastUpdate);
|
|
756
|
+
console.log(`actualWaitTime: ${actualWaitTime} seconds`);
|
|
757
|
+
await waitFor$1(actualWaitTime, signal2);
|
|
758
|
+
const waitedSeconds = (secondsSinceLastUpdate + actualWaitTime).toFixed(2);
|
|
759
|
+
return `✅ Waited for ${waitedSeconds} seconds.`;
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
);
|
|
763
|
+
tools.set(
|
|
764
|
+
"ask_user",
|
|
765
|
+
tool({
|
|
766
|
+
description: "Ask the user a question and wait for their answer. Use this if you need more information or clarification.",
|
|
767
|
+
inputSchema: z.object({
|
|
768
|
+
question: z.string()
|
|
769
|
+
}),
|
|
770
|
+
execute: async function(input2, { signal: signal2 }) {
|
|
771
|
+
if (!this.onAskUser) {
|
|
772
|
+
throw new Error("ask_user tool requires onAskUser callback to be set");
|
|
773
|
+
}
|
|
774
|
+
const answer = await this.onAskUser(input2.question, { signal: signal2 });
|
|
775
|
+
return `User answered: ${answer}`;
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
tools.set(
|
|
780
|
+
"click_element_by_index",
|
|
781
|
+
tool({
|
|
782
|
+
description: "Click element by index",
|
|
783
|
+
inputSchema: z.object({
|
|
784
|
+
index: z.int().min(0)
|
|
785
|
+
}),
|
|
786
|
+
execute: async function(input2) {
|
|
787
|
+
const result2 = await this.pageController.clickElement(input2.index);
|
|
788
|
+
return result2.message;
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
);
|
|
792
|
+
tools.set(
|
|
793
|
+
"input_text",
|
|
794
|
+
tool({
|
|
795
|
+
description: "Click and type text into an interactive input element",
|
|
796
|
+
inputSchema: z.object({
|
|
797
|
+
index: z.int().min(0),
|
|
798
|
+
text: z.string()
|
|
799
|
+
}),
|
|
800
|
+
execute: async function(input2) {
|
|
801
|
+
const result2 = await this.pageController.inputText(input2.index, input2.text);
|
|
802
|
+
return result2.message;
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
);
|
|
806
|
+
tools.set(
|
|
807
|
+
"select_dropdown_option",
|
|
808
|
+
tool({
|
|
809
|
+
description: "Select dropdown option for interactive element index by the text of the option you want to select",
|
|
810
|
+
inputSchema: z.object({
|
|
811
|
+
index: z.int().min(0),
|
|
812
|
+
text: z.string()
|
|
813
|
+
}),
|
|
814
|
+
execute: async function(input2) {
|
|
815
|
+
const result2 = await this.pageController.selectOption(input2.index, input2.text);
|
|
816
|
+
return result2.message;
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
tools.set(
|
|
821
|
+
"scroll",
|
|
822
|
+
tool({
|
|
823
|
+
description: "Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
|
|
824
|
+
inputSchema: z.object({
|
|
825
|
+
down: z.boolean().default(true),
|
|
826
|
+
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
|
827
|
+
pixels: z.number().int().min(0).optional(),
|
|
828
|
+
index: z.number().int().min(0).optional()
|
|
829
|
+
}),
|
|
830
|
+
execute: async function(input2) {
|
|
831
|
+
const result2 = await this.pageController.scroll({
|
|
832
|
+
...input2,
|
|
833
|
+
numPages: input2.num_pages
|
|
834
|
+
});
|
|
835
|
+
return result2.message;
|
|
836
|
+
}
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
tools.set(
|
|
840
|
+
"scroll_horizontally",
|
|
841
|
+
tool({
|
|
842
|
+
description: "Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
|
|
843
|
+
inputSchema: z.object({
|
|
844
|
+
right: z.boolean().default(true),
|
|
845
|
+
pixels: z.number().int().min(0),
|
|
846
|
+
index: z.number().int().min(0).optional()
|
|
847
|
+
}),
|
|
848
|
+
execute: async function(input2) {
|
|
849
|
+
const result2 = await this.pageController.scrollHorizontally(input2);
|
|
850
|
+
return result2.message;
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
tools.set(
|
|
855
|
+
"execute_javascript",
|
|
856
|
+
tool({
|
|
857
|
+
description: "Execute JavaScript code on the current page. Supports async/await syntax. Use with caution! An `AbortSignal` named `signal` is available in scope: long-running async code MUST honor it (e.g. `await fetch(url, { signal })`, or `signal.throwIfAborted()` in loops)",
|
|
858
|
+
inputSchema: z.object({
|
|
859
|
+
script: z.string()
|
|
860
|
+
}),
|
|
861
|
+
execute: async function(input2, { signal: signal2 }) {
|
|
862
|
+
const result2 = await this.pageController.executeJavascript(input2.script, signal2);
|
|
863
|
+
signal2.throwIfAborted();
|
|
864
|
+
return result2.message;
|
|
865
|
+
}
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
class PageAgentCore extends EventTarget {
|
|
869
|
+
id = uid();
|
|
870
|
+
config;
|
|
871
|
+
tools;
|
|
872
|
+
/** PageController for DOM operations */
|
|
873
|
+
pageController;
|
|
874
|
+
task = "";
|
|
875
|
+
taskId = "";
|
|
876
|
+
/** History events */
|
|
877
|
+
history = [];
|
|
878
|
+
/** Whether this agent has been disposed */
|
|
879
|
+
disposed = false;
|
|
880
|
+
/**
|
|
881
|
+
* Called when the agent needs to ask the user questions.
|
|
882
|
+
* If unset, the `ask_user` tool will be disabled.
|
|
883
|
+
* Implementations should reject the promise when `signal` aborts.
|
|
884
|
+
* @example onAskUser: (q) => window.prompt(q) || ''
|
|
885
|
+
*/
|
|
886
|
+
onAskUser;
|
|
887
|
+
#status = "idle";
|
|
888
|
+
#llm;
|
|
889
|
+
/**
|
|
890
|
+
* Task cancellation primitive: its signal reaches the LLM fetch, tools
|
|
891
|
+
* (via `ctx.signal`) and async callbacks. Aborted only by `stop`/`dispose`
|
|
892
|
+
* (during a task) or task setup, always WITHOUT a reason so `signal.reason`
|
|
893
|
+
* stays a standard `AbortError`.
|
|
894
|
+
*/
|
|
895
|
+
#abortController = new AbortController();
|
|
896
|
+
#observations = [];
|
|
897
|
+
/** Resolves when the current run has fully settled. Awaited by `stop()`. */
|
|
898
|
+
#running = Promise.resolve();
|
|
899
|
+
#lastResult = null;
|
|
900
|
+
/** internal states during a single task execution */
|
|
901
|
+
#states = {
|
|
902
|
+
/** Accumulated wait time in seconds */
|
|
903
|
+
totalWaitTime: 0,
|
|
904
|
+
/** For detecting navigation */
|
|
905
|
+
lastURL: "",
|
|
906
|
+
/** Browser state */
|
|
907
|
+
browserState: null
|
|
908
|
+
};
|
|
909
|
+
constructor(config) {
|
|
910
|
+
super();
|
|
911
|
+
this.config = { ...config, maxSteps: config.maxSteps ?? 40 };
|
|
912
|
+
this.#llm = new LLM(this.config);
|
|
913
|
+
this.tools = new Map(tools);
|
|
914
|
+
this.pageController = config.pageController;
|
|
915
|
+
this.#llm.addEventListener("retry", (e) => {
|
|
916
|
+
const { attempt, maxAttempts, lastError } = e.detail;
|
|
917
|
+
this.#emitActivity({ type: "retrying", attempt, maxAttempts });
|
|
918
|
+
this.history.push({
|
|
919
|
+
type: "error",
|
|
920
|
+
message: String(lastError),
|
|
921
|
+
rawResponse: lastError.rawResponse
|
|
922
|
+
});
|
|
923
|
+
this.history.push({
|
|
924
|
+
type: "retry",
|
|
925
|
+
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
|
|
926
|
+
attempt,
|
|
927
|
+
maxAttempts
|
|
928
|
+
});
|
|
929
|
+
this.#emitHistoryChange();
|
|
930
|
+
});
|
|
931
|
+
if (this.config.customTools) {
|
|
932
|
+
for (const [name, tool2] of Object.entries(this.config.customTools)) {
|
|
933
|
+
if (tool2 === null) {
|
|
934
|
+
this.tools.delete(name);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
this.tools.set(name, tool2);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (!this.config.experimentalScriptExecutionTool) {
|
|
941
|
+
this.tools.delete("execute_javascript");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/** Get current agent status */
|
|
945
|
+
get status() {
|
|
946
|
+
return this.#status;
|
|
947
|
+
}
|
|
948
|
+
/** Result of the most recent run, or `null` before the first run completes. */
|
|
949
|
+
get lastResult() {
|
|
950
|
+
return this.#lastResult;
|
|
951
|
+
}
|
|
952
|
+
/** Emit statuschange event */
|
|
953
|
+
#emitStatusChange() {
|
|
954
|
+
this.dispatchEvent(new Event("statuschange"));
|
|
955
|
+
}
|
|
956
|
+
/** Emit historychange event */
|
|
957
|
+
#emitHistoryChange(pushHistoricalEvent) {
|
|
958
|
+
if (pushHistoricalEvent) this.history.push(pushHistoricalEvent);
|
|
959
|
+
this.dispatchEvent(new Event("historychange"));
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Emit activity event - for transient UI feedback
|
|
963
|
+
* @param activity - Current agent activity
|
|
964
|
+
*/
|
|
965
|
+
#emitActivity(activity) {
|
|
966
|
+
this.dispatchEvent(new CustomEvent("activity", { detail: activity }));
|
|
967
|
+
}
|
|
968
|
+
/** Update status and emit event */
|
|
969
|
+
#setStatus(status) {
|
|
970
|
+
if (this.#status !== status) {
|
|
971
|
+
this.#status = status;
|
|
972
|
+
this.#emitStatusChange();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Push an observation message to the history event stream.
|
|
977
|
+
* This will be visible in <agent_history> and remain persistent in memory across steps.
|
|
978
|
+
* @experimental @internal
|
|
979
|
+
* @note history change will be emitted before next step starts
|
|
980
|
+
*/
|
|
981
|
+
pushObservation(content) {
|
|
982
|
+
this.#observations.push(content);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Stop the current task and wait until the run has fully settled (including lifecycle hooks).
|
|
986
|
+
* @note never await .stop() in a lifecycle hook.
|
|
987
|
+
*/
|
|
988
|
+
async stop() {
|
|
989
|
+
if (this.#status !== "running") return;
|
|
990
|
+
this.#abortController.abort();
|
|
991
|
+
await this.#running;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* external errors (pre-checks/config/hooks) will threw;
|
|
995
|
+
* agent errors will be caught and added to history, and return a failed result
|
|
996
|
+
*/
|
|
997
|
+
async execute(task) {
|
|
998
|
+
if (this.disposed) throw new Error("PageAgent has been disposed. Create a new instance.");
|
|
999
|
+
if (this.#status === "running") throw new Error("A task is already running.");
|
|
1000
|
+
if (!task) throw new Error("Task is required");
|
|
1001
|
+
this.task = task;
|
|
1002
|
+
this.taskId = uid();
|
|
1003
|
+
this.history = [];
|
|
1004
|
+
this.#observations = [];
|
|
1005
|
+
this.#states = { totalWaitTime: 0, lastURL: "", browserState: null };
|
|
1006
|
+
this.#abortController = new AbortController();
|
|
1007
|
+
const signal2 = this.#abortController.signal;
|
|
1008
|
+
let resolveRunning;
|
|
1009
|
+
this.#running = new Promise((r) => resolveRunning = r);
|
|
1010
|
+
this.#setStatus("running");
|
|
1011
|
+
this.#emitHistoryChange();
|
|
1012
|
+
if (!this.onAskUser) this.tools.delete("ask_user");
|
|
1013
|
+
const onBeforeStep = this.config.onBeforeStep;
|
|
1014
|
+
const onAfterStep = this.config.onAfterStep;
|
|
1015
|
+
const onBeforeTask = this.config.onBeforeTask;
|
|
1016
|
+
const onAfterTask = this.config.onAfterTask;
|
|
1017
|
+
const stepDelay = this.config.stepDelay ?? 0.4;
|
|
1018
|
+
const maxSteps = this.config.maxSteps;
|
|
1019
|
+
let step = 0;
|
|
1020
|
+
let taskResult;
|
|
1021
|
+
let finalStatus = "error";
|
|
1022
|
+
await suppress(() => this.pageController.showMask());
|
|
1023
|
+
try {
|
|
1024
|
+
await onBeforeTask?.(this);
|
|
1025
|
+
while (true) {
|
|
1026
|
+
await onBeforeStep?.(this, step);
|
|
1027
|
+
try {
|
|
1028
|
+
console.group(`step: ${step}`);
|
|
1029
|
+
if (step > 0) await waitFor$1(stepDelay, signal2);
|
|
1030
|
+
signal2.throwIfAborted();
|
|
1031
|
+
console.log(chalk.blue.bold("👀 Observing..."));
|
|
1032
|
+
this.#states.browserState = await this.pageController.getBrowserState();
|
|
1033
|
+
await this.#handleObservations(step);
|
|
1034
|
+
const messages = [
|
|
1035
|
+
{ role: "system", content: this.#getSystemPrompt() },
|
|
1036
|
+
{ role: "user", content: await this.#assembleUserPrompt() }
|
|
1037
|
+
];
|
|
1038
|
+
const macroTool = { AgentOutput: this.#packMacroTool() };
|
|
1039
|
+
console.log(chalk.blue.bold("🧠 Thinking..."));
|
|
1040
|
+
this.#emitActivity({ type: "thinking" });
|
|
1041
|
+
const result2 = await this.#llm.invoke(messages, macroTool, signal2, {
|
|
1042
|
+
toolChoiceName: "AgentOutput",
|
|
1043
|
+
normalizeResponse: (res) => normalizeResponse(res, this.tools)
|
|
1044
|
+
});
|
|
1045
|
+
const macroResult = result2.toolResult;
|
|
1046
|
+
const input2 = macroResult.input;
|
|
1047
|
+
const output2 = macroResult.output;
|
|
1048
|
+
const reflection = {
|
|
1049
|
+
evaluation_previous_goal: input2.evaluation_previous_goal,
|
|
1050
|
+
memory: input2.memory,
|
|
1051
|
+
next_goal: input2.next_goal
|
|
1052
|
+
};
|
|
1053
|
+
const actionName = Object.keys(input2.action)[0];
|
|
1054
|
+
const action = {
|
|
1055
|
+
name: actionName,
|
|
1056
|
+
input: input2.action[actionName],
|
|
1057
|
+
output: output2
|
|
1058
|
+
};
|
|
1059
|
+
this.#emitHistoryChange({
|
|
1060
|
+
type: "step",
|
|
1061
|
+
stepIndex: step,
|
|
1062
|
+
reflection,
|
|
1063
|
+
action,
|
|
1064
|
+
usage: result2.usage,
|
|
1065
|
+
rawResponse: result2.rawResponse,
|
|
1066
|
+
rawRequest: result2.rawRequest
|
|
1067
|
+
});
|
|
1068
|
+
if (actionName === "done") {
|
|
1069
|
+
const success = action.input?.success ?? false;
|
|
1070
|
+
const data = action.input?.text || "no text provided";
|
|
1071
|
+
console.log(chalk.green.bold("Task completed"), success, data);
|
|
1072
|
+
taskResult = { success, data, history: this.history };
|
|
1073
|
+
this.#lastResult = taskResult;
|
|
1074
|
+
finalStatus = "completed";
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
} catch (error2) {
|
|
1078
|
+
const isAbortError = error2?.name === "AbortError";
|
|
1079
|
+
if (!isAbortError) console.error("Task failed", error2);
|
|
1080
|
+
const message = isAbortError ? "Tarefa interrompida" : String(error2);
|
|
1081
|
+
this.#emitActivity({ type: "error", message });
|
|
1082
|
+
this.#emitHistoryChange({ type: "error", message, rawResponse: error2 });
|
|
1083
|
+
taskResult = { success: false, data: message, history: this.history };
|
|
1084
|
+
this.#lastResult = taskResult;
|
|
1085
|
+
finalStatus = isAbortError ? "stopped" : "error";
|
|
1086
|
+
break;
|
|
1087
|
+
} finally {
|
|
1088
|
+
console.groupEnd();
|
|
1089
|
+
await onAfterStep?.(this, this.history);
|
|
1090
|
+
}
|
|
1091
|
+
step++;
|
|
1092
|
+
if (step > maxSteps) {
|
|
1093
|
+
const message = "Step count exceeded maximum limit";
|
|
1094
|
+
console.error(message);
|
|
1095
|
+
this.#emitActivity({ type: "error", message });
|
|
1096
|
+
this.#emitHistoryChange({ type: "error", message });
|
|
1097
|
+
taskResult = { success: false, data: message, history: this.history };
|
|
1098
|
+
this.#lastResult = taskResult;
|
|
1099
|
+
finalStatus = "error";
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
await onAfterTask?.(this, taskResult);
|
|
1104
|
+
return taskResult;
|
|
1105
|
+
} catch (error2) {
|
|
1106
|
+
this.#emitActivity({ type: "error", message: String(error2) });
|
|
1107
|
+
finalStatus = "error";
|
|
1108
|
+
throw error2;
|
|
1109
|
+
} finally {
|
|
1110
|
+
await suppress(() => this.pageController.cleanUpHighlights());
|
|
1111
|
+
await suppress(() => this.pageController.hideMask());
|
|
1112
|
+
this.#abortController.abort();
|
|
1113
|
+
resolveRunning();
|
|
1114
|
+
this.#setStatus(finalStatus);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Merge all tools into a single MacroTool with the following input:
|
|
1119
|
+
* - thinking: string
|
|
1120
|
+
* - evaluation_previous_goal: string
|
|
1121
|
+
* - memory: string
|
|
1122
|
+
* - next_goal: string
|
|
1123
|
+
* - action: { toolName: toolInput }
|
|
1124
|
+
* where action must be selected from tools defined in this.tools
|
|
1125
|
+
*/
|
|
1126
|
+
#packMacroTool() {
|
|
1127
|
+
const tools2 = this.tools;
|
|
1128
|
+
const actionSchemas = Array.from(tools2.entries()).map(([toolName, tool2]) => {
|
|
1129
|
+
return z.object({ [toolName]: tool2.inputSchema }).describe(tool2.description);
|
|
1130
|
+
});
|
|
1131
|
+
const actionSchema = z.union(actionSchemas);
|
|
1132
|
+
const macroToolSchema = z.object({
|
|
1133
|
+
// thinking: z.string().optional(),
|
|
1134
|
+
evaluation_previous_goal: z.string().optional(),
|
|
1135
|
+
memory: z.string().optional(),
|
|
1136
|
+
next_goal: z.string().optional(),
|
|
1137
|
+
action: actionSchema
|
|
1138
|
+
});
|
|
1139
|
+
return {
|
|
1140
|
+
description: "You MUST call this tool every step!",
|
|
1141
|
+
inputSchema: macroToolSchema,
|
|
1142
|
+
execute: async (input2) => {
|
|
1143
|
+
const signal2 = this.#abortController.signal;
|
|
1144
|
+
signal2.throwIfAborted();
|
|
1145
|
+
console.log(chalk.blue.bold("MacroTool input"), input2);
|
|
1146
|
+
const action = input2.action;
|
|
1147
|
+
const toolName = Object.keys(action)[0];
|
|
1148
|
+
const toolInput = action[toolName];
|
|
1149
|
+
const reflectionLines2 = [];
|
|
1150
|
+
if (input2.evaluation_previous_goal)
|
|
1151
|
+
reflectionLines2.push(`✅: ${input2.evaluation_previous_goal}`);
|
|
1152
|
+
if (input2.memory) reflectionLines2.push(`💾: ${input2.memory}`);
|
|
1153
|
+
if (input2.next_goal) reflectionLines2.push(`🎯: ${input2.next_goal}`);
|
|
1154
|
+
const reflectionText = reflectionLines2.length > 0 ? reflectionLines2.join("\n") : "";
|
|
1155
|
+
if (reflectionText) {
|
|
1156
|
+
console.log(reflectionText);
|
|
1157
|
+
}
|
|
1158
|
+
const tool2 = tools2.get(toolName);
|
|
1159
|
+
assert(tool2, `Tool ${toolName} not found`);
|
|
1160
|
+
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput);
|
|
1161
|
+
this.#emitActivity({ type: "executing", tool: toolName, input: toolInput });
|
|
1162
|
+
const startTime = Date.now();
|
|
1163
|
+
const result2 = await tool2.execute.bind(this)(toolInput, { signal: signal2 });
|
|
1164
|
+
signal2.throwIfAborted();
|
|
1165
|
+
const duration = Date.now() - startTime;
|
|
1166
|
+
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result2);
|
|
1167
|
+
this.#emitActivity({
|
|
1168
|
+
type: "executed",
|
|
1169
|
+
tool: toolName,
|
|
1170
|
+
input: toolInput,
|
|
1171
|
+
output: result2,
|
|
1172
|
+
duration
|
|
1173
|
+
});
|
|
1174
|
+
if (toolName === "wait") {
|
|
1175
|
+
this.#states.totalWaitTime += toolInput?.seconds || 0;
|
|
1176
|
+
} else {
|
|
1177
|
+
this.#states.totalWaitTime = 0;
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
input: input2,
|
|
1181
|
+
output: result2
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Get system prompt, dynamically replace language settings based on configured language
|
|
1188
|
+
*/
|
|
1189
|
+
#getSystemPrompt() {
|
|
1190
|
+
if (this.config.customSystemPrompt) {
|
|
1191
|
+
return this.config.customSystemPrompt;
|
|
1192
|
+
}
|
|
1193
|
+
const targetLanguage = this.config.language === "pt-BR" ? "Portuguese (Brazil)" : this.config.language === "zh-CN" ? "Chinese" : "English";
|
|
1194
|
+
const systemPrompt = SYSTEM_PROMPT.replace(
|
|
1195
|
+
/Default working language: \*\*.*?\*\*/,
|
|
1196
|
+
`Default working language: **${targetLanguage}**`
|
|
1197
|
+
);
|
|
1198
|
+
return systemPrompt;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Get instructions from config
|
|
1202
|
+
*/
|
|
1203
|
+
async #getInstructions() {
|
|
1204
|
+
const { instructions, experimentalLlmsTxt } = this.config;
|
|
1205
|
+
const systemInstructions = instructions?.system?.trim();
|
|
1206
|
+
let pageInstructions;
|
|
1207
|
+
const url = this.#states.browserState?.url || "";
|
|
1208
|
+
if (instructions?.getPageInstructions && url) {
|
|
1209
|
+
try {
|
|
1210
|
+
pageInstructions = instructions.getPageInstructions(url)?.trim();
|
|
1211
|
+
} catch (error2) {
|
|
1212
|
+
console.error(
|
|
1213
|
+
chalk.red("[PageAgent] Failed to execute getPageInstructions callback:"),
|
|
1214
|
+
error2
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : void 0;
|
|
1219
|
+
if (!systemInstructions && !pageInstructions && !llmsTxt) return "";
|
|
1220
|
+
let result2 = "<instructions>\n";
|
|
1221
|
+
if (systemInstructions) {
|
|
1222
|
+
result2 += `<system_instructions>
|
|
1223
|
+
${systemInstructions}
|
|
1224
|
+
</system_instructions>
|
|
1225
|
+
`;
|
|
1226
|
+
}
|
|
1227
|
+
if (pageInstructions) {
|
|
1228
|
+
result2 += `<page_instructions>
|
|
1229
|
+
${pageInstructions}
|
|
1230
|
+
</page_instructions>
|
|
1231
|
+
`;
|
|
1232
|
+
}
|
|
1233
|
+
if (llmsTxt) {
|
|
1234
|
+
result2 += `<llms_txt>
|
|
1235
|
+
${llmsTxt}
|
|
1236
|
+
</llms_txt>
|
|
1237
|
+
`;
|
|
1238
|
+
}
|
|
1239
|
+
result2 += "</instructions>\n\n";
|
|
1240
|
+
return result2;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Generate system observations before each step
|
|
1244
|
+
* @todo loop detection
|
|
1245
|
+
* @todo console error
|
|
1246
|
+
*/
|
|
1247
|
+
async #handleObservations(step) {
|
|
1248
|
+
if (this.#states.totalWaitTime >= 3) {
|
|
1249
|
+
this.pushObservation(
|
|
1250
|
+
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const currentURL = this.#states.browserState?.url || "";
|
|
1254
|
+
if (currentURL !== this.#states.lastURL) {
|
|
1255
|
+
this.pushObservation(`A página navegou para → ${currentURL}`);
|
|
1256
|
+
this.#states.lastURL = currentURL;
|
|
1257
|
+
await waitFor$1(0.5);
|
|
1258
|
+
}
|
|
1259
|
+
const remaining = this.config.maxSteps - step;
|
|
1260
|
+
if (remaining === 5) {
|
|
1261
|
+
this.pushObservation(
|
|
1262
|
+
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
|
1263
|
+
);
|
|
1264
|
+
} else if (remaining === 2) {
|
|
1265
|
+
this.pushObservation(
|
|
1266
|
+
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (this.#observations.length > 0) {
|
|
1270
|
+
for (const content of this.#observations) {
|
|
1271
|
+
this.history.push({ type: "observation", content });
|
|
1272
|
+
console.log(chalk.cyan("Observation:"), content);
|
|
1273
|
+
}
|
|
1274
|
+
this.#observations = [];
|
|
1275
|
+
this.#emitHistoryChange();
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async #assembleUserPrompt() {
|
|
1279
|
+
const browserState = this.#states.browserState;
|
|
1280
|
+
let prompt = "";
|
|
1281
|
+
prompt += await this.#getInstructions();
|
|
1282
|
+
const stepCount = this.history.filter((e) => e.type === "step").length;
|
|
1283
|
+
prompt += "<agent_state>\n";
|
|
1284
|
+
prompt += "<user_request>\n";
|
|
1285
|
+
prompt += `${this.task}
|
|
1286
|
+
`;
|
|
1287
|
+
prompt += "</user_request>\n";
|
|
1288
|
+
prompt += "<step_info>\n";
|
|
1289
|
+
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
|
|
1290
|
+
`;
|
|
1291
|
+
prompt += `Current time: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
1292
|
+
`;
|
|
1293
|
+
prompt += "</step_info>\n";
|
|
1294
|
+
prompt += "</agent_state>\n\n";
|
|
1295
|
+
prompt += "<agent_history>\n";
|
|
1296
|
+
let stepIndex = 0;
|
|
1297
|
+
for (const event of this.history) {
|
|
1298
|
+
if (event.type === "step") {
|
|
1299
|
+
stepIndex++;
|
|
1300
|
+
prompt += `<step_${stepIndex}>
|
|
1301
|
+
`;
|
|
1302
|
+
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
|
|
1303
|
+
`;
|
|
1304
|
+
prompt += `Memory: ${event.reflection.memory}
|
|
1305
|
+
`;
|
|
1306
|
+
prompt += `Next Goal: ${event.reflection.next_goal}
|
|
1307
|
+
`;
|
|
1308
|
+
prompt += `Action Results: ${event.action.output}
|
|
1309
|
+
`;
|
|
1310
|
+
prompt += `</step_${stepIndex}>
|
|
1311
|
+
`;
|
|
1312
|
+
} else if (event.type === "observation") {
|
|
1313
|
+
prompt += `<sys>${event.content}</sys>
|
|
1314
|
+
`;
|
|
1315
|
+
} else if (event.type === "user_takeover") {
|
|
1316
|
+
prompt += `<sys>User took over control and made changes to the page</sys>
|
|
1317
|
+
`;
|
|
1318
|
+
} else if (event.type === "error") ;
|
|
1319
|
+
}
|
|
1320
|
+
prompt += "</agent_history>\n\n";
|
|
1321
|
+
let pageContent = browserState.content;
|
|
1322
|
+
if (this.config.transformPageContent) {
|
|
1323
|
+
pageContent = await this.config.transformPageContent(pageContent);
|
|
1324
|
+
}
|
|
1325
|
+
prompt += "<browser_state>\n";
|
|
1326
|
+
prompt += browserState.header + "\n";
|
|
1327
|
+
prompt += pageContent + "\n";
|
|
1328
|
+
prompt += browserState.footer + "\n\n";
|
|
1329
|
+
prompt += "</browser_state>\n\n";
|
|
1330
|
+
return prompt;
|
|
1331
|
+
}
|
|
1332
|
+
dispose() {
|
|
1333
|
+
console.log("Disposing PageAgent...");
|
|
1334
|
+
this.disposed = true;
|
|
1335
|
+
this.pageController.dispose();
|
|
1336
|
+
this.#abortController.abort();
|
|
1337
|
+
this.dispatchEvent(new Event("dispose"));
|
|
1338
|
+
this.config.onDispose?.(this);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function isHTMLElement(el) {
|
|
1342
|
+
return !!el && el.nodeType === 1;
|
|
1343
|
+
}
|
|
1344
|
+
function isInputElement(el) {
|
|
1345
|
+
return el?.nodeType === 1 && el.tagName === "INPUT";
|
|
1346
|
+
}
|
|
1347
|
+
function isTextAreaElement(el) {
|
|
1348
|
+
return el?.nodeType === 1 && el.tagName === "TEXTAREA";
|
|
1349
|
+
}
|
|
1350
|
+
function isSelectElement(el) {
|
|
1351
|
+
return el?.nodeType === 1 && el.tagName === "SELECT";
|
|
1352
|
+
}
|
|
1353
|
+
function isAnchorElement(el) {
|
|
1354
|
+
return el?.nodeType === 1 && el.tagName === "A";
|
|
1355
|
+
}
|
|
1356
|
+
function getIframeOffset(element) {
|
|
1357
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
1358
|
+
if (!frame) return { x: 0, y: 0 };
|
|
1359
|
+
const rect = frame.getBoundingClientRect();
|
|
1360
|
+
return { x: rect.left, y: rect.top };
|
|
1361
|
+
}
|
|
1362
|
+
function getNativeValueSetter(element) {
|
|
1363
|
+
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "value").set;
|
|
1364
|
+
}
|
|
1365
|
+
async function waitFor(seconds) {
|
|
1366
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
1367
|
+
}
|
|
1368
|
+
async function movePointerToElement(element, x, y) {
|
|
1369
|
+
const offset = getIframeOffset(element);
|
|
1370
|
+
window.dispatchEvent(
|
|
1371
|
+
new CustomEvent("PageAgent::MovePointerTo", {
|
|
1372
|
+
detail: { x: x + offset.x, y: y + offset.y }
|
|
1373
|
+
})
|
|
1374
|
+
);
|
|
1375
|
+
await waitFor(0.3);
|
|
1376
|
+
}
|
|
1377
|
+
async function clickPointer() {
|
|
1378
|
+
window.dispatchEvent(new CustomEvent("PageAgent::ClickPointer"));
|
|
1379
|
+
}
|
|
1380
|
+
async function enablePassThrough() {
|
|
1381
|
+
window.dispatchEvent(new CustomEvent("PageAgent::EnablePassThrough"));
|
|
1382
|
+
}
|
|
1383
|
+
async function disablePassThrough() {
|
|
1384
|
+
window.dispatchEvent(new CustomEvent("PageAgent::DisablePassThrough"));
|
|
1385
|
+
}
|
|
1386
|
+
function getElementByIndex(selectorMap, index) {
|
|
1387
|
+
const interactiveNode = selectorMap.get(index);
|
|
1388
|
+
if (!interactiveNode) {
|
|
1389
|
+
throw new Error(`No interactive element found at index ${index}`);
|
|
1390
|
+
}
|
|
1391
|
+
const element = interactiveNode.ref;
|
|
1392
|
+
if (!element) {
|
|
1393
|
+
throw new Error(`Element at index ${index} does not have a reference`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!isHTMLElement(element)) {
|
|
1396
|
+
throw new Error(`Element at index ${index} is not an HTMLElement`);
|
|
1397
|
+
}
|
|
1398
|
+
return element;
|
|
1399
|
+
}
|
|
1400
|
+
let lastClickedElement = null;
|
|
1401
|
+
function blurLastClickedElement() {
|
|
1402
|
+
if (lastClickedElement) {
|
|
1403
|
+
lastClickedElement.dispatchEvent(new PointerEvent("pointerout", { bubbles: true }));
|
|
1404
|
+
lastClickedElement.dispatchEvent(new PointerEvent("pointerleave", { bubbles: false }));
|
|
1405
|
+
lastClickedElement.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
|
|
1406
|
+
lastClickedElement.dispatchEvent(new MouseEvent("mouseleave", { bubbles: false }));
|
|
1407
|
+
lastClickedElement.blur();
|
|
1408
|
+
lastClickedElement = null;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async function clickElement(element) {
|
|
1412
|
+
blurLastClickedElement();
|
|
1413
|
+
lastClickedElement = element;
|
|
1414
|
+
await scrollIntoViewIfNeeded(element);
|
|
1415
|
+
const frame = element.ownerDocument.defaultView?.frameElement;
|
|
1416
|
+
if (frame) await scrollIntoViewIfNeeded(frame);
|
|
1417
|
+
const rect = element.getBoundingClientRect();
|
|
1418
|
+
const x = rect.left + rect.width / 2;
|
|
1419
|
+
const y = rect.top + rect.height / 2;
|
|
1420
|
+
await movePointerToElement(element, x, y);
|
|
1421
|
+
await clickPointer();
|
|
1422
|
+
await waitFor(0.1);
|
|
1423
|
+
const doc = element.ownerDocument;
|
|
1424
|
+
await enablePassThrough();
|
|
1425
|
+
const hitTarget = doc.elementFromPoint(x, y);
|
|
1426
|
+
await disablePassThrough();
|
|
1427
|
+
const target = hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element;
|
|
1428
|
+
const pointerOpts = {
|
|
1429
|
+
bubbles: true,
|
|
1430
|
+
cancelable: true,
|
|
1431
|
+
clientX: x,
|
|
1432
|
+
clientY: y,
|
|
1433
|
+
pointerType: "mouse"
|
|
1434
|
+
};
|
|
1435
|
+
const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 };
|
|
1436
|
+
target.dispatchEvent(new PointerEvent("pointerover", pointerOpts));
|
|
1437
|
+
target.dispatchEvent(new PointerEvent("pointerenter", { ...pointerOpts, bubbles: false }));
|
|
1438
|
+
target.dispatchEvent(new MouseEvent("mouseover", mouseOpts));
|
|
1439
|
+
target.dispatchEvent(new MouseEvent("mouseenter", { ...mouseOpts, bubbles: false }));
|
|
1440
|
+
target.dispatchEvent(new PointerEvent("pointerdown", pointerOpts));
|
|
1441
|
+
target.dispatchEvent(new MouseEvent("mousedown", mouseOpts));
|
|
1442
|
+
element.focus({ preventScroll: true });
|
|
1443
|
+
target.dispatchEvent(new PointerEvent("pointerup", pointerOpts));
|
|
1444
|
+
target.dispatchEvent(new MouseEvent("mouseup", mouseOpts));
|
|
1445
|
+
target.click();
|
|
1446
|
+
await waitFor(0.2);
|
|
1447
|
+
}
|
|
1448
|
+
async function inputTextElement(element, text) {
|
|
1449
|
+
const isContentEditable = element.isContentEditable;
|
|
1450
|
+
if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {
|
|
1451
|
+
throw new Error("Element is not an input, textarea, or contenteditable");
|
|
1452
|
+
}
|
|
1453
|
+
await clickElement(element);
|
|
1454
|
+
if (isContentEditable) {
|
|
1455
|
+
if (element.dispatchEvent(
|
|
1456
|
+
new InputEvent("beforeinput", {
|
|
1457
|
+
bubbles: true,
|
|
1458
|
+
cancelable: true,
|
|
1459
|
+
inputType: "deleteContent"
|
|
1460
|
+
})
|
|
1461
|
+
)) {
|
|
1462
|
+
element.innerText = "";
|
|
1463
|
+
element.dispatchEvent(
|
|
1464
|
+
new InputEvent("input", {
|
|
1465
|
+
bubbles: true,
|
|
1466
|
+
inputType: "deleteContent"
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
if (element.dispatchEvent(
|
|
1471
|
+
new InputEvent("beforeinput", {
|
|
1472
|
+
bubbles: true,
|
|
1473
|
+
cancelable: true,
|
|
1474
|
+
inputType: "insertText",
|
|
1475
|
+
data: text
|
|
1476
|
+
})
|
|
1477
|
+
)) {
|
|
1478
|
+
element.innerText = text;
|
|
1479
|
+
element.dispatchEvent(
|
|
1480
|
+
new InputEvent("input", {
|
|
1481
|
+
bubbles: true,
|
|
1482
|
+
inputType: "insertText",
|
|
1483
|
+
data: text
|
|
1484
|
+
})
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
const planASucceeded = element.innerText.trim() === text.trim();
|
|
1488
|
+
if (!planASucceeded) {
|
|
1489
|
+
element.focus();
|
|
1490
|
+
const doc = element.ownerDocument;
|
|
1491
|
+
const selection = (doc.defaultView || window).getSelection();
|
|
1492
|
+
const range = doc.createRange();
|
|
1493
|
+
range.selectNodeContents(element);
|
|
1494
|
+
selection?.removeAllRanges();
|
|
1495
|
+
selection?.addRange(range);
|
|
1496
|
+
doc.execCommand("delete", false);
|
|
1497
|
+
doc.execCommand("insertText", false, text);
|
|
1498
|
+
}
|
|
1499
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1500
|
+
element.blur();
|
|
1501
|
+
} else {
|
|
1502
|
+
getNativeValueSetter(element).call(element, text);
|
|
1503
|
+
}
|
|
1504
|
+
if (!isContentEditable) {
|
|
1505
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1506
|
+
}
|
|
1507
|
+
await waitFor(0.1);
|
|
1508
|
+
}
|
|
1509
|
+
async function selectOptionElement(selectElement, optionText) {
|
|
1510
|
+
if (!isSelectElement(selectElement)) {
|
|
1511
|
+
throw new Error("Element is not a select element");
|
|
1512
|
+
}
|
|
1513
|
+
const options = Array.from(selectElement.options);
|
|
1514
|
+
const option = options.find((opt) => opt.textContent?.trim() === optionText.trim());
|
|
1515
|
+
if (!option) {
|
|
1516
|
+
throw new Error(`Option with text "${optionText}" not found in select element`);
|
|
1517
|
+
}
|
|
1518
|
+
selectElement.value = option.value;
|
|
1519
|
+
selectElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1520
|
+
await waitFor(0.1);
|
|
1521
|
+
}
|
|
1522
|
+
async function scrollIntoViewIfNeeded(element) {
|
|
1523
|
+
const el = element;
|
|
1524
|
+
if (typeof el.scrollIntoViewIfNeeded === "function") {
|
|
1525
|
+
el.scrollIntoViewIfNeeded();
|
|
1526
|
+
} else {
|
|
1527
|
+
element.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async function scrollVertically(scroll_amount, element) {
|
|
1531
|
+
if (element) {
|
|
1532
|
+
const targetElement = element;
|
|
1533
|
+
let currentElement = targetElement;
|
|
1534
|
+
let scrollSuccess = false;
|
|
1535
|
+
let scrolledElement = null;
|
|
1536
|
+
let scrollDelta = 0;
|
|
1537
|
+
let attempts = 0;
|
|
1538
|
+
const dy2 = scroll_amount;
|
|
1539
|
+
while (currentElement && attempts < 10) {
|
|
1540
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
1541
|
+
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
|
|
1542
|
+
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight;
|
|
1543
|
+
if (hasScrollableY && canScrollVertically) {
|
|
1544
|
+
const beforeScroll = currentElement.scrollTop;
|
|
1545
|
+
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight;
|
|
1546
|
+
let scrollAmount = dy2 / 3;
|
|
1547
|
+
if (scrollAmount > 0) {
|
|
1548
|
+
scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
1549
|
+
} else {
|
|
1550
|
+
scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
1551
|
+
}
|
|
1552
|
+
currentElement.scrollTop = beforeScroll + scrollAmount;
|
|
1553
|
+
const afterScroll = currentElement.scrollTop;
|
|
1554
|
+
const actualScrollDelta = afterScroll - beforeScroll;
|
|
1555
|
+
if (Math.abs(actualScrollDelta) > 0.5) {
|
|
1556
|
+
scrollSuccess = true;
|
|
1557
|
+
scrolledElement = currentElement;
|
|
1558
|
+
scrollDelta = actualScrollDelta;
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (currentElement === document.body || currentElement === document.documentElement) {
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1565
|
+
currentElement = currentElement.parentElement;
|
|
1566
|
+
attempts++;
|
|
1567
|
+
}
|
|
1568
|
+
if (scrollSuccess) {
|
|
1569
|
+
return `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`;
|
|
1570
|
+
} else {
|
|
1571
|
+
return `No scrollable container found for element (${targetElement.tagName})`;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const dy = scroll_amount;
|
|
1575
|
+
const bigEnough = (el2) => el2.clientHeight >= window.innerHeight * 0.5;
|
|
1576
|
+
const canScroll = (el2) => Boolean(
|
|
1577
|
+
el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowY) && el2.scrollHeight > el2.clientHeight && bigEnough(el2)
|
|
1578
|
+
);
|
|
1579
|
+
let el = document.activeElement;
|
|
1580
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
1581
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
1582
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
1583
|
+
const scrollBefore = window.scrollY;
|
|
1584
|
+
const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
|
|
1585
|
+
window.scrollBy(0, dy);
|
|
1586
|
+
const scrollAfter = window.scrollY;
|
|
1587
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
1588
|
+
if (Math.abs(scrolled) < 1) {
|
|
1589
|
+
return dy > 0 ? `⚠️ Already at the bottom of the page, cannot scroll down further.` : `⚠️ Already at the top of the page, cannot scroll up further.`;
|
|
1590
|
+
}
|
|
1591
|
+
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
|
|
1592
|
+
const reachedTop = dy < 0 && scrollAfter <= 1;
|
|
1593
|
+
if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`;
|
|
1594
|
+
if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`;
|
|
1595
|
+
return `✅ Scrolled page by ${scrolled}px.`;
|
|
1596
|
+
} else {
|
|
1597
|
+
const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
|
|
1598
|
+
console.log(`[PageController] ${warningMsg}`);
|
|
1599
|
+
const scrollBefore = el.scrollTop;
|
|
1600
|
+
const scrollMax = el.scrollHeight - el.clientHeight;
|
|
1601
|
+
el.scrollBy({ top: dy, behavior: "smooth" });
|
|
1602
|
+
await waitFor(0.1);
|
|
1603
|
+
const scrollAfter = el.scrollTop;
|
|
1604
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
1605
|
+
if (Math.abs(scrolled) < 1) {
|
|
1606
|
+
return dy > 0 ? `⚠️ ${warningMsg} Already at the bottom of container (${el.tagName}), cannot scroll down further.` : `⚠️ ${warningMsg} Already at the top of container (${el.tagName}), cannot scroll up further.`;
|
|
1607
|
+
}
|
|
1608
|
+
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
|
|
1609
|
+
const reachedTop = dy < 0 && scrollAfter <= 1;
|
|
1610
|
+
if (reachedBottom)
|
|
1611
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the bottom.`;
|
|
1612
|
+
if (reachedTop)
|
|
1613
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the top.`;
|
|
1614
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px.`;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function scrollHorizontally(scroll_amount, element) {
|
|
1618
|
+
if (element) {
|
|
1619
|
+
const targetElement = element;
|
|
1620
|
+
let currentElement = targetElement;
|
|
1621
|
+
let scrollSuccess = false;
|
|
1622
|
+
let scrolledElement = null;
|
|
1623
|
+
let scrollDelta = 0;
|
|
1624
|
+
let attempts = 0;
|
|
1625
|
+
const dx2 = scroll_amount;
|
|
1626
|
+
while (currentElement && attempts < 10) {
|
|
1627
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
1628
|
+
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
|
|
1629
|
+
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth;
|
|
1630
|
+
if (hasScrollableX && canScrollHorizontally) {
|
|
1631
|
+
const beforeScroll = currentElement.scrollLeft;
|
|
1632
|
+
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth;
|
|
1633
|
+
let scrollAmount = dx2 / 3;
|
|
1634
|
+
if (scrollAmount > 0) {
|
|
1635
|
+
scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
|
|
1636
|
+
} else {
|
|
1637
|
+
scrollAmount = Math.max(scrollAmount, -beforeScroll);
|
|
1638
|
+
}
|
|
1639
|
+
currentElement.scrollLeft = beforeScroll + scrollAmount;
|
|
1640
|
+
const afterScroll = currentElement.scrollLeft;
|
|
1641
|
+
const actualScrollDelta = afterScroll - beforeScroll;
|
|
1642
|
+
if (Math.abs(actualScrollDelta) > 0.5) {
|
|
1643
|
+
scrollSuccess = true;
|
|
1644
|
+
scrolledElement = currentElement;
|
|
1645
|
+
scrollDelta = actualScrollDelta;
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (currentElement === document.body || currentElement === document.documentElement) {
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
currentElement = currentElement.parentElement;
|
|
1653
|
+
attempts++;
|
|
1654
|
+
}
|
|
1655
|
+
if (scrollSuccess) {
|
|
1656
|
+
return `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`;
|
|
1657
|
+
} else {
|
|
1658
|
+
return `No horizontally scrollable container found for element (${targetElement.tagName})`;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const dx = scroll_amount;
|
|
1662
|
+
const bigEnough = (el2) => el2.clientWidth >= window.innerWidth * 0.5;
|
|
1663
|
+
const canScroll = (el2) => Boolean(
|
|
1664
|
+
el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowX) && el2.scrollWidth > el2.clientWidth && bigEnough(el2)
|
|
1665
|
+
);
|
|
1666
|
+
let el = document.activeElement;
|
|
1667
|
+
while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
|
|
1668
|
+
el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
|
|
1669
|
+
if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
|
|
1670
|
+
const scrollBefore = window.scrollX;
|
|
1671
|
+
const scrollMax = document.documentElement.scrollWidth - window.innerWidth;
|
|
1672
|
+
window.scrollBy(dx, 0);
|
|
1673
|
+
const scrollAfter = window.scrollX;
|
|
1674
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
1675
|
+
if (Math.abs(scrolled) < 1) {
|
|
1676
|
+
return dx > 0 ? `⚠️ Already at the right edge of the page, cannot scroll right further.` : `⚠️ Already at the left edge of the page, cannot scroll left further.`;
|
|
1677
|
+
}
|
|
1678
|
+
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
|
|
1679
|
+
const reachedLeft = dx < 0 && scrollAfter <= 1;
|
|
1680
|
+
if (reachedRight)
|
|
1681
|
+
return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`;
|
|
1682
|
+
if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`;
|
|
1683
|
+
return `✅ Scrolled page horizontally by ${scrolled}px.`;
|
|
1684
|
+
} else {
|
|
1685
|
+
const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
|
|
1686
|
+
console.log(`[PageController] ${warningMsg}`);
|
|
1687
|
+
const scrollBefore = el.scrollLeft;
|
|
1688
|
+
const scrollMax = el.scrollWidth - el.clientWidth;
|
|
1689
|
+
el.scrollBy({ left: dx, behavior: "smooth" });
|
|
1690
|
+
await waitFor(0.1);
|
|
1691
|
+
const scrollAfter = el.scrollLeft;
|
|
1692
|
+
const scrolled = scrollAfter - scrollBefore;
|
|
1693
|
+
if (Math.abs(scrolled) < 1) {
|
|
1694
|
+
return dx > 0 ? `⚠️ ${warningMsg} Already at the right edge of container (${el.tagName}), cannot scroll right further.` : `⚠️ ${warningMsg} Already at the left edge of container (${el.tagName}), cannot scroll left further.`;
|
|
1695
|
+
}
|
|
1696
|
+
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
|
|
1697
|
+
const reachedLeft = dx < 0 && scrollAfter <= 1;
|
|
1698
|
+
if (reachedRight)
|
|
1699
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the right edge.`;
|
|
1700
|
+
if (reachedLeft)
|
|
1701
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the left edge.`;
|
|
1702
|
+
return `✅ ${warningMsg} Scrolled container (${el.tagName}) horizontally by ${scrolled}px.`;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const domTree = (args = {
|
|
1706
|
+
doHighlightElements: true,
|
|
1707
|
+
focusHighlightIndex: -1,
|
|
1708
|
+
viewportExpansion: 0,
|
|
1709
|
+
debugMode: false,
|
|
1710
|
+
/**
|
|
1711
|
+
* @edit
|
|
1712
|
+
*/
|
|
1713
|
+
/** @type {Element[]} */
|
|
1714
|
+
interactiveBlacklist: [],
|
|
1715
|
+
/** @type {Element[]} */
|
|
1716
|
+
interactiveWhitelist: [],
|
|
1717
|
+
highlightOpacity: 0.1,
|
|
1718
|
+
highlightLabelOpacity: 0.5
|
|
1719
|
+
}) => {
|
|
1720
|
+
const { interactiveBlacklist, interactiveWhitelist, highlightOpacity, highlightLabelOpacity } = args;
|
|
1721
|
+
const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
|
|
1722
|
+
let highlightIndex = 0;
|
|
1723
|
+
const extraData = /* @__PURE__ */ new WeakMap();
|
|
1724
|
+
function addExtraData(element, data) {
|
|
1725
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
1726
|
+
extraData.set(element, { ...extraData.get(element), ...data });
|
|
1727
|
+
}
|
|
1728
|
+
const DOM_CACHE = {
|
|
1729
|
+
boundingRects: /* @__PURE__ */ new WeakMap(),
|
|
1730
|
+
clientRects: /* @__PURE__ */ new WeakMap(),
|
|
1731
|
+
computedStyles: /* @__PURE__ */ new WeakMap(),
|
|
1732
|
+
clearCache: () => {
|
|
1733
|
+
DOM_CACHE.boundingRects = /* @__PURE__ */ new WeakMap();
|
|
1734
|
+
DOM_CACHE.clientRects = /* @__PURE__ */ new WeakMap();
|
|
1735
|
+
DOM_CACHE.computedStyles = /* @__PURE__ */ new WeakMap();
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
function getCachedBoundingRect(element) {
|
|
1739
|
+
if (!element) return null;
|
|
1740
|
+
if (DOM_CACHE.boundingRects.has(element)) {
|
|
1741
|
+
return DOM_CACHE.boundingRects.get(element);
|
|
1742
|
+
}
|
|
1743
|
+
const rect = element.getBoundingClientRect();
|
|
1744
|
+
if (rect) {
|
|
1745
|
+
DOM_CACHE.boundingRects.set(element, rect);
|
|
1746
|
+
}
|
|
1747
|
+
return rect;
|
|
1748
|
+
}
|
|
1749
|
+
function getCachedComputedStyle(element) {
|
|
1750
|
+
if (!element) return null;
|
|
1751
|
+
if (DOM_CACHE.computedStyles.has(element)) {
|
|
1752
|
+
return DOM_CACHE.computedStyles.get(element);
|
|
1753
|
+
}
|
|
1754
|
+
const style = window.getComputedStyle(element);
|
|
1755
|
+
if (style) {
|
|
1756
|
+
DOM_CACHE.computedStyles.set(element, style);
|
|
1757
|
+
}
|
|
1758
|
+
return style;
|
|
1759
|
+
}
|
|
1760
|
+
function getCachedClientRects(element) {
|
|
1761
|
+
if (!element) return null;
|
|
1762
|
+
if (DOM_CACHE.clientRects.has(element)) {
|
|
1763
|
+
return DOM_CACHE.clientRects.get(element);
|
|
1764
|
+
}
|
|
1765
|
+
const rects = element.getClientRects();
|
|
1766
|
+
if (rects) {
|
|
1767
|
+
DOM_CACHE.clientRects.set(element, rects);
|
|
1768
|
+
}
|
|
1769
|
+
return rects;
|
|
1770
|
+
}
|
|
1771
|
+
const DOM_HASH_MAP = {};
|
|
1772
|
+
const ID = { current: 0 };
|
|
1773
|
+
const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
|
|
1774
|
+
function highlightElement(element, index, parentIframe = null) {
|
|
1775
|
+
if (!element) return index;
|
|
1776
|
+
const overlays = [];
|
|
1777
|
+
let label = null;
|
|
1778
|
+
let labelWidth = 20;
|
|
1779
|
+
let labelHeight = 16;
|
|
1780
|
+
let cleanupFn = null;
|
|
1781
|
+
try {
|
|
1782
|
+
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
1783
|
+
if (!container) {
|
|
1784
|
+
container = document.createElement("div");
|
|
1785
|
+
container.id = HIGHLIGHT_CONTAINER_ID;
|
|
1786
|
+
container.style.position = "fixed";
|
|
1787
|
+
container.style.pointerEvents = "none";
|
|
1788
|
+
container.style.top = "0";
|
|
1789
|
+
container.style.left = "0";
|
|
1790
|
+
container.style.width = "100%";
|
|
1791
|
+
container.style.height = "100%";
|
|
1792
|
+
container.style.zIndex = "2147483640";
|
|
1793
|
+
container.style.backgroundColor = "transparent";
|
|
1794
|
+
document.body.appendChild(container);
|
|
1795
|
+
}
|
|
1796
|
+
const rects = element.getClientRects();
|
|
1797
|
+
if (!rects || rects.length === 0) return index;
|
|
1798
|
+
const colors = [
|
|
1799
|
+
"#FF0000",
|
|
1800
|
+
"#00FF00",
|
|
1801
|
+
"#0000FF",
|
|
1802
|
+
"#FFA500",
|
|
1803
|
+
"#800080",
|
|
1804
|
+
"#008080",
|
|
1805
|
+
"#FF69B4",
|
|
1806
|
+
"#4B0082",
|
|
1807
|
+
"#FF4500",
|
|
1808
|
+
"#2E8B57",
|
|
1809
|
+
"#DC143C",
|
|
1810
|
+
"#4682B4"
|
|
1811
|
+
];
|
|
1812
|
+
const colorIndex = index % colors.length;
|
|
1813
|
+
let baseColor = colors[colorIndex];
|
|
1814
|
+
const backgroundColor = baseColor + Math.floor(highlightOpacity * 255).toString(16).padStart(2, "0");
|
|
1815
|
+
baseColor = baseColor + Math.floor(highlightLabelOpacity * 255).toString(16).padStart(2, "0");
|
|
1816
|
+
let iframeOffset = { x: 0, y: 0 };
|
|
1817
|
+
if (parentIframe) {
|
|
1818
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
1819
|
+
iframeOffset.x = iframeRect.left;
|
|
1820
|
+
iframeOffset.y = iframeRect.top;
|
|
1821
|
+
}
|
|
1822
|
+
const fragment = document.createDocumentFragment();
|
|
1823
|
+
for (const rect of rects) {
|
|
1824
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1825
|
+
const overlay = document.createElement("div");
|
|
1826
|
+
overlay.style.position = "fixed";
|
|
1827
|
+
overlay.style.border = `2px solid ${baseColor}`;
|
|
1828
|
+
overlay.style.backgroundColor = backgroundColor;
|
|
1829
|
+
overlay.style.pointerEvents = "none";
|
|
1830
|
+
overlay.style.boxSizing = "border-box";
|
|
1831
|
+
const top = rect.top + iframeOffset.y;
|
|
1832
|
+
const left = rect.left + iframeOffset.x;
|
|
1833
|
+
overlay.style.top = `${top}px`;
|
|
1834
|
+
overlay.style.left = `${left}px`;
|
|
1835
|
+
overlay.style.width = `${rect.width}px`;
|
|
1836
|
+
overlay.style.height = `${rect.height}px`;
|
|
1837
|
+
fragment.appendChild(overlay);
|
|
1838
|
+
overlays.push({ element: overlay, initialRect: rect });
|
|
1839
|
+
}
|
|
1840
|
+
const firstRect = rects[0];
|
|
1841
|
+
label = document.createElement("div");
|
|
1842
|
+
label.className = "playwright-highlight-label";
|
|
1843
|
+
label.style.position = "fixed";
|
|
1844
|
+
label.style.background = baseColor;
|
|
1845
|
+
label.style.color = "white";
|
|
1846
|
+
label.style.padding = "1px 4px";
|
|
1847
|
+
label.style.borderRadius = "4px";
|
|
1848
|
+
label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
|
|
1849
|
+
label.textContent = index.toString();
|
|
1850
|
+
labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth;
|
|
1851
|
+
labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight;
|
|
1852
|
+
const firstRectTop = firstRect.top + iframeOffset.y;
|
|
1853
|
+
const firstRectLeft = firstRect.left + iframeOffset.x;
|
|
1854
|
+
let labelTop = firstRectTop + 2;
|
|
1855
|
+
let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;
|
|
1856
|
+
if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
|
|
1857
|
+
labelTop = firstRectTop - labelHeight - 2;
|
|
1858
|
+
labelLeft = firstRectLeft + firstRect.width - labelWidth;
|
|
1859
|
+
if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft;
|
|
1860
|
+
}
|
|
1861
|
+
labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
|
|
1862
|
+
labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));
|
|
1863
|
+
label.style.top = `${labelTop}px`;
|
|
1864
|
+
label.style.left = `${labelLeft}px`;
|
|
1865
|
+
fragment.appendChild(label);
|
|
1866
|
+
const updatePositions = () => {
|
|
1867
|
+
const newRects = element.getClientRects();
|
|
1868
|
+
let newIframeOffset = { x: 0, y: 0 };
|
|
1869
|
+
if (parentIframe) {
|
|
1870
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
1871
|
+
newIframeOffset.x = iframeRect.left;
|
|
1872
|
+
newIframeOffset.y = iframeRect.top;
|
|
1873
|
+
}
|
|
1874
|
+
overlays.forEach((overlayData, i) => {
|
|
1875
|
+
if (i < newRects.length) {
|
|
1876
|
+
const newRect = newRects[i];
|
|
1877
|
+
const newTop = newRect.top + newIframeOffset.y;
|
|
1878
|
+
const newLeft = newRect.left + newIframeOffset.x;
|
|
1879
|
+
overlayData.element.style.top = `${newTop}px`;
|
|
1880
|
+
overlayData.element.style.left = `${newLeft}px`;
|
|
1881
|
+
overlayData.element.style.width = `${newRect.width}px`;
|
|
1882
|
+
overlayData.element.style.height = `${newRect.height}px`;
|
|
1883
|
+
overlayData.element.style.display = newRect.width === 0 || newRect.height === 0 ? "none" : "block";
|
|
1884
|
+
} else {
|
|
1885
|
+
overlayData.element.style.display = "none";
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
if (newRects.length < overlays.length) {
|
|
1889
|
+
for (let i = newRects.length; i < overlays.length; i++) {
|
|
1890
|
+
overlays[i].element.style.display = "none";
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (label && newRects.length > 0) {
|
|
1894
|
+
const firstNewRect = newRects[0];
|
|
1895
|
+
const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
|
|
1896
|
+
const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;
|
|
1897
|
+
let newLabelTop = firstNewRectTop + 2;
|
|
1898
|
+
let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;
|
|
1899
|
+
if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
|
|
1900
|
+
newLabelTop = firstNewRectTop - labelHeight - 2;
|
|
1901
|
+
newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
|
|
1902
|
+
if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
|
|
1903
|
+
}
|
|
1904
|
+
newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
|
|
1905
|
+
newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));
|
|
1906
|
+
label.style.top = `${newLabelTop}px`;
|
|
1907
|
+
label.style.left = `${newLabelLeft}px`;
|
|
1908
|
+
label.style.display = "block";
|
|
1909
|
+
} else if (label) {
|
|
1910
|
+
label.style.display = "none";
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
const throttleFunction = (func, delay) => {
|
|
1914
|
+
let lastCall = 0;
|
|
1915
|
+
return (...args2) => {
|
|
1916
|
+
const now = performance.now();
|
|
1917
|
+
if (now - lastCall < delay) return;
|
|
1918
|
+
lastCall = now;
|
|
1919
|
+
return func(...args2);
|
|
1920
|
+
};
|
|
1921
|
+
};
|
|
1922
|
+
const throttledUpdatePositions = throttleFunction(updatePositions, 16);
|
|
1923
|
+
window.addEventListener("scroll", throttledUpdatePositions, true);
|
|
1924
|
+
window.addEventListener("resize", throttledUpdatePositions);
|
|
1925
|
+
cleanupFn = () => {
|
|
1926
|
+
window.removeEventListener("scroll", throttledUpdatePositions, true);
|
|
1927
|
+
window.removeEventListener("resize", throttledUpdatePositions);
|
|
1928
|
+
overlays.forEach((overlay) => overlay.element.remove());
|
|
1929
|
+
if (label) label.remove();
|
|
1930
|
+
};
|
|
1931
|
+
container.appendChild(fragment);
|
|
1932
|
+
return index + 1;
|
|
1933
|
+
} finally {
|
|
1934
|
+
if (cleanupFn) {
|
|
1935
|
+
(window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(
|
|
1936
|
+
cleanupFn
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function isScrollableElement(element) {
|
|
1942
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
const style = getCachedComputedStyle(element);
|
|
1946
|
+
if (!style) return null;
|
|
1947
|
+
const display = style.display;
|
|
1948
|
+
if (display === "inline" || display === "inline-block") {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
const overflowX = style.overflowX;
|
|
1952
|
+
const overflowY = style.overflowY;
|
|
1953
|
+
const hasScrollbarSignal = style.scrollbarWidth && style.scrollbarWidth !== "auto" || style.scrollbarGutter && style.scrollbarGutter !== "auto";
|
|
1954
|
+
const scrollableX = overflowX === "auto" || overflowX === "scroll";
|
|
1955
|
+
const scrollableY = overflowY === "auto" || overflowY === "scroll";
|
|
1956
|
+
if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
const scrollWidth = element.scrollWidth - element.clientWidth;
|
|
1960
|
+
const scrollHeight = element.scrollHeight - element.clientHeight;
|
|
1961
|
+
const threshold = 4;
|
|
1962
|
+
if (scrollWidth < threshold && scrollHeight < threshold) {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
const distanceToTop = element.scrollTop;
|
|
1972
|
+
const distanceToLeft = element.scrollLeft;
|
|
1973
|
+
const distanceToRight = element.scrollWidth - element.clientWidth - element.scrollLeft;
|
|
1974
|
+
const distanceToBottom = element.scrollHeight - element.clientHeight - element.scrollTop;
|
|
1975
|
+
const scrollData = {
|
|
1976
|
+
top: distanceToTop,
|
|
1977
|
+
right: distanceToRight,
|
|
1978
|
+
bottom: distanceToBottom,
|
|
1979
|
+
left: distanceToLeft
|
|
1980
|
+
};
|
|
1981
|
+
addExtraData(element, {
|
|
1982
|
+
scrollable: true,
|
|
1983
|
+
scrollData
|
|
1984
|
+
});
|
|
1985
|
+
return scrollData;
|
|
1986
|
+
}
|
|
1987
|
+
function isTextNodeVisible(textNode) {
|
|
1988
|
+
try {
|
|
1989
|
+
if (viewportExpansion === -1) {
|
|
1990
|
+
const parentElement2 = textNode.parentElement;
|
|
1991
|
+
if (!parentElement2) return false;
|
|
1992
|
+
try {
|
|
1993
|
+
return parentElement2.checkVisibility({
|
|
1994
|
+
checkOpacity: true,
|
|
1995
|
+
checkVisibilityCSS: true
|
|
1996
|
+
});
|
|
1997
|
+
} catch (e) {
|
|
1998
|
+
const style = window.getComputedStyle(parentElement2);
|
|
1999
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const range = document.createRange();
|
|
2003
|
+
range.selectNodeContents(textNode);
|
|
2004
|
+
const rects = range.getClientRects();
|
|
2005
|
+
if (!rects || rects.length === 0) {
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
let isAnyRectVisible = false;
|
|
2009
|
+
let isAnyRectInViewport = false;
|
|
2010
|
+
for (const rect of rects) {
|
|
2011
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
2012
|
+
isAnyRectVisible = true;
|
|
2013
|
+
if (!(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
2014
|
+
isAnyRectInViewport = true;
|
|
2015
|
+
break;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (!isAnyRectVisible || !isAnyRectInViewport) {
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
const parentElement = textNode.parentElement;
|
|
2023
|
+
if (!parentElement) return false;
|
|
2024
|
+
try {
|
|
2025
|
+
return parentElement.checkVisibility({
|
|
2026
|
+
checkOpacity: true,
|
|
2027
|
+
checkVisibilityCSS: true
|
|
2028
|
+
});
|
|
2029
|
+
} catch (e) {
|
|
2030
|
+
const style = window.getComputedStyle(parentElement);
|
|
2031
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
2032
|
+
}
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
console.warn("Error checking text node visibility:", e);
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
function isElementAccepted(element) {
|
|
2039
|
+
if (!element || !element.tagName) return false;
|
|
2040
|
+
const alwaysAccept = /* @__PURE__ */ new Set([
|
|
2041
|
+
"body",
|
|
2042
|
+
"div",
|
|
2043
|
+
"main",
|
|
2044
|
+
"article",
|
|
2045
|
+
"section",
|
|
2046
|
+
"nav",
|
|
2047
|
+
"header",
|
|
2048
|
+
"footer"
|
|
2049
|
+
]);
|
|
2050
|
+
const tagName = element.tagName.toLowerCase();
|
|
2051
|
+
if (alwaysAccept.has(tagName)) return true;
|
|
2052
|
+
const leafElementDenyList = /* @__PURE__ */ new Set([
|
|
2053
|
+
"svg",
|
|
2054
|
+
"script",
|
|
2055
|
+
"style",
|
|
2056
|
+
"link",
|
|
2057
|
+
"meta",
|
|
2058
|
+
"noscript",
|
|
2059
|
+
"template"
|
|
2060
|
+
]);
|
|
2061
|
+
return !leafElementDenyList.has(tagName);
|
|
2062
|
+
}
|
|
2063
|
+
function isElementVisible(element) {
|
|
2064
|
+
const style = getCachedComputedStyle(element);
|
|
2065
|
+
return element.offsetWidth > 0 && element.offsetHeight > 0 && style?.visibility !== "hidden" && style?.display !== "none";
|
|
2066
|
+
}
|
|
2067
|
+
function isInteractiveElement(element) {
|
|
2068
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
if (interactiveBlacklist.includes(element)) {
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
if (interactiveWhitelist.includes(element)) {
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
const tagName = element.tagName.toLowerCase();
|
|
2078
|
+
const style = getCachedComputedStyle(element);
|
|
2079
|
+
const interactiveCursors = /* @__PURE__ */ new Set([
|
|
2080
|
+
"pointer",
|
|
2081
|
+
// Link/clickable elements
|
|
2082
|
+
"move",
|
|
2083
|
+
// Movable elements
|
|
2084
|
+
"text",
|
|
2085
|
+
// Text selection
|
|
2086
|
+
"grab",
|
|
2087
|
+
// Grabbable elements
|
|
2088
|
+
"grabbing",
|
|
2089
|
+
// Currently grabbing
|
|
2090
|
+
"cell",
|
|
2091
|
+
// Table cell selection
|
|
2092
|
+
"copy",
|
|
2093
|
+
// Copy operation
|
|
2094
|
+
"alias",
|
|
2095
|
+
// Alias creation
|
|
2096
|
+
"all-scroll",
|
|
2097
|
+
// Scrollable content
|
|
2098
|
+
"col-resize",
|
|
2099
|
+
// Column resize
|
|
2100
|
+
"context-menu",
|
|
2101
|
+
// Context menu available
|
|
2102
|
+
"crosshair",
|
|
2103
|
+
// Precise selection
|
|
2104
|
+
"e-resize",
|
|
2105
|
+
// East resize
|
|
2106
|
+
"ew-resize",
|
|
2107
|
+
// East-west resize
|
|
2108
|
+
"help",
|
|
2109
|
+
// Help available
|
|
2110
|
+
"n-resize",
|
|
2111
|
+
// North resize
|
|
2112
|
+
"ne-resize",
|
|
2113
|
+
// Northeast resize
|
|
2114
|
+
"nesw-resize",
|
|
2115
|
+
// Northeast-southwest resize
|
|
2116
|
+
"ns-resize",
|
|
2117
|
+
// North-south resize
|
|
2118
|
+
"nw-resize",
|
|
2119
|
+
// Northwest resize
|
|
2120
|
+
"nwse-resize",
|
|
2121
|
+
// Northwest-southeast resize
|
|
2122
|
+
"row-resize",
|
|
2123
|
+
// Row resize
|
|
2124
|
+
"s-resize",
|
|
2125
|
+
// South resize
|
|
2126
|
+
"se-resize",
|
|
2127
|
+
// Southeast resize
|
|
2128
|
+
"sw-resize",
|
|
2129
|
+
// Southwest resize
|
|
2130
|
+
"vertical-text",
|
|
2131
|
+
// Vertical text selection
|
|
2132
|
+
"w-resize",
|
|
2133
|
+
// West resize
|
|
2134
|
+
"zoom-in",
|
|
2135
|
+
// Zoom in
|
|
2136
|
+
"zoom-out"
|
|
2137
|
+
// Zoom out
|
|
2138
|
+
]);
|
|
2139
|
+
const nonInteractiveCursors = /* @__PURE__ */ new Set([
|
|
2140
|
+
"not-allowed",
|
|
2141
|
+
// Action not allowed
|
|
2142
|
+
"no-drop",
|
|
2143
|
+
// Drop not allowed
|
|
2144
|
+
"wait",
|
|
2145
|
+
// Processing
|
|
2146
|
+
"progress",
|
|
2147
|
+
// In progress
|
|
2148
|
+
"initial",
|
|
2149
|
+
// Initial value
|
|
2150
|
+
"inherit"
|
|
2151
|
+
// Inherited value
|
|
2152
|
+
//? Let's just include all potentially clickable elements that are not specifically blocked
|
|
2153
|
+
// 'none', // No cursor
|
|
2154
|
+
// 'default', // Default cursor
|
|
2155
|
+
// 'auto', // Browser default
|
|
2156
|
+
]);
|
|
2157
|
+
function doesElementHaveInteractivePointer(element2) {
|
|
2158
|
+
if (element2.tagName.toLowerCase() === "html") return false;
|
|
2159
|
+
if (style?.cursor && interactiveCursors.has(style.cursor)) return true;
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
let isInteractiveCursor = doesElementHaveInteractivePointer(element);
|
|
2163
|
+
if (isInteractiveCursor) {
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
const interactiveElements = /* @__PURE__ */ new Set([
|
|
2167
|
+
"a",
|
|
2168
|
+
// Links
|
|
2169
|
+
"button",
|
|
2170
|
+
// Buttons
|
|
2171
|
+
"input",
|
|
2172
|
+
// All input types (text, checkbox, radio, etc.)
|
|
2173
|
+
"select",
|
|
2174
|
+
// Dropdown menus
|
|
2175
|
+
"textarea",
|
|
2176
|
+
// Text areas
|
|
2177
|
+
"details",
|
|
2178
|
+
// Expandable details
|
|
2179
|
+
"summary",
|
|
2180
|
+
// Summary element (clickable part of details)
|
|
2181
|
+
"label",
|
|
2182
|
+
// Form labels (often clickable)
|
|
2183
|
+
"option",
|
|
2184
|
+
// Select options
|
|
2185
|
+
"optgroup",
|
|
2186
|
+
// Option groups
|
|
2187
|
+
"fieldset",
|
|
2188
|
+
// Form fieldsets (can be interactive with legend)
|
|
2189
|
+
"legend"
|
|
2190
|
+
// Fieldset legends
|
|
2191
|
+
]);
|
|
2192
|
+
const explicitDisableTags = /* @__PURE__ */ new Set([
|
|
2193
|
+
"disabled",
|
|
2194
|
+
// Standard disabled attribute
|
|
2195
|
+
// 'aria-disabled', // ARIA disabled state
|
|
2196
|
+
"readonly"
|
|
2197
|
+
// Read-only state
|
|
2198
|
+
// 'aria-readonly', // ARIA read-only state
|
|
2199
|
+
// 'aria-hidden', // Hidden from accessibility
|
|
2200
|
+
// 'hidden', // Hidden attribute
|
|
2201
|
+
// 'inert', // Inert attribute
|
|
2202
|
+
// 'aria-inert', // ARIA inert state
|
|
2203
|
+
// 'tabindex="-1"', // Removed from tab order
|
|
2204
|
+
// 'aria-hidden="true"' // Hidden from screen readers
|
|
2205
|
+
]);
|
|
2206
|
+
if (interactiveElements.has(tagName)) {
|
|
2207
|
+
if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
for (const disableTag of explicitDisableTags) {
|
|
2211
|
+
if (element.hasAttribute(disableTag) || element.getAttribute(disableTag) === "true" || element.getAttribute(disableTag) === "") {
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (element.disabled) {
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
if (element.readOnly) {
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
if (element.inert) {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
return true;
|
|
2225
|
+
}
|
|
2226
|
+
const role = element.getAttribute("role");
|
|
2227
|
+
const ariaRole = element.getAttribute("aria-role");
|
|
2228
|
+
if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
|
|
2229
|
+
return true;
|
|
2230
|
+
}
|
|
2231
|
+
if (element.classList && (element.classList.contains("button") || element.classList.contains("dropdown-toggle") || element.getAttribute("data-index") || element.getAttribute("data-toggle") === "dropdown" || element.getAttribute("aria-haspopup") === "true")) {
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
2235
|
+
"button",
|
|
2236
|
+
// Directly clickable element
|
|
2237
|
+
// 'link', // Clickable link
|
|
2238
|
+
"menu",
|
|
2239
|
+
// Menu container (ARIA menus)
|
|
2240
|
+
"menubar",
|
|
2241
|
+
// Menu bar container
|
|
2242
|
+
"menuitem",
|
|
2243
|
+
// Clickable menu item
|
|
2244
|
+
"menuitemradio",
|
|
2245
|
+
// Radio-style menu item (selectable)
|
|
2246
|
+
"menuitemcheckbox",
|
|
2247
|
+
// Checkbox-style menu item (toggleable)
|
|
2248
|
+
"radio",
|
|
2249
|
+
// Radio button (selectable)
|
|
2250
|
+
"checkbox",
|
|
2251
|
+
// Checkbox (toggleable)
|
|
2252
|
+
"tab",
|
|
2253
|
+
// Tab (clickable to switch content)
|
|
2254
|
+
"switch",
|
|
2255
|
+
// Toggle switch (clickable to change state)
|
|
2256
|
+
"slider",
|
|
2257
|
+
// Slider control (draggable)
|
|
2258
|
+
"spinbutton",
|
|
2259
|
+
// Number input with up/down controls
|
|
2260
|
+
"combobox",
|
|
2261
|
+
// Dropdown with text input
|
|
2262
|
+
"searchbox",
|
|
2263
|
+
// Search input field
|
|
2264
|
+
"textbox",
|
|
2265
|
+
// Text input field
|
|
2266
|
+
"listbox",
|
|
2267
|
+
// Selectable list
|
|
2268
|
+
"option",
|
|
2269
|
+
// Selectable option in a list
|
|
2270
|
+
"scrollbar"
|
|
2271
|
+
// Scrollable control
|
|
2272
|
+
]);
|
|
2273
|
+
const hasInteractiveRole = interactiveElements.has(tagName) || role && interactiveRoles.has(role) || ariaRole && interactiveRoles.has(ariaRole);
|
|
2274
|
+
if (hasInteractiveRole) return true;
|
|
2275
|
+
try {
|
|
2276
|
+
if (typeof getEventListeners === "function") {
|
|
2277
|
+
const listeners = getEventListeners(element);
|
|
2278
|
+
const mouseEvents = ["click", "mousedown", "mouseup", "dblclick"];
|
|
2279
|
+
for (const eventType of mouseEvents) {
|
|
2280
|
+
if (listeners[eventType] && listeners[eventType].length > 0) {
|
|
2281
|
+
return true;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
2286
|
+
if (typeof getEventListenersForNode === "function") {
|
|
2287
|
+
const listeners = getEventListenersForNode(element);
|
|
2288
|
+
const interactionEvents = [
|
|
2289
|
+
"click",
|
|
2290
|
+
"mousedown",
|
|
2291
|
+
"mouseup",
|
|
2292
|
+
"keydown",
|
|
2293
|
+
"keyup",
|
|
2294
|
+
"submit",
|
|
2295
|
+
"change",
|
|
2296
|
+
"input",
|
|
2297
|
+
"focus",
|
|
2298
|
+
"blur"
|
|
2299
|
+
];
|
|
2300
|
+
for (const eventType of interactionEvents) {
|
|
2301
|
+
for (const listener of listeners) {
|
|
2302
|
+
if (listener.type === eventType) {
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const commonMouseAttrs = ["onclick", "onmousedown", "onmouseup", "ondblclick"];
|
|
2309
|
+
for (const attr of commonMouseAttrs) {
|
|
2310
|
+
if (element.hasAttribute(attr) || typeof element[attr] === "function") {
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} catch (e) {
|
|
2315
|
+
}
|
|
2316
|
+
if (isScrollableElement(element)) {
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
function isTopElement(element) {
|
|
2322
|
+
if (viewportExpansion === -1) {
|
|
2323
|
+
return true;
|
|
2324
|
+
}
|
|
2325
|
+
const rects = getCachedClientRects(element);
|
|
2326
|
+
if (!rects || rects.length === 0) {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
let isAnyRectInViewport = false;
|
|
2330
|
+
for (const rect2 of rects) {
|
|
2331
|
+
if (rect2.width > 0 && rect2.height > 0 && !// Only check non-empty rects
|
|
2332
|
+
(rect2.bottom < -viewportExpansion || rect2.top > window.innerHeight + viewportExpansion || rect2.right < -viewportExpansion || rect2.left > window.innerWidth + viewportExpansion)) {
|
|
2333
|
+
isAnyRectInViewport = true;
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
if (!isAnyRectInViewport) {
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2340
|
+
let doc = element.ownerDocument;
|
|
2341
|
+
if (doc !== window.document) {
|
|
2342
|
+
return true;
|
|
2343
|
+
}
|
|
2344
|
+
let rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0);
|
|
2345
|
+
if (!rect) {
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
const shadowRoot = element.getRootNode();
|
|
2349
|
+
if (shadowRoot instanceof ShadowRoot) {
|
|
2350
|
+
const centerX = rect.left + rect.width / 2;
|
|
2351
|
+
const centerY = rect.top + rect.height / 2;
|
|
2352
|
+
try {
|
|
2353
|
+
const topEl = shadowRoot.elementFromPoint(centerX, centerY);
|
|
2354
|
+
if (!topEl) return false;
|
|
2355
|
+
let current = topEl;
|
|
2356
|
+
while (current && current !== shadowRoot) {
|
|
2357
|
+
if (current === element) return true;
|
|
2358
|
+
current = current.parentElement;
|
|
2359
|
+
}
|
|
2360
|
+
return false;
|
|
2361
|
+
} catch (e) {
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
const margin = 5;
|
|
2366
|
+
const checkPoints = [
|
|
2367
|
+
// Initially only this was used, but it was not enough
|
|
2368
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
2369
|
+
{ x: rect.left + margin, y: rect.top + margin },
|
|
2370
|
+
// top left
|
|
2371
|
+
// { x: rect.right - margin, y: rect.top + margin }, // top right
|
|
2372
|
+
// { x: rect.left + margin, y: rect.bottom - margin }, // bottom left
|
|
2373
|
+
{ x: rect.right - margin, y: rect.bottom - margin }
|
|
2374
|
+
// bottom right
|
|
2375
|
+
];
|
|
2376
|
+
return checkPoints.some(({ x, y }) => {
|
|
2377
|
+
try {
|
|
2378
|
+
const topEl = document.elementFromPoint(x, y);
|
|
2379
|
+
if (!topEl) return false;
|
|
2380
|
+
let current = topEl;
|
|
2381
|
+
while (current && current !== document.documentElement) {
|
|
2382
|
+
if (current === element) return true;
|
|
2383
|
+
current = current.parentElement;
|
|
2384
|
+
}
|
|
2385
|
+
return false;
|
|
2386
|
+
} catch (e) {
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function isInExpandedViewport(element, viewportExpansion2) {
|
|
2392
|
+
if (viewportExpansion2 === -1) {
|
|
2393
|
+
return true;
|
|
2394
|
+
}
|
|
2395
|
+
const rects = element.getClientRects();
|
|
2396
|
+
if (!rects || rects.length === 0) {
|
|
2397
|
+
const boundingRect = getCachedBoundingRect(element);
|
|
2398
|
+
if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
|
|
2399
|
+
return false;
|
|
2400
|
+
}
|
|
2401
|
+
return !(boundingRect.bottom < -viewportExpansion2 || boundingRect.top > window.innerHeight + viewportExpansion2 || boundingRect.right < -viewportExpansion2 || boundingRect.left > window.innerWidth + viewportExpansion2);
|
|
2402
|
+
}
|
|
2403
|
+
for (const rect of rects) {
|
|
2404
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
2405
|
+
if (!(rect.bottom < -viewportExpansion2 || rect.top > window.innerHeight + viewportExpansion2 || rect.right < -viewportExpansion2 || rect.left > window.innerWidth + viewportExpansion2)) {
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
const INTERACTIVE_ARIA_ATTRS = [
|
|
2412
|
+
"aria-expanded",
|
|
2413
|
+
"aria-checked",
|
|
2414
|
+
"aria-selected",
|
|
2415
|
+
"aria-pressed",
|
|
2416
|
+
"aria-haspopup",
|
|
2417
|
+
"aria-controls",
|
|
2418
|
+
"aria-owns",
|
|
2419
|
+
"aria-activedescendant",
|
|
2420
|
+
"aria-valuenow",
|
|
2421
|
+
"aria-valuetext",
|
|
2422
|
+
"aria-valuemax",
|
|
2423
|
+
"aria-valuemin",
|
|
2424
|
+
"aria-autocomplete"
|
|
2425
|
+
];
|
|
2426
|
+
function hasInteractiveAria(el) {
|
|
2427
|
+
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
|
|
2428
|
+
if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true;
|
|
2429
|
+
}
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
function isInteractiveCandidate(element) {
|
|
2433
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
2434
|
+
const tagName = element.tagName.toLowerCase();
|
|
2435
|
+
const interactiveElements = /* @__PURE__ */ new Set([
|
|
2436
|
+
"a",
|
|
2437
|
+
"button",
|
|
2438
|
+
"input",
|
|
2439
|
+
"select",
|
|
2440
|
+
"textarea",
|
|
2441
|
+
"details",
|
|
2442
|
+
"summary",
|
|
2443
|
+
"label"
|
|
2444
|
+
]);
|
|
2445
|
+
if (interactiveElements.has(tagName)) return true;
|
|
2446
|
+
const hasQuickInteractiveAttr = element.hasAttribute("onclick") || element.hasAttribute("role") || element.hasAttribute("tabindex") || hasInteractiveAria(element) || element.hasAttribute("data-action") || element.getAttribute("contenteditable") === "true";
|
|
2447
|
+
return hasQuickInteractiveAttr;
|
|
2448
|
+
}
|
|
2449
|
+
const DISTINCT_INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
2450
|
+
"a",
|
|
2451
|
+
"button",
|
|
2452
|
+
"input",
|
|
2453
|
+
"select",
|
|
2454
|
+
"textarea",
|
|
2455
|
+
"summary",
|
|
2456
|
+
"details",
|
|
2457
|
+
"label",
|
|
2458
|
+
"option",
|
|
2459
|
+
"li"
|
|
2460
|
+
]);
|
|
2461
|
+
const DISTINCT_INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
2462
|
+
"button",
|
|
2463
|
+
"link",
|
|
2464
|
+
"menuitem",
|
|
2465
|
+
"menuitemradio",
|
|
2466
|
+
"menuitemcheckbox",
|
|
2467
|
+
"radio",
|
|
2468
|
+
"checkbox",
|
|
2469
|
+
"tab",
|
|
2470
|
+
"switch",
|
|
2471
|
+
"slider",
|
|
2472
|
+
"spinbutton",
|
|
2473
|
+
"combobox",
|
|
2474
|
+
"searchbox",
|
|
2475
|
+
"textbox",
|
|
2476
|
+
"listbox",
|
|
2477
|
+
"listitem",
|
|
2478
|
+
"treeitem",
|
|
2479
|
+
"row",
|
|
2480
|
+
"option",
|
|
2481
|
+
"scrollbar"
|
|
2482
|
+
]);
|
|
2483
|
+
function isHeuristicallyInteractive(element) {
|
|
2484
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
2485
|
+
if (!isElementVisible(element)) return false;
|
|
2486
|
+
const hasInteractiveAttributes = element.hasAttribute("role") || element.hasAttribute("tabindex") || element.hasAttribute("onclick") || typeof element.onclick === "function";
|
|
2487
|
+
const hasInteractiveClass = /\b(btn|clickable|menu|item|entry|link)\b/i.test(
|
|
2488
|
+
element.className || ""
|
|
2489
|
+
);
|
|
2490
|
+
const isInKnownContainer = Boolean(
|
|
2491
|
+
element.closest('button,a,[role="button"],.menu,.dropdown,.list,.toolbar')
|
|
2492
|
+
);
|
|
2493
|
+
const hasVisibleChildren = [...element.children].some(isElementVisible);
|
|
2494
|
+
const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);
|
|
2495
|
+
return (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) && hasVisibleChildren && isInKnownContainer && !isParentBody;
|
|
2496
|
+
}
|
|
2497
|
+
function isElementDistinctInteraction(element) {
|
|
2498
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
const tagName = element.tagName.toLowerCase();
|
|
2502
|
+
const role = element.getAttribute("role");
|
|
2503
|
+
if (tagName === "iframe") {
|
|
2504
|
+
return true;
|
|
2505
|
+
}
|
|
2506
|
+
if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
|
|
2507
|
+
return true;
|
|
2508
|
+
}
|
|
2509
|
+
if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
if (element.isContentEditable || element.getAttribute("contenteditable") === "true") {
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2515
|
+
if (element.hasAttribute("data-testid") || element.hasAttribute("data-cy") || element.hasAttribute("data-test")) {
|
|
2516
|
+
return true;
|
|
2517
|
+
}
|
|
2518
|
+
if (element.hasAttribute("onclick") || typeof element.onclick === "function") {
|
|
2519
|
+
return true;
|
|
2520
|
+
}
|
|
2521
|
+
if (hasInteractiveAria(element)) {
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
try {
|
|
2525
|
+
const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
|
|
2526
|
+
if (typeof getEventListenersForNode === "function") {
|
|
2527
|
+
const listeners = getEventListenersForNode(element);
|
|
2528
|
+
const interactionEvents = [
|
|
2529
|
+
"click",
|
|
2530
|
+
"mousedown",
|
|
2531
|
+
"mouseup",
|
|
2532
|
+
"keydown",
|
|
2533
|
+
"keyup",
|
|
2534
|
+
"submit",
|
|
2535
|
+
"change",
|
|
2536
|
+
"input",
|
|
2537
|
+
"focus",
|
|
2538
|
+
"blur"
|
|
2539
|
+
];
|
|
2540
|
+
for (const eventType of interactionEvents) {
|
|
2541
|
+
for (const listener of listeners) {
|
|
2542
|
+
if (listener.type === eventType) {
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const commonEventAttrs = [
|
|
2549
|
+
"onmousedown",
|
|
2550
|
+
"onmouseup",
|
|
2551
|
+
"onkeydown",
|
|
2552
|
+
"onkeyup",
|
|
2553
|
+
"onsubmit",
|
|
2554
|
+
"onchange",
|
|
2555
|
+
"oninput",
|
|
2556
|
+
"onfocus",
|
|
2557
|
+
"onblur"
|
|
2558
|
+
];
|
|
2559
|
+
if (commonEventAttrs.some((attr) => element.hasAttribute(attr))) {
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2562
|
+
} catch (e) {
|
|
2563
|
+
}
|
|
2564
|
+
if (isHeuristicallyInteractive(element)) {
|
|
2565
|
+
return true;
|
|
2566
|
+
}
|
|
2567
|
+
if (extraData.get(element)?.scrollable) {
|
|
2568
|
+
return true;
|
|
2569
|
+
}
|
|
2570
|
+
return false;
|
|
2571
|
+
}
|
|
2572
|
+
function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
|
|
2573
|
+
if (!nodeData.isInteractive) return false;
|
|
2574
|
+
let shouldHighlight = false;
|
|
2575
|
+
if (!isParentHighlighted) {
|
|
2576
|
+
shouldHighlight = true;
|
|
2577
|
+
} else {
|
|
2578
|
+
if (isElementDistinctInteraction(node)) {
|
|
2579
|
+
shouldHighlight = true;
|
|
2580
|
+
} else {
|
|
2581
|
+
shouldHighlight = false;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (shouldHighlight) {
|
|
2585
|
+
nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);
|
|
2586
|
+
if (nodeData.isInViewport || viewportExpansion === -1) {
|
|
2587
|
+
nodeData.highlightIndex = highlightIndex++;
|
|
2588
|
+
if (doHighlightElements) {
|
|
2589
|
+
if (focusHighlightIndex >= 0) {
|
|
2590
|
+
if (focusHighlightIndex === nodeData.highlightIndex) {
|
|
2591
|
+
highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
2592
|
+
}
|
|
2593
|
+
} else {
|
|
2594
|
+
highlightElement(node, nodeData.highlightIndex, parentIframe);
|
|
2595
|
+
}
|
|
2596
|
+
return true;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
|
|
2603
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID || node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
|
|
2607
|
+
return null;
|
|
2608
|
+
}
|
|
2609
|
+
if (node.dataset?.browserUseIgnore === "true" || node.dataset?.pageAgentIgnore === "true") {
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
if (node.getAttribute && node.getAttribute("aria-hidden") === "true") {
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
if (node === document.body) {
|
|
2616
|
+
const nodeData2 = {
|
|
2617
|
+
tagName: "body",
|
|
2618
|
+
attributes: {},
|
|
2619
|
+
xpath: "/body",
|
|
2620
|
+
children: []
|
|
2621
|
+
};
|
|
2622
|
+
for (const child of node.childNodes) {
|
|
2623
|
+
const domElement = buildDomTree(child, parentIframe, false);
|
|
2624
|
+
if (domElement) nodeData2.children.push(domElement);
|
|
2625
|
+
}
|
|
2626
|
+
const id2 = `${ID.current++}`;
|
|
2627
|
+
DOM_HASH_MAP[id2] = nodeData2;
|
|
2628
|
+
return id2;
|
|
2629
|
+
}
|
|
2630
|
+
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
|
|
2631
|
+
return null;
|
|
2632
|
+
}
|
|
2633
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2634
|
+
const textContent = node.textContent?.trim();
|
|
2635
|
+
if (!textContent) {
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
const parentElement = node.parentElement;
|
|
2639
|
+
if (!parentElement || parentElement.tagName.toLowerCase() === "script") {
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
const id2 = `${ID.current++}`;
|
|
2643
|
+
DOM_HASH_MAP[id2] = {
|
|
2644
|
+
type: "TEXT_NODE",
|
|
2645
|
+
text: textContent,
|
|
2646
|
+
isVisible: isTextNodeVisible(node)
|
|
2647
|
+
};
|
|
2648
|
+
return id2;
|
|
2649
|
+
}
|
|
2650
|
+
if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
|
|
2651
|
+
return null;
|
|
2652
|
+
}
|
|
2653
|
+
if (viewportExpansion !== -1 && !node.shadowRoot) {
|
|
2654
|
+
const rect = getCachedBoundingRect(node);
|
|
2655
|
+
const style = getCachedComputedStyle(node);
|
|
2656
|
+
const isFixedOrSticky = style && (style.position === "fixed" || style.position === "sticky");
|
|
2657
|
+
const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
|
|
2658
|
+
if (!rect || !isFixedOrSticky && !hasSize && (rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
|
|
2659
|
+
return null;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const nodeData = {
|
|
2663
|
+
tagName: node.tagName.toLowerCase(),
|
|
2664
|
+
attributes: {},
|
|
2665
|
+
/**
|
|
2666
|
+
* @edit no need for xpath
|
|
2667
|
+
*/
|
|
2668
|
+
// xpath: getXPathTree(node, true),
|
|
2669
|
+
children: []
|
|
2670
|
+
};
|
|
2671
|
+
if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === "iframe" || node.tagName.toLowerCase() === "body") {
|
|
2672
|
+
const attributeNames = node.getAttributeNames?.() || [];
|
|
2673
|
+
for (const name of attributeNames) {
|
|
2674
|
+
const value = node.getAttribute(name);
|
|
2675
|
+
nodeData.attributes[name] = value;
|
|
2676
|
+
}
|
|
2677
|
+
if (node.tagName.toLowerCase() === "input" && (node.type === "checkbox" || node.type === "radio")) {
|
|
2678
|
+
nodeData.attributes.checked = node.checked ? "true" : "false";
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
let nodeWasHighlighted = false;
|
|
2682
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2683
|
+
nodeData.isVisible = isElementVisible(node);
|
|
2684
|
+
if (nodeData.isVisible) {
|
|
2685
|
+
nodeData.isTopElement = isTopElement(node);
|
|
2686
|
+
const role = node.getAttribute("role");
|
|
2687
|
+
const isMenuContainer = role === "menu" || role === "menubar" || role === "listbox";
|
|
2688
|
+
if (nodeData.isTopElement || isMenuContainer) {
|
|
2689
|
+
nodeData.isInteractive = isInteractiveElement(node);
|
|
2690
|
+
nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
|
|
2691
|
+
nodeData.ref = node;
|
|
2692
|
+
if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
|
|
2693
|
+
const attributeNames = node.getAttributeNames?.() || [];
|
|
2694
|
+
for (const name of attributeNames) {
|
|
2695
|
+
const value = node.getAttribute(name);
|
|
2696
|
+
nodeData.attributes[name] = value;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (node.tagName) {
|
|
2703
|
+
const tagName = node.tagName.toLowerCase();
|
|
2704
|
+
if (tagName === "iframe") {
|
|
2705
|
+
try {
|
|
2706
|
+
const iframeDoc = node.contentDocument || node.contentWindow?.document;
|
|
2707
|
+
if (iframeDoc) {
|
|
2708
|
+
for (const child of iframeDoc.childNodes) {
|
|
2709
|
+
const domElement = buildDomTree(child, node, false);
|
|
2710
|
+
if (domElement) nodeData.children.push(domElement);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
} catch (e) {
|
|
2714
|
+
console.warn("Unable to access iframe:", e);
|
|
2715
|
+
}
|
|
2716
|
+
} else if (node.isContentEditable || node.getAttribute("contenteditable") === "true" || node.id === "tinymce" || node.classList.contains("mce-content-body") || tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_")) {
|
|
2717
|
+
for (const child of node.childNodes) {
|
|
2718
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
2719
|
+
if (domElement) nodeData.children.push(domElement);
|
|
2720
|
+
}
|
|
2721
|
+
} else {
|
|
2722
|
+
if (node.shadowRoot) {
|
|
2723
|
+
nodeData.shadowRoot = true;
|
|
2724
|
+
for (const child of node.shadowRoot.childNodes) {
|
|
2725
|
+
const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
|
|
2726
|
+
if (domElement) nodeData.children.push(domElement);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
for (const child of node.childNodes) {
|
|
2730
|
+
const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;
|
|
2731
|
+
const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);
|
|
2732
|
+
if (domElement) nodeData.children.push(domElement);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (nodeData.tagName === "a" && nodeData.children.length === 0 && !nodeData.attributes.href) {
|
|
2737
|
+
const rect = getCachedBoundingRect(node);
|
|
2738
|
+
const hasSize = rect && rect.width > 0 && rect.height > 0 || node.offsetWidth > 0 || node.offsetHeight > 0;
|
|
2739
|
+
if (!hasSize) {
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
nodeData.extra = extraData.get(node) || null;
|
|
2744
|
+
const id = `${ID.current++}`;
|
|
2745
|
+
DOM_HASH_MAP[id] = nodeData;
|
|
2746
|
+
return id;
|
|
2747
|
+
}
|
|
2748
|
+
const rootId = buildDomTree(document.body);
|
|
2749
|
+
DOM_CACHE.clearCache();
|
|
2750
|
+
return { rootId, map: DOM_HASH_MAP };
|
|
2751
|
+
};
|
|
2752
|
+
const DEFAULT_VIEWPORT_EXPANSION = -1;
|
|
2753
|
+
function resolveViewportExpansion(viewportExpansion) {
|
|
2754
|
+
return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION;
|
|
2755
|
+
}
|
|
2756
|
+
const SEMANTIC_TAGS = /* @__PURE__ */ new Set([
|
|
2757
|
+
"nav",
|
|
2758
|
+
"menu",
|
|
2759
|
+
// 'main',
|
|
2760
|
+
"header",
|
|
2761
|
+
"footer",
|
|
2762
|
+
"aside",
|
|
2763
|
+
// 'article',
|
|
2764
|
+
// 'form',
|
|
2765
|
+
"dialog"
|
|
2766
|
+
]);
|
|
2767
|
+
const newElementsCache = /* @__PURE__ */ new WeakMap();
|
|
2768
|
+
function getFlatTree(config) {
|
|
2769
|
+
const viewportExpansion = resolveViewportExpansion(config.viewportExpansion);
|
|
2770
|
+
const interactiveBlacklist = [];
|
|
2771
|
+
for (const item of config.interactiveBlacklist || []) {
|
|
2772
|
+
if (typeof item === "function") {
|
|
2773
|
+
interactiveBlacklist.push(item());
|
|
2774
|
+
} else {
|
|
2775
|
+
interactiveBlacklist.push(item);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
const interactiveWhitelist = [];
|
|
2779
|
+
for (const item of config.interactiveWhitelist || []) {
|
|
2780
|
+
if (typeof item === "function") {
|
|
2781
|
+
interactiveWhitelist.push(item());
|
|
2782
|
+
} else {
|
|
2783
|
+
interactiveWhitelist.push(item);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
const elements = domTree({
|
|
2787
|
+
doHighlightElements: true,
|
|
2788
|
+
debugMode: true,
|
|
2789
|
+
focusHighlightIndex: -1,
|
|
2790
|
+
viewportExpansion,
|
|
2791
|
+
interactiveBlacklist,
|
|
2792
|
+
interactiveWhitelist,
|
|
2793
|
+
highlightOpacity: config.highlightOpacity ?? 0,
|
|
2794
|
+
highlightLabelOpacity: config.highlightLabelOpacity ?? 0.1
|
|
2795
|
+
});
|
|
2796
|
+
const currentUrl = window.location.href;
|
|
2797
|
+
for (const nodeId in elements.map) {
|
|
2798
|
+
const node = elements.map[nodeId];
|
|
2799
|
+
if (node.isInteractive && node.ref) {
|
|
2800
|
+
const ref = node.ref;
|
|
2801
|
+
if (!newElementsCache.has(ref)) {
|
|
2802
|
+
newElementsCache.set(ref, currentUrl);
|
|
2803
|
+
node.isNew = true;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return elements;
|
|
2808
|
+
}
|
|
2809
|
+
const globRegexCache = /* @__PURE__ */ new Map();
|
|
2810
|
+
function globToRegex(pattern) {
|
|
2811
|
+
let regex = globRegexCache.get(pattern);
|
|
2812
|
+
if (!regex) {
|
|
2813
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2814
|
+
regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
2815
|
+
globRegexCache.set(pattern, regex);
|
|
2816
|
+
}
|
|
2817
|
+
return regex;
|
|
2818
|
+
}
|
|
2819
|
+
function matchAttributes(attrs, patterns) {
|
|
2820
|
+
const result2 = {};
|
|
2821
|
+
for (const pattern of patterns) {
|
|
2822
|
+
if (pattern.includes("*")) {
|
|
2823
|
+
const regex = globToRegex(pattern);
|
|
2824
|
+
for (const key of Object.keys(attrs)) {
|
|
2825
|
+
if (regex.test(key) && attrs[key].trim()) {
|
|
2826
|
+
result2[key] = attrs[key].trim();
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
} else {
|
|
2830
|
+
const value = attrs[pattern];
|
|
2831
|
+
if (value && value.trim()) {
|
|
2832
|
+
result2[pattern] = value.trim();
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return result2;
|
|
2837
|
+
}
|
|
2838
|
+
function flatTreeToString(flatTree, includeAttributes = [], keepSemanticTags = false) {
|
|
2839
|
+
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
|
2840
|
+
"title",
|
|
2841
|
+
"type",
|
|
2842
|
+
"checked",
|
|
2843
|
+
"name",
|
|
2844
|
+
"role",
|
|
2845
|
+
"value",
|
|
2846
|
+
"placeholder",
|
|
2847
|
+
"data-date-format",
|
|
2848
|
+
"alt",
|
|
2849
|
+
"aria-label",
|
|
2850
|
+
"aria-expanded",
|
|
2851
|
+
"data-state",
|
|
2852
|
+
"aria-checked",
|
|
2853
|
+
// @edit added for better form handling
|
|
2854
|
+
"id",
|
|
2855
|
+
"for",
|
|
2856
|
+
// for jump check
|
|
2857
|
+
"target",
|
|
2858
|
+
// absolute position dropdown menu
|
|
2859
|
+
"aria-haspopup",
|
|
2860
|
+
"aria-controls",
|
|
2861
|
+
"aria-owns",
|
|
2862
|
+
// content editable
|
|
2863
|
+
"contenteditable"
|
|
2864
|
+
];
|
|
2865
|
+
const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES];
|
|
2866
|
+
const capTextLength = (text, maxLength) => {
|
|
2867
|
+
if (text.length > maxLength) {
|
|
2868
|
+
return text.substring(0, maxLength) + "...";
|
|
2869
|
+
}
|
|
2870
|
+
return text;
|
|
2871
|
+
};
|
|
2872
|
+
const buildTreeNode = (nodeId) => {
|
|
2873
|
+
const node = flatTree.map[nodeId];
|
|
2874
|
+
if (!node) return null;
|
|
2875
|
+
if (node.type === "TEXT_NODE") {
|
|
2876
|
+
const textNode = node;
|
|
2877
|
+
return {
|
|
2878
|
+
type: "text",
|
|
2879
|
+
text: textNode.text,
|
|
2880
|
+
isVisible: textNode.isVisible,
|
|
2881
|
+
parent: null,
|
|
2882
|
+
children: []
|
|
2883
|
+
};
|
|
2884
|
+
} else {
|
|
2885
|
+
const elementNode = node;
|
|
2886
|
+
const children = [];
|
|
2887
|
+
if (elementNode.children) {
|
|
2888
|
+
for (const childId of elementNode.children) {
|
|
2889
|
+
const child = buildTreeNode(childId);
|
|
2890
|
+
if (child) {
|
|
2891
|
+
child.parent = null;
|
|
2892
|
+
children.push(child);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
return {
|
|
2897
|
+
type: "element",
|
|
2898
|
+
tagName: elementNode.tagName,
|
|
2899
|
+
attributes: elementNode.attributes ?? {},
|
|
2900
|
+
isVisible: elementNode.isVisible ?? false,
|
|
2901
|
+
isInteractive: elementNode.isInteractive ?? false,
|
|
2902
|
+
isTopElement: elementNode.isTopElement ?? false,
|
|
2903
|
+
isNew: elementNode.isNew ?? false,
|
|
2904
|
+
highlightIndex: elementNode.highlightIndex,
|
|
2905
|
+
parent: null,
|
|
2906
|
+
children,
|
|
2907
|
+
extra: elementNode.extra ?? {}
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
const setParentReferences = (node, parent = null) => {
|
|
2912
|
+
node.parent = parent;
|
|
2913
|
+
for (const child of node.children) {
|
|
2914
|
+
setParentReferences(child, node);
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
const rootNode = buildTreeNode(flatTree.rootId);
|
|
2918
|
+
if (!rootNode) return "";
|
|
2919
|
+
setParentReferences(rootNode);
|
|
2920
|
+
const hasParentWithHighlightIndex = (node) => {
|
|
2921
|
+
let current = node.parent;
|
|
2922
|
+
while (current) {
|
|
2923
|
+
if (current.type === "element" && current.highlightIndex !== void 0) {
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
current = current.parent;
|
|
2927
|
+
}
|
|
2928
|
+
return false;
|
|
2929
|
+
};
|
|
2930
|
+
const processNode = (node, depth, result22) => {
|
|
2931
|
+
let nextDepth = depth;
|
|
2932
|
+
const depthStr = " ".repeat(depth);
|
|
2933
|
+
if (node.type === "element") {
|
|
2934
|
+
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName);
|
|
2935
|
+
if (node.highlightIndex !== void 0) {
|
|
2936
|
+
nextDepth += 1;
|
|
2937
|
+
const text = getAllTextTillNextClickableElement(node);
|
|
2938
|
+
let attributesHtmlStr = "";
|
|
2939
|
+
if (includeAttrs.length > 0 && node.attributes) {
|
|
2940
|
+
const attributesToInclude = matchAttributes(node.attributes, includeAttrs);
|
|
2941
|
+
const keys = Object.keys(attributesToInclude);
|
|
2942
|
+
if (keys.length > 1) {
|
|
2943
|
+
const keysToRemove = /* @__PURE__ */ new Set();
|
|
2944
|
+
const seenValues = {};
|
|
2945
|
+
for (const key of keys) {
|
|
2946
|
+
const value = attributesToInclude[key];
|
|
2947
|
+
if (value.length > 5) {
|
|
2948
|
+
if (value in seenValues) {
|
|
2949
|
+
keysToRemove.add(key);
|
|
2950
|
+
} else {
|
|
2951
|
+
seenValues[value] = key;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
for (const key of keysToRemove) {
|
|
2956
|
+
delete attributesToInclude[key];
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (attributesToInclude.role === node.tagName) {
|
|
2960
|
+
delete attributesToInclude.role;
|
|
2961
|
+
}
|
|
2962
|
+
const attrsToRemoveIfTextMatches = ["aria-label", "placeholder", "title"];
|
|
2963
|
+
for (const attr of attrsToRemoveIfTextMatches) {
|
|
2964
|
+
if (attributesToInclude[attr] && attributesToInclude[attr].toLowerCase().trim() === text.toLowerCase().trim()) {
|
|
2965
|
+
delete attributesToInclude[attr];
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
if (Object.keys(attributesToInclude).length > 0) {
|
|
2969
|
+
attributesHtmlStr = Object.entries(attributesToInclude).map(([key, value]) => `${key}=${capTextLength(value, 20)}`).join(" ");
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const highlightIndicator = node.isNew ? `*[${node.highlightIndex}]` : `[${node.highlightIndex}]`;
|
|
2973
|
+
let line = `${depthStr}${highlightIndicator}<${node.tagName ?? ""}`;
|
|
2974
|
+
if (attributesHtmlStr) {
|
|
2975
|
+
line += ` ${attributesHtmlStr}`;
|
|
2976
|
+
}
|
|
2977
|
+
if (node.extra) {
|
|
2978
|
+
if (node.extra.scrollable) {
|
|
2979
|
+
let scrollDataText = "";
|
|
2980
|
+
if (node.extra.scrollData?.left)
|
|
2981
|
+
scrollDataText += `left=${node.extra.scrollData.left}, `;
|
|
2982
|
+
if (node.extra.scrollData?.top) scrollDataText += `top=${node.extra.scrollData.top}, `;
|
|
2983
|
+
if (node.extra.scrollData?.right)
|
|
2984
|
+
scrollDataText += `right=${node.extra.scrollData.right}, `;
|
|
2985
|
+
if (node.extra.scrollData?.bottom)
|
|
2986
|
+
scrollDataText += `bottom=${node.extra.scrollData.bottom}`;
|
|
2987
|
+
line += ` data-scrollable="${scrollDataText}"`;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if (text) {
|
|
2991
|
+
const trimmedText = text.trim();
|
|
2992
|
+
if (!attributesHtmlStr) {
|
|
2993
|
+
line += " ";
|
|
2994
|
+
}
|
|
2995
|
+
line += `>${trimmedText}`;
|
|
2996
|
+
} else if (!attributesHtmlStr) {
|
|
2997
|
+
line += " ";
|
|
2998
|
+
}
|
|
2999
|
+
line += " />";
|
|
3000
|
+
result22.push(line);
|
|
3001
|
+
}
|
|
3002
|
+
const emitSemantic = isSemantic && node.highlightIndex === void 0;
|
|
3003
|
+
const mark = emitSemantic ? result22.length : -1;
|
|
3004
|
+
if (emitSemantic) {
|
|
3005
|
+
result22.push(`${depthStr}<${node.tagName}>`);
|
|
3006
|
+
nextDepth += 1;
|
|
3007
|
+
}
|
|
3008
|
+
for (const child of node.children) {
|
|
3009
|
+
processNode(child, nextDepth, result22);
|
|
3010
|
+
}
|
|
3011
|
+
if (emitSemantic) {
|
|
3012
|
+
if (result22.length === mark + 1) {
|
|
3013
|
+
result22.pop();
|
|
3014
|
+
} else {
|
|
3015
|
+
result22.push(`${depthStr}</${node.tagName}>`);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
} else if (node.type === "text") {
|
|
3019
|
+
if (hasParentWithHighlightIndex(node)) {
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
if (node.parent && node.parent.type === "element" && node.parent.isVisible && node.parent.isTopElement) {
|
|
3023
|
+
result22.push(`${depthStr}${node.text ?? ""}`);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
const result2 = [];
|
|
3028
|
+
processNode(rootNode, 0, result2);
|
|
3029
|
+
return result2.join("\n");
|
|
3030
|
+
}
|
|
3031
|
+
const getAllTextTillNextClickableElement = (node, maxDepth = -1) => {
|
|
3032
|
+
const textParts = [];
|
|
3033
|
+
const collectText = (currentNode, currentDepth) => {
|
|
3034
|
+
if (maxDepth !== -1 && currentDepth > maxDepth) {
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
if (currentNode.type === "element" && currentNode !== node && currentNode.highlightIndex !== void 0) {
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
if (currentNode.type === "text" && currentNode.text) {
|
|
3041
|
+
textParts.push(currentNode.text);
|
|
3042
|
+
} else if (currentNode.type === "element") {
|
|
3043
|
+
for (const child of currentNode.children) {
|
|
3044
|
+
collectText(child, currentDepth + 1);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
collectText(node, 0);
|
|
3049
|
+
return textParts.join("\n").trim();
|
|
3050
|
+
};
|
|
3051
|
+
function getSelectorMap(flatTree) {
|
|
3052
|
+
const selectorMap = /* @__PURE__ */ new Map();
|
|
3053
|
+
const keys = Object.keys(flatTree.map);
|
|
3054
|
+
for (const key of keys) {
|
|
3055
|
+
const node = flatTree.map[key];
|
|
3056
|
+
if (node.isInteractive && typeof node.highlightIndex === "number") {
|
|
3057
|
+
selectorMap.set(node.highlightIndex, node);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return selectorMap;
|
|
3061
|
+
}
|
|
3062
|
+
function getElementTextMap(simplifiedHTML) {
|
|
3063
|
+
const lines = simplifiedHTML.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3064
|
+
const elementTextMap = /* @__PURE__ */ new Map();
|
|
3065
|
+
for (const line of lines) {
|
|
3066
|
+
const regex = /^\[(\d+)\]<[^>]+>([^<]*)/;
|
|
3067
|
+
const match = regex.exec(line);
|
|
3068
|
+
if (match) {
|
|
3069
|
+
const index = parseInt(match[1], 10);
|
|
3070
|
+
elementTextMap.set(index, line);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
return elementTextMap;
|
|
3074
|
+
}
|
|
3075
|
+
function cleanUpHighlights() {
|
|
3076
|
+
const cleanupFunctions = window._highlightCleanupFunctions || [];
|
|
3077
|
+
for (const cleanup of cleanupFunctions) {
|
|
3078
|
+
if (typeof cleanup === "function") {
|
|
3079
|
+
cleanup();
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
window._highlightCleanupFunctions = [];
|
|
3083
|
+
}
|
|
3084
|
+
window.addEventListener("popstate", () => {
|
|
3085
|
+
cleanUpHighlights();
|
|
3086
|
+
});
|
|
3087
|
+
window.addEventListener("hashchange", () => {
|
|
3088
|
+
cleanUpHighlights();
|
|
3089
|
+
});
|
|
3090
|
+
window.addEventListener("beforeunload", () => {
|
|
3091
|
+
cleanUpHighlights();
|
|
3092
|
+
});
|
|
3093
|
+
const navigation = window.navigation;
|
|
3094
|
+
if (navigation && typeof navigation.addEventListener === "function") {
|
|
3095
|
+
navigation.addEventListener("navigate", () => {
|
|
3096
|
+
cleanUpHighlights();
|
|
3097
|
+
});
|
|
3098
|
+
} else {
|
|
3099
|
+
let currentUrl = window.location.href;
|
|
3100
|
+
setInterval(() => {
|
|
3101
|
+
if (window.location.href !== currentUrl) {
|
|
3102
|
+
currentUrl = window.location.href;
|
|
3103
|
+
cleanUpHighlights();
|
|
3104
|
+
}
|
|
3105
|
+
}, 500);
|
|
3106
|
+
}
|
|
3107
|
+
function getPageInfo() {
|
|
3108
|
+
const viewport_width = window.innerWidth;
|
|
3109
|
+
const viewport_height = window.innerHeight;
|
|
3110
|
+
const page_width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0);
|
|
3111
|
+
const page_height = Math.max(
|
|
3112
|
+
document.documentElement.scrollHeight,
|
|
3113
|
+
document.body.scrollHeight || 0
|
|
3114
|
+
);
|
|
3115
|
+
const scroll_x = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || 0;
|
|
3116
|
+
const scroll_y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;
|
|
3117
|
+
const pixels_below = Math.max(0, page_height - (window.innerHeight + scroll_y));
|
|
3118
|
+
const pixels_right = Math.max(0, page_width - (window.innerWidth + scroll_x));
|
|
3119
|
+
return {
|
|
3120
|
+
// Current viewport dimensions
|
|
3121
|
+
viewport_width,
|
|
3122
|
+
viewport_height,
|
|
3123
|
+
// Total page dimensions
|
|
3124
|
+
page_width,
|
|
3125
|
+
page_height,
|
|
3126
|
+
// Current scroll position
|
|
3127
|
+
scroll_x,
|
|
3128
|
+
scroll_y,
|
|
3129
|
+
pixels_above: scroll_y,
|
|
3130
|
+
pixels_below,
|
|
3131
|
+
pages_above: viewport_height > 0 ? scroll_y / viewport_height : 0,
|
|
3132
|
+
pages_below: viewport_height > 0 ? pixels_below / viewport_height : 0,
|
|
3133
|
+
total_pages: viewport_height > 0 ? page_height / viewport_height : 0,
|
|
3134
|
+
current_page_position: scroll_y / Math.max(1, page_height - viewport_height),
|
|
3135
|
+
pixels_left: scroll_x,
|
|
3136
|
+
pixels_right
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
function patchReact(pageController) {
|
|
3140
|
+
const reactRootElements = document.querySelectorAll(
|
|
3141
|
+
'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root'
|
|
3142
|
+
);
|
|
3143
|
+
for (const element of reactRootElements) {
|
|
3144
|
+
element.setAttribute("data-page-agent-not-interactive", "true");
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
class PageController extends EventTarget {
|
|
3148
|
+
config;
|
|
3149
|
+
/** Corresponds to eval_page in browser-use */
|
|
3150
|
+
flatTree = null;
|
|
3151
|
+
/**
|
|
3152
|
+
* All highlighted index-mapped interactive elements
|
|
3153
|
+
* Corresponds to DOMState.selector_map in browser-use
|
|
3154
|
+
*/
|
|
3155
|
+
selectorMap = /* @__PURE__ */ new Map();
|
|
3156
|
+
/** Index -> element text description mapping */
|
|
3157
|
+
elementTextMap = /* @__PURE__ */ new Map();
|
|
3158
|
+
/**
|
|
3159
|
+
* Simplified HTML for LLM consumption.
|
|
3160
|
+
* Corresponds to clickable_elements_to_string in browser-use
|
|
3161
|
+
*/
|
|
3162
|
+
simplifiedHTML = "<EMPTY>";
|
|
3163
|
+
/** last time the tree was updated */
|
|
3164
|
+
lastTimeUpdate = 0;
|
|
3165
|
+
/** Whether the tree has been indexed at least once */
|
|
3166
|
+
isIndexed = false;
|
|
3167
|
+
/** Visual mask overlay for blocking user interaction during automation */
|
|
3168
|
+
mask = null;
|
|
3169
|
+
maskReady = null;
|
|
3170
|
+
constructor(config = {}) {
|
|
3171
|
+
super();
|
|
3172
|
+
this.config = config;
|
|
3173
|
+
patchReact();
|
|
3174
|
+
if (config.enableMask) this.initMask();
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* FORK: expõe, somente leitura, a árvore achatada da última `updateTree()`.
|
|
3178
|
+
* O perfilador da página precisa descrever os mesmos elementos que o agente vê, e `flatTree`
|
|
3179
|
+
* é privado (o pacote nem exporta o namespace `dom`). O campo continua privado: quem lê
|
|
3180
|
+
* não pode substituir a árvore do agente. Ver vendor/UPSTREAM.md.
|
|
3181
|
+
*/
|
|
3182
|
+
readFlatTree() {
|
|
3183
|
+
return this.flatTree;
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* FORK: expõe, somente leitura, o mapa índice -> elemento interativo da última `updateTree()`.
|
|
3187
|
+
* Mesmo motivo de `readFlatTree()`; o tipo `ReadonlyMap` impede `set`/`delete` por quem lê.
|
|
3188
|
+
*/
|
|
3189
|
+
readSelectorMap() {
|
|
3190
|
+
return this.selectorMap;
|
|
3191
|
+
}
|
|
3192
|
+
/**
|
|
3193
|
+
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
|
|
3194
|
+
*/
|
|
3195
|
+
initMask() {
|
|
3196
|
+
if (this.maskReady !== null) return;
|
|
3197
|
+
this.maskReady = (async () => {
|
|
3198
|
+
const { SimulatorMask } = await import("./SimulatorMask-DIoKZt1z.js");
|
|
3199
|
+
this.mask = new SimulatorMask();
|
|
3200
|
+
})();
|
|
3201
|
+
}
|
|
3202
|
+
// ======= State Queries =======
|
|
3203
|
+
/**
|
|
3204
|
+
* Get current page URL
|
|
3205
|
+
*/
|
|
3206
|
+
async getCurrentUrl() {
|
|
3207
|
+
return window.location.href;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Get last tree update timestamp
|
|
3211
|
+
*/
|
|
3212
|
+
async getLastUpdateTime() {
|
|
3213
|
+
return this.lastTimeUpdate;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Get structured browser state for LLM consumption.
|
|
3217
|
+
* Automatically calls updateTree() to refresh the DOM state.
|
|
3218
|
+
*/
|
|
3219
|
+
async getBrowserState() {
|
|
3220
|
+
const url = window.location.href;
|
|
3221
|
+
const title = document.title;
|
|
3222
|
+
const pi = getPageInfo();
|
|
3223
|
+
const viewportExpansion = resolveViewportExpansion(this.config.viewportExpansion);
|
|
3224
|
+
await this.updateTree();
|
|
3225
|
+
const content = this.simplifiedHTML;
|
|
3226
|
+
const titleLine = `Current Page: [${title}](${url})`;
|
|
3227
|
+
const pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`;
|
|
3228
|
+
const elementsLabel = viewportExpansion === -1 ? "Interactive elements from top layer of the current page (full page):" : "Interactive elements from top layer of the current page inside the viewport:";
|
|
3229
|
+
const hasContentAbove = pi.pixels_above > 4;
|
|
3230
|
+
const scrollHintAbove = hasContentAbove && viewportExpansion !== -1 ? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...` : "[Start of page]";
|
|
3231
|
+
const header2 = `${titleLine}
|
|
3232
|
+
${pageInfoLine}
|
|
3233
|
+
|
|
3234
|
+
${elementsLabel}
|
|
3235
|
+
|
|
3236
|
+
${scrollHintAbove}`;
|
|
3237
|
+
const hasContentBelow = pi.pixels_below > 4;
|
|
3238
|
+
const footer = hasContentBelow && viewportExpansion !== -1 ? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...` : "[End of page]";
|
|
3239
|
+
return { url, title, header: header2, content, footer };
|
|
3240
|
+
}
|
|
3241
|
+
// ======= DOM Tree Operations =======
|
|
3242
|
+
/**
|
|
3243
|
+
* Update DOM tree, returns simplified HTML for LLM.
|
|
3244
|
+
* This is the main method to refresh the page state.
|
|
3245
|
+
* Automatically bypasses mask during DOM extraction if enabled.
|
|
3246
|
+
*/
|
|
3247
|
+
async updateTree() {
|
|
3248
|
+
this.dispatchEvent(new Event("beforeUpdate"));
|
|
3249
|
+
this.lastTimeUpdate = Date.now();
|
|
3250
|
+
if (this.mask) {
|
|
3251
|
+
this.mask.wrapper.style.pointerEvents = "none";
|
|
3252
|
+
}
|
|
3253
|
+
cleanUpHighlights();
|
|
3254
|
+
const blacklist = [
|
|
3255
|
+
...this.config.interactiveBlacklist || [],
|
|
3256
|
+
...Array.from(document.querySelectorAll("[data-page-agent-not-interactive]"))
|
|
3257
|
+
];
|
|
3258
|
+
this.flatTree = getFlatTree({
|
|
3259
|
+
...this.config,
|
|
3260
|
+
interactiveBlacklist: blacklist
|
|
3261
|
+
});
|
|
3262
|
+
this.simplifiedHTML = flatTreeToString(
|
|
3263
|
+
this.flatTree,
|
|
3264
|
+
this.config.includeAttributes,
|
|
3265
|
+
this.config.keepSemanticTags
|
|
3266
|
+
);
|
|
3267
|
+
this.selectorMap.clear();
|
|
3268
|
+
this.selectorMap = getSelectorMap(this.flatTree);
|
|
3269
|
+
this.elementTextMap.clear();
|
|
3270
|
+
this.elementTextMap = getElementTextMap(this.simplifiedHTML);
|
|
3271
|
+
this.isIndexed = true;
|
|
3272
|
+
if (this.mask) {
|
|
3273
|
+
this.mask.wrapper.style.pointerEvents = "auto";
|
|
3274
|
+
}
|
|
3275
|
+
this.dispatchEvent(new Event("afterUpdate"));
|
|
3276
|
+
return this.simplifiedHTML;
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* Clean up all element highlights
|
|
3280
|
+
*/
|
|
3281
|
+
async cleanUpHighlights() {
|
|
3282
|
+
console.log("[PageController] cleanUpHighlights");
|
|
3283
|
+
cleanUpHighlights();
|
|
3284
|
+
}
|
|
3285
|
+
// ======= Element Actions =======
|
|
3286
|
+
/**
|
|
3287
|
+
* Ensure the tree has been indexed before any index-based operation.
|
|
3288
|
+
* Throws if updateTree() hasn't been called yet.
|
|
3289
|
+
*/
|
|
3290
|
+
assertIndexed() {
|
|
3291
|
+
if (!this.isIndexed) {
|
|
3292
|
+
throw new Error("DOM tree not indexed yet. Can not perform actions on elements.");
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Click element by index
|
|
3297
|
+
*/
|
|
3298
|
+
async clickElement(index) {
|
|
3299
|
+
try {
|
|
3300
|
+
this.assertIndexed();
|
|
3301
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
3302
|
+
const elemText = this.elementTextMap.get(index);
|
|
3303
|
+
await clickElement(element);
|
|
3304
|
+
if (isAnchorElement(element) && element.target === "_blank") {
|
|
3305
|
+
return {
|
|
3306
|
+
success: true,
|
|
3307
|
+
message: `OK: elemento (${elemText ?? index}) clicado. ATENÇÃO: o link abriu em uma nova aba.`
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
return {
|
|
3311
|
+
success: true,
|
|
3312
|
+
message: `OK: elemento (${elemText ?? index}) clicado.`
|
|
3313
|
+
};
|
|
3314
|
+
} catch (error2) {
|
|
3315
|
+
return {
|
|
3316
|
+
success: false,
|
|
3317
|
+
message: `ERRO: não foi possível clicar no elemento: ${error2}`
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Input text into element by index
|
|
3323
|
+
*/
|
|
3324
|
+
async inputText(index, text) {
|
|
3325
|
+
try {
|
|
3326
|
+
this.assertIndexed();
|
|
3327
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
3328
|
+
const elemText = this.elementTextMap.get(index);
|
|
3329
|
+
await inputTextElement(element, text);
|
|
3330
|
+
return {
|
|
3331
|
+
success: true,
|
|
3332
|
+
message: `OK: texto (${text}) digitado no elemento (${elemText ?? index}).`
|
|
3333
|
+
};
|
|
3334
|
+
} catch (error2) {
|
|
3335
|
+
return {
|
|
3336
|
+
success: false,
|
|
3337
|
+
message: `ERRO: não foi possível digitar o texto: ${error2}`
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Select dropdown option by index and option text
|
|
3343
|
+
*/
|
|
3344
|
+
async selectOption(index, optionText) {
|
|
3345
|
+
try {
|
|
3346
|
+
this.assertIndexed();
|
|
3347
|
+
const element = getElementByIndex(this.selectorMap, index);
|
|
3348
|
+
const elemText = this.elementTextMap.get(index);
|
|
3349
|
+
await selectOptionElement(element, optionText);
|
|
3350
|
+
return {
|
|
3351
|
+
success: true,
|
|
3352
|
+
message: `OK: opção (${optionText}) escolhida no elemento (${elemText ?? index}).`
|
|
3353
|
+
};
|
|
3354
|
+
} catch (error2) {
|
|
3355
|
+
return {
|
|
3356
|
+
success: false,
|
|
3357
|
+
message: `ERRO: não foi possível escolher a opção: ${error2}`
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Scroll vertically
|
|
3363
|
+
*/
|
|
3364
|
+
async scroll(options) {
|
|
3365
|
+
try {
|
|
3366
|
+
const { down, numPages, pixels, index } = options;
|
|
3367
|
+
this.assertIndexed();
|
|
3368
|
+
const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1);
|
|
3369
|
+
const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
|
|
3370
|
+
const message = await scrollVertically(scrollAmount, element);
|
|
3371
|
+
return {
|
|
3372
|
+
success: true,
|
|
3373
|
+
message
|
|
3374
|
+
};
|
|
3375
|
+
} catch (error2) {
|
|
3376
|
+
return {
|
|
3377
|
+
success: false,
|
|
3378
|
+
message: `ERRO: não foi possível rolar a página: ${error2}`
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Scroll horizontally
|
|
3384
|
+
*/
|
|
3385
|
+
async scrollHorizontally(options) {
|
|
3386
|
+
try {
|
|
3387
|
+
const { right, pixels, index } = options;
|
|
3388
|
+
this.assertIndexed();
|
|
3389
|
+
const scrollAmount = pixels * (right ? 1 : -1);
|
|
3390
|
+
const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
|
|
3391
|
+
const message = await scrollHorizontally(scrollAmount, element);
|
|
3392
|
+
return {
|
|
3393
|
+
success: true,
|
|
3394
|
+
message
|
|
3395
|
+
};
|
|
3396
|
+
} catch (error2) {
|
|
3397
|
+
return {
|
|
3398
|
+
success: false,
|
|
3399
|
+
message: `ERRO: não foi possível rolar na horizontal: ${error2}`
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Execute arbitrary JavaScript on the page.
|
|
3405
|
+
* The optional `signal` is exposed to the script scope so cooperative code
|
|
3406
|
+
* can abort promptly when the task is stopped.
|
|
3407
|
+
*/
|
|
3408
|
+
async executeJavascript(script, signal) {
|
|
3409
|
+
try {
|
|
3410
|
+
const asyncFunction = eval(`(async (signal) => { ${script} })`);
|
|
3411
|
+
const result = await asyncFunction(signal);
|
|
3412
|
+
return {
|
|
3413
|
+
success: true,
|
|
3414
|
+
message: `OK: JavaScript executado. Resultado: ${result}`
|
|
3415
|
+
};
|
|
3416
|
+
} catch (error2) {
|
|
3417
|
+
return {
|
|
3418
|
+
success: false,
|
|
3419
|
+
message: `ERRO: falha ao executar JavaScript: ${error2}`
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
// ======= Mask Operations =======
|
|
3424
|
+
/**
|
|
3425
|
+
* Show the visual mask overlay.
|
|
3426
|
+
* Only works after mask is setup.
|
|
3427
|
+
*/
|
|
3428
|
+
async showMask() {
|
|
3429
|
+
await this.maskReady;
|
|
3430
|
+
this.mask?.show();
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Hide the visual mask overlay.
|
|
3434
|
+
* Only works after mask is setup.
|
|
3435
|
+
*/
|
|
3436
|
+
async hideMask() {
|
|
3437
|
+
await this.maskReady;
|
|
3438
|
+
this.mask?.hide();
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Dispose and clean up resources
|
|
3442
|
+
*/
|
|
3443
|
+
dispose() {
|
|
3444
|
+
cleanUpHighlights();
|
|
3445
|
+
this.flatTree = null;
|
|
3446
|
+
this.selectorMap.clear();
|
|
3447
|
+
this.elementTextMap.clear();
|
|
3448
|
+
this.simplifiedHTML = "<EMPTY>";
|
|
3449
|
+
this.isIndexed = false;
|
|
3450
|
+
this.mask?.dispose();
|
|
3451
|
+
this.mask = null;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
const enUS = {
|
|
3455
|
+
ui: {
|
|
3456
|
+
panel: {
|
|
3457
|
+
ready: "Ready",
|
|
3458
|
+
thinking: "Thinking...",
|
|
3459
|
+
taskInput: "Enter new task, describe steps in detail, press Enter to submit",
|
|
3460
|
+
userAnswerPrompt: "Please answer the question above, press Enter to submit",
|
|
3461
|
+
taskTerminated: "Task terminated",
|
|
3462
|
+
taskCompleted: "Task completed",
|
|
3463
|
+
userAnswer: "User answer: {{input}}",
|
|
3464
|
+
question: "Question: {{question}}",
|
|
3465
|
+
waitingPlaceholder: "Waiting for task to start...",
|
|
3466
|
+
stop: "Stop",
|
|
3467
|
+
close: "Close",
|
|
3468
|
+
expand: "Expand history",
|
|
3469
|
+
collapse: "Collapse history",
|
|
3470
|
+
step: "Step {{number}}",
|
|
3471
|
+
// FORK: textos que estavam cravados em inglês dentro do Panel.ts.
|
|
3472
|
+
userTakeover: "User takeover",
|
|
3473
|
+
retrying: "Retrying",
|
|
3474
|
+
errorLabel: "Error",
|
|
3475
|
+
reflectionEvaluation: "Assessment",
|
|
3476
|
+
reflectionMemory: "Memory",
|
|
3477
|
+
reflectionNextGoal: "Next goal",
|
|
3478
|
+
// FORK: nome de quem fala em cada mensagem, e rótulos das regiões.
|
|
3479
|
+
authorPerson: "You",
|
|
3480
|
+
authorAgent: "Agent",
|
|
3481
|
+
authorReasoning: "Reasoning",
|
|
3482
|
+
authorAction: "Action",
|
|
3483
|
+
authorResult: "Result",
|
|
3484
|
+
authorPage: "Page",
|
|
3485
|
+
authorSystem: "System",
|
|
3486
|
+
historyLabel: "Agent activity history",
|
|
3487
|
+
panelLabel: "Agent panel"
|
|
3488
|
+
},
|
|
3489
|
+
tools: {
|
|
3490
|
+
clicking: "Clicking element [{{index}}]...",
|
|
3491
|
+
inputting: "Inputting text to element [{{index}}]...",
|
|
3492
|
+
selecting: 'Selecting option "{{text}}"...',
|
|
3493
|
+
scrolling: "Scrolling page...",
|
|
3494
|
+
waiting: "Waiting {{seconds}} seconds...",
|
|
3495
|
+
askingUser: "Asking user...",
|
|
3496
|
+
done: "Task done",
|
|
3497
|
+
clicked: "Clicked element [{{index}}]",
|
|
3498
|
+
inputted: 'Inputted text "{{text}}"',
|
|
3499
|
+
selected: 'Selected option "{{text}}"',
|
|
3500
|
+
scrolled: "Page scrolled",
|
|
3501
|
+
waited: "Wait completed",
|
|
3502
|
+
executing: "Executing {{toolName}}...",
|
|
3503
|
+
resultSuccess: "success",
|
|
3504
|
+
resultFailure: "failed",
|
|
3505
|
+
resultError: "error"
|
|
3506
|
+
},
|
|
3507
|
+
errors: {
|
|
3508
|
+
elementNotFound: "No interactive element found at index {{index}}",
|
|
3509
|
+
taskRequired: "Task description is required",
|
|
3510
|
+
executionFailed: "Task execution failed",
|
|
3511
|
+
notInputElement: "Element is not an input or textarea",
|
|
3512
|
+
notSelectElement: "Element is not a select element",
|
|
3513
|
+
optionNotFound: 'Option "{{text}}" not found'
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
const ptBR = {
|
|
3518
|
+
ui: {
|
|
3519
|
+
panel: {
|
|
3520
|
+
ready: "Pronto",
|
|
3521
|
+
thinking: "Pensando...",
|
|
3522
|
+
taskInput: "Descreva a próxima tarefa com os passos em detalhe e tecle Enter",
|
|
3523
|
+
userAnswerPrompt: "Responda à pergunta acima e tecle Enter",
|
|
3524
|
+
taskTerminated: "Tarefa interrompida",
|
|
3525
|
+
taskCompleted: "Tarefa concluída",
|
|
3526
|
+
userAnswer: "Resposta: {{input}}",
|
|
3527
|
+
question: "Pergunta: {{question}}",
|
|
3528
|
+
waitingPlaceholder: "Aguardando o início da tarefa...",
|
|
3529
|
+
stop: "Parar",
|
|
3530
|
+
close: "Fechar",
|
|
3531
|
+
expand: "Abrir histórico",
|
|
3532
|
+
collapse: "Fechar histórico",
|
|
3533
|
+
step: "Passo {{number}}",
|
|
3534
|
+
userTakeover: "A pessoa assumiu o controle",
|
|
3535
|
+
retrying: "Tentando de novo",
|
|
3536
|
+
errorLabel: "Erro",
|
|
3537
|
+
reflectionEvaluation: "Avaliação",
|
|
3538
|
+
reflectionMemory: "Memória",
|
|
3539
|
+
reflectionNextGoal: "Próximo passo",
|
|
3540
|
+
authorPerson: "Você",
|
|
3541
|
+
authorAgent: "Agente",
|
|
3542
|
+
authorReasoning: "Raciocínio",
|
|
3543
|
+
authorAction: "Ação",
|
|
3544
|
+
authorResult: "Resultado",
|
|
3545
|
+
authorPage: "Página",
|
|
3546
|
+
authorSystem: "Sistema",
|
|
3547
|
+
historyLabel: "Histórico de atividade do agente",
|
|
3548
|
+
panelLabel: "Painel do agente"
|
|
3549
|
+
},
|
|
3550
|
+
tools: {
|
|
3551
|
+
clicking: "Clicando no elemento [{{index}}]...",
|
|
3552
|
+
inputting: "Digitando no elemento [{{index}}]...",
|
|
3553
|
+
selecting: 'Escolhendo a opção "{{text}}"...',
|
|
3554
|
+
scrolling: "Rolando a página...",
|
|
3555
|
+
waiting: "Aguardando {{seconds}} segundos...",
|
|
3556
|
+
askingUser: "Perguntando à pessoa...",
|
|
3557
|
+
done: "Tarefa encerrada",
|
|
3558
|
+
clicked: "Elemento [{{index}}] clicado",
|
|
3559
|
+
inputted: 'Texto "{{text}}" digitado',
|
|
3560
|
+
selected: 'Opção "{{text}}" escolhida',
|
|
3561
|
+
scrolled: "Página rolada",
|
|
3562
|
+
waited: "Espera concluída",
|
|
3563
|
+
executing: "Executando {{toolName}}...",
|
|
3564
|
+
resultSuccess: "sucesso",
|
|
3565
|
+
resultFailure: "falha",
|
|
3566
|
+
resultError: "erro"
|
|
3567
|
+
},
|
|
3568
|
+
errors: {
|
|
3569
|
+
elementNotFound: "Nenhum elemento interativo no índice {{index}}",
|
|
3570
|
+
taskRequired: "A descrição da tarefa é obrigatória",
|
|
3571
|
+
executionFailed: "A execução da tarefa falhou",
|
|
3572
|
+
notInputElement: "O elemento não é um campo de texto",
|
|
3573
|
+
notSelectElement: "O elemento não é uma lista de seleção",
|
|
3574
|
+
optionNotFound: 'Opção "{{text}}" não encontrada'
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
};
|
|
3578
|
+
const locales = {
|
|
3579
|
+
"en-US": enUS,
|
|
3580
|
+
"pt-BR": ptBR
|
|
3581
|
+
};
|
|
3582
|
+
class I18n {
|
|
3583
|
+
language;
|
|
3584
|
+
translations;
|
|
3585
|
+
/**
|
|
3586
|
+
* FORK: aceita qualquer identificador de língua, porque o corpo já trata o caso desconhecido
|
|
3587
|
+
* caindo em 'en-US'. O tipo estreito contradizia essa checagem: ou ela era morta, ou o tipo
|
|
3588
|
+
* mentia. O núcleo conhece línguas que o painel ainda não traduz, e isso é normal.
|
|
3589
|
+
*/
|
|
3590
|
+
constructor(language = "en-US") {
|
|
3591
|
+
this.language = language in locales ? language : "en-US";
|
|
3592
|
+
this.translations = locales[this.language];
|
|
3593
|
+
}
|
|
3594
|
+
// 类型安全的翻译方法
|
|
3595
|
+
t(key, params) {
|
|
3596
|
+
const value = this.getNestedValue(this.translations, key);
|
|
3597
|
+
if (!value) {
|
|
3598
|
+
console.warn(`Translation key "${key}" not found for language "${this.language}"`);
|
|
3599
|
+
return key;
|
|
3600
|
+
}
|
|
3601
|
+
if (params) {
|
|
3602
|
+
return this.interpolate(value, params);
|
|
3603
|
+
}
|
|
3604
|
+
return value;
|
|
3605
|
+
}
|
|
3606
|
+
getNestedValue(obj, path) {
|
|
3607
|
+
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
3608
|
+
}
|
|
3609
|
+
interpolate(template, params) {
|
|
3610
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
3611
|
+
return params[key] != null ? params[key].toString() : match;
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
getLanguage() {
|
|
3615
|
+
return this.language;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
function truncate(text, maxLength) {
|
|
3619
|
+
if (text.length > maxLength) {
|
|
3620
|
+
return text.substring(0, maxLength) + "...";
|
|
3621
|
+
}
|
|
3622
|
+
return text;
|
|
3623
|
+
}
|
|
3624
|
+
function escapeHtml(text) {
|
|
3625
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3626
|
+
}
|
|
3627
|
+
const wrapper = "_wrapper_1rq3n_1";
|
|
3628
|
+
const background = "_background_1rq3n_55";
|
|
3629
|
+
const header = "_header_1rq3n_115";
|
|
3630
|
+
const pulse = "_pulse_1rq3n_1";
|
|
3631
|
+
const retryPulse = "_retryPulse_1rq3n_1";
|
|
3632
|
+
const statusTextFadeOut = "_statusTextFadeOut_1rq3n_1";
|
|
3633
|
+
const statusTextFadeIn = "_statusTextFadeIn_1rq3n_1";
|
|
3634
|
+
const statusSection = "_statusSection_1rq3n_137";
|
|
3635
|
+
const indicator = "_indicator_1rq3n_144";
|
|
3636
|
+
const thinking = "_thinking_1rq3n_153";
|
|
3637
|
+
const tool_executing = "_tool_executing_1rq3n_158";
|
|
3638
|
+
const retry = "_retry_1rq3n_163";
|
|
3639
|
+
const completed = "_completed_1rq3n_169";
|
|
3640
|
+
const input = "_input_1rq3n_170";
|
|
3641
|
+
const output = "_output_1rq3n_171";
|
|
3642
|
+
const error = "_error_1rq3n_176";
|
|
3643
|
+
const statusText = "_statusText_1rq3n_182";
|
|
3644
|
+
const fadeOut = "_fadeOut_1rq3n_194";
|
|
3645
|
+
const fadeIn = "_fadeIn_1rq3n_198";
|
|
3646
|
+
const controls = "_controls_1rq3n_204";
|
|
3647
|
+
const controlButton = "_controlButton_1rq3n_209";
|
|
3648
|
+
const stopButton = "_stopButton_1rq3n_228";
|
|
3649
|
+
const historySectionWrapper = "_historySectionWrapper_1rq3n_262";
|
|
3650
|
+
const expanded = "_expanded_1rq3n_294";
|
|
3651
|
+
const historySection = "_historySection_1rq3n_262";
|
|
3652
|
+
const historyRow = "_historyRow_1rq3n_320";
|
|
3653
|
+
const fromPessoa = "_fromPessoa_1rq3n_328";
|
|
3654
|
+
const fromAgente = "_fromAgente_1rq3n_332";
|
|
3655
|
+
const fromSistema = "_fromSistema_1rq3n_333";
|
|
3656
|
+
const historyItem = "_historyItem_1rq3n_338";
|
|
3657
|
+
const observation = "_observation_1rq3n_374";
|
|
3658
|
+
const question = "_question_1rq3n_378";
|
|
3659
|
+
const doneSuccess = "_doneSuccess_1rq3n_382";
|
|
3660
|
+
const doneFailure = "_doneFailure_1rq3n_390";
|
|
3661
|
+
const historyHeader = "_historyHeader_1rq3n_402";
|
|
3662
|
+
const historyAuthor = "_historyAuthor_1rq3n_410";
|
|
3663
|
+
const historyMeta = "_historyMeta_1rq3n_416";
|
|
3664
|
+
const statusIcon = "_statusIcon_1rq3n_426";
|
|
3665
|
+
const historyContent = "_historyContent_1rq3n_439";
|
|
3666
|
+
const reflectionLine = "_reflectionLine_1rq3n_446";
|
|
3667
|
+
const reflectionLines = "_reflectionLines_1rq3n_451";
|
|
3668
|
+
const reflectionLabel = "_reflectionLabel_1rq3n_462";
|
|
3669
|
+
const inputSectionWrapper = "_inputSectionWrapper_1rq3n_544";
|
|
3670
|
+
const hidden = "_hidden_1rq3n_567";
|
|
3671
|
+
const inputSection = "_inputSection_1rq3n_544";
|
|
3672
|
+
const taskInput = "_taskInput_1rq3n_578";
|
|
3673
|
+
const celebrate = "_celebrate_1rq3n_1";
|
|
3674
|
+
const shimmer = "_shimmer_1rq3n_1";
|
|
3675
|
+
const styles = {
|
|
3676
|
+
wrapper,
|
|
3677
|
+
"mask-running": "_mask-running_1rq3n_1",
|
|
3678
|
+
background,
|
|
3679
|
+
header,
|
|
3680
|
+
pulse,
|
|
3681
|
+
retryPulse,
|
|
3682
|
+
statusTextFadeOut,
|
|
3683
|
+
statusTextFadeIn,
|
|
3684
|
+
statusSection,
|
|
3685
|
+
indicator,
|
|
3686
|
+
thinking,
|
|
3687
|
+
tool_executing,
|
|
3688
|
+
retry,
|
|
3689
|
+
completed,
|
|
3690
|
+
input,
|
|
3691
|
+
output,
|
|
3692
|
+
error,
|
|
3693
|
+
statusText,
|
|
3694
|
+
fadeOut,
|
|
3695
|
+
fadeIn,
|
|
3696
|
+
controls,
|
|
3697
|
+
controlButton,
|
|
3698
|
+
stopButton,
|
|
3699
|
+
historySectionWrapper,
|
|
3700
|
+
expanded,
|
|
3701
|
+
historySection,
|
|
3702
|
+
historyRow,
|
|
3703
|
+
fromPessoa,
|
|
3704
|
+
fromAgente,
|
|
3705
|
+
fromSistema,
|
|
3706
|
+
historyItem,
|
|
3707
|
+
observation,
|
|
3708
|
+
question,
|
|
3709
|
+
doneSuccess,
|
|
3710
|
+
doneFailure,
|
|
3711
|
+
historyHeader,
|
|
3712
|
+
historyAuthor,
|
|
3713
|
+
historyMeta,
|
|
3714
|
+
statusIcon,
|
|
3715
|
+
historyContent,
|
|
3716
|
+
reflectionLine,
|
|
3717
|
+
reflectionLines,
|
|
3718
|
+
reflectionLabel,
|
|
3719
|
+
inputSectionWrapper,
|
|
3720
|
+
hidden,
|
|
3721
|
+
inputSection,
|
|
3722
|
+
taskInput,
|
|
3723
|
+
celebrate,
|
|
3724
|
+
shimmer
|
|
3725
|
+
};
|
|
3726
|
+
function createCard({ icon, content, meta, type, from = "agente", author }) {
|
|
3727
|
+
const typeClass = type ? styles[type] : "";
|
|
3728
|
+
const fromClass = from === "pessoa" ? styles.fromPessoa : from === "sistema" ? styles.fromSistema : styles.fromAgente;
|
|
3729
|
+
const contentHtml = Array.isArray(content) ? `<div class="${styles.reflectionLines}">${content.map(
|
|
3730
|
+
(line) => `<span class="${styles.reflectionLine}"><span class="${styles.reflectionLabel}">${escapeHtml(line.label)}</span>${escapeHtml(line.text)}</span>`
|
|
3731
|
+
).join("")}</div>` : `<span>${escapeHtml(content)}</span>`;
|
|
3732
|
+
const cabecalho = `
|
|
3733
|
+
<div class="${styles.historyHeader}">
|
|
3734
|
+
<span class="${styles.statusIcon}">${icon}</span>
|
|
3735
|
+
<span class="${styles.historyAuthor}" data-autor="${from}">${escapeHtml(author || "")}</span>
|
|
3736
|
+
${meta ? `<span class="${styles.historyMeta}">${escapeHtml(meta)}</span>` : ""}
|
|
3737
|
+
</div>`;
|
|
3738
|
+
return `
|
|
3739
|
+
<div class="${styles.historyRow} ${fromClass}">
|
|
3740
|
+
<div class="${styles.historyItem} ${typeClass}" data-mensagem="${from}">
|
|
3741
|
+
${cabecalho}
|
|
3742
|
+
<div class="${styles.historyContent}" data-conteudo>${contentHtml}</div>
|
|
3743
|
+
</div>
|
|
3744
|
+
</div>
|
|
3745
|
+
`;
|
|
3746
|
+
}
|
|
3747
|
+
function createReflectionLines(reflection, labels) {
|
|
3748
|
+
const lines = [];
|
|
3749
|
+
if (reflection.evaluation_previous_goal) {
|
|
3750
|
+
lines.push({ label: labels.evaluation, text: reflection.evaluation_previous_goal });
|
|
3751
|
+
}
|
|
3752
|
+
if (reflection.memory) {
|
|
3753
|
+
lines.push({ label: labels.memory, text: reflection.memory });
|
|
3754
|
+
}
|
|
3755
|
+
if (reflection.next_goal) {
|
|
3756
|
+
lines.push({ label: labels.nextGoal, text: reflection.next_goal });
|
|
3757
|
+
}
|
|
3758
|
+
return lines;
|
|
3759
|
+
}
|
|
3760
|
+
const svg = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
|
|
3761
|
+
const iconTask = svg('<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2"/>');
|
|
3762
|
+
const iconThinking = svg('<path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8z"/><path d="M18 15l.9 2.1L21 18l-2.1.9L18 21l-.9-2.1L15 18l2.1-.9z"/>');
|
|
3763
|
+
const iconObservation = svg('<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.5"/>');
|
|
3764
|
+
const iconUser = svg('<circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5"/>');
|
|
3765
|
+
const iconRetry = svg('<path d="M20 11a8 8 0 1 0-2.3 6.3"/><path d="M20 5v6h-6"/>');
|
|
3766
|
+
const iconError = svg('<circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.2h.01"/>');
|
|
3767
|
+
const iconDone = svg('<circle cx="12" cy="12" r="9"/><path d="M8.2 12.3l2.6 2.6 5-5.2"/>');
|
|
3768
|
+
const iconQuestion = svg('<circle cx="12" cy="12" r="9"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.3 2.4c-.6.2-.9.8-.9 1.4v.3"/><path d="M12 16.6h.01"/>');
|
|
3769
|
+
const iconAnswer = svg('<path d="M20 14.5a2.5 2.5 0 0 1-2.5 2.5H9l-5 3.5V6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5z"/>');
|
|
3770
|
+
const iconTool = svg('<path d="M5 7.5l3.5 3.5L5 14.5"/><path d="M11.5 15.5H19"/><rect x="2.5" y="3.5" width="19" height="17" rx="2.5"/>');
|
|
3771
|
+
class Panel {
|
|
3772
|
+
#wrapper;
|
|
3773
|
+
#indicator;
|
|
3774
|
+
#statusText;
|
|
3775
|
+
#historySection;
|
|
3776
|
+
#expandButton;
|
|
3777
|
+
#actionButton;
|
|
3778
|
+
#inputSection;
|
|
3779
|
+
#taskInput;
|
|
3780
|
+
#agent;
|
|
3781
|
+
#config;
|
|
3782
|
+
#isExpanded = false;
|
|
3783
|
+
#i18n;
|
|
3784
|
+
#userAnswerResolver = null;
|
|
3785
|
+
#isWaitingForUserAnswer = false;
|
|
3786
|
+
#headerUpdateTimer = null;
|
|
3787
|
+
#pendingHeaderText = null;
|
|
3788
|
+
#isAnimating = false;
|
|
3789
|
+
// Event handlers (bound for removal)
|
|
3790
|
+
#onStatusChange = () => this.#handleStatusChange();
|
|
3791
|
+
#onHistoryChange = () => this.#handleHistoryChange();
|
|
3792
|
+
#onActivity = (e) => this.#handleActivity(e.detail);
|
|
3793
|
+
#onAgentDispose = () => this.dispose();
|
|
3794
|
+
get wrapper() {
|
|
3795
|
+
return this.#wrapper;
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Create a Panel bound to an agent
|
|
3799
|
+
* @param agent - Agent instance that implements PanelAgentAdapter
|
|
3800
|
+
* @param config - Optional panel configuration
|
|
3801
|
+
*/
|
|
3802
|
+
constructor(agent, config = {}) {
|
|
3803
|
+
this.#agent = agent;
|
|
3804
|
+
this.#config = config;
|
|
3805
|
+
this.#i18n = new I18n(config.language ?? "en-US");
|
|
3806
|
+
this.#agent.onAskUser = (question2, options) => this.#askUser(question2, options?.signal);
|
|
3807
|
+
this.#wrapper = this.#createWrapper();
|
|
3808
|
+
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`);
|
|
3809
|
+
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`);
|
|
3810
|
+
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`);
|
|
3811
|
+
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`);
|
|
3812
|
+
this.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`);
|
|
3813
|
+
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`);
|
|
3814
|
+
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`);
|
|
3815
|
+
this.#agent.addEventListener("statuschange", this.#onStatusChange);
|
|
3816
|
+
this.#agent.addEventListener("historychange", this.#onHistoryChange);
|
|
3817
|
+
this.#agent.addEventListener("activity", this.#onActivity);
|
|
3818
|
+
this.#agent.addEventListener("dispose", this.#onAgentDispose);
|
|
3819
|
+
this.#setupEventListeners();
|
|
3820
|
+
this.#startHeaderUpdateLoop();
|
|
3821
|
+
this.#showInputArea();
|
|
3822
|
+
this.hide();
|
|
3823
|
+
}
|
|
3824
|
+
// ========== Agent event handlers ==========
|
|
3825
|
+
/** Handle agent status change */
|
|
3826
|
+
#handleStatusChange() {
|
|
3827
|
+
const status = this.#agent.status;
|
|
3828
|
+
const failed = status === "completed" && this.#agent.lastResult?.success === false;
|
|
3829
|
+
this.#updateStatusIndicator(failed ? "error" : status);
|
|
3830
|
+
if (status === "running") {
|
|
3831
|
+
this.#actionButton.textContent = "■";
|
|
3832
|
+
this.#actionButton.title = this.#i18n.t("ui.panel.stop");
|
|
3833
|
+
} else {
|
|
3834
|
+
this.#actionButton.textContent = "X";
|
|
3835
|
+
this.#actionButton.title = this.#i18n.t("ui.panel.close");
|
|
3836
|
+
}
|
|
3837
|
+
if (status === "running") {
|
|
3838
|
+
this.show();
|
|
3839
|
+
this.#hideInputArea();
|
|
3840
|
+
}
|
|
3841
|
+
if (status === "completed" || status === "error" || status === "stopped") {
|
|
3842
|
+
if (!this.#isExpanded) {
|
|
3843
|
+
this.#expand();
|
|
3844
|
+
}
|
|
3845
|
+
if (this.#shouldShowInputArea()) {
|
|
3846
|
+
this.#showInputArea();
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
/** Handle agent history change - re-render history list from agent.history */
|
|
3851
|
+
#handleHistoryChange() {
|
|
3852
|
+
this.#renderHistory();
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Handle agent activity - transient state for immediate UI feedback
|
|
3856
|
+
* Activity events are NOT persisted in history, only used for header bar updates
|
|
3857
|
+
*/
|
|
3858
|
+
#handleActivity(activity) {
|
|
3859
|
+
switch (activity.type) {
|
|
3860
|
+
case "thinking":
|
|
3861
|
+
this.#pendingHeaderText = this.#i18n.t("ui.panel.thinking");
|
|
3862
|
+
this.#updateStatusIndicator("thinking");
|
|
3863
|
+
break;
|
|
3864
|
+
case "executing":
|
|
3865
|
+
this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input);
|
|
3866
|
+
this.#updateStatusIndicator("executing");
|
|
3867
|
+
break;
|
|
3868
|
+
case "executed":
|
|
3869
|
+
this.#pendingHeaderText = truncate(activity.output, 50);
|
|
3870
|
+
break;
|
|
3871
|
+
case "retrying":
|
|
3872
|
+
this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`;
|
|
3873
|
+
this.#updateStatusIndicator("retrying");
|
|
3874
|
+
break;
|
|
3875
|
+
case "error":
|
|
3876
|
+
this.#pendingHeaderText = truncate(activity.message, 50);
|
|
3877
|
+
this.#updateStatusIndicator("error");
|
|
3878
|
+
break;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Ask for user input (internal, called by agent via onAskUser).
|
|
3883
|
+
* Rejects when `signal` aborts (task stopped or disposed), cleaning up the
|
|
3884
|
+
* question card and pending state so the agent loop can settle.
|
|
3885
|
+
*/
|
|
3886
|
+
#askUser(question2, signal2) {
|
|
3887
|
+
return new Promise((resolve, reject) => {
|
|
3888
|
+
this.#isWaitingForUserAnswer = true;
|
|
3889
|
+
this.#userAnswerResolver = resolve;
|
|
3890
|
+
if (!this.#isExpanded) {
|
|
3891
|
+
this.#expand();
|
|
3892
|
+
}
|
|
3893
|
+
const tempCard = document.createElement("div");
|
|
3894
|
+
tempCard.innerHTML = createCard({
|
|
3895
|
+
icon: iconQuestion,
|
|
3896
|
+
content: this.#i18n.t("ui.panel.question", { question: question2 }),
|
|
3897
|
+
type: "question",
|
|
3898
|
+
author: this.#i18n.t("ui.panel.authorAgent")
|
|
3899
|
+
});
|
|
3900
|
+
const cardElement = tempCard.firstElementChild;
|
|
3901
|
+
cardElement.setAttribute("data-temp-card", "true");
|
|
3902
|
+
this.#historySection.appendChild(cardElement);
|
|
3903
|
+
this.#scrollToBottom();
|
|
3904
|
+
this.#showInputArea(this.#i18n.t("ui.panel.userAnswerPrompt"));
|
|
3905
|
+
signal2?.addEventListener(
|
|
3906
|
+
"abort",
|
|
3907
|
+
() => {
|
|
3908
|
+
this.#removeTempCards();
|
|
3909
|
+
this.#isWaitingForUserAnswer = false;
|
|
3910
|
+
this.#userAnswerResolver = null;
|
|
3911
|
+
reject(signal2.reason);
|
|
3912
|
+
},
|
|
3913
|
+
{ once: true }
|
|
3914
|
+
);
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
/** Remove temporary question cards (only direct children for safety) */
|
|
3918
|
+
#removeTempCards() {
|
|
3919
|
+
Array.from(this.#historySection.children).forEach((child) => {
|
|
3920
|
+
if (child.getAttribute("data-temp-card") === "true") {
|
|
3921
|
+
child.remove();
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3925
|
+
// ========== Public control methods ==========
|
|
3926
|
+
show() {
|
|
3927
|
+
this.wrapper.style.display = "block";
|
|
3928
|
+
this.wrapper.getBoundingClientRect();
|
|
3929
|
+
this.wrapper.style.opacity = "1";
|
|
3930
|
+
this.wrapper.style.transform = "translateX(-50%) translateY(0)";
|
|
3931
|
+
}
|
|
3932
|
+
hide() {
|
|
3933
|
+
this.wrapper.style.opacity = "0";
|
|
3934
|
+
this.wrapper.style.transform = "translateX(-50%) translateY(20px)";
|
|
3935
|
+
this.wrapper.style.display = "none";
|
|
3936
|
+
}
|
|
3937
|
+
reset() {
|
|
3938
|
+
this.#statusText.textContent = this.#i18n.t("ui.panel.ready");
|
|
3939
|
+
this.#updateStatusIndicator("thinking");
|
|
3940
|
+
this.#renderHistory();
|
|
3941
|
+
this.#collapse();
|
|
3942
|
+
this.#isWaitingForUserAnswer = false;
|
|
3943
|
+
this.#userAnswerResolver = null;
|
|
3944
|
+
this.#showInputArea();
|
|
3945
|
+
}
|
|
3946
|
+
expand() {
|
|
3947
|
+
this.#expand();
|
|
3948
|
+
}
|
|
3949
|
+
collapse() {
|
|
3950
|
+
this.#collapse();
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Dispose panel and clean up event listeners
|
|
3954
|
+
*/
|
|
3955
|
+
dispose() {
|
|
3956
|
+
this.#agent.removeEventListener("statuschange", this.#onStatusChange);
|
|
3957
|
+
this.#agent.removeEventListener("historychange", this.#onHistoryChange);
|
|
3958
|
+
this.#agent.removeEventListener("activity", this.#onActivity);
|
|
3959
|
+
this.#agent.removeEventListener("dispose", this.#onAgentDispose);
|
|
3960
|
+
this.#isWaitingForUserAnswer = false;
|
|
3961
|
+
this.#stopHeaderUpdateLoop();
|
|
3962
|
+
this.wrapper.remove();
|
|
3963
|
+
}
|
|
3964
|
+
// ========== Private methods ==========
|
|
3965
|
+
#getToolExecutingText(toolName, args) {
|
|
3966
|
+
const a = args;
|
|
3967
|
+
switch (toolName) {
|
|
3968
|
+
case "click_element_by_index":
|
|
3969
|
+
return this.#i18n.t("ui.tools.clicking", { index: a.index });
|
|
3970
|
+
case "input_text":
|
|
3971
|
+
return this.#i18n.t("ui.tools.inputting", { index: a.index });
|
|
3972
|
+
case "select_dropdown_option":
|
|
3973
|
+
return this.#i18n.t("ui.tools.selecting", { text: a.text });
|
|
3974
|
+
case "scroll":
|
|
3975
|
+
return this.#i18n.t("ui.tools.scrolling");
|
|
3976
|
+
case "wait":
|
|
3977
|
+
return this.#i18n.t("ui.tools.waiting", { seconds: a.seconds });
|
|
3978
|
+
case "ask_user":
|
|
3979
|
+
return this.#i18n.t("ui.tools.askingUser");
|
|
3980
|
+
case "done":
|
|
3981
|
+
return this.#i18n.t("ui.tools.done");
|
|
3982
|
+
default:
|
|
3983
|
+
return this.#i18n.t("ui.tools.executing", { toolName });
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Action button handler: stop when running, close (dispose) when idle
|
|
3988
|
+
*/
|
|
3989
|
+
#handleActionButton() {
|
|
3990
|
+
if (this.#agent.status === "running") {
|
|
3991
|
+
this.#agent.stop();
|
|
3992
|
+
} else {
|
|
3993
|
+
this.#agent.dispose();
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Submit task
|
|
3998
|
+
*/
|
|
3999
|
+
#submitTask() {
|
|
4000
|
+
const input2 = this.#taskInput.value.trim();
|
|
4001
|
+
if (!input2) return;
|
|
4002
|
+
this.#hideInputArea();
|
|
4003
|
+
if (this.#isWaitingForUserAnswer) {
|
|
4004
|
+
this.#handleUserAnswer(input2);
|
|
4005
|
+
} else {
|
|
4006
|
+
this.#agent.execute(input2);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Handle user answer
|
|
4011
|
+
*/
|
|
4012
|
+
#handleUserAnswer(input2) {
|
|
4013
|
+
this.#removeTempCards();
|
|
4014
|
+
this.#isWaitingForUserAnswer = false;
|
|
4015
|
+
if (this.#userAnswerResolver) {
|
|
4016
|
+
this.#userAnswerResolver(input2);
|
|
4017
|
+
this.#userAnswerResolver = null;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Show input area
|
|
4022
|
+
*/
|
|
4023
|
+
#showInputArea(placeholder) {
|
|
4024
|
+
this.#taskInput.value = "";
|
|
4025
|
+
this.#taskInput.placeholder = placeholder || this.#i18n.t("ui.panel.taskInput");
|
|
4026
|
+
this.#inputSection.classList.remove(styles.hidden);
|
|
4027
|
+
setTimeout(() => {
|
|
4028
|
+
this.#taskInput.focus();
|
|
4029
|
+
}, 100);
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Hide input area
|
|
4033
|
+
*/
|
|
4034
|
+
#hideInputArea() {
|
|
4035
|
+
this.#inputSection.classList.add(styles.hidden);
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Check if input area should be shown
|
|
4039
|
+
*/
|
|
4040
|
+
#shouldShowInputArea() {
|
|
4041
|
+
if (this.#isWaitingForUserAnswer) return true;
|
|
4042
|
+
const history = this.#agent.history;
|
|
4043
|
+
if (history.length === 0) {
|
|
4044
|
+
return true;
|
|
4045
|
+
}
|
|
4046
|
+
const status = this.#agent.status;
|
|
4047
|
+
const isTaskEnded = status === "completed" || status === "error" || status === "stopped";
|
|
4048
|
+
if (isTaskEnded) {
|
|
4049
|
+
return this.#config.promptForNextTask ?? true;
|
|
4050
|
+
}
|
|
4051
|
+
return false;
|
|
4052
|
+
}
|
|
4053
|
+
#createWrapper() {
|
|
4054
|
+
const taskInputMaxLength = 1e3;
|
|
4055
|
+
const wrapper2 = document.createElement("div");
|
|
4056
|
+
wrapper2.id = "page-agent-runtime_agent-panel";
|
|
4057
|
+
wrapper2.className = styles.wrapper;
|
|
4058
|
+
wrapper2.setAttribute("data-browser-use-ignore", "true");
|
|
4059
|
+
wrapper2.setAttribute("data-page-agent-ignore", "true");
|
|
4060
|
+
wrapper2.setAttribute("role", "complementary");
|
|
4061
|
+
wrapper2.setAttribute("aria-label", this.#i18n.t("ui.panel.panelLabel"));
|
|
4062
|
+
wrapper2.innerHTML = `
|
|
4063
|
+
<div class="${styles.background}"></div>
|
|
4064
|
+
<div class="${styles.historySectionWrapper}">
|
|
4065
|
+
<!-- FORK: role=log + aria-live faz o leitor de tela anunciar cada mensagem nova sem
|
|
4066
|
+
roubar o foco de quem está usando a página. aria-relevant=additions evita
|
|
4067
|
+
reanunciar o histórico inteiro a cada render. -->
|
|
4068
|
+
<div
|
|
4069
|
+
class="${styles.historySection}"
|
|
4070
|
+
role="log"
|
|
4071
|
+
aria-live="polite"
|
|
4072
|
+
aria-relevant="additions"
|
|
4073
|
+
aria-label="${this.#i18n.t("ui.panel.historyLabel")}"
|
|
4074
|
+
>
|
|
4075
|
+
<div class="${styles.historyRow} ${styles.fromSistema}">
|
|
4076
|
+
<div class="${styles.historyItem}" data-mensagem="sistema">
|
|
4077
|
+
<div class="${styles.historyHeader}">
|
|
4078
|
+
<span class="${styles.statusIcon}">${iconThinking}</span>
|
|
4079
|
+
<span class="${styles.historyAuthor}" data-autor="sistema">${this.#i18n.t("ui.panel.authorSystem")}</span>
|
|
4080
|
+
</div>
|
|
4081
|
+
<div class="${styles.historyContent}" data-conteudo><span>${this.#i18n.t("ui.panel.waitingPlaceholder")}</span></div>
|
|
4082
|
+
</div>
|
|
4083
|
+
</div>
|
|
4084
|
+
</div>
|
|
4085
|
+
</div>
|
|
4086
|
+
<div class="${styles.header}">
|
|
4087
|
+
<div class="${styles.statusSection}">
|
|
4088
|
+
<div class="${styles.indicator} ${styles.thinking}"></div>
|
|
4089
|
+
<div class="${styles.statusText}">${this.#i18n.t("ui.panel.ready")}</div>
|
|
4090
|
+
</div>
|
|
4091
|
+
<div class="${styles.controls}">
|
|
4092
|
+
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t("ui.panel.expand")}">
|
|
4093
|
+
▼
|
|
4094
|
+
</button>
|
|
4095
|
+
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t("ui.panel.close")}">
|
|
4096
|
+
X
|
|
4097
|
+
</button>
|
|
4098
|
+
</div>
|
|
4099
|
+
</div>
|
|
4100
|
+
<div class="${styles.inputSectionWrapper} ${styles.hidden}">
|
|
4101
|
+
<div class="${styles.inputSection}">
|
|
4102
|
+
<input
|
|
4103
|
+
type="text"
|
|
4104
|
+
class="${styles.taskInput}"
|
|
4105
|
+
maxlength="${taskInputMaxLength}"
|
|
4106
|
+
/>
|
|
4107
|
+
</div>
|
|
4108
|
+
</div>
|
|
4109
|
+
`;
|
|
4110
|
+
document.body.appendChild(wrapper2);
|
|
4111
|
+
return wrapper2;
|
|
4112
|
+
}
|
|
4113
|
+
#setupEventListeners() {
|
|
4114
|
+
const header2 = this.wrapper.querySelector(`.${styles.header}`);
|
|
4115
|
+
header2.addEventListener("click", (e) => {
|
|
4116
|
+
if (e.target.closest(`.${styles.controlButton}`)) {
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
this.#toggle();
|
|
4120
|
+
});
|
|
4121
|
+
this.#expandButton.addEventListener("click", (e) => {
|
|
4122
|
+
e.stopPropagation();
|
|
4123
|
+
this.#toggle();
|
|
4124
|
+
});
|
|
4125
|
+
this.#actionButton.addEventListener("click", (e) => {
|
|
4126
|
+
e.stopPropagation();
|
|
4127
|
+
this.#handleActionButton();
|
|
4128
|
+
});
|
|
4129
|
+
this.#taskInput.addEventListener("keydown", (e) => {
|
|
4130
|
+
if (e.isComposing) return;
|
|
4131
|
+
if (e.key === "Enter") {
|
|
4132
|
+
e.preventDefault();
|
|
4133
|
+
this.#submitTask();
|
|
4134
|
+
}
|
|
4135
|
+
});
|
|
4136
|
+
this.#inputSection.addEventListener("click", (e) => {
|
|
4137
|
+
e.stopPropagation();
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
#toggle() {
|
|
4141
|
+
if (this.#isExpanded) {
|
|
4142
|
+
this.#collapse();
|
|
4143
|
+
} else {
|
|
4144
|
+
this.#expand();
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
#expand() {
|
|
4148
|
+
this.#isExpanded = true;
|
|
4149
|
+
this.wrapper.classList.add(styles.expanded);
|
|
4150
|
+
this.#expandButton.textContent = "▲";
|
|
4151
|
+
}
|
|
4152
|
+
#collapse() {
|
|
4153
|
+
this.#isExpanded = false;
|
|
4154
|
+
this.wrapper.classList.remove(styles.expanded);
|
|
4155
|
+
this.#expandButton.textContent = "▼";
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* Start periodic header update loop
|
|
4159
|
+
*/
|
|
4160
|
+
#startHeaderUpdateLoop() {
|
|
4161
|
+
this.#headerUpdateTimer = setInterval(() => {
|
|
4162
|
+
this.#checkAndUpdateHeader();
|
|
4163
|
+
}, 450);
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Stop periodic header update loop
|
|
4167
|
+
*/
|
|
4168
|
+
#stopHeaderUpdateLoop() {
|
|
4169
|
+
if (this.#headerUpdateTimer) {
|
|
4170
|
+
clearInterval(this.#headerUpdateTimer);
|
|
4171
|
+
this.#headerUpdateTimer = null;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
/**
|
|
4175
|
+
* Check if header needs update and trigger animation if not currently animating
|
|
4176
|
+
*/
|
|
4177
|
+
#checkAndUpdateHeader() {
|
|
4178
|
+
if (!this.#pendingHeaderText || this.#isAnimating) {
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
if (this.#statusText.textContent === this.#pendingHeaderText) {
|
|
4182
|
+
this.#pendingHeaderText = null;
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
const textToShow = this.#pendingHeaderText;
|
|
4186
|
+
this.#pendingHeaderText = null;
|
|
4187
|
+
this.#animateTextChange(textToShow);
|
|
4188
|
+
}
|
|
4189
|
+
/**
|
|
4190
|
+
* Animate text change with fade out/in effect
|
|
4191
|
+
*/
|
|
4192
|
+
#animateTextChange(newText) {
|
|
4193
|
+
this.#isAnimating = true;
|
|
4194
|
+
this.#statusText.classList.add(styles.fadeOut);
|
|
4195
|
+
setTimeout(() => {
|
|
4196
|
+
this.#statusText.textContent = newText;
|
|
4197
|
+
this.#statusText.classList.remove(styles.fadeOut);
|
|
4198
|
+
this.#statusText.classList.add(styles.fadeIn);
|
|
4199
|
+
setTimeout(() => {
|
|
4200
|
+
this.#statusText.classList.remove(styles.fadeIn);
|
|
4201
|
+
this.#isAnimating = false;
|
|
4202
|
+
}, 300);
|
|
4203
|
+
}, 150);
|
|
4204
|
+
}
|
|
4205
|
+
#updateStatusIndicator(type) {
|
|
4206
|
+
const variant = type === "running" ? "thinking" : type;
|
|
4207
|
+
this.#indicator.className = styles.indicator;
|
|
4208
|
+
if (variant !== "idle" && variant !== "stopped") {
|
|
4209
|
+
this.#indicator.classList.add(styles[variant]);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
#scrollToBottom() {
|
|
4213
|
+
setTimeout(() => {
|
|
4214
|
+
this.#historySection.scrollTop = this.#historySection.scrollHeight;
|
|
4215
|
+
}, 0);
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Render history directly from agent.history
|
|
4219
|
+
*
|
|
4220
|
+
* Renders:
|
|
4221
|
+
* 1. Task (first item, from agent.task)
|
|
4222
|
+
* 2. Reflection cards (evaluation, memory, next_goal)
|
|
4223
|
+
* 3. Tool execution with output
|
|
4224
|
+
* 4. Observations
|
|
4225
|
+
*/
|
|
4226
|
+
#renderHistory() {
|
|
4227
|
+
const items = [];
|
|
4228
|
+
const task = this.#agent.task;
|
|
4229
|
+
if (task) {
|
|
4230
|
+
items.push(this.#createTaskCard(task));
|
|
4231
|
+
}
|
|
4232
|
+
const history = this.#agent.history;
|
|
4233
|
+
for (const event of history) {
|
|
4234
|
+
items.push(...this.#createHistoryCards(event));
|
|
4235
|
+
}
|
|
4236
|
+
this.#historySection.innerHTML = items.join("");
|
|
4237
|
+
this.#scrollToBottom();
|
|
4238
|
+
}
|
|
4239
|
+
#createTaskCard(task) {
|
|
4240
|
+
return createCard({
|
|
4241
|
+
icon: iconTask,
|
|
4242
|
+
content: task,
|
|
4243
|
+
type: "input",
|
|
4244
|
+
from: "pessoa",
|
|
4245
|
+
author: this.#i18n.t("ui.panel.authorPerson")
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
/** Create cards for a history event */
|
|
4249
|
+
#createHistoryCards(event) {
|
|
4250
|
+
const cards = [];
|
|
4251
|
+
const meta = event.type === "step" && event.stepIndex !== void 0 ? this.#i18n.t("ui.panel.step", {
|
|
4252
|
+
number: (event.stepIndex + 1).toString()
|
|
4253
|
+
}) : void 0;
|
|
4254
|
+
if (event.type === "step") {
|
|
4255
|
+
if (event.reflection) {
|
|
4256
|
+
const lines = createReflectionLines(event.reflection, {
|
|
4257
|
+
evaluation: this.#i18n.t("ui.panel.reflectionEvaluation"),
|
|
4258
|
+
memory: this.#i18n.t("ui.panel.reflectionMemory"),
|
|
4259
|
+
nextGoal: this.#i18n.t("ui.panel.reflectionNextGoal")
|
|
4260
|
+
});
|
|
4261
|
+
if (lines.length > 0) {
|
|
4262
|
+
cards.push(
|
|
4263
|
+
createCard({
|
|
4264
|
+
icon: iconThinking,
|
|
4265
|
+
content: lines,
|
|
4266
|
+
meta,
|
|
4267
|
+
author: this.#i18n.t("ui.panel.authorReasoning")
|
|
4268
|
+
})
|
|
4269
|
+
);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
const action = event.action;
|
|
4273
|
+
if (action) {
|
|
4274
|
+
cards.push(...this.#createActionCards(action, meta));
|
|
4275
|
+
}
|
|
4276
|
+
} else if (event.type === "observation") {
|
|
4277
|
+
cards.push(
|
|
4278
|
+
createCard({
|
|
4279
|
+
icon: iconObservation,
|
|
4280
|
+
content: event.content || "",
|
|
4281
|
+
meta,
|
|
4282
|
+
type: "observation",
|
|
4283
|
+
author: this.#i18n.t("ui.panel.authorPage")
|
|
4284
|
+
})
|
|
4285
|
+
);
|
|
4286
|
+
} else if (event.type === "user_takeover") {
|
|
4287
|
+
cards.push(
|
|
4288
|
+
createCard({
|
|
4289
|
+
icon: iconUser,
|
|
4290
|
+
content: this.#i18n.t("ui.panel.userTakeover"),
|
|
4291
|
+
meta,
|
|
4292
|
+
type: "input",
|
|
4293
|
+
from: "pessoa",
|
|
4294
|
+
author: this.#i18n.t("ui.panel.authorPerson")
|
|
4295
|
+
})
|
|
4296
|
+
);
|
|
4297
|
+
} else if (event.type === "retry") {
|
|
4298
|
+
const retryInfo = `${event.message || this.#i18n.t("ui.panel.retrying")} (${event.attempt}/${event.maxAttempts})`;
|
|
4299
|
+
cards.push(
|
|
4300
|
+
createCard({
|
|
4301
|
+
icon: iconRetry,
|
|
4302
|
+
content: retryInfo,
|
|
4303
|
+
meta,
|
|
4304
|
+
type: "observation",
|
|
4305
|
+
from: "sistema",
|
|
4306
|
+
author: this.#i18n.t("ui.panel.authorSystem")
|
|
4307
|
+
})
|
|
4308
|
+
);
|
|
4309
|
+
} else if (event.type === "error") {
|
|
4310
|
+
cards.push(
|
|
4311
|
+
createCard({
|
|
4312
|
+
icon: iconError,
|
|
4313
|
+
content: event.message || this.#i18n.t("ui.panel.errorLabel"),
|
|
4314
|
+
meta,
|
|
4315
|
+
type: "observation",
|
|
4316
|
+
from: "sistema",
|
|
4317
|
+
author: this.#i18n.t("ui.panel.authorSystem")
|
|
4318
|
+
})
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
4321
|
+
return cards;
|
|
4322
|
+
}
|
|
4323
|
+
/** Create cards for an action */
|
|
4324
|
+
#createActionCards(action, meta) {
|
|
4325
|
+
const cards = [];
|
|
4326
|
+
if (action.name === "done") {
|
|
4327
|
+
const input2 = action.input;
|
|
4328
|
+
const text = input2.text || action.output || "";
|
|
4329
|
+
if (text) {
|
|
4330
|
+
cards.push(
|
|
4331
|
+
createCard({
|
|
4332
|
+
icon: iconDone,
|
|
4333
|
+
content: text,
|
|
4334
|
+
meta,
|
|
4335
|
+
type: "output",
|
|
4336
|
+
author: this.#i18n.t("ui.panel.authorAgent")
|
|
4337
|
+
})
|
|
4338
|
+
);
|
|
4339
|
+
}
|
|
4340
|
+
} else if (action.name === "ask_user") {
|
|
4341
|
+
const input2 = action.input;
|
|
4342
|
+
const answer = action.output.replace(/^User answered:\s*/i, "");
|
|
4343
|
+
cards.push(
|
|
4344
|
+
createCard({
|
|
4345
|
+
icon: iconQuestion,
|
|
4346
|
+
content: this.#i18n.t("ui.panel.question", { question: input2.question || "" }),
|
|
4347
|
+
meta,
|
|
4348
|
+
type: "question",
|
|
4349
|
+
author: this.#i18n.t("ui.panel.authorAgent")
|
|
4350
|
+
})
|
|
4351
|
+
);
|
|
4352
|
+
cards.push(
|
|
4353
|
+
createCard({
|
|
4354
|
+
icon: iconAnswer,
|
|
4355
|
+
content: this.#i18n.t("ui.panel.userAnswer", { input: answer }),
|
|
4356
|
+
meta,
|
|
4357
|
+
type: "input",
|
|
4358
|
+
from: "pessoa",
|
|
4359
|
+
author: this.#i18n.t("ui.panel.authorPerson")
|
|
4360
|
+
})
|
|
4361
|
+
);
|
|
4362
|
+
} else {
|
|
4363
|
+
const toolText = this.#getToolExecutingText(action.name, action.input);
|
|
4364
|
+
cards.push(
|
|
4365
|
+
createCard({ icon: iconTool, content: toolText, meta, author: this.#i18n.t("ui.panel.authorAction") })
|
|
4366
|
+
);
|
|
4367
|
+
if (action.output?.length > 0) {
|
|
4368
|
+
cards.push(
|
|
4369
|
+
createCard({
|
|
4370
|
+
icon: iconTool,
|
|
4371
|
+
content: action.output,
|
|
4372
|
+
meta,
|
|
4373
|
+
type: "output",
|
|
4374
|
+
author: this.#i18n.t("ui.panel.authorResult")
|
|
4375
|
+
})
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
return cards;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
class PageAgent extends PageAgentCore {
|
|
4383
|
+
panel;
|
|
4384
|
+
constructor(config) {
|
|
4385
|
+
const pageController = new PageController({
|
|
4386
|
+
...config,
|
|
4387
|
+
enableMask: config.enableMask ?? true
|
|
4388
|
+
});
|
|
4389
|
+
super({ ...config, pageController });
|
|
4390
|
+
this.panel = new Panel(this, {
|
|
4391
|
+
language: config.language,
|
|
4392
|
+
promptForNextTask: config.promptForNextTask
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
function getRecognitionCtor() {
|
|
4397
|
+
const w = globalThis;
|
|
4398
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
4399
|
+
}
|
|
4400
|
+
const STATUS_LISTENING = "listening";
|
|
4401
|
+
const STATUS_SPEAKING = "speaking";
|
|
4402
|
+
const STATUS_IDLE = "idle";
|
|
4403
|
+
function createVoice(options = {}) {
|
|
4404
|
+
const lang = options.lang ?? "pt-BR";
|
|
4405
|
+
const rate = options.rate ?? 1;
|
|
4406
|
+
const pitch = options.pitch ?? 1;
|
|
4407
|
+
const inputEnabled = options.input ?? true;
|
|
4408
|
+
const outputEnabled = options.output ?? false;
|
|
4409
|
+
const listenTimeoutMs = options.listenTimeoutMs ?? 3e4;
|
|
4410
|
+
const Recognition = inputEnabled ? getRecognitionCtor() : null;
|
|
4411
|
+
const synth = typeof window !== "undefined" ? window.speechSynthesis : void 0;
|
|
4412
|
+
const supported = {
|
|
4413
|
+
input: Boolean(inputEnabled && Recognition),
|
|
4414
|
+
output: Boolean(outputEnabled && synth)
|
|
4415
|
+
};
|
|
4416
|
+
let status = STATUS_IDLE;
|
|
4417
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
4418
|
+
function setStatus(next) {
|
|
4419
|
+
if (status === next) return;
|
|
4420
|
+
status = next;
|
|
4421
|
+
statusListeners.forEach((cb) => {
|
|
4422
|
+
try {
|
|
4423
|
+
cb(next);
|
|
4424
|
+
} catch {
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
let activeRecognition = null;
|
|
4429
|
+
let currentVoice = null;
|
|
4430
|
+
if (synth) {
|
|
4431
|
+
const pickVoice = () => {
|
|
4432
|
+
if (options.voiceURI) {
|
|
4433
|
+
currentVoice = synth.getVoices().find((v) => v.voiceURI === options.voiceURI) ?? null;
|
|
4434
|
+
} else {
|
|
4435
|
+
const voices = synth.getVoices();
|
|
4436
|
+
const prefix = lang.split("-")[0].toLowerCase();
|
|
4437
|
+
currentVoice = voices.find((v) => v.lang?.toLowerCase() === lang.toLowerCase()) ?? voices.find((v) => v.lang?.toLowerCase().startsWith(prefix)) ?? null;
|
|
4438
|
+
}
|
|
4439
|
+
};
|
|
4440
|
+
pickVoice();
|
|
4441
|
+
synth.onvoiceschanged = pickVoice;
|
|
4442
|
+
}
|
|
4443
|
+
function speak(text, opts) {
|
|
4444
|
+
if (!synth || !outputEnabled) return;
|
|
4445
|
+
const clean = (text ?? "").trim();
|
|
4446
|
+
if (!clean) return;
|
|
4447
|
+
if (opts?.interrupt !== false) {
|
|
4448
|
+
synth.cancel();
|
|
4449
|
+
}
|
|
4450
|
+
const utterance = new SpeechSynthesisUtterance(clean);
|
|
4451
|
+
utterance.lang = lang;
|
|
4452
|
+
utterance.rate = rate;
|
|
4453
|
+
utterance.pitch = pitch;
|
|
4454
|
+
if (currentVoice) utterance.voice = currentVoice;
|
|
4455
|
+
utterance.onend = () => setStatus(STATUS_IDLE);
|
|
4456
|
+
utterance.onerror = () => setStatus(STATUS_IDLE);
|
|
4457
|
+
setStatus(STATUS_SPEAKING);
|
|
4458
|
+
synth.speak(utterance);
|
|
4459
|
+
}
|
|
4460
|
+
function listen() {
|
|
4461
|
+
if (!Recognition) {
|
|
4462
|
+
return Promise.reject(new Error("Voz de entrada não suportada neste navegador"));
|
|
4463
|
+
}
|
|
4464
|
+
return new Promise((resolve, reject) => {
|
|
4465
|
+
const recognition = new Recognition();
|
|
4466
|
+
activeRecognition = recognition;
|
|
4467
|
+
let finalText = "";
|
|
4468
|
+
let settled = false;
|
|
4469
|
+
let timer;
|
|
4470
|
+
const finish = (fn) => {
|
|
4471
|
+
if (settled) return;
|
|
4472
|
+
settled = true;
|
|
4473
|
+
if (timer) clearTimeout(timer);
|
|
4474
|
+
if (activeRecognition === recognition) activeRecognition = null;
|
|
4475
|
+
setStatus(STATUS_IDLE);
|
|
4476
|
+
fn();
|
|
4477
|
+
};
|
|
4478
|
+
timer = setTimeout(() => {
|
|
4479
|
+
try {
|
|
4480
|
+
recognition.stop();
|
|
4481
|
+
} catch {
|
|
4482
|
+
}
|
|
4483
|
+
finish(() => reject(new Error("Tempo de escuta esgotado")));
|
|
4484
|
+
}, listenTimeoutMs);
|
|
4485
|
+
recognition.lang = lang;
|
|
4486
|
+
recognition.continuous = false;
|
|
4487
|
+
recognition.interimResults = true;
|
|
4488
|
+
recognition.maxAlternatives = 1;
|
|
4489
|
+
recognition.onstart = () => setStatus(STATUS_LISTENING);
|
|
4490
|
+
recognition.onresult = (event) => {
|
|
4491
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
4492
|
+
const result2 = event.results[i];
|
|
4493
|
+
if (result2?.isFinal) finalText = result2[0]?.transcript ?? finalText;
|
|
4494
|
+
}
|
|
4495
|
+
};
|
|
4496
|
+
recognition.onerror = (event) => {
|
|
4497
|
+
const code = event?.error;
|
|
4498
|
+
if (code === "no-speech" || code === "aborted") {
|
|
4499
|
+
finish(() => resolve(finalText.trim()));
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
finish(() => reject(new Error(`Erro de reconhecimento de voz: ${code ?? "desconhecido"}`)));
|
|
4503
|
+
};
|
|
4504
|
+
recognition.onend = () => {
|
|
4505
|
+
finish(() => resolve(finalText.trim()));
|
|
4506
|
+
};
|
|
4507
|
+
try {
|
|
4508
|
+
recognition.start();
|
|
4509
|
+
} catch (error2) {
|
|
4510
|
+
finish(() => reject(error2 instanceof Error ? error2 : new Error(String(error2))));
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
return {
|
|
4515
|
+
supported,
|
|
4516
|
+
listen,
|
|
4517
|
+
stopListening() {
|
|
4518
|
+
try {
|
|
4519
|
+
activeRecognition?.stop();
|
|
4520
|
+
} catch {
|
|
4521
|
+
}
|
|
4522
|
+
activeRecognition = null;
|
|
4523
|
+
setStatus(STATUS_IDLE);
|
|
4524
|
+
},
|
|
4525
|
+
speak,
|
|
4526
|
+
stopSpeaking() {
|
|
4527
|
+
if (!synth) return;
|
|
4528
|
+
synth.cancel();
|
|
4529
|
+
setStatus(STATUS_IDLE);
|
|
4530
|
+
},
|
|
4531
|
+
onStatus(cb) {
|
|
4532
|
+
statusListeners.add(cb);
|
|
4533
|
+
return () => statusListeners.delete(cb);
|
|
4534
|
+
}
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
const DEFAULTS = {
|
|
4538
|
+
language: "pt-BR",
|
|
4539
|
+
maxSteps: 30,
|
|
4540
|
+
enableMask: true,
|
|
4541
|
+
viewportExpansion: 0,
|
|
4542
|
+
promptForNextTask: true,
|
|
4543
|
+
position: "bottom-right",
|
|
4544
|
+
launcherLabel: "Abrir assistente da página",
|
|
4545
|
+
open: false
|
|
4546
|
+
};
|
|
4547
|
+
const LAUNCHER_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l1.9 4.6L18.5 9l-4.6 1.9L12 15.5l-1.9-4.6L5.5 9l4.6-1.4L12 3z"/><path d="M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2z"/></svg>';
|
|
4548
|
+
const MIC_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>';
|
|
4549
|
+
function positionCss(position) {
|
|
4550
|
+
switch (position) {
|
|
4551
|
+
case "bottom-left":
|
|
4552
|
+
return { bottom: "20px", left: "20px", right: "auto", top: "auto" };
|
|
4553
|
+
case "top-right":
|
|
4554
|
+
return { top: "20px", right: "20px", bottom: "auto", left: "auto" };
|
|
4555
|
+
case "top-left":
|
|
4556
|
+
return { top: "20px", left: "20px", bottom: "auto", right: "auto" };
|
|
4557
|
+
default:
|
|
4558
|
+
return { bottom: "20px", right: "20px", top: "auto", left: "auto" };
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
function logHookError(name, error2) {
|
|
4562
|
+
console.error(`[page-agent-widget] gancho ${name} falhou (a tarefa segue):`, error2);
|
|
4563
|
+
}
|
|
4564
|
+
function mergeExtensions(host, extensions) {
|
|
4565
|
+
const customTools = { ...host.customTools };
|
|
4566
|
+
for (const extension of extensions) Object.assign(customTools, extension.customTools);
|
|
4567
|
+
const system = [host.instructions?.system, ...extensions.map((e) => e.instructions?.system)].map((text) => text?.trim()).filter((text) => Boolean(text)).join("\n\n");
|
|
4568
|
+
const pageProviders = [host.instructions?.getPageInstructions, ...extensions.map((e) => e.instructions?.getPageInstructions)].filter(
|
|
4569
|
+
(fn) => typeof fn === "function"
|
|
4570
|
+
);
|
|
4571
|
+
const getPageInstructions = pageProviders.length ? (url) => {
|
|
4572
|
+
const parts = [];
|
|
4573
|
+
for (const provide of pageProviders) {
|
|
4574
|
+
try {
|
|
4575
|
+
const text = provide(url)?.trim();
|
|
4576
|
+
if (text) parts.push(text);
|
|
4577
|
+
} catch (error2) {
|
|
4578
|
+
logHookError("getPageInstructions", error2);
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
return parts.join("\n\n") || void 0;
|
|
4582
|
+
} : void 0;
|
|
4583
|
+
const sources = [host.hooks, ...extensions.map((e) => e.hooks)].filter((h) => Boolean(h));
|
|
4584
|
+
const chain = (name) => {
|
|
4585
|
+
const fns = sources.map((source) => source[name]).filter((fn) => typeof fn === "function");
|
|
4586
|
+
if (!fns.length) return void 0;
|
|
4587
|
+
return (async (...args) => {
|
|
4588
|
+
for (const fn of fns) {
|
|
4589
|
+
try {
|
|
4590
|
+
await fn(...args);
|
|
4591
|
+
} catch (error2) {
|
|
4592
|
+
logHookError(String(name), error2);
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
});
|
|
4596
|
+
};
|
|
4597
|
+
const transforms = sources.map((source) => source.transformPageContent).filter((fn) => typeof fn === "function");
|
|
4598
|
+
const transformPageContent = transforms.length ? async (content) => {
|
|
4599
|
+
let current = content;
|
|
4600
|
+
for (const transform of transforms) {
|
|
4601
|
+
try {
|
|
4602
|
+
current = await transform(current);
|
|
4603
|
+
} catch (error2) {
|
|
4604
|
+
logHookError("transformPageContent", error2);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
return current;
|
|
4608
|
+
} : void 0;
|
|
4609
|
+
return {
|
|
4610
|
+
customTools,
|
|
4611
|
+
instructions: system || getPageInstructions ? { ...system ? { system } : {}, ...getPageInstructions ? { getPageInstructions } : {} } : void 0,
|
|
4612
|
+
onBeforeStep: chain("onBeforeStep"),
|
|
4613
|
+
onAfterStep: chain("onAfterStep"),
|
|
4614
|
+
onBeforeTask: chain("onBeforeTask"),
|
|
4615
|
+
onAfterTask: chain("onAfterTask"),
|
|
4616
|
+
onDispose: chain("onDispose"),
|
|
4617
|
+
transformPageContent
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
function mountWidget$1(options = {}) {
|
|
4621
|
+
const position = options.position ?? DEFAULTS.position;
|
|
4622
|
+
const launcherLabel = options.launcherLabel ?? DEFAULTS.launcherLabel;
|
|
4623
|
+
const open = options.open ?? DEFAULTS.open;
|
|
4624
|
+
let agent = null;
|
|
4625
|
+
let voice = null;
|
|
4626
|
+
let micButton = null;
|
|
4627
|
+
let listening = false;
|
|
4628
|
+
let configPromise = null;
|
|
4629
|
+
async function ensureConfig() {
|
|
4630
|
+
if (configPromise) return configPromise;
|
|
4631
|
+
if (!options.configUrl) return;
|
|
4632
|
+
configPromise = (async () => {
|
|
4633
|
+
const res = await fetch(options.configUrl, { headers: { accept: "application/json" } });
|
|
4634
|
+
if (!res.ok) throw new Error(`mountWidget: configUrl respondeu ${res.status} — o gateway está no ar?`);
|
|
4635
|
+
const server = await res.json();
|
|
4636
|
+
for (const key of ["model", "language", "maxSteps", "promptForNextTask", "instructions", "voice", "brain", "debug"]) {
|
|
4637
|
+
const value = server[key];
|
|
4638
|
+
if (value !== void 0 && value !== null) {
|
|
4639
|
+
options[key] = value;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
})();
|
|
4643
|
+
return configPromise;
|
|
4644
|
+
}
|
|
4645
|
+
function ensureVoice() {
|
|
4646
|
+
if (!voice) voice = createVoice(options.voice ?? {});
|
|
4647
|
+
return voice;
|
|
4648
|
+
}
|
|
4649
|
+
async function ensureAgent() {
|
|
4650
|
+
if (agent && !agent.disposed) return agent;
|
|
4651
|
+
await ensureConfig();
|
|
4652
|
+
const v = ensureVoice();
|
|
4653
|
+
const model = options.model;
|
|
4654
|
+
const baseURL = options.baseURL;
|
|
4655
|
+
if (!model || !baseURL) {
|
|
4656
|
+
throw new Error(
|
|
4657
|
+
options.configUrl ? "mountWidget: o gateway não enviou `model` e/ou falta `baseURL` no front. Confira AGENT_MODEL no gateway e a baseURL do widget." : "mountWidget: configure `model` e `baseURL` (config central do LLM) antes de abrir o agente."
|
|
4658
|
+
);
|
|
4659
|
+
}
|
|
4660
|
+
const resolved = options.resolveExtensions ? await options.resolveExtensions(options) : [];
|
|
4661
|
+
const merged = mergeExtensions(options, [...options.extensions ?? [], ...resolved]);
|
|
4662
|
+
agent = new PageAgent({
|
|
4663
|
+
model,
|
|
4664
|
+
baseURL,
|
|
4665
|
+
apiKey: options.apiKey,
|
|
4666
|
+
transformRequestBody: options.transformRequestBody,
|
|
4667
|
+
customFetch: options.customFetch,
|
|
4668
|
+
maxRetries: options.maxRetries,
|
|
4669
|
+
language: options.language ?? DEFAULTS.language,
|
|
4670
|
+
maxSteps: options.maxSteps ?? DEFAULTS.maxSteps,
|
|
4671
|
+
enableMask: options.enableMask ?? DEFAULTS.enableMask,
|
|
4672
|
+
viewportExpansion: options.viewportExpansion ?? DEFAULTS.viewportExpansion,
|
|
4673
|
+
instructions: merged.instructions,
|
|
4674
|
+
customTools: merged.customTools,
|
|
4675
|
+
onBeforeStep: merged.onBeforeStep,
|
|
4676
|
+
onAfterStep: merged.onAfterStep,
|
|
4677
|
+
onBeforeTask: merged.onBeforeTask,
|
|
4678
|
+
onAfterTask: merged.onAfterTask,
|
|
4679
|
+
onDispose: merged.onDispose,
|
|
4680
|
+
transformPageContent: merged.transformPageContent,
|
|
4681
|
+
promptForNextTask: options.promptForNextTask ?? DEFAULTS.promptForNextTask
|
|
4682
|
+
});
|
|
4683
|
+
wireVoiceOutput(agent, v);
|
|
4684
|
+
injectMicButton(agent, v);
|
|
4685
|
+
return agent;
|
|
4686
|
+
}
|
|
4687
|
+
function wireVoiceOutput(a, v) {
|
|
4688
|
+
const speakOnComplete = options.voice?.speakOnComplete ?? true;
|
|
4689
|
+
const speakSteps = options.voice?.speakSteps ?? false;
|
|
4690
|
+
const onStatus = () => {
|
|
4691
|
+
const status = a.status;
|
|
4692
|
+
const result2 = a.lastResult;
|
|
4693
|
+
if (status === "completed" || status === "error") {
|
|
4694
|
+
if (speakOnComplete && v.supported.output) {
|
|
4695
|
+
if (status === "error") {
|
|
4696
|
+
v.speak("A tarefa encontrou um problema e foi parada.");
|
|
4697
|
+
} else if (result2?.success) {
|
|
4698
|
+
v.speak("Tarefa concluída com sucesso.");
|
|
4699
|
+
} else {
|
|
4700
|
+
v.speak("A tarefa terminou, mas o resultado não foi confirmado.");
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
return;
|
|
4704
|
+
}
|
|
4705
|
+
if (speakSteps && status === "running" && v.supported.output) {
|
|
4706
|
+
const lastStep = [...a.history].reverse().find((h) => h.type === "step");
|
|
4707
|
+
const goal = lastStep?.reflection?.next_goal;
|
|
4708
|
+
if (goal) v.speak(goal, { interrupt: true });
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
a.addEventListener("statuschange", onStatus);
|
|
4712
|
+
a.addEventListener("activity", (event) => {
|
|
4713
|
+
const detail = event.detail;
|
|
4714
|
+
if (detail?.type === "error" && detail.message && v.supported.output) {
|
|
4715
|
+
v.speak(detail.message, { interrupt: true });
|
|
4716
|
+
}
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
function injectMicButton(a, v) {
|
|
4720
|
+
const wrapper2 = a.panel.wrapper;
|
|
4721
|
+
const input2 = wrapper2.querySelector("input");
|
|
4722
|
+
if (!input2 || !v.supported.input) return;
|
|
4723
|
+
if (wrapper2.querySelector('[data-page-agent-voice="mic"]')) return;
|
|
4724
|
+
if (micButton) micButton.remove();
|
|
4725
|
+
micButton = document.createElement("button");
|
|
4726
|
+
micButton.type = "button";
|
|
4727
|
+
micButton.className = "page-agent-voice-mic";
|
|
4728
|
+
micButton.setAttribute("data-page-agent-voice", "mic");
|
|
4729
|
+
micButton.setAttribute("aria-label", "Ditar tarefa por voz");
|
|
4730
|
+
micButton.title = "Ditar tarefa por voz";
|
|
4731
|
+
micButton.innerHTML = MIC_ICON;
|
|
4732
|
+
micButton.style.display = "inline-flex";
|
|
4733
|
+
const host = input2.parentElement ?? wrapper2;
|
|
4734
|
+
host.insertBefore(micButton, input2);
|
|
4735
|
+
v.onStatus((s) => {
|
|
4736
|
+
micButton?.classList.toggle("is-listening", s === "listening");
|
|
4737
|
+
});
|
|
4738
|
+
micButton.addEventListener("click", async () => {
|
|
4739
|
+
if (listening) {
|
|
4740
|
+
v.stopListening();
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
listening = true;
|
|
4744
|
+
try {
|
|
4745
|
+
const transcript = await v.listen();
|
|
4746
|
+
if (transcript) {
|
|
4747
|
+
input2.value = transcript;
|
|
4748
|
+
input2.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
4749
|
+
}
|
|
4750
|
+
} catch (error2) {
|
|
4751
|
+
input2.setAttribute("placeholder", error2 instanceof Error ? error2.message : "Não foi possível ouvir; digite a tarefa.");
|
|
4752
|
+
setTimeout(() => input2.removeAttribute("placeholder"), 6e3);
|
|
4753
|
+
} finally {
|
|
4754
|
+
listening = false;
|
|
4755
|
+
}
|
|
4756
|
+
});
|
|
4757
|
+
}
|
|
4758
|
+
const launcher = document.createElement("button");
|
|
4759
|
+
launcher.type = "button";
|
|
4760
|
+
launcher.className = "page-agent-widget-launcher";
|
|
4761
|
+
launcher.setAttribute("aria-label", launcherLabel);
|
|
4762
|
+
launcher.title = launcherLabel;
|
|
4763
|
+
launcher.innerHTML = LAUNCHER_ICON;
|
|
4764
|
+
const pos = positionCss(position);
|
|
4765
|
+
Object.assign(launcher.style, pos);
|
|
4766
|
+
let panelVisible = false;
|
|
4767
|
+
async function openPanel(silent = false) {
|
|
4768
|
+
try {
|
|
4769
|
+
const a = await ensureAgent();
|
|
4770
|
+
a.panel.show();
|
|
4771
|
+
panelVisible = true;
|
|
4772
|
+
} catch (error2) {
|
|
4773
|
+
if (silent) console.error("[page-agent-widget] não abriu o agente:", error2);
|
|
4774
|
+
else throw error2;
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
function closePanel() {
|
|
4778
|
+
agent?.panel.hide?.();
|
|
4779
|
+
panelVisible = false;
|
|
4780
|
+
}
|
|
4781
|
+
launcher.addEventListener("click", () => {
|
|
4782
|
+
if (panelVisible) {
|
|
4783
|
+
closePanel();
|
|
4784
|
+
} else {
|
|
4785
|
+
void openPanel(true);
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
document.body.appendChild(launcher);
|
|
4789
|
+
if (open) {
|
|
4790
|
+
void openPanel(true);
|
|
4791
|
+
}
|
|
4792
|
+
const widget = {
|
|
4793
|
+
get agent() {
|
|
4794
|
+
return agent;
|
|
4795
|
+
},
|
|
4796
|
+
get voice() {
|
|
4797
|
+
return voice;
|
|
4798
|
+
},
|
|
4799
|
+
launcher,
|
|
4800
|
+
ready() {
|
|
4801
|
+
return ensureAgent();
|
|
4802
|
+
},
|
|
4803
|
+
async run(task) {
|
|
4804
|
+
const a = await ensureAgent();
|
|
4805
|
+
return await a.execute(task);
|
|
4806
|
+
},
|
|
4807
|
+
async stop() {
|
|
4808
|
+
if (agent) await agent.stop();
|
|
4809
|
+
},
|
|
4810
|
+
show() {
|
|
4811
|
+
return openPanel(false);
|
|
4812
|
+
},
|
|
4813
|
+
hide() {
|
|
4814
|
+
closePanel();
|
|
4815
|
+
},
|
|
4816
|
+
toggle() {
|
|
4817
|
+
launcher.click();
|
|
4818
|
+
},
|
|
4819
|
+
setConfig(patch) {
|
|
4820
|
+
Object.assign(options, patch);
|
|
4821
|
+
if (agent) {
|
|
4822
|
+
agent.dispose();
|
|
4823
|
+
agent = null;
|
|
4824
|
+
}
|
|
4825
|
+
if (panelVisible) {
|
|
4826
|
+
panelVisible = false;
|
|
4827
|
+
void openPanel(true);
|
|
4828
|
+
}
|
|
4829
|
+
},
|
|
4830
|
+
unmount() {
|
|
4831
|
+
voice?.stopSpeaking();
|
|
4832
|
+
voice?.stopListening();
|
|
4833
|
+
agent?.dispose();
|
|
4834
|
+
agent = null;
|
|
4835
|
+
voice = null;
|
|
4836
|
+
micButton?.remove();
|
|
4837
|
+
micButton = null;
|
|
4838
|
+
launcher.remove();
|
|
4839
|
+
}
|
|
4840
|
+
};
|
|
4841
|
+
return widget;
|
|
4842
|
+
}
|
|
4843
|
+
function mountWidget(options = {}) {
|
|
4844
|
+
const { brain: brainOption, ...rest } = options;
|
|
4845
|
+
const previous = rest.resolveExtensions;
|
|
4846
|
+
return mountWidget$1({
|
|
4847
|
+
...rest,
|
|
4848
|
+
brain: Boolean(brainOption),
|
|
4849
|
+
resolveExtensions: async (resolved) => {
|
|
4850
|
+
const extensions = previous ? [...await previous(resolved)] : [];
|
|
4851
|
+
if (resolved.brain) {
|
|
4852
|
+
const { createBrainExtension } = await import("./brain-extension-DDuKTYrJ.js").then((n) => n.d);
|
|
4853
|
+
extensions.push(createBrainExtension(typeof brainOption === "object" ? brainOption : {}));
|
|
4854
|
+
}
|
|
4855
|
+
if (resolved.debug) {
|
|
4856
|
+
const { createDebugExtension } = await import("./debug-extension-D0P_sNkc.js");
|
|
4857
|
+
extensions.push(createDebugExtension());
|
|
4858
|
+
}
|
|
4859
|
+
return extensions;
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
export {
|
|
4864
|
+
PageAgent,
|
|
4865
|
+
PageAgentCore,
|
|
4866
|
+
createVoice,
|
|
4867
|
+
mountWidget,
|
|
4868
|
+
tool
|
|
4869
|
+
};
|
|
4870
|
+
//# sourceMappingURL=page-agent.js.map
|