@locode2/harness 0.1.0-preview.1
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/README.md +27 -0
- package/THIRD_PARTY_LICENSES.txt +32769 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/apps/host/dist/document-parser-worker.js +215 -0
- package/apps/host/dist/frontend-runtime-worker.js +1755 -0
- package/apps/host/dist/index.js +23380 -0
- package/apps/web/dist/assets/LocalMonacoEditor-CXkKwxiP.css +1 -0
- package/apps/web/dist/assets/LocalMonacoEditor-WKAHQmzG.js +940 -0
- package/apps/web/dist/assets/LogsView-BFFFSFB-.js +1 -0
- package/apps/web/dist/assets/PluginsView-DM_ZnENS.js +1 -0
- package/apps/web/dist/assets/SettingsView-Cb5Q4ile.js +1 -0
- package/apps/web/dist/assets/abap-DLDM7-KI.js +1 -0
- package/apps/web/dist/assets/apex-DNDY2TF8.js +1 -0
- package/apps/web/dist/assets/azcli-Y6nb8tq_.js +1 -0
- package/apps/web/dist/assets/bat-BwHxbl9M.js +1 -0
- package/apps/web/dist/assets/bicep-CFznDFnq.js +2 -0
- package/apps/web/dist/assets/cameligo-Bf6VGUru.js +1 -0
- package/apps/web/dist/assets/clojure-Dnu-v4kV.js +1 -0
- package/apps/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/apps/web/dist/assets/coffee-Bd8akH9Z.js +1 -0
- package/apps/web/dist/assets/cpp-BbWJElDN.js +1 -0
- package/apps/web/dist/assets/csharp-Co3qMtFm.js +1 -0
- package/apps/web/dist/assets/csp-D-4FJmMZ.js +1 -0
- package/apps/web/dist/assets/css-DdJfP1eB.js +3 -0
- package/apps/web/dist/assets/css.worker-11mhRRLC.js +97 -0
- package/apps/web/dist/assets/cssMode-L5f5LAEn.js +1 -0
- package/apps/web/dist/assets/cypher-cTPe9QuQ.js +1 -0
- package/apps/web/dist/assets/dart-BOtBlQCF.js +1 -0
- package/apps/web/dist/assets/dockerfile-BG73LgW2.js +1 -0
- package/apps/web/dist/assets/ecl-BEgZUVRK.js +1 -0
- package/apps/web/dist/assets/editor.worker-DysRFSaV.js +30 -0
- package/apps/web/dist/assets/elixir-BkW5O-1t.js +1 -0
- package/apps/web/dist/assets/flow9-BeJ5waoc.js +1 -0
- package/apps/web/dist/assets/freemarker2-UOwftS75.js +3 -0
- package/apps/web/dist/assets/fsharp-PahG7c26.js +1 -0
- package/apps/web/dist/assets/go-acbASCJo.js +1 -0
- package/apps/web/dist/assets/graphql-BxJiqAUM.js +1 -0
- package/apps/web/dist/assets/handlebars-C_E41ffr.js +1 -0
- package/apps/web/dist/assets/hcl-DtV1sZF8.js +1 -0
- package/apps/web/dist/assets/html-HsrixH2x.js +1 -0
- package/apps/web/dist/assets/html.worker-C8Tw8ncf.js +474 -0
- package/apps/web/dist/assets/htmlMode-DipGmEOi.js +1 -0
- package/apps/web/dist/assets/index-COUF63cT.js +213 -0
- package/apps/web/dist/assets/index-SNSmF2-c.css +1 -0
- package/apps/web/dist/assets/ini-Kd9XrMLS.js +1 -0
- package/apps/web/dist/assets/java-CXBNlu9o.js +1 -0
- package/apps/web/dist/assets/javascript-D36WEqF9.js +1 -0
- package/apps/web/dist/assets/json.worker-BzkYZgJ7.js +62 -0
- package/apps/web/dist/assets/jsonMode-D9BrIats.js +7 -0
- package/apps/web/dist/assets/julia-cl7-CwDS.js +1 -0
- package/apps/web/dist/assets/kotlin-s7OhZKlX.js +1 -0
- package/apps/web/dist/assets/less-9HpZscsL.js +2 -0
- package/apps/web/dist/assets/lexon-OrD6JF1K.js +1 -0
- package/apps/web/dist/assets/liquid-DES89Tjc.js +1 -0
- package/apps/web/dist/assets/lock-keyhole-jo-FJcGq.js +1 -0
- package/apps/web/dist/assets/lspLanguageFeatures-YKkgcn4q.js +4 -0
- package/apps/web/dist/assets/lua-Cyyb5UIc.js +1 -0
- package/apps/web/dist/assets/m3-B8OfTtLu.js +1 -0
- package/apps/web/dist/assets/markdown-BFxVWTOG.js +1 -0
- package/apps/web/dist/assets/mdx-D1Bfax8e.js +1 -0
- package/apps/web/dist/assets/mips-CiqrrVzr.js +1 -0
- package/apps/web/dist/assets/msdax-DmeGPVcC.js +1 -0
- package/apps/web/dist/assets/mysql-C_tMU-Nz.js +1 -0
- package/apps/web/dist/assets/objective-c-BDtDVThU.js +1 -0
- package/apps/web/dist/assets/pascal-vHIfCaH5.js +1 -0
- package/apps/web/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
- package/apps/web/dist/assets/perl-Ub6l9XKa.js +1 -0
- package/apps/web/dist/assets/pgsql-BlNEE0v7.js +1 -0
- package/apps/web/dist/assets/php-BBUBE1dy.js +1 -0
- package/apps/web/dist/assets/pla-DSh2-awV.js +1 -0
- package/apps/web/dist/assets/postiats-CocnycG-.js +1 -0
- package/apps/web/dist/assets/powerquery-tScXyioY.js +1 -0
- package/apps/web/dist/assets/powershell-COWaemsV.js +1 -0
- package/apps/web/dist/assets/protobuf-Brw8urJB.js +2 -0
- package/apps/web/dist/assets/pug-8SOpv6rk.js +1 -0
- package/apps/web/dist/assets/python-DKhPhY0N.js +1 -0
- package/apps/web/dist/assets/qsharp-Bw9ernYp.js +1 -0
- package/apps/web/dist/assets/r-j7ic8hl3.js +1 -0
- package/apps/web/dist/assets/razor-Cha8qrI6.js +1 -0
- package/apps/web/dist/assets/redis-Bu5POkcn.js +1 -0
- package/apps/web/dist/assets/redshift-Bs9aos_-.js +1 -0
- package/apps/web/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
- package/apps/web/dist/assets/ruby-zBfavPgS.js +1 -0
- package/apps/web/dist/assets/rust-BzKRNQWT.js +1 -0
- package/apps/web/dist/assets/sb-BBc9UKZt.js +1 -0
- package/apps/web/dist/assets/scala-D9hQfWCl.js +1 -0
- package/apps/web/dist/assets/scheme-BPhDTwHR.js +1 -0
- package/apps/web/dist/assets/scss-CBJaRo0y.js +3 -0
- package/apps/web/dist/assets/shell-DiJ1NA_G.js +1 -0
- package/apps/web/dist/assets/solidity-Db0IVjzk.js +1 -0
- package/apps/web/dist/assets/sophia-CnS9iZB_.js +1 -0
- package/apps/web/dist/assets/sparql-CJmd_6j2.js +1 -0
- package/apps/web/dist/assets/sql-ClhHkBeG.js +1 -0
- package/apps/web/dist/assets/st-CHwy0fLd.js +1 -0
- package/apps/web/dist/assets/swift-CnmFD0ga.js +1 -0
- package/apps/web/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
- package/apps/web/dist/assets/tcl-Dm6ycUr_.js +1 -0
- package/apps/web/dist/assets/ts.worker-Bupg6llt.js +67722 -0
- package/apps/web/dist/assets/tsMode-CEhssXv8.js +11 -0
- package/apps/web/dist/assets/twig-Csy3S7wG.js +1 -0
- package/apps/web/dist/assets/typescript-DWqmw58R.js +1 -0
- package/apps/web/dist/assets/typespec-Btyra-wh.js +1 -0
- package/apps/web/dist/assets/vb-Db0cS2oM.js +1 -0
- package/apps/web/dist/assets/wgsl-DumH7NcR.js +298 -0
- package/apps/web/dist/assets/workers-BmZvNFRq.js +1 -0
- package/apps/web/dist/assets/xml-WQTAFV9b.js +1 -0
- package/apps/web/dist/assets/yaml-Ke2Lf79d.js +1 -0
- package/apps/web/dist/index.html +15 -0
- package/bin/launch.mjs +71 -0
- package/bin/locode-mini.mjs +12 -0
- package/bin/options.mjs +49 -0
- package/bin/restore-assets.mjs +40 -0
- package/bundled-plugins/fast-frontend/.gitignore +2 -0
- package/bundled-plugins/fast-frontend/README.md +29 -0
- package/bundled-plugins/fast-frontend/harness.plugin.json +123 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/PUBLIC_CONTRACT.md +139 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/contracts/app.schema.json +24 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/contracts/route.schema.json +52 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/examples/app.json +9 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/examples/route.json +8 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/index.html +17 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/package.json +31 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/scripts/smoke-build.mjs +40 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/inspector/index.ts +612 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/main.tsx +17 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/sdk/index.ts +1028 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/shell/RuntimeApp.tsx +380 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/shell/runtime.css +246 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/tsconfig.json +20 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/types/runtime.consumer-smoke.ts +70 -0
- package/bundled-plugins/fast-frontend/runtimes/antd-admin/types/runtime.d.ts +227 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/.gitignore +9 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/.harness/template.json +10 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/index.html +13 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/package.json +34 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/pnpm-lock.yaml +1451 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/pnpm-workspace.yaml +4 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/public/generated/.gitkeep +1 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/scripts/validate-routes.mjs +153 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/app/App.tsx +27 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/app/NotFoundPage.tsx +15 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/features/.gitkeep +1 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/layouts/MainLayout.tsx +77 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/main.tsx +14 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/mocks/dashboard.ts +48 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/pages/dashboard/page.tsx +151 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/pages/dashboard/route.ts +9 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/definition.ts +21 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/menu.tsx +44 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/registry.ts +48 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/router.tsx +34 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/shared/components/business/.gitkeep +1 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/shared/components/business/BusinessProviders.tsx +9 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/styles/global.css +180 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/src/vite-env.d.ts +1 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.app.json +24 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.json +7 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.node.json +17 -0
- package/bundled-plugins/fast-frontend/templates/antd-admin/vite.config.ts +39 -0
- package/npm-shrinkwrap.json +9737 -0
- package/package.json +80 -0
- package/release-integrity.json +168 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
const VERSION = 1 as const;
|
|
2
|
+
const SOURCE_ATTRIBUTE = "data-harness-source";
|
|
3
|
+
const READY = "harness.preview-annotations.ready";
|
|
4
|
+
const CANDIDATES = "harness.preview-annotations.candidates";
|
|
5
|
+
const CANDIDATES_RESULT = "harness.preview-annotations.candidates-result";
|
|
6
|
+
const SYNC = "harness.preview-annotations.sync";
|
|
7
|
+
const MODE = "harness.preview-annotations.mode";
|
|
8
|
+
const MODIFIER = "harness.preview-annotations.modifier";
|
|
9
|
+
const CANCEL = "harness.preview-annotations.cancel";
|
|
10
|
+
const STATE = "harness.preview-annotations.state";
|
|
11
|
+
const SUBMIT = "harness.preview-annotations.submit";
|
|
12
|
+
const MODE_CHANGED = "harness.preview-annotations.mode-changed";
|
|
13
|
+
const MAX_ANNOTATIONS = 8;
|
|
14
|
+
const MAX_GESTURE_POINTS = 128;
|
|
15
|
+
|
|
16
|
+
interface Point { x: number; y: number }
|
|
17
|
+
interface LocalCandidate {
|
|
18
|
+
token: string;
|
|
19
|
+
elementType: string;
|
|
20
|
+
visibleText: string;
|
|
21
|
+
tokenIndex: number;
|
|
22
|
+
score: number;
|
|
23
|
+
}
|
|
24
|
+
interface ResolvedCandidate extends LocalCandidate {
|
|
25
|
+
path: string;
|
|
26
|
+
startLine: number;
|
|
27
|
+
endLine: number;
|
|
28
|
+
}
|
|
29
|
+
interface Annotation extends Omit<ResolvedCandidate, "score"> {
|
|
30
|
+
id: string;
|
|
31
|
+
requirement: string;
|
|
32
|
+
previewRevision?: number;
|
|
33
|
+
stale?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const channelId = new URL(window.location.href).searchParams.get("__harness_runtime_storage_channel");
|
|
37
|
+
if (!channelId || channelId.length > 128 || window.parent === window) {
|
|
38
|
+
// Static exports and standalone Runtime pages do not install the inspector.
|
|
39
|
+
} else {
|
|
40
|
+
installInspector(channelId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function installInspector(channel: string): void {
|
|
44
|
+
let locked = false;
|
|
45
|
+
let modifierDown = false;
|
|
46
|
+
let points: Point[] = [];
|
|
47
|
+
let drawing = false;
|
|
48
|
+
let resolving = false;
|
|
49
|
+
let requestSequence = 0;
|
|
50
|
+
let pendingCandidates: LocalCandidate[] = [];
|
|
51
|
+
let annotations: Annotation[] = [];
|
|
52
|
+
let editing: Annotation | undefined;
|
|
53
|
+
|
|
54
|
+
const host = document.createElement("div");
|
|
55
|
+
host.id = "harness-visual-inspector";
|
|
56
|
+
host.style.cssText = "position:fixed;inset:0;z-index:2147483647;pointer-events:none;";
|
|
57
|
+
document.documentElement.append(host);
|
|
58
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
59
|
+
shadow.innerHTML = `
|
|
60
|
+
<style>
|
|
61
|
+
:host{all:initial;--violet:#6557f5;--violet-dark:#5145e5;--ink:#182033;--muted:#68738a}*{box-sizing:border-box}
|
|
62
|
+
button,select,textarea{font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif}
|
|
63
|
+
button{border:0;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.45}
|
|
64
|
+
#capture{position:fixed;inset:0;z-index:1;pointer-events:none;cursor:crosshair;touch-action:none}
|
|
65
|
+
#capture.active{pointer-events:auto}#capture.resolving{cursor:progress}
|
|
66
|
+
#capture svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
|
|
67
|
+
.stroke{fill:none;stroke:var(--violet);stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 3px 7px rgba(79,70,229,.3))}.stroke.closed{fill:rgba(101,87,245,.07)}
|
|
68
|
+
.stroke.pending{animation:stroke-pulse .8s ease-in-out infinite alternate}@keyframes stroke-pulse{to{stroke-opacity:.5;filter:drop-shadow(0 4px 11px rgba(79,70,229,.48))}}
|
|
69
|
+
#boxes{position:fixed;inset:0;z-index:2;pointer-events:none}
|
|
70
|
+
.box{position:absolute;pointer-events:none}
|
|
71
|
+
.ring{position:absolute;inset:-6px;border:3px solid rgba(101,87,245,.9);background:rgba(101,87,245,.055);box-shadow:0 0 0 1px rgba(129,117,255,.24);filter:drop-shadow(0 3px 6px rgba(79,70,229,.24))}
|
|
72
|
+
.pin,.note-badge{display:grid;place-items:center;flex:none;width:24px;height:24px;border-radius:999px;background:linear-gradient(145deg,#7769ff,#5145e5);color:#fff;font:700 12px/1 system-ui;box-shadow:0 4px 12px rgba(70,57,210,.34)}
|
|
73
|
+
.pin{position:absolute;left:-13px;top:-15px}.box.stale .ring{border-color:#f59e0b;background:rgba(245,158,11,.07)}.box.stale .pin{background:linear-gradient(145deg,#fbbf24,#d97706)}
|
|
74
|
+
.note{position:absolute;z-index:3;width:min(280px,calc(100vw - 24px));min-height:42px;padding:8px 10px;border:1px solid rgba(255,255,255,.86);border-radius:14px;background:linear-gradient(145deg,rgba(255,255,255,.86),rgba(247,248,255,.72));color:var(--ink);box-shadow:0 12px 34px rgba(31,38,70,.18),inset 0 1px 0 rgba(255,255,255,.9);backdrop-filter:blur(18px) saturate(145%);pointer-events:auto;outline:none;transition:background .16s ease,box-shadow .16s ease,transform .16s ease}
|
|
75
|
+
.note:hover,.note:focus-within{z-index:4;background:rgba(255,255,255,.94);box-shadow:0 18px 48px rgba(31,38,70,.24),inset 0 1px 0 #fff;transform:translateY(-1px)}
|
|
76
|
+
.note-summary{display:flex;min-width:0;align-items:center;gap:9px}.note-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:600 13px/1.35 system-ui}.note-stale{display:none;color:#b45309;font:600 11px/1 system-ui}.note.stale .note-stale{display:inline}
|
|
77
|
+
.note-detail{display:none;margin:8px 2px 0 33px;padding-top:8px;border-top:1px solid rgba(148,163,184,.28)}.note:hover .note-detail,.note:focus-within .note-detail{display:block}
|
|
78
|
+
.note-target{overflow:hidden;color:var(--muted);font:11px/1.4 system-ui;text-overflow:ellipsis;white-space:nowrap}.note-actions{display:flex;gap:6px;margin-top:7px}.note-actions button{display:inline-flex;align-items:center;gap:4px;padding:6px 8px;border-radius:8px;background:rgba(99,102,241,.09);color:#4f46e5;font:600 11px/1 system-ui}.note-actions .danger{background:rgba(244,63,94,.08);color:#be123c}
|
|
79
|
+
#toast{position:fixed;top:16px;left:50%;z-index:7;transform:translateX(-50%);display:none;max-width:min(480px,calc(100vw - 32px));padding:10px 14px;border:1px solid rgba(255,255,255,.18);border-radius:12px;background:rgba(17,24,39,.88);color:#fff;font:13px/1.5 system-ui;box-shadow:0 12px 36px rgba(0,0,0,.28);backdrop-filter:blur(16px)}
|
|
80
|
+
#editor{position:fixed;z-index:6;display:none;width:min(390px,calc(100vw - 32px));padding:16px;border:1px solid rgba(255,255,255,.9);border-radius:18px;background:linear-gradient(145deg,rgba(255,255,255,.9),rgba(247,248,255,.78));color:var(--ink);box-shadow:0 24px 64px rgba(31,38,70,.25),inset 0 1px 0 #fff;font-family:system-ui;pointer-events:auto;backdrop-filter:blur(22px) saturate(150%)}
|
|
81
|
+
#editor-close{position:absolute;top:10px;right:10px;width:28px;height:28px;padding:0;border-radius:9px;background:transparent;color:#7c879b;font:500 20px/1 system-ui}#editor-close:hover{background:rgba(100,116,139,.1);color:#334155}
|
|
82
|
+
.title{max-width:calc(100% - 34px);margin:0 0 3px;font-size:14px;font-weight:700}.meta{margin-bottom:11px;color:#7c879b;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
83
|
+
select,textarea{width:100%;border:1px solid rgba(148,163,184,.48);border-radius:11px;background:rgba(255,255,255,.68);color:var(--ink);font:13px/1.5 system-ui;outline:none}select{height:36px;padding:0 10px;margin-bottom:9px}textarea{min-height:84px;padding:10px 11px;resize:vertical}select:focus,textarea:focus{border-color:#7568ff;box-shadow:0 0 0 3px rgba(101,87,245,.13);background:rgba(255,255,255,.9)}
|
|
84
|
+
.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:11px}.actions button{border-radius:9px;padding:8px 12px;font:600 12px/1 system-ui}.ghost{background:rgba(100,116,139,.09);color:#536078}.primary{background:linear-gradient(145deg,#6d5df6,#5145e5);color:#fff;box-shadow:0 5px 14px rgba(79,70,229,.25)}
|
|
85
|
+
#drawbar,#batch{position:fixed;left:50%;z-index:5;transform:translateX(-50%);display:none;align-items:center;gap:10px;max-width:calc(100vw - 32px);border:1px solid rgba(255,255,255,.86);background:linear-gradient(145deg,rgba(255,255,255,.86),rgba(246,247,255,.72));color:var(--ink);box-shadow:0 16px 46px rgba(31,38,70,.22),inset 0 1px 0 #fff;font:12px/1 system-ui;pointer-events:auto;backdrop-filter:blur(20px) saturate(150%)}
|
|
86
|
+
#drawbar{bottom:18px;padding:9px 10px;border-radius:17px}.draw-icon{display:grid;place-items:center;width:34px;height:34px;border-radius:12px;background:rgba(101,87,245,.11);color:var(--violet);font:700 20px/1 system-ui}.draw-title{color:var(--violet-dark);font-weight:700;white-space:nowrap}.draw-hint{padding:0 8px;border-left:1px solid rgba(148,163,184,.28);color:#69748a;white-space:nowrap}.draw-actions{display:flex;gap:6px;margin-left:4px}.draw-actions button,#batch button{border-radius:9px;padding:8px 10px;background:rgba(100,116,139,.09);color:#536078;font:600 12px/1 system-ui}.draw-actions .done,#batch .send{background:linear-gradient(145deg,#6d5df6,#5145e5);color:#fff}
|
|
87
|
+
#batch{bottom:18px;padding:9px 10px;border-radius:15px}.count{padding:0 5px;color:#59657a;white-space:nowrap}.batch-spacer{width:1px;height:20px;background:rgba(148,163,184,.3)}
|
|
88
|
+
@media(max-width:640px){.draw-hint{display:none}.note{width:min(240px,calc(100vw - 24px))}#drawbar{gap:6px}.draw-actions button{padding:8px}.draw-title{display:none}}
|
|
89
|
+
</style>
|
|
90
|
+
<div id="capture"><svg aria-hidden="true"><path class="stroke" d=""/></svg></div>
|
|
91
|
+
<div id="boxes"></div><div id="toast" role="status"></div>
|
|
92
|
+
<div id="editor" role="dialog" aria-label="填写圈选修改要求">
|
|
93
|
+
<button id="editor-close" aria-label="关闭标注编辑器">×</button>
|
|
94
|
+
<div class="title" id="element"></div><div class="meta" id="location"></div>
|
|
95
|
+
<select id="candidate" aria-label="切换候选元素"></select>
|
|
96
|
+
<textarea id="requirement" placeholder="描述要怎样修改…(Ctrl/⌘+Enter 保存)"></textarea>
|
|
97
|
+
<div class="actions"><button class="ghost" id="cancel">取消</button><button class="primary" id="save">保存标注</button></div>
|
|
98
|
+
</div>
|
|
99
|
+
<div id="drawbar" role="status"><span class="draw-icon">◯</span><strong class="draw-title">圈选</strong><span class="draw-hint">按住鼠标直接画圈,松开即可标注</span><div class="draw-actions"><button id="undo">↶ 撤销</button><button class="done" id="finish">✓ 完成</button></div></div>
|
|
100
|
+
<div id="batch"><span class="count"></span><span class="batch-spacer"></span><button id="clear">清空</button><button class="send" id="submit">送到 AI 助手</button></div>`;
|
|
101
|
+
const capture = required<HTMLElement>(shadow, "#capture");
|
|
102
|
+
const stroke = required<SVGPathElement>(shadow, ".stroke");
|
|
103
|
+
const boxes = required<HTMLElement>(shadow, "#boxes");
|
|
104
|
+
const toast = required<HTMLElement>(shadow, "#toast");
|
|
105
|
+
const editor = required<HTMLElement>(shadow, "#editor");
|
|
106
|
+
const location = required<HTMLElement>(shadow, "#location");
|
|
107
|
+
const element = required<HTMLElement>(shadow, "#element");
|
|
108
|
+
const candidate = required<HTMLSelectElement>(shadow, "#candidate");
|
|
109
|
+
const requirement = required<HTMLTextAreaElement>(shadow, "#requirement");
|
|
110
|
+
const drawbar = required<HTMLElement>(shadow, "#drawbar");
|
|
111
|
+
const batch = required<HTMLElement>(shadow, "#batch");
|
|
112
|
+
const undo = required<HTMLButtonElement>(shadow, "#undo");
|
|
113
|
+
const submit = required<HTMLButtonElement>(shadow, "#submit");
|
|
114
|
+
const clearStroke = () => {
|
|
115
|
+
stroke.setAttribute("d", "");
|
|
116
|
+
stroke.classList.remove("closed");
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const post = (message: Record<string, unknown>) => window.parent.postMessage({ ...message, version: VERSION, channelId: channel }, "*");
|
|
120
|
+
const setCapture = () => {
|
|
121
|
+
capture.classList.toggle("active", !editing && !resolving && (locked || modifierDown));
|
|
122
|
+
capture.classList.toggle("resolving", resolving);
|
|
123
|
+
stroke.classList.toggle("pending", resolving);
|
|
124
|
+
};
|
|
125
|
+
const announceMode = () => post({ type: MODE_CHANGED, locked, active: locked || modifierDown });
|
|
126
|
+
const showToast = (message: string) => {
|
|
127
|
+
toast.textContent = message;
|
|
128
|
+
toast.style.display = "block";
|
|
129
|
+
window.setTimeout(() => { if (toast.textContent === message) toast.style.display = "none"; }, 2_400);
|
|
130
|
+
};
|
|
131
|
+
const sourceElements = (token: string): Element[] => [...document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`)]
|
|
132
|
+
.filter((entry) => entry.getAttribute(SOURCE_ATTRIBUTE) === token);
|
|
133
|
+
const render = () => {
|
|
134
|
+
boxes.replaceChildren();
|
|
135
|
+
annotations.forEach((annotation, index) => {
|
|
136
|
+
const target = sourceElements(annotation.token)[annotation.tokenIndex];
|
|
137
|
+
if (!target) return;
|
|
138
|
+
const rect = target.getBoundingClientRect();
|
|
139
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
140
|
+
const box = document.createElement("div");
|
|
141
|
+
box.className = `box${annotation.stale ? " stale" : ""}`;
|
|
142
|
+
box.style.cssText = `left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px`;
|
|
143
|
+
const ring = document.createElement("span");
|
|
144
|
+
ring.className = "ring";
|
|
145
|
+
ring.style.borderRadius = targetBorderRadius(target);
|
|
146
|
+
const num = document.createElement("span");
|
|
147
|
+
num.className = "pin";
|
|
148
|
+
num.textContent = String(index + 1);
|
|
149
|
+
box.append(ring, num);
|
|
150
|
+
|
|
151
|
+
const noteWidth = Math.min(280, Math.max(180, window.innerWidth - 24));
|
|
152
|
+
const noteHeight = 126;
|
|
153
|
+
const gap = 16;
|
|
154
|
+
let noteLeft: number;
|
|
155
|
+
let noteTop = Math.max(12, Math.min(rect.top + rect.height / 2 - 21, window.innerHeight - noteHeight - 12));
|
|
156
|
+
if (rect.right + gap + noteWidth <= window.innerWidth - 12) noteLeft = rect.right + gap;
|
|
157
|
+
else if (rect.left - gap - noteWidth >= 12) noteLeft = rect.left - gap - noteWidth;
|
|
158
|
+
else {
|
|
159
|
+
noteLeft = Math.max(12, Math.min(rect.left, window.innerWidth - noteWidth - 12));
|
|
160
|
+
noteTop = rect.bottom + gap + noteHeight <= window.innerHeight
|
|
161
|
+
? rect.bottom + gap
|
|
162
|
+
: Math.max(12, rect.top - noteHeight - gap);
|
|
163
|
+
}
|
|
164
|
+
const note = document.createElement("div");
|
|
165
|
+
note.className = `note${annotation.stale ? " stale" : ""}`;
|
|
166
|
+
note.tabIndex = 0;
|
|
167
|
+
note.setAttribute("role", "note");
|
|
168
|
+
note.setAttribute("aria-label", `标注 ${index + 1}:${annotation.requirement}`);
|
|
169
|
+
note.style.cssText = `left:${noteLeft}px;top:${noteTop}px;width:${noteWidth}px`;
|
|
170
|
+
const summary = document.createElement("div");
|
|
171
|
+
summary.className = "note-summary";
|
|
172
|
+
const badge = document.createElement("span");
|
|
173
|
+
badge.className = "note-badge";
|
|
174
|
+
badge.textContent = String(index + 1);
|
|
175
|
+
const text = document.createElement("span");
|
|
176
|
+
text.className = "note-text";
|
|
177
|
+
text.textContent = annotation.requirement;
|
|
178
|
+
const stale = document.createElement("span");
|
|
179
|
+
stale.className = "note-stale";
|
|
180
|
+
stale.textContent = "需重新圈选";
|
|
181
|
+
summary.append(badge, text, stale);
|
|
182
|
+
const detail = document.createElement("div");
|
|
183
|
+
detail.className = "note-detail";
|
|
184
|
+
const targetMeta = document.createElement("div");
|
|
185
|
+
targetMeta.className = "note-target";
|
|
186
|
+
targetMeta.textContent = `${annotation.elementType}${annotation.visibleText ? ` · ${annotation.visibleText}` : ""}`;
|
|
187
|
+
const actions = document.createElement("div");
|
|
188
|
+
actions.className = "note-actions";
|
|
189
|
+
const edit = document.createElement("button");
|
|
190
|
+
edit.type = "button";
|
|
191
|
+
edit.textContent = "✎ 编辑";
|
|
192
|
+
edit.addEventListener("click", () => openEditor(
|
|
193
|
+
[{ ...annotation, score: 0 }],
|
|
194
|
+
Math.max(16, Math.min(rect.right + 12, window.innerWidth - 406)),
|
|
195
|
+
Math.max(16, rect.bottom + 12),
|
|
196
|
+
annotation,
|
|
197
|
+
));
|
|
198
|
+
const remove = document.createElement("button");
|
|
199
|
+
remove.type = "button";
|
|
200
|
+
remove.className = "danger";
|
|
201
|
+
remove.textContent = "⌫ 删除";
|
|
202
|
+
remove.addEventListener("click", () => {
|
|
203
|
+
annotations = annotations.filter((entry) => entry.id !== annotation.id);
|
|
204
|
+
syncState();
|
|
205
|
+
});
|
|
206
|
+
actions.append(edit, remove);
|
|
207
|
+
detail.append(targetMeta, actions);
|
|
208
|
+
note.append(summary, detail);
|
|
209
|
+
boxes.append(box, note);
|
|
210
|
+
});
|
|
211
|
+
const active = locked || modifierDown;
|
|
212
|
+
drawbar.style.display = active && !editing ? "flex" : "none";
|
|
213
|
+
batch.style.display = annotations.length > 0 && !editing && !active ? "flex" : "none";
|
|
214
|
+
required<HTMLElement>(shadow, ".draw-hint").textContent = resolving
|
|
215
|
+
? "正在定位元素…"
|
|
216
|
+
: "按住鼠标直接画圈,松开即可标注";
|
|
217
|
+
undo.disabled = annotations.length === 0;
|
|
218
|
+
submit.disabled = annotations.some((annotation) => annotation.stale);
|
|
219
|
+
submit.title = submit.disabled ? "存在需重新圈选的失效标注" : "追加到 AI 助手草稿";
|
|
220
|
+
required<HTMLElement>(shadow, "#batch .count").textContent = `已标注 ${annotations.length}/${MAX_ANNOTATIONS}`;
|
|
221
|
+
};
|
|
222
|
+
const syncState = () => {
|
|
223
|
+
render();
|
|
224
|
+
post({ type: STATE, annotations });
|
|
225
|
+
};
|
|
226
|
+
const closeEditor = () => {
|
|
227
|
+
editing = undefined;
|
|
228
|
+
pendingCandidates = [];
|
|
229
|
+
editor.style.display = "none";
|
|
230
|
+
setCapture();
|
|
231
|
+
render();
|
|
232
|
+
};
|
|
233
|
+
const selectCandidate = (index: number) => {
|
|
234
|
+
const selected = pendingCandidates[index] as ResolvedCandidate | undefined;
|
|
235
|
+
if (!selected) return;
|
|
236
|
+
candidate.value = String(index);
|
|
237
|
+
location.textContent = `${selected.path}:${selected.startLine}-${selected.endLine}`;
|
|
238
|
+
element.textContent = `${selected.elementType}${selected.visibleText ? ` · “${selected.visibleText}”` : ""}`;
|
|
239
|
+
};
|
|
240
|
+
const openEditor = (resolved: ResolvedCandidate[], x: number, y: number, current?: Annotation) => {
|
|
241
|
+
pendingCandidates = resolved;
|
|
242
|
+
editing = current ?? ({ ...resolved[0]!, id: crypto.randomUUID(), requirement: "" });
|
|
243
|
+
candidate.replaceChildren(...resolved.map((entry, index) => {
|
|
244
|
+
const option = document.createElement("option");
|
|
245
|
+
option.value = String(index);
|
|
246
|
+
option.textContent = `${index === 0 ? "推荐 · " : ""}${entry.elementType}${entry.visibleText ? ` · ${entry.visibleText}` : ""}`;
|
|
247
|
+
return option;
|
|
248
|
+
}));
|
|
249
|
+
candidate.style.display = resolved.length > 1 ? "block" : "none";
|
|
250
|
+
const selectedIndex = current ? Math.max(0, resolved.findIndex((entry) => entry.token === current.token && entry.tokenIndex === current.tokenIndex)) : 0;
|
|
251
|
+
selectCandidate(selectedIndex);
|
|
252
|
+
requirement.value = current?.requirement ?? "";
|
|
253
|
+
editor.style.display = "block";
|
|
254
|
+
const width = Math.min(390, window.innerWidth - 32);
|
|
255
|
+
editor.style.left = `${Math.max(16, Math.min(x + 12, window.innerWidth - width - 16))}px`;
|
|
256
|
+
editor.style.top = `${Math.max(16, Math.min(y + 12, window.innerHeight - 250))}px`;
|
|
257
|
+
setCapture();
|
|
258
|
+
render();
|
|
259
|
+
requirement.focus();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const finishDrawing = () => {
|
|
263
|
+
drawing = false;
|
|
264
|
+
if (annotations.length >= MAX_ANNOTATIONS) {
|
|
265
|
+
clearStroke();
|
|
266
|
+
showToast(`每批最多 ${MAX_ANNOTATIONS} 条标注,请先提交或删除。`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const trail = simplifyGesture(points);
|
|
270
|
+
const gesture = drawingPolygon(trail);
|
|
271
|
+
stroke.setAttribute("d", pathFor(gesture, true));
|
|
272
|
+
stroke.classList.add("closed");
|
|
273
|
+
const candidates = rankCandidates(gesture, capture, trail);
|
|
274
|
+
if (candidates.length === 0) {
|
|
275
|
+
clearStroke();
|
|
276
|
+
showToast("该区域暂不可圈改,请圈选业务 JSX 渲染的元素。");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
pendingCandidates = candidates;
|
|
280
|
+
const requestId = `${Date.now().toString(36)}-${++requestSequence}`;
|
|
281
|
+
const anchor = points[points.length - 1] ?? gesture[gesture.length - 1]!;
|
|
282
|
+
pendingRequests.set(requestId, { candidates, anchor });
|
|
283
|
+
resolving = true;
|
|
284
|
+
setCapture();
|
|
285
|
+
render();
|
|
286
|
+
post({ type: CANDIDATES, requestId, candidates });
|
|
287
|
+
};
|
|
288
|
+
const pendingRequests = new Map<string, { candidates: LocalCandidate[]; anchor: Point }>();
|
|
289
|
+
|
|
290
|
+
capture.addEventListener("pointerdown", (event) => {
|
|
291
|
+
if (editing || !(locked || modifierDown) || event.button !== 0) return;
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
event.stopPropagation();
|
|
294
|
+
drawing = true;
|
|
295
|
+
resolving = false;
|
|
296
|
+
points = [{ x: event.clientX, y: event.clientY }];
|
|
297
|
+
clearStroke();
|
|
298
|
+
stroke.setAttribute("d", pathFor(points));
|
|
299
|
+
try { capture.setPointerCapture(event.pointerId); } catch { /* Synthetic accessibility/test input may not own the pointer. */ }
|
|
300
|
+
}, true);
|
|
301
|
+
capture.addEventListener("pointermove", (event) => {
|
|
302
|
+
if (!drawing) return;
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
appendPointerSamples(points, event);
|
|
305
|
+
stroke.setAttribute("d", pathFor(points));
|
|
306
|
+
}, true);
|
|
307
|
+
capture.addEventListener("pointerup", (event) => {
|
|
308
|
+
if (!drawing) return;
|
|
309
|
+
event.preventDefault();
|
|
310
|
+
event.stopPropagation();
|
|
311
|
+
appendPointerSamples(points, event);
|
|
312
|
+
finishDrawing();
|
|
313
|
+
}, true);
|
|
314
|
+
capture.addEventListener("pointercancel", () => {
|
|
315
|
+
drawing = false;
|
|
316
|
+
resolving = false;
|
|
317
|
+
points = [];
|
|
318
|
+
clearStroke();
|
|
319
|
+
}, true);
|
|
320
|
+
capture.addEventListener("click", (event) => { if (locked || modifierDown) { event.preventDefault(); event.stopPropagation(); } }, true);
|
|
321
|
+
|
|
322
|
+
const cancelCurrent = () => {
|
|
323
|
+
if (drawing || resolving || pendingRequests.size > 0) {
|
|
324
|
+
drawing = false;
|
|
325
|
+
resolving = false;
|
|
326
|
+
points = [];
|
|
327
|
+
pendingRequests.clear();
|
|
328
|
+
clearStroke();
|
|
329
|
+
setCapture();
|
|
330
|
+
render();
|
|
331
|
+
} else if (editing) closeEditor();
|
|
332
|
+
else if (locked) {
|
|
333
|
+
locked = false;
|
|
334
|
+
setCapture();
|
|
335
|
+
render();
|
|
336
|
+
announceMode();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
window.addEventListener("keydown", (event) => {
|
|
341
|
+
if (event.key === "Control" || event.key === "Meta") {
|
|
342
|
+
modifierDown = true;
|
|
343
|
+
setCapture();
|
|
344
|
+
render();
|
|
345
|
+
announceMode();
|
|
346
|
+
}
|
|
347
|
+
if (event.key === "Escape") cancelCurrent();
|
|
348
|
+
}, true);
|
|
349
|
+
window.addEventListener("keyup", (event) => {
|
|
350
|
+
if (event.key === "Control" || event.key === "Meta") {
|
|
351
|
+
modifierDown = false;
|
|
352
|
+
setCapture();
|
|
353
|
+
render();
|
|
354
|
+
announceMode();
|
|
355
|
+
}
|
|
356
|
+
}, true);
|
|
357
|
+
window.addEventListener("blur", () => {
|
|
358
|
+
modifierDown = false;
|
|
359
|
+
drawing = false;
|
|
360
|
+
resolving = false;
|
|
361
|
+
points = [];
|
|
362
|
+
clearStroke();
|
|
363
|
+
setCapture();
|
|
364
|
+
render();
|
|
365
|
+
announceMode();
|
|
366
|
+
});
|
|
367
|
+
window.addEventListener("scroll", render, true);
|
|
368
|
+
window.addEventListener("resize", render);
|
|
369
|
+
|
|
370
|
+
candidate.addEventListener("change", () => selectCandidate(Number(candidate.value)));
|
|
371
|
+
required<HTMLElement>(shadow, "#cancel").addEventListener("click", closeEditor);
|
|
372
|
+
required<HTMLElement>(shadow, "#editor-close").addEventListener("click", closeEditor);
|
|
373
|
+
const save = () => {
|
|
374
|
+
const text = requirement.value.trim();
|
|
375
|
+
const selected = pendingCandidates[Number(candidate.value)] as ResolvedCandidate | undefined;
|
|
376
|
+
if (!text || !selected || !editing) {
|
|
377
|
+
showToast("请先填写修改要求。");
|
|
378
|
+
requirement.focus();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const next: Annotation = { ...selected, id: editing.id, requirement: text };
|
|
382
|
+
const prior = annotations.findIndex((entry) => entry.id === next.id);
|
|
383
|
+
annotations = prior >= 0 ? annotations.map((entry, index) => index === prior ? next : entry) : [...annotations, next];
|
|
384
|
+
closeEditor();
|
|
385
|
+
syncState();
|
|
386
|
+
};
|
|
387
|
+
required<HTMLElement>(shadow, "#save").addEventListener("click", save);
|
|
388
|
+
requirement.addEventListener("keydown", (event) => {
|
|
389
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { event.preventDefault(); save(); }
|
|
390
|
+
if (event.key === "Escape") { event.preventDefault(); closeEditor(); }
|
|
391
|
+
});
|
|
392
|
+
required<HTMLElement>(shadow, "#clear").addEventListener("click", () => { annotations = []; syncState(); });
|
|
393
|
+
undo.addEventListener("click", () => {
|
|
394
|
+
if (annotations.length === 0) return;
|
|
395
|
+
annotations = annotations.slice(0, -1);
|
|
396
|
+
syncState();
|
|
397
|
+
});
|
|
398
|
+
required<HTMLElement>(shadow, "#finish").addEventListener("click", () => {
|
|
399
|
+
locked = false;
|
|
400
|
+
modifierDown = false;
|
|
401
|
+
resolving = false;
|
|
402
|
+
points = [];
|
|
403
|
+
pendingRequests.clear();
|
|
404
|
+
clearStroke();
|
|
405
|
+
setCapture();
|
|
406
|
+
render();
|
|
407
|
+
announceMode();
|
|
408
|
+
});
|
|
409
|
+
submit.addEventListener("click", () => post({ type: SUBMIT }));
|
|
410
|
+
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
|
411
|
+
if (event.source !== window.parent || !record(event.data) || event.data.version !== VERSION || event.data.channelId !== channel) return;
|
|
412
|
+
if (event.data.type === MODE && typeof event.data.locked === "boolean") {
|
|
413
|
+
locked = event.data.locked;
|
|
414
|
+
setCapture();
|
|
415
|
+
render();
|
|
416
|
+
announceMode();
|
|
417
|
+
} else if (event.data.type === MODIFIER && typeof event.data.active === "boolean") {
|
|
418
|
+
modifierDown = event.data.active;
|
|
419
|
+
setCapture();
|
|
420
|
+
render();
|
|
421
|
+
announceMode();
|
|
422
|
+
} else if (event.data.type === CANCEL) {
|
|
423
|
+
cancelCurrent();
|
|
424
|
+
} else if (event.data.type === SYNC && Array.isArray(event.data.annotations)) {
|
|
425
|
+
annotations = event.data.annotations.flatMap(parseAnnotation).slice(0, MAX_ANNOTATIONS);
|
|
426
|
+
render();
|
|
427
|
+
} else if (event.data.type === CANDIDATES_RESULT && typeof event.data.requestId === "string") {
|
|
428
|
+
const pending = pendingRequests.get(event.data.requestId);
|
|
429
|
+
pendingRequests.delete(event.data.requestId);
|
|
430
|
+
if (!pending) return;
|
|
431
|
+
resolving = false;
|
|
432
|
+
points = [];
|
|
433
|
+
clearStroke();
|
|
434
|
+
setCapture();
|
|
435
|
+
render();
|
|
436
|
+
if (typeof event.data.error === "string") { showToast(event.data.error); return; }
|
|
437
|
+
if (!Array.isArray(event.data.candidates)) return;
|
|
438
|
+
const resolved = event.data.candidates.flatMap(parseResolvedCandidate);
|
|
439
|
+
if (resolved.length === 0) { showToast("该区域暂不可圈改,请换一个业务元素。 "); return; }
|
|
440
|
+
openEditor(resolved, pending.anchor.x, pending.anchor.y);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
post({ type: READY });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function required<T extends Element>(root: ShadowRoot, selector: string): T {
|
|
447
|
+
const value = root.querySelector<T>(selector);
|
|
448
|
+
if (!value) throw new Error(`Missing inspector element: ${selector}`);
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
453
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseResolvedCandidate(value: unknown): ResolvedCandidate[] {
|
|
457
|
+
if (!record(value) || typeof value.token !== "string" || typeof value.path !== "string" ||
|
|
458
|
+
typeof value.elementType !== "string" || typeof value.visibleText !== "string" ||
|
|
459
|
+
!Number.isSafeInteger(value.tokenIndex) || !Number.isSafeInteger(value.startLine) ||
|
|
460
|
+
!Number.isSafeInteger(value.endLine)) return [];
|
|
461
|
+
return [value as unknown as ResolvedCandidate];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function parseAnnotation(value: unknown): Annotation[] {
|
|
465
|
+
const parsed = parseResolvedCandidate(value)[0];
|
|
466
|
+
if (!parsed || !record(value) || typeof value.id !== "string" || typeof value.requirement !== "string" || !value.requirement.trim()) return [];
|
|
467
|
+
return [{
|
|
468
|
+
...parsed,
|
|
469
|
+
id: value.id,
|
|
470
|
+
requirement: value.requirement,
|
|
471
|
+
...(Number.isSafeInteger(value.previewRevision) ? { previewRevision: value.previewRevision as number } : {}),
|
|
472
|
+
...(value.stale === true ? { stale: true } : {}),
|
|
473
|
+
}];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function pathFor(points: Point[], closed = false): string {
|
|
477
|
+
return points.length === 0 ? "" : `M ${points.map((point) => `${point.x} ${point.y}`).join(" L ")}${closed ? " Z" : ""}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function appendPointerSamples(points: Point[], event: PointerEvent): void {
|
|
481
|
+
const samples = typeof event.getCoalescedEvents === "function" ? event.getCoalescedEvents() : [event];
|
|
482
|
+
for (const sample of samples.length > 0 ? samples : [event]) {
|
|
483
|
+
const next = { x: sample.clientX, y: sample.clientY };
|
|
484
|
+
const prior = points[points.length - 1];
|
|
485
|
+
if (!prior || Math.hypot(next.x - prior.x, next.y - prior.y) >= 2) points.push(next);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function simplifyGesture(points: Point[], maximum = MAX_GESTURE_POINTS): Point[] {
|
|
490
|
+
if (points.length <= maximum) return [...points];
|
|
491
|
+
return Array.from({ length: maximum }, (_, index) => points[Math.round(index * (points.length - 1) / (maximum - 1))]!);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function targetBorderRadius(target: Element): string {
|
|
495
|
+
const radius = window.getComputedStyle(target).borderRadius.trim();
|
|
496
|
+
return radius && radius !== "0px" ? radius : "8px";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function drawingPolygon(points: Point[]): Point[] {
|
|
500
|
+
if (points.length >= 3) {
|
|
501
|
+
const xs = points.map((point) => point.x);
|
|
502
|
+
const ys = points.map((point) => point.y);
|
|
503
|
+
if (Math.max(...xs) - Math.min(...xs) >= 4 && Math.max(...ys) - Math.min(...ys) >= 4) return points;
|
|
504
|
+
}
|
|
505
|
+
const center = points[points.length - 1] ?? points[0] ?? { x: 0, y: 0 };
|
|
506
|
+
return Array.from({ length: 16 }, (_, index) => {
|
|
507
|
+
const angle = index / 16 * Math.PI * 2;
|
|
508
|
+
return { x: center.x + Math.cos(angle) * 12, y: center.y + Math.sin(angle) * 12 };
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function pointInPolygon(point: Point, polygon: Point[]): boolean {
|
|
513
|
+
let inside = false;
|
|
514
|
+
for (let left = 0, right = polygon.length - 1; left < polygon.length; right = left++) {
|
|
515
|
+
const a = polygon[left]!;
|
|
516
|
+
const b = polygon[right]!;
|
|
517
|
+
if ((a.y > point.y) !== (b.y > point.y) && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;
|
|
518
|
+
}
|
|
519
|
+
return inside;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function rankCandidates(polygon: Point[], capture: HTMLElement, trail: Point[] = polygon): LocalCandidate[] {
|
|
523
|
+
const allPoints = [...polygon, ...trail];
|
|
524
|
+
const xs = allPoints.map((point) => point.x);
|
|
525
|
+
const ys = allPoints.map((point) => point.y);
|
|
526
|
+
const bounds = { left: Math.min(...xs), right: Math.max(...xs), top: Math.min(...ys), bottom: Math.max(...ys) };
|
|
527
|
+
if (bounds.right - bounds.left < 4 || bounds.bottom - bounds.top < 4) return [];
|
|
528
|
+
const scored = new Map<Element, number>();
|
|
529
|
+
const trailHits = new Map<Element, number>();
|
|
530
|
+
let sampledPoints = 0;
|
|
531
|
+
let trailSamples = 0;
|
|
532
|
+
capture.style.pointerEvents = "none";
|
|
533
|
+
try {
|
|
534
|
+
const xStep = Math.max(8, (bounds.right - bounds.left) / 12);
|
|
535
|
+
const yStep = Math.max(8, (bounds.bottom - bounds.top) / 12);
|
|
536
|
+
for (let y = bounds.top; y <= bounds.bottom; y += yStep) {
|
|
537
|
+
for (let x = bounds.left; x <= bounds.right; x += xStep) {
|
|
538
|
+
if (!pointInPolygon({ x, y }, polygon)) continue;
|
|
539
|
+
sampledPoints += 1;
|
|
540
|
+
const markedAtPoint = markedElementsAt(x, y);
|
|
541
|
+
for (const target of markedAtPoint) scored.set(target, (scored.get(target) ?? 0) + 1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (const point of simplifyGesture(trail, 64)) {
|
|
545
|
+
trailSamples += 1;
|
|
546
|
+
const markedAlongTrail = new Set<Element>();
|
|
547
|
+
for (const [offsetX, offsetY] of [[0, 0], [6, 0], [-6, 0], [0, 6], [0, -6]] as const) {
|
|
548
|
+
for (const target of markedElementsAt(point.x + offsetX, point.y + offsetY)) markedAlongTrail.add(target);
|
|
549
|
+
}
|
|
550
|
+
for (const target of markedAlongTrail) {
|
|
551
|
+
if (!scored.has(target)) scored.set(target, 0);
|
|
552
|
+
trailHits.set(target, (trailHits.get(target) ?? 0) + 1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
capture.style.pointerEvents = "";
|
|
557
|
+
}
|
|
558
|
+
const center = { x: (bounds.left + bounds.right) / 2, y: (bounds.top + bounds.bottom) / 2 };
|
|
559
|
+
return [...scored].map(([target, hits]) => {
|
|
560
|
+
const token = target.getAttribute(SOURCE_ATTRIBUTE)!;
|
|
561
|
+
const rect = target.getBoundingClientRect();
|
|
562
|
+
const overlapWidth = Math.max(0, Math.min(bounds.right, rect.right) - Math.max(bounds.left, rect.left));
|
|
563
|
+
const overlapHeight = Math.max(0, Math.min(bounds.bottom, rect.bottom) - Math.max(bounds.top, rect.top));
|
|
564
|
+
const targetCoverage = Math.min(1, overlapWidth * overlapHeight / Math.max(1, rect.width * rect.height));
|
|
565
|
+
const lassoCoverage = hits / Math.max(1, sampledPoints);
|
|
566
|
+
const trailCoverage = (trailHits.get(target) ?? 0) / Math.max(1, trailSamples);
|
|
567
|
+
const interactive = target.matches("button,a,input,select,textarea,[role=button],[role=link]") ? 40 : 0;
|
|
568
|
+
const containsCenter = center.x >= rect.left && center.x <= rect.right && center.y >= rect.top && center.y <= rect.bottom ? 36 : 0;
|
|
569
|
+
const depth = Math.min(20, elementDepth(target));
|
|
570
|
+
const visibleText = normalizeVisibleText(target.getAttribute("aria-label") || target.textContent || "");
|
|
571
|
+
const tokenIndex = sourceElementsForRank(token).indexOf(target);
|
|
572
|
+
const score = targetCoverage * 140 + lassoCoverage * 90 + trailCoverage * 120 + interactive + containsCenter + depth;
|
|
573
|
+
return { token, elementType: elementType(target), visibleText, tokenIndex: Math.max(0, tokenIndex), score };
|
|
574
|
+
}).sort((left, right) => right.score - left.score).slice(0, 5);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function markedElementsAt(x: number, y: number): Set<Element> {
|
|
578
|
+
const marked = new Set<Element>();
|
|
579
|
+
for (const hit of document.elementsFromPoint(x, y).slice(0, 8)) {
|
|
580
|
+
let current: Element | null = hit;
|
|
581
|
+
let inspected = 0;
|
|
582
|
+
while (current && inspected < 8) {
|
|
583
|
+
if (current.hasAttribute(SOURCE_ATTRIBUTE)) marked.add(current);
|
|
584
|
+
current = current.parentElement;
|
|
585
|
+
inspected += 1;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return marked;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function sourceElementsForRank(token: string): Element[] {
|
|
592
|
+
return [...document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`)].filter((entry) => entry.getAttribute(SOURCE_ATTRIBUTE) === token);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizeVisibleText(value: string): string {
|
|
596
|
+
return value
|
|
597
|
+
.replace(/\s+/gu, " ")
|
|
598
|
+
.trim()
|
|
599
|
+
.replace(/([\p{Script=Han}]) (?=[\p{Script=Han}])/gu, "$1")
|
|
600
|
+
.slice(0, 80);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function elementDepth(element: Element): number {
|
|
604
|
+
let depth = 0;
|
|
605
|
+
for (let current: Element | null = element; current; current = current.parentElement) depth += 1;
|
|
606
|
+
return depth;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function elementType(element: Element): string {
|
|
610
|
+
const role = element.getAttribute("role");
|
|
611
|
+
return role ? `${element.tagName.toLowerCase()}[role=${role}]` : element.tagName.toLowerCase();
|
|
612
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "antd/dist/reset.css";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
|
|
5
|
+
import { RuntimeApp } from "./shell/RuntimeApp.js";
|
|
6
|
+
import "./shell/runtime.css";
|
|
7
|
+
|
|
8
|
+
const root = document.getElementById("root");
|
|
9
|
+
if (root === null) {
|
|
10
|
+
throw new Error("Harness frontend runtime requires a #root element");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
createRoot(root).render(
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<RuntimeApp />
|
|
16
|
+
</StrictMode>,
|
|
17
|
+
);
|