From 8978ac6c082628144a5778ff56adc6444579c5f4 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Wed, 5 Aug 2026 11:41:57 -0400 Subject: [PATCH 01/24] [Improvement] Theme & layout args, changes: - theme design vars - set default layout - set default theme - use design vars in todo app - update README - add theme & layout to appserver - retain appearance for legacy usage - add theme & layout to registry and session - 44 tests passing --- config/apps/theme/bootstrap.yaml | 21 ++++ config/apps/theme/challenging_font.yaml | 22 ++++ config/apps/theme/dark.yaml | 21 ++++ config/apps/theme/default.yaml | 23 +++++ config/apps/theme/material.yaml | 21 ++++ config/apps/theme/mono.yaml | 21 ++++ config/apps/theme/solarized.yaml | 21 ++++ config/apps/todo/layout/default.yaml | 3 + config/apps/todo/layout/kanban_board.yaml | 4 + src/open_apps/theme.py | 119 ++++++++++++++++++++++ 10 files changed, 276 insertions(+) create mode 100644 config/apps/theme/bootstrap.yaml create mode 100644 config/apps/theme/challenging_font.yaml create mode 100644 config/apps/theme/dark.yaml create mode 100644 config/apps/theme/default.yaml create mode 100644 config/apps/theme/material.yaml create mode 100644 config/apps/theme/mono.yaml create mode 100644 config/apps/theme/solarized.yaml create mode 100644 config/apps/todo/layout/default.yaml create mode 100644 config/apps/todo/layout/kanban_board.yaml create mode 100644 src/open_apps/theme.py diff --git a/config/apps/theme/bootstrap.yaml b/config/apps/theme/bootstrap.yaml new file mode 100644 index 00000000..cb2a8bfd --- /dev/null +++ b/config/apps/theme/bootstrap.yaml @@ -0,0 +1,21 @@ +# @package apps.theme +# Bootstrap 5 look approximated with design tokens (no Bootstrap CSS needed). +name: bootstrap +import_url: "" +tokens: + color-bg: "#ffffff" + color-surface: "#f8f9fa" + color-fg: "#212529" + color-muted: "#6c757d" + color-border: "#dee2e6" + color-primary: "#0d6efd" + color-on-primary: "#ffffff" + color-accent: "#198754" + color-danger: "#dc3545" + color-neutral: "#6c757d" + color-btn-fg: "#ffffff" + font-family: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "0.375rem" + space: "8px" diff --git a/config/apps/theme/challenging_font.yaml b/config/apps/theme/challenging_font.yaml new file mode 100644 index 00000000..9e1842b5 --- /dev/null +++ b/config/apps/theme/challenging_font.yaml @@ -0,0 +1,22 @@ +# @package apps.theme +# Faithful port of the former todo `challenging_font` appearance variant: +# the default palette rendered in a hard-to-read script face. +name: challenging_font +import_url: "" +tokens: + color-bg: "#ffffff" + color-surface: "#f4f5f7" + color-fg: "#1a1a1a" + color-muted: "#6b7280" + color-border: "#dfe1e6" + color-primary: "#2563eb" + color-on-primary: "#ffffff" + color-accent: "#2563eb" + color-danger: "#b22222" + color-neutral: "#36454f" + color-btn-fg: "#ffffff" + font-family: "'Brush Script MT', cursive" + font-heading: "inherit" + font-size-base: "16px" + radius: "8px" + space: "8px" diff --git a/config/apps/theme/dark.yaml b/config/apps/theme/dark.yaml new file mode 100644 index 00000000..0f22dd41 --- /dev/null +++ b/config/apps/theme/dark.yaml @@ -0,0 +1,21 @@ +# @package apps.theme +# Faithful port of the former todo `dark_theme` appearance variant. +name: dark +import_url: "" +tokens: + color-bg: "#ffffff" + color-surface: "#000000" + color-fg: "#ffffff" + color-muted: "#9ca3af" + color-border: "#ffffff" + color-primary: "#000000" + color-on-primary: "#ffffff" + color-accent: "#ffffff" + color-danger: "#ffffff" + color-neutral: "#ffffff" + color-btn-fg: "#000000" + font-family: "'Times New Roman', serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "8px" + space: "8px" diff --git a/config/apps/theme/default.yaml b/config/apps/theme/default.yaml new file mode 100644 index 00000000..e10a9381 --- /dev/null +++ b/config/apps/theme/default.yaml @@ -0,0 +1,23 @@ +# @package apps.theme +# Shared design tokens. Selected globally with `apps/theme=` or per app +# with `apps..theme=`. Every `tokens` entry becomes a CSS custom +# property (`color-primary` -> `--color-primary`) consumed via `var(--...)`. +name: default +import_url: "" +tokens: + color-bg: "#ffffff" + color-surface: "#f4f5f7" + color-fg: "#1a1a1a" + color-muted: "#6b7280" + color-border: "#dfe1e6" + color-primary: "#2563eb" # add / primary action + color-on-primary: "#ffffff" + color-accent: "#2563eb" # save action + color-danger: "#b22222" # remove / destructive + color-neutral: "#36454f" # edit / secondary + color-btn-fg: "#ffffff" # text on edit/remove/save buttons + font-family: "'Times New Roman', serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "8px" + space: "8px" diff --git a/config/apps/theme/material.yaml b/config/apps/theme/material.yaml new file mode 100644 index 00000000..46c16eef --- /dev/null +++ b/config/apps/theme/material.yaml @@ -0,0 +1,21 @@ +# @package apps.theme +# Material Design look approximated with design tokens (no MUI CSS needed). +name: material +import_url: "" +tokens: + color-bg: "#ffffff" + color-surface: "#f5f5f5" + color-fg: "#212121" + color-muted: "#757575" + color-border: "#e0e0e0" + color-primary: "#6200ee" + color-on-primary: "#ffffff" + color-accent: "#03dac6" + color-danger: "#b00020" + color-neutral: "#757575" + color-btn-fg: "#ffffff" + font-family: "'Roboto', 'Helvetica Neue', sans-serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "4px" + space: "8px" diff --git a/config/apps/theme/mono.yaml b/config/apps/theme/mono.yaml new file mode 100644 index 00000000..da0ec8b8 --- /dev/null +++ b/config/apps/theme/mono.yaml @@ -0,0 +1,21 @@ +# @package apps.theme +# Faithful port of the former todo `black_and_white` appearance variant. +name: mono +import_url: "" +tokens: + color-bg: "#000000" + color-surface: "#ffffff" + color-fg: "#000000" + color-muted: "#000000" + color-border: "#000000" + color-primary: "#000000" + color-on-primary: "#ffffff" + color-accent: "#000000" + color-danger: "#000000" + color-neutral: "#000000" + color-btn-fg: "#ffffff" + font-family: "'Times New Roman', serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "8px" + space: "8px" diff --git a/config/apps/theme/solarized.yaml b/config/apps/theme/solarized.yaml new file mode 100644 index 00000000..c50333f8 --- /dev/null +++ b/config/apps/theme/solarized.yaml @@ -0,0 +1,21 @@ +# @package apps.theme +# Solarized Light (Ethan Schoonover palette). +name: solarized +import_url: "" +tokens: + color-bg: "#fdf6e3" + color-surface: "#eee8d5" + color-fg: "#657b83" + color-muted: "#93a1a1" + color-border: "#93a1a1" + color-primary: "#268bd2" + color-on-primary: "#fdf6e3" + color-accent: "#2aa198" + color-danger: "#dc322f" + color-neutral: "#586e75" + color-btn-fg: "#fdf6e3" + font-family: "'Inter', system-ui, sans-serif" + font-heading: "inherit" + font-size-base: "16px" + radius: "8px" + space: "8px" diff --git a/config/apps/todo/layout/default.yaml b/config/apps/todo/layout/default.yaml new file mode 100644 index 00000000..f00b4298 --- /dev/null +++ b/config/apps/todo/layout/default.yaml @@ -0,0 +1,3 @@ +# @package apps.todo +# Structure only -- no colors/fonts (those come from the shared theme). +layout: default diff --git a/config/apps/todo/layout/kanban_board.yaml b/config/apps/todo/layout/kanban_board.yaml new file mode 100644 index 00000000..0a0ddb80 --- /dev/null +++ b/config/apps/todo/layout/kanban_board.yaml @@ -0,0 +1,4 @@ +# @package apps.todo +# Structure only -- colors/fonts come from the shared theme. Select with +# `apps/todo/layout=kanban_board`. +layout: kanban_board diff --git a/src/open_apps/theme.py b/src/open_apps/theme.py new file mode 100644 index 00000000..ef5e318c --- /dev/null +++ b/src/open_apps/theme.py @@ -0,0 +1,119 @@ +""" +Copyright (c) Meta Platforms, Inc. and affiliates. +All rights reserved. +This source code is licensed under the license found in the +LICENSE file in the root directory of this source tree. + +Shared design-token theming for OpenApps. + +A *theme* is a set of design tokens (colors, typography, shape, spacing) +defined once in ``config/apps/theme/.yaml`` and shared across every +app. Selecting a theme emits a ``:root { --token: value }`` block that all +apps consume via ``var(--token)``. This decouples *look* (theme) from +*structure* (each app's ``layout``). + +Selection is done with Hydra overrides: + +* ``apps/theme=solarized`` -> global default for every app +* ``apps.todo.theme=solarized`` -> override a single app (falls back to + the global theme when the app's ``theme`` field is null/unset) + +A theme file looks like:: + + # @package apps.theme + name: solarized + import_url: "" # optional external stylesheet escape hatch + tokens: + color-bg: "#fdf6e3" + color-fg: "#657b83" + color-primary: "#268bd2" + font-family: "'Inter', sans-serif" + radius: "8px" + ... + +The ``tokens`` mapping is open-ended: every ``key: value`` becomes the CSS +custom property ``--key: value``, so apps can introduce new tokens without +touching this module. +""" +from __future__ import annotations + +from pathlib import Path + +import yaml +from fasthtml.common import Style + +# Repo-root/config/apps/theme -- this file lives at src/open_apps/theme.py. +_THEME_DIR = Path(__file__).resolve().parents[2] / "config" / "apps" / "theme" + +_DEFAULT_THEME = "default" + + +def _as_plain(value): + """Coerce an OmegaConf node (or anything mapping-like) to a plain dict.""" + if value is None: + return {} + # OmegaConf DictConfig exposes ``items``; so does a plain dict. + if hasattr(value, "items"): + return {k: v for k, v in value.items()} + return dict(value) + + +def load_theme(name: str) -> dict: + """Load a theme's tokens from ``config/apps/theme/.yaml``. + + Returns a dict with at least ``name``, ``tokens`` and ``import_url``. + Falls back to the default theme when ``name`` is unknown so a bad + override degrades gracefully instead of raising. + """ + path = _THEME_DIR / f"{name}.yaml" + if not path.exists(): + path = _THEME_DIR / f"{_DEFAULT_THEME}.yaml" + data = yaml.safe_load(path.read_text()) or {} + data.setdefault("name", name) + data.setdefault("tokens", {}) + data.setdefault("import_url", "") + return data + + +def resolve_theme(apps_config, app_name: str) -> dict: + """Resolve the effective theme for ``app_name``. + + ``apps_config`` is the ``config.apps`` node handed to every app as + ``app.config``. Precedence: per-app ``apps..theme`` (a theme + name string) overrides the global ``apps.theme`` group; a null/unset + per-app value inherits the global theme. + """ + app_cfg = getattr(apps_config, app_name, None) + per_app = getattr(app_cfg, "theme", None) if app_cfg is not None else None + if per_app: + return load_theme(str(per_app)) + + global_theme = getattr(apps_config, "theme", None) + if global_theme is not None: + theme = _as_plain(global_theme) + theme.setdefault("tokens", {}) + theme["tokens"] = _as_plain(theme["tokens"]) + return theme + + return load_theme(_DEFAULT_THEME) + + +def render_theme_tokens(theme: dict) -> Style: + """Build the ``:root`` CSS-variable block (plus optional import) for a theme. + + ``theme`` is the dict returned by :func:`resolve_theme` / :func:`load_theme`. + """ + tokens = _as_plain(theme.get("tokens", {})) + lines = "\n".join(f" --{key}: {value};" for key, value in tokens.items()) + import_url = (theme.get("import_url") or "").strip() + import_rule = f'@import url("{import_url}");\n' if import_url else "" + css = f"{import_rule}:root {{\n{lines}\n}}" + return Style(css) + + +def theme_style(apps_config, app_name: str) -> Style: + """Convenience: resolve + render the token block for ``app_name`` in one call. + + Call this per-request so live ``reconfigure`` theme swaps take effect. + """ + return render_theme_tokens(resolve_theme(apps_config, app_name)) From fad98727033a72c3b6ca8a4ea4c463c3f8fa13d3 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Tue, 18 Aug 2026 19:55:54 -0400 Subject: [PATCH 02/24] Replace real cluster identifiers with placeholders These are on the public tip, not just in history. The earlier cleanup commits scrubbed config/mode/slurm_cluster.yaml and docs/agents.md but missed these two files, so the account, QOS, partition and a node hostname have been readable in a checkout of main this whole time. scripts/conduct_slurm.sh --account, --qos and --partition now use the same *_replace_me placeholders as config/mode/slurm_cluster.yaml. SBATCH directives are parsed before the shell runs, so they cannot be env-substituted -- the header now says they are placeholders and points at submit-time flags, which override the directives. sbatch rejects the job until one or the other names a real allocation, which is the right failure for a public repo. config/agent/gemma-4-computer-use.yaml Example hostname in a comment. This does not touch history -- f584c60 and the commits after it still carry these strings on the public remote. Whether to rewrite that is a separate decision; force-pushing alone would not remove the objects from GitHub anyway. --- config/agent/gemma-4-computer-use.yaml | 2 +- scripts/conduct_slurm.sh | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/config/agent/gemma-4-computer-use.yaml b/config/agent/gemma-4-computer-use.yaml index a7b95e64..28b2043c 100644 --- a/config/agent/gemma-4-computer-use.yaml +++ b/config/agent/gemma-4-computer-use.yaml @@ -9,7 +9,7 @@ api_version: null client_type: "vllm" # For client_type=vllm the URL is built as http://${hostname}:${port}/v1 # (base_url is ignored). The vLLM node changes every SLURM allocation, so pass it -# at launch, e.g.: uv run launch_agent.py agent=gemma-4-e2b-it agent.hostname=h200-000-026 +# at launch, e.g.: uv run launch_agent.py agent=gemma-4-e2b-it agent.hostname=node-001 hostname: null port: "8000" # vLLM does not check the key, but the OpenAI client requires a non-empty string. diff --git a/scripts/conduct_slurm.sh b/scripts/conduct_slurm.sh index 730c3d32..af7c66d4 100755 --- a/scripts/conduct_slurm.sh +++ b/scripts/conduct_slurm.sh @@ -12,14 +12,20 @@ # AGENTS="gemma-4-computer-use" COUNT=20 MAX_PARALLEL=4 sbatch scripts/conduct_slurm.sh # # Override discovery by pinning the node explicitly: -# VLLM_HOST=h200-000-026 AGENTS=gemma-4-e2b-it COUNT=1 sbatch scripts/conduct_slurm.sh +# VLLM_HOST=node-001 AGENTS=gemma-4-e2b-it COUNT=1 sbatch scripts/conduct_slurm.sh # # Any extra CLI args are forwarded verbatim to conduct.sh -> launch_agent.py. # +# The account, QOS and partition below are placeholders — sbatch will reject the +# job until they name a real allocation. Edit them for your cluster, or leave +# them and override at submit time, which takes precedence over these lines: +# +# sbatch --account=... --qos=... --partition=... scripts/conduct_slurm.sh +# #SBATCH --job-name=openapps-eval -#SBATCH --account=memorization -#SBATCH --qos=h200_memorization_high -#SBATCH --partition=h200 +#SBATCH --account=example_replace_me +#SBATCH --qos=qos_example_replace_me +#SBATCH --partition=partition_example_replace_me #SBATCH --nodes=1 #SBATCH --ntasks-per-node=1 #SBATCH --cpus-per-task=2 From 980becca7455c0f8afa04a4fb9d70f0dfe1d46aa Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 20 Aug 2026 19:58:05 -0400 Subject: [PATCH 03/24] Serve htmx and Pico locally instead of from a CDN fast_app()/FastHTML() load htmx, Pico and three helper scripts from cdn.jsdelivr.net by default. The eval nodes have no outbound network, so none of it arrives there, and nothing anywhere reports that: * htmx missing makes every hx-* attribute inert. /todo alone carries 15 hx-put attributes. A checkbox still toggles on click -- that is native browser behaviour, not htmx -- so the screenshot shows the interaction landing while no PUT is sent, /todo_all never changes, and the task scores zero. The trajectory reads as a model that clicked the right element. * Pico missing renders every page unstyled, which changes the observation a screenshot-scored agent is graded on. Vendors htmx 2.0.4 (0BSD) and Pico 2.1.1 (MIT) under apps/assets/vendor, served by the static route that already serves jquery and fontawesome the same way. Versions are pinned in the filename: the default pulled pico@latest, so a run's styling depended on the day it ran. Every FastHTML construction site now passes default_hdrs=False and takes headers from open_apps.frontend.local_hdrs(). That also removes a duplicate htmx 1.9.10 from unpkg in messenger, which was racing the 2.0.4 FastHTML injected, and two picolink references in codeeditor whose hdrs are rebuilt in set_environment. Out of scope here, and still listed in the test allowlist: Tailwind, daisyUI, Font Awesome, highlight.js, phosphor-icons and Leaflet. Those change appearance rather than behaviour; Leaflet needs the map app rethought. tests/test_no_egress.py ratchets this. The load-bearing case is test_interactive_routes_load_htmx_locally: a page with hx-* attributes must load htmx from local assets. Verified it has teeth by reverting todo_app to fast_app() -- three tests fail with the fix in the message. Full suite: 704 passed. --- src/open_apps/apps/assets/vendor/README.md | 57 +++++++ .../apps/assets/vendor/htmx-2.0.4.min.js | 1 + .../apps/assets/vendor/pico-2.1.1.min.css | 4 + src/open_apps/apps/calendar_app/main.py | 4 +- src/open_apps/apps/codeeditor_app/main.py | 9 +- src/open_apps/apps/messenger_app/main.py | 10 +- src/open_apps/apps/start_page/helper.py | 10 +- src/open_apps/apps/todo_app/main.py | 3 +- src/open_apps/frontend.py | 69 ++++++++ tests/test_no_egress.py | 153 ++++++++++++++++++ 10 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 src/open_apps/apps/assets/vendor/README.md create mode 100644 src/open_apps/apps/assets/vendor/htmx-2.0.4.min.js create mode 100644 src/open_apps/apps/assets/vendor/pico-2.1.1.min.css create mode 100644 src/open_apps/frontend.py create mode 100644 tests/test_no_egress.py diff --git a/src/open_apps/apps/assets/vendor/README.md b/src/open_apps/apps/assets/vendor/README.md new file mode 100644 index 00000000..be64e1a2 --- /dev/null +++ b/src/open_apps/apps/assets/vendor/README.md @@ -0,0 +1,57 @@ +# Vendored frontend assets + +Third-party JavaScript and CSS, committed rather than fetched at page load. + +## Why these are checked in + +`fast_app()` / `FastHTML()` load htmx, Pico and three helper scripts from +`cdn.jsdelivr.net` by default. On a host with no outbound network — which is +what the SLURM eval nodes are — none of it arrives, and the failure is silent +and severe: + +* **htmx missing** means every `hx-get` / `hx-post` / `hx-put` in every app is + inert. A checkbox still *appears* to toggle, because that is native browser + behaviour, but no request is sent and no server state changes. An agent + clicks the right element, the screenshot shows the click landed, and the task + scores zero. +* **Pico missing** means every page renders unstyled. For a vision agent scored + on screenshots, that changes the observation itself. + +Neither failure raises an error anywhere. Serving these locally is what makes +the environment behave the same offline as it does on a laptop. + +There is precedent: `../js/jquery.min.js` and `../css/fontawesome-all.min.css` +are already vendored the same way. + +## Contents + +| File | Version | Upstream | License | +|------|---------|----------|---------| +| `htmx-2.0.4.min.js` | 2.0.4 | https://github.com/bigskysoftware/htmx | 0BSD | +| `pico-2.1.1.min.css` | 2.1.1 | https://github.com/picocss/pico | MIT | + +Both are permissive and require no notice retention beyond the copyright +headers already inside the files. Pico's header is intact at the top of the +CSS; htmx's minified bundle carries no header, so its copyright is recorded +here: *Copyright (c) 2020, Big Sky Software — Zero-Clause BSD.* + +Versions are pinned in the filename on purpose. FastHTML's default pulled +`@picocss/pico@latest`, so the styling of an eval run depended on when it ran. + +## Updating + +```sh +V=2.0.5 +curl -fsSL "https://cdn.jsdelivr.net/npm/htmx.org@${V}/dist/htmx.min.js" \ + -o "src/open_apps/apps/assets/vendor/htmx-${V}.min.js" +``` + +Then update the constant in `src/open_apps/frontend.py`, delete the old file, +and update the table above. `tests/test_no_egress.py` will fail if a page ends +up referencing an origin that isn't explicitly allowed. + +## What is deliberately *not* vendored + +`fasthtml.js`, `surreal.js` and `css-scope-inline` are the remaining FastHTML +defaults. Nothing in this repo uses them, so they are switched off rather than +copied in — every vendored file is one more thing to keep patched. diff --git a/src/open_apps/apps/assets/vendor/htmx-2.0.4.min.js b/src/open_apps/apps/assets/vendor/htmx-2.0.4.min.js new file mode 100644 index 00000000..59937d71 --- /dev/null +++ b/src/open_apps/apps/assets/vendor/htmx-2.0.4.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/src/open_apps/apps/assets/vendor/pico-2.1.1.min.css b/src/open_apps/apps/assets/vendor/pico-2.1.1.min.css new file mode 100644 index 00000000..e10ec26d --- /dev/null +++ b/src/open_apps/apps/assets/vendor/pico-2.1.1.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";/*! + * Pico CSS ✨ v2.1.1 (https://picocss.com) + * Copyright 2019-2025 - Licensed under MIT + */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(2, 154, 232, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#0172ad;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 114, 173, 0.5);--pico-primary-hover:#015887;--pico-primary-hover-background:#02659a;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(2, 154, 232, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:host(:not([data-theme])) details summary[role=button].contrast:not(.outline)::after,:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:host(:not([data-theme])) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before,:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown>a::after,details.dropdown>button::after,details.dropdown>summary::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown>summary:not([role]):active,details.dropdown>summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown>summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown>summary:not([role]):focus-visible{outline:0}details.dropdown>summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown>summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown>summary::after{transform:rotate(0) translateX(0)}nav details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown>summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown>summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown>summary+ul[dir=rtl]{right:0;left:auto}details.dropdown>summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown>summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown>summary+ul li a:active,details.dropdown>summary+ul li a:focus,details.dropdown>summary+ul li a:focus-visible,details.dropdown>summary+ul li a:hover,details.dropdown>summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown>summary+ul li label{width:100%}details.dropdown>summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open]>summary{margin-bottom:0}details.dropdown[open]>summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open]>summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header .close,dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article .close,dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} \ No newline at end of file diff --git a/src/open_apps/apps/calendar_app/main.py b/src/open_apps/apps/calendar_app/main.py index 573d0590..1b568f9b 100644 --- a/src/open_apps/apps/calendar_app/main.py +++ b/src/open_apps/apps/calendar_app/main.py @@ -12,6 +12,7 @@ Ul, Li, Hr, Article, Button, RedirectResponse, Container, MarkdownJS, HighlightJS, database, dataclass) from datetime import datetime, timedelta +from src.open_apps.frontend import local_hdrs import calendar import os import logging @@ -185,8 +186,9 @@ def generate_styles_from_config(config): app, rt = fast_app( - pico=True, + default_hdrs=False, hdrs=( + *local_hdrs(), MarkdownJS(), HighlightJS(langs=["python", "javascript", "html", "css"]), Script(src="https://unpkg.com/@phosphor-icons/web"), diff --git a/src/open_apps/apps/codeeditor_app/main.py b/src/open_apps/apps/codeeditor_app/main.py index de29785c..5c3565a0 100644 --- a/src/open_apps/apps/codeeditor_app/main.py +++ b/src/open_apps/apps/codeeditor_app/main.py @@ -11,10 +11,12 @@ import json from starlette.responses import Response from src.open_apps.apps.start_page.helper import create_logo_header +from src.open_apps.frontend import local_hdrs # Global variables _base_hdrs_no_highlight = ( - picolink, + # Pico + htmx from apps/assets/vendor, not jsdelivr (see frontend.py). + *local_hdrs(), Script(src="https://cdn.tailwindcss.com"), Link( rel="stylesheet", @@ -33,7 +35,7 @@ logo_title_container = None # Initialize app with default headers -app = FastHTML(hdrs=_base_hdrs, cls="p-4") +app = FastHTML(hdrs=[*local_hdrs(), *_base_hdrs], cls="p-4", default_hdrs=False) import yaml import os @@ -91,7 +93,8 @@ def set_environment(config): update_db_from_hydra(config) print(f"- Code editor filesystem created under {current_dir}") _base_hdrs_with_highlight = ( - picolink, + # Pico + htmx from apps/assets/vendor, not jsdelivr (see frontend.py). + *local_hdrs(), Script(src="https://cdn.tailwindcss.com"), Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css"), Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css"), diff --git a/src/open_apps/apps/messenger_app/main.py b/src/open_apps/apps/messenger_app/main.py index b815be7e..7c3921a6 100644 --- a/src/open_apps/apps/messenger_app/main.py +++ b/src/open_apps/apps/messenger_app/main.py @@ -11,6 +11,7 @@ import ast import json from src.open_apps.apps.start_page.helper import create_logo_header +from src.open_apps.frontend import local_hdrs @dataclass @@ -253,8 +254,11 @@ class Messages: """) ) _base_hdrs = ( - picolink, - Script(src="https://unpkg.com/htmx.org@1.9.10"), # Add this line if not present + # Pico and htmx served from apps/assets/vendor rather than a CDN. This app + # previously loaded picolink (Pico from jsdelivr) plus htmx 1.9.10 from + # unpkg, on top of the htmx 2.0.4 FastHTML injects by default -- two htmx + # versions racing on a page, both of which vanish on an offline host. + *local_hdrs(), Script(src="https://cdn.tailwindcss.com"), Link( rel="stylesheet", @@ -266,7 +270,7 @@ class Messages: ), _base_chat_script, ) -app = FastHTML(hdrs=_base_hdrs, cls="p-4 max-w-lg mx-auto") +app = FastHTML(hdrs=_base_hdrs, cls="p-4 max-w-lg mx-auto", default_hdrs=False) def set_environment(config): diff --git a/src/open_apps/apps/start_page/helper.py b/src/open_apps/apps/start_page/helper.py index 38aa7f67..203da42d 100644 --- a/src/open_apps/apps/start_page/helper.py +++ b/src/open_apps/apps/start_page/helper.py @@ -10,6 +10,8 @@ import subprocess from pathlib import Path +from open_apps.frontend import local_hdrs + # src/proficiency_playground/playground_server BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -598,7 +600,13 @@ def get_app(hdrs=None, *args, **kwargs): href="/assets/css/main.css", ) ) - app = FastHTML(hdrs=hdrs, *args, **kwargs) + # This is the app that actually serves every route -- the other apps' routes + # are mounted onto it -- so its headers are what the browser sees. htmx and + # Pico come from apps/assets/vendor via the static route below rather than + # from jsdelivr; default_hdrs=False is what stops FastHTML prepending the + # CDN copies. See src/open_apps/frontend.py for why that matters. + hdrs = local_hdrs() + hdrs + app = FastHTML(hdrs=hdrs, *args, default_hdrs=False, **kwargs) @app.get("/{fname:path}.{ext:static}") def static(fname: str, ext: str): diff --git a/src/open_apps/apps/todo_app/main.py b/src/open_apps/apps/todo_app/main.py index 5fd7fb98..7712b54f 100644 --- a/src/open_apps/apps/todo_app/main.py +++ b/src/open_apps/apps/todo_app/main.py @@ -9,6 +9,7 @@ import json from typing import List from src.open_apps.apps.start_page.helper import create_logo_header +from src.open_apps.frontend import local_hdrs @dataclass @@ -18,7 +19,7 @@ class Todo: done: bool -app, rt = fast_app() +app, rt = fast_app(default_hdrs=False, hdrs=local_hdrs()) logo_title_container = None styles = Style("") diff --git a/src/open_apps/frontend.py b/src/open_apps/frontend.py new file mode 100644 index 00000000..1b08c5dc --- /dev/null +++ b/src/open_apps/frontend.py @@ -0,0 +1,69 @@ +""" +Copyright (c) Meta Platforms, Inc. and affiliates. +All rights reserved. +This source code is licensed under the license found in the +LICENSE file in the root directory of this source tree. + +Locally-served frontend headers. + +``fast_app()`` and ``FastHTML()`` default to loading htmx, Pico and three +helper scripts from ``cdn.jsdelivr.net``. That is fine on a laptop and useless +on an eval node with no outbound network, where the assets never arrive and +nothing says so: + +* without htmx, every ``hx-*`` attribute in every app is inert — a checkbox + still toggles visually, because that is the browser's own behaviour, but no + request is sent and no server state changes, so the task scores zero; +* without Pico, every page renders unstyled, which for a screenshot-scored + agent changes the observation itself. + +Every app therefore constructs its FastHTML instance with +``default_hdrs=False`` and takes its headers from here instead. The files live +in ``apps/assets/vendor/`` and are served by the static route in +``apps/start_page/helper.py``. + +Usage:: + + from open_apps.frontend import local_hdrs + + app, rt = fast_app(default_hdrs=False, hdrs=local_hdrs()) + +``default_hdrs=False`` is not optional. Omit it and FastHTML prepends the CDN +tags anyway, the page works on a laptop, and the regression only shows up as a +run of zero-reward episodes on the cluster. +""" +from __future__ import annotations + +from fasthtml.common import Link, Script + +# Pinned by filename. FastHTML's default pulled `@picocss/pico@latest`, which +# made the styling of an eval run depend on the day it ran. +HTMX_FILENAME = "htmx-2.0.4.min.js" +PICO_FILENAME = "pico-2.1.1.min.css" + +VENDOR_URL = "/assets/vendor" + +HTMX_URL = f"{VENDOR_URL}/{HTMX_FILENAME}" +PICO_URL = f"{VENDOR_URL}/{PICO_FILENAME}" + + +def local_hdrs(pico: bool = True, htmx: bool = True) -> list: + """Return the header elements FastHTML would otherwise load from a CDN. + + Args: + pico: include the Pico baseline stylesheet. Pass ``False`` for an app + that brings its own full stylesheet and only needs the behaviour. + htmx: include htmx. Effectively always wanted — every app in this repo + drives its state changes through ``hx-*`` attributes — but kept + explicit so a static page can opt out. + + Returns a fresh list each call: FastHTML mutates the ``hdrs`` list it is + given, so a shared module-level constant would accumulate one app's headers + onto the next. + """ + hdrs = [] + if pico: + hdrs.append(Link(rel="stylesheet", href=PICO_URL)) + if htmx: + hdrs.append(Script(src=HTMX_URL)) + return hdrs diff --git a/tests/test_no_egress.py b/tests/test_no_egress.py new file mode 100644 index 00000000..55533c7c --- /dev/null +++ b/tests/test_no_egress.py @@ -0,0 +1,153 @@ +""" +Copyright (c) Meta Platforms, Inc. and affiliates. +All rights reserved. +This source code is licensed under the license found in the +LICENSE file in the root directory of this source tree. +""" + +""" +Guards against pages that only work when the host has internet. + +The eval nodes have no outbound network. Anything a page fetches from a CDN +simply does not arrive there, and nothing reports it -- the page still returns +200, the DOM still renders, and the run still produces a trajectory. The two +failures that matter: + +* **htmx missing** makes every ``hx-*`` attribute inert. A checkbox still + toggles when clicked, because that is the browser's own behaviour, so a + screenshot shows the interaction landing while no request is sent and no + server state changes. Every task depending on that state scores zero, and + the trajectory looks like a model that clicked the right thing. +* **Pico missing** renders every page unstyled, which changes the observation a + screenshot-scored agent is graded on. + +``test_interactive_routes_load_htmx_locally`` is the one that catches the first +case. The rest keep the cleanup from regressing. +""" + +from pathlib import Path + +import pytest +import re +from hydra import compose, initialize +from starlette.testclient import TestClient + +from open_apps.apps.start_page.main import ( + app, + initialize_routes_and_configure_task, +) +from open_apps.frontend import HTMX_URL, PICO_URL + +# Every route a browser (or an agent) actually lands on. +ROUTES = ["/", "/todo", "/calendar", "/messages", "/codeeditor/", "/maps"] + +# External origins each route is still allowed to reference, by hostname. +# +# This is a ratchet, not a target: a new entry means new egress and should be +# argued for, while entries disappear as apps move onto local assets. Empty +# lists are the goal state and two routes are already there. +# +# Everything below is a styling or widget dependency. htmx and Pico are +# deliberately absent -- they are covered by their own stricter test, because +# they are the two whose absence changes behaviour rather than appearance. +ALLOWED_EXTERNAL_HOSTS = { + "/": set(), + "/todo": set(), + "/calendar": {"cdn.jsdelivr.net", "unpkg.com"}, # highlight.js, phosphor-icons + "/messages": {"cdn.jsdelivr.net", "cdn.tailwindcss.com", "cdnjs.cloudflare.com"}, + "/codeeditor/": {"cdn.jsdelivr.net", "cdn.tailwindcss.com"}, + "/maps": {"cdnjs.cloudflare.com", "unpkg.com"}, # leaflet + awesome-markers +} + +_URL_RE = re.compile(r'(?:src|href)="(https?://[^"]+)"') +_HX_RE = re.compile(r"\bhx-(?:get|post|put|delete|patch)=") + + +def external_urls(html: str) -> list[str]: + """Absolute URLs the page tells the browser to go fetch. + + TestClient serves from ``http://testserver``, so its own origin is not + external and is filtered out. + """ + return [u for u in _URL_RE.findall(html) if "testserver" not in u] + + +@pytest.fixture(scope="module") +def client(tmp_path_factory): + # A dedicated temp dir rather than the shared ``getbasetemp()``: the apps + # seed their tables with fixed primary keys at startup, so re-initializing + # over another module's database collides on insert. + logs_dir = tmp_path_factory.mktemp("no_egress") + with initialize(version_base=None, config_path="../config/"): + config = compose(config_name="config", overrides=[f"logs_dir={logs_dir}"]) + Path(config.logs_dir).mkdir(parents=True, exist_ok=True) + Path(config.databases_dir).mkdir(parents=True, exist_ok=True) + initialize_routes_and_configure_task(config.apps) + return TestClient(app) + + +@pytest.mark.parametrize("route", ROUTES) +def test_htmx_and_pico_never_come_from_a_cdn(client, route): + """No page may fetch htmx or Pico from an external origin. + + FastHTML's default headers do exactly this, so any app constructed without + ``default_hdrs=False`` reintroduces it. That is the regression this catches: + it passes on a laptop either way, and only diverges on an offline host. + """ + offenders = [ + u for u in external_urls(client.get(route).text) + if "htmx" in u.lower() or "pico" in u.lower() + ] + assert not offenders, ( + f"{route} fetches htmx/Pico from a CDN: {offenders}. " + "Construct the app with default_hdrs=False and hdrs=local_hdrs() " + "(see src/open_apps/frontend.py)." + ) + + +@pytest.mark.parametrize("route", ROUTES) +def test_interactive_routes_load_htmx_locally(client, route): + """A page with ``hx-*`` attributes must load htmx from local assets. + + This is the pairing that actually broke: ``/todo`` carries 15 ``hx-put`` + attributes and htmx came from jsdelivr, so offline every one of them was + dead while the page looked and behaved almost normally. + + Routes with no ``hx-*`` attributes are skipped rather than required to load + htmx -- there is no reason to ship it to a page that does not use it. + """ + html = client.get(route).text + hx_count = len(_HX_RE.findall(html)) + if hx_count == 0: + pytest.skip(f"{route} has no hx-* attributes") + assert HTMX_URL in html, ( + f"{route} has {hx_count} hx-* attributes but does not load {HTMX_URL}. " + "Offline, every one of those interactions silently does nothing." + ) + + +@pytest.mark.parametrize("route", ROUTES) +def test_external_origins_match_allowlist(client, route): + """Fail on *new* egress; allow the known, still-to-be-cleaned dependencies.""" + hosts = {u.split("/")[2] for u in external_urls(client.get(route).text)} + unexpected = hosts - ALLOWED_EXTERNAL_HOSTS[route] + assert not unexpected, ( + f"{route} gained new external origins: {sorted(unexpected)}. " + "Vendor the asset under apps/assets/vendor, or add the host to " + "ALLOWED_EXTERNAL_HOSTS with a reason." + ) + + +@pytest.mark.parametrize("asset_url", [HTMX_URL, PICO_URL]) +def test_vendored_assets_are_served(client, asset_url): + """The vendored files exist and the static route actually serves them. + + A broken path here fails exactly like the CDN did -- 404, no error, dead + interactions -- so it is worth asserting rather than assuming. + """ + response = client.get(asset_url) + assert response.status_code == 200, f"{asset_url} -> {response.status_code}" + assert len(response.content) > 10_000, ( + f"{asset_url} served only {len(response.content)} bytes; " + "the file is probably a truncated or failed download." + ) From 935968ac45b1b1718bba100e7513fc0e6eafa02a Mon Sep 17 00:00:00 2001 From: Smokey Date: Mon, 24 Aug 2026 15:16:43 -0400 Subject: [PATCH 04/24] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/open_apps/apps/messenger_app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/open_apps/apps/messenger_app/main.py b/src/open_apps/apps/messenger_app/main.py index 7c3921a6..44e992bf 100644 --- a/src/open_apps/apps/messenger_app/main.py +++ b/src/open_apps/apps/messenger_app/main.py @@ -11,7 +11,7 @@ import ast import json from src.open_apps.apps.start_page.helper import create_logo_header -from src.open_apps.frontend import local_hdrs +from open_apps.frontend import local_hdrs @dataclass From 0d0c8db9594d69d4bd9e0f1da1e78707f37890d0 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Mon, 24 Aug 2026 15:18:54 -0400 Subject: [PATCH 05/24] [Tweak] Remove redundant string literal --- tests/test_no_egress.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_no_egress.py b/tests/test_no_egress.py index 55533c7c..ee3b002b 100644 --- a/tests/test_no_egress.py +++ b/tests/test_no_egress.py @@ -3,9 +3,7 @@ All rights reserved. This source code is licensed under the license found in the LICENSE file in the root directory of this source tree. -""" -""" Guards against pages that only work when the host has internet. The eval nodes have no outbound network. Anything a page fetches from a CDN From 81e8ec9d7840d528048e9de91bacdb0072192aa9 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Mon, 17 Aug 2026 14:28:07 -0400 Subject: [PATCH 06/24] [Improvement] Per-model coordinate spaces via coord_scale Vision models disagree about what an (x, y) in their output means, and getting it wrong is silent: the click lands somewhere plausible and the episode scores 0 without an error. Gemma clicking off-target under screenshot-only is this failure. Add action_parsers/coords.py::rescale_xy as the single conversion point and route every parser through ActionParser.rescale: - ActionParser gains a coord_scale field. None = raw viewport pixels, N = a normalized [0, N) grid. - qwen3vl's hardcoded _COORD_SPACE = 1000 becomes coord_scale = 1000, so it is now overridable rather than baked in. Behavior unchanged. - uitars was discarding its viewport argument and passing raw pixels straight through, so no normalized-grid model could use that grammar. It now threads a rescale hook into flexible_parser/uitars_parser, applied to click, right_single, and the scroll magnitude (which UI-TARS reads off a point, so it shares the coordinate space). coord_scale defaults to None, keeping the UI-TARS path byte-identical -- pinned by a regression test. - Plumb AgentArgs.coord_scale -> VLLMAgent -> get_action_parser, so a family's default can be overridden per model in yaml. Passing None means "keep the family default", not "raw pixels". rescale_xy applies one scalar against each viewport axis independently, which is what a square normalized grid means; it cannot express a model predicting in its own non-square resized image space. Noted in the docstring. Also log a warning on the (1920, 1080) viewport fallback in vLLM_prompt. It is only reachable with use_screenshot off, but under a non-1080p preset it would silently miscale every rescaled coordinate. Gemma's actual convention is unmeasured -- the Gemma lineage bins locations to 0-1024, but that is inference from the family, not data. A wrong scale fails exactly like no scale, so README documents a calibration recipe using set_of_marks_coordinates.json as the oracle rather than shipping a guessed default. --- config/agent/default.yaml | 9 ++ src/open_apps/agent/README.md | 34 ++++++- .../agent/action_parsers/__init__.py | 23 ++++- src/open_apps/agent/action_parsers/base.py | 11 +++ src/open_apps/agent/action_parsers/coords.py | 41 +++++++++ src/open_apps/agent/action_parsers/qwen3vl.py | 21 ++--- src/open_apps/agent/action_parsers/uitars.py | 18 +++- src/open_apps/agent/utils.py | 33 +++++-- src/open_apps/agent/vLLM_agent.py | 8 +- src/open_apps/agent/vLLM_prompt.py | 8 ++ tests/test_action_parsers.py | 91 ++++++++++++++++++- 11 files changed, 267 insertions(+), 30 deletions(-) create mode 100644 src/open_apps/agent/action_parsers/coords.py diff --git a/config/agent/default.yaml b/config/agent/default.yaml index c14c72af..5fbe0c72 100644 --- a/config/agent/default.yaml +++ b/config/agent/default.yaml @@ -54,6 +54,15 @@ save_som: False # Add a set of marks to the screenshot. # extract_visible_tag: False # Add a "visible" tag to visible elements in the AXTree. # extract_clickable_tag: False # Add a "clickable" tag to clickable elements in the AXTree. extract_coords: False # Add the coordinates of the elements. + +# --- coordinate space --- +# What an (x, y) in the model's output means. null = raw viewport pixels +# (UI-TARS, GPT-4o); an integer N = a normalized [0, N) grid, converted to +# pixels by action_parsers.coords.rescale_xy. Qwen-VL / GLM-VL use 1000; the +# PaliGemma/Gemma lineage bins locations to 1024. Leave null unless you have +# measured the model's convention -- a wrong scale clicks somewhere plausible +# and scores 0 silently. +coord_scale: null # filter_visible_elements_only: False # filter elements that are not visible # use_focused_element: False # use focused element diff --git a/src/open_apps/agent/README.md b/src/open_apps/agent/README.md index facba6da..ed37c3e5 100644 --- a/src/open_apps/agent/README.md +++ b/src/open_apps/agent/README.md @@ -67,7 +67,39 @@ The `vllm_prompt.py` has several key components: - Improve the prompt such that the model can output according to instruction. - Improve the response parser to be more lenient in parsing. Note that if you change the prompt format, you might need to change the parser! - Enable multi-action? (for example, don't some tasks require fill and click at the same time step?) - - Figure out how to map coordinates correctly! Vision models use coordinates and it's unclear when a coordinate is correct but just off by a factor or a coordinate is wrong. + +## Coordinate Spaces + +Vision models disagree about what an `(x, y)` in their output means, and getting +it wrong is silent: the click lands somewhere plausible and the episode scores 0 +without an error. `action_parsers/coords.py::rescale_xy` is the single place that +conversion happens; every parser goes through `ActionParser.rescale`. + +| convention | `coord_scale` | who | +| --- | --- | --- | +| raw viewport pixels | `null` | UI-TARS 1.5, GPT-4o-style computer use | +| normalized 0-1000 | `1000` | Qwen-VL, GLM-VL | +| normalized [0, N) | `N` | PaliGemma/Gemma-lineage `` bins are 0-1024 | + +Each parser family carries a default (`uitars`: null, `qwen3vl`: 1000). Override +per model in the agent yaml with `coord_scale: N`. Note this is one scalar applied +against each viewport axis, which is what a *square* normalized grid means — it +cannot express a model predicting in its own non-square resized image space. + +### Calibrating a new model + +Don't guess the scale, measure it: + +1. Run a few episodes with `save_dir` set. Each step writes ground-truth element + boxes to `/set_of_marks_coordinates.json` (see `utils.save_som_coordinates`). +2. For a step where the model clearly intended a particular element, compare the + raw predicted `(x, y)` (kept verbatim in `displayed_action`) against that + element's `bbox`. +3. `predicted / actual` should come out near a constant ratio per axis. A ratio + of ~0.52 on a 1920-wide viewport means the model is emitting 0-1000; ~0.53 + means 0-1024. A ratio near 1.0 with scattered error means the model is + grounding badly, not scaling wrong — no `coord_scale` will fix that. Fall back + to targeting elements by set-of-marks bid (`save_som: true`) instead. ## Configuration Options diff --git a/src/open_apps/agent/action_parsers/__init__.py b/src/open_apps/agent/action_parsers/__init__.py index d094756b..6f740e4d 100644 --- a/src/open_apps/agent/action_parsers/__init__.py +++ b/src/open_apps/agent/action_parsers/__init__.py @@ -3,6 +3,7 @@ Add a family: write an ``ActionParser`` subclass and register it below. """ from .base import ActionParser, ActionParserResult +from .coords import rescale_xy from .uitars import UITarsActionParser from .qwen3vl import Qwen3VLActionParser @@ -12,12 +13,28 @@ } -def get_action_parser(name: str | None) -> ActionParser: +def get_action_parser( + name: str | None, coord_scale: int | None = None +) -> ActionParser: + """Build the action_parser for ``name``. + + ``coord_scale`` overrides the family's default coordinate space when given; + None keeps the subclass default (uitars: raw pixels, qwen3vl: 0-1000). + """ if not name: name = "uitars" if name not in REGISTRY: raise ValueError(f"Unknown action_parser {name!r}. Available: {sorted(REGISTRY)}") - return REGISTRY[name]() + cls = REGISTRY[name] + if coord_scale is None: + return cls() + return cls(coord_scale=coord_scale) -__all__ = ["ActionParser", "ActionParserResult", "REGISTRY", "get_action_parser"] +__all__ = [ + "ActionParser", + "ActionParserResult", + "REGISTRY", + "get_action_parser", + "rescale_xy", +] diff --git a/src/open_apps/agent/action_parsers/base.py b/src/open_apps/agent/action_parsers/base.py index e46c7979..14a71534 100644 --- a/src/open_apps/agent/action_parsers/base.py +++ b/src/open_apps/agent/action_parsers/base.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import TypedDict +from .coords import rescale_xy + class ActionParserResult(TypedDict, total=False): action: str @@ -16,9 +18,18 @@ class ActionParserResult(TypedDict, total=False): @dataclass class ActionParser: + # Coordinate convention of this model family: None = raw viewport pixels, + # N = normalized [0, N) grid. Subclasses override the default; an agent yaml + # can override the subclass via ``agent.coord_scale``. See coords.rescale_xy. + coord_scale: int | None = None + def default_prompts(self) -> dict: return {} def parse(self, response: str, viewport: tuple[int, int]) -> ActionParserResult: """``viewport`` is (width, height) px. Raise ParseError on bad output.""" raise NotImplementedError + + def rescale(self, x: float, y: float, viewport: tuple[int, int]) -> tuple[int, int]: + """Convert one model-space (x, y) pair into viewport pixels.""" + return rescale_xy(x, y, self.coord_scale, viewport) diff --git a/src/open_apps/agent/action_parsers/coords.py b/src/open_apps/agent/action_parsers/coords.py new file mode 100644 index 00000000..14a66fbc --- /dev/null +++ b/src/open_apps/agent/action_parsers/coords.py @@ -0,0 +1,41 @@ +"""Coordinate-space conversion shared by every action_parser. + +Models disagree on what an (x, y) in their output means. Three conventions are +in play here: + +* raw viewport pixels -- UI-TARS 1.5, GPT-4o-style computer use. ``coord_scale=None``. +* 0-1000 normalized -- Qwen-VL, GLM-VL. ``coord_scale=1000``. +* any other normalized [0, N) grid -- e.g. PaliGemma/Gemma-lineage ```` + bins are 0-1024. ``coord_scale=1024``. + +Getting this wrong is silent: the click lands somewhere plausible-looking on the +page and the episode just scores 0. Set ``coord_scale`` per model family (see +``ActionParser.coord_scale``), never per call site. +""" +from __future__ import annotations + + +def rescale_xy( + x: float, + y: float, + coord_scale: int | None, + viewport: tuple[int, int], +) -> tuple[int, int]: + """Map (x, y) into viewport pixels. + + coord_scale=None: raw pixels (UI-TARS, GPT-4o). coord_scale=1000: + Qwen-VL / GLM-VL 0-1000 normalized. coord_scale=N: any normalized + [0, N) convention. + + ``coord_scale`` is a single scalar applied against each viewport axis + independently, which is what a square normalized grid means. It cannot + express a model that predicts in its own non-square resized image space + (that needs a separate scale per axis). + """ + vw, vh = viewport + if coord_scale: + return ( + int(round(float(x) * vw / coord_scale)), + int(round(float(y) * vh / coord_scale)), + ) + return int(round(float(x))), int(round(float(y))) diff --git a/src/open_apps/agent/action_parsers/qwen3vl.py b/src/open_apps/agent/action_parsers/qwen3vl.py index 3eca495c..c2b2d93f 100644 --- a/src/open_apps/agent/action_parsers/qwen3vl.py +++ b/src/open_apps/agent/action_parsers/qwen3vl.py @@ -8,22 +8,24 @@ import json import re +from dataclasses import dataclass from agentlab.llm.llm_utils import ParseError from .base import ActionParser, ActionParserResult -# The model is prompted with a fictional 1000x1000 screen, so all coordinates -# and scroll deltas it emits are in [0, 1000). -_COORD_SPACE = 1000 - # Closing tag may be absent on truncation; json.loads decides well-formedness. _TOOL_CALL_RE = re.compile(r"\s*(\{.*?\})\s*(?:|$)", re.DOTALL) _THINK_RE = re.compile(r"(.*?)", re.DOTALL | re.IGNORECASE) _ACTION_LINE_RE = re.compile(r"^\s*Action:\s*(.+?)\s*$", re.MULTILINE | re.IGNORECASE) +@dataclass class Qwen3VLActionParser(ActionParser): + # The model is prompted with a fictional 1000x1000 screen, so all + # coordinates and scroll deltas it emits are in [0, 1000). + coord_scale: int | None = 1000 + def parse(self, response: str, viewport: tuple[int, int]) -> ActionParserResult: response = (response or "").strip() if not response: @@ -83,7 +85,7 @@ def _to_browsergym(self, action_name, args: dict, viewport: tuple[int, int]) -> raise ParseError( f"scroll 'delta' must be a [dx, dy] list, got {delta!r}." ) - dx, dy = self._rescale(delta, viewport) + dx, dy = self.rescale(delta[0], delta[1], viewport) return f"scroll({dx}, {dy})" if action_name == "wait": @@ -103,11 +105,4 @@ def _xy(self, args: dict, viewport: tuple[int, int]) -> tuple[int, int]: coord = args.get("coordinate") if not (isinstance(coord, (list, tuple)) and len(coord) == 2): raise ParseError(f"'coordinate' must be a [x, y] list, got {coord!r}.") - return self._rescale(coord, viewport) - - def _rescale(self, xy, viewport: tuple[int, int]) -> tuple[int, int]: - vw, vh = viewport - return ( - int(round(float(xy[0]) * vw / _COORD_SPACE)), - int(round(float(xy[1]) * vh / _COORD_SPACE)), - ) + return self.rescale(coord[0], coord[1], viewport) diff --git a/src/open_apps/agent/action_parsers/uitars.py b/src/open_apps/agent/action_parsers/uitars.py index 95290f0d..abd0380c 100644 --- a/src/open_apps/agent/action_parsers/uitars.py +++ b/src/open_apps/agent/action_parsers/uitars.py @@ -1,11 +1,25 @@ -"""UI-TARS action_parser (default): / ReAct, raw pixel coordinates.""" +"""UI-TARS action_parser (default): / ReAct grammar. + +UI-TARS grounds in raw viewport pixels, so ``coord_scale`` defaults to None and +coordinates pass through untouched. Other models reuse this grammar with a +normalized coordinate grid (Gemma emits a 0-N box); set ``agent.coord_scale`` +in their yaml and the same parser converts to pixels. +""" from __future__ import annotations +from dataclasses import dataclass + from open_apps.agent.utils import flexible_parser from .base import ActionParser, ActionParserResult +@dataclass class UITarsActionParser(ActionParser): + coord_scale: int | None = None + def parse(self, response: str, viewport: tuple[int, int]) -> ActionParserResult: - return flexible_parser(response) + return flexible_parser( + response, + rescale=lambda x, y: self.rescale(x, y, viewport), + ) diff --git a/src/open_apps/agent/utils.py b/src/open_apps/agent/utils.py index d8063f36..d571dcd9 100644 --- a/src/open_apps/agent/utils.py +++ b/src/open_apps/agent/utils.py @@ -187,9 +187,12 @@ def retry( raise ParseError(f"Could not parse a valid value after {n_retry} retries.") -def flexible_parser(response: str) -> dict: +def flexible_parser(response: str, rescale=None) -> dict: """ A parser that tries to correct or interpret the LLMs output into a valid policy, e.g. if it did not close a parenthesis or tag. + + ``rescale`` is an optional ``(x, y) -> (x, y)`` callable mapping the model's + coordinate space onto viewport pixels; None leaves coordinates untouched. """ response = response.strip() result = {"action": None, "think": None} @@ -257,7 +260,7 @@ def flexible_parser(response: str) -> dict: # HACK to help UI TARS: remap UI TARS native actions to browser gym actions result["displayed_action"] = result["action"] # store model native actions - result = uitars_parser(result) + result = uitars_parser(result, rescale=rescale) return result @@ -321,11 +324,21 @@ def _normalize_hotkey_key(key: str) -> "str | None": return "+".join(out) -def uitars_parser(result): - "Translates UITARS actions to browser gym actions" +def uitars_parser(result, rescale=None): + """Translates UITARS actions to browser gym actions. + + ``rescale`` is an optional ``(x, y) -> (x, y)`` callable converting the + model's coordinate space to viewport pixels (see + ``open_apps.agent.action_parsers.coords.rescale_xy``). UI-TARS itself emits + raw pixels, so the default is a no-op; models reusing this grammar with a + normalized grid (Gemma) pass one in. + """ # note karenu: I am not sure if the translation is perfect # in particular if the coord are just transferable like that, but looks reasonable in practice # also both browsergym and uitars docs are ass, so i have to guess + if rescale is None: + def rescale(x, y): + return int(round(float(x))), int(round(float(y))) # UITARS API -> BrowserGym API @@ -341,7 +354,8 @@ def uitars_parser(result): f"Could not parse two integer coordinates from click action: {result['action']!r}. " "Expected format like click(point='(x,y)'), click(start_box='(x,y)'), or click(x=X, y=Y)." ) - result["action"] = f"mouse_click(x={int(coords[0])}, y={int(coords[1])})" + x, y = rescale(coords[0], coords[1]) + result["action"] = f"mouse_click(x={x}, y={y})" # type(content=text) -> keyboard_type(text=text) if result["action"].startswith("type(content="): result["action"] = translate_uitars_type_action(result["action"]) @@ -355,7 +369,9 @@ def uitars_parser(result): nums = re.findall(r"-?\d+", result["action"]) if dir_match and len(nums) >= 2: direction = dir_match.group(1).lower() - x, y = int(nums[0]), int(nums[1]) + # The magnitude is read off the point, so it is in the same space as + # a click coordinate and needs the same conversion. + x, y = rescale(nums[0], nums[1]) if direction == "down": dx, dy = 0, y elif direction == "up": @@ -373,9 +389,8 @@ def uitars_parser(result): f"Could not parse two integer coordinates from right_single action: {result['action']!r}. " "Expected format like right_single(point='(x,y)')." ) - result["action"] = ( - f"mouse_click(x={int(coords[0])}, y={int(coords[1])}, button='right')" - ) + x, y = rescale(coords[0], coords[1]) + result["action"] = f"mouse_click(x={x}, y={y}, button='right')" # hotkey(key='ctrl alt e') -> keyboard_press(key='Control+Alt+e') if result["action"].startswith("hotkey(key="): key_comb = re.findall(r"hotkey\(key='(.*?)'\)", result["action"]) diff --git a/src/open_apps/agent/vLLM_agent.py b/src/open_apps/agent/vLLM_agent.py index f40eff12..9e9dafcd 100644 --- a/src/open_apps/agent/vLLM_agent.py +++ b/src/open_apps/agent/vLLM_agent.py @@ -230,6 +230,10 @@ class AgentArgs(AgentLabAgentArgs): # Per-model-family action_parser (parser + coordinate space). See # open_apps.agent.action_parsers. Default preserves the flexible_parser path. action_parser: str = "uitars" + # Coordinate space the model predicts in: None = raw viewport pixels, + # N = normalized [0, N) grid (Qwen-VL/GLM-VL use 1000). None keeps the + # action_parser's own default. See action_parsers.coords.rescale_xy. + coord_scale: int = None # User-message sections to render; None = legacy default. See # VllmMainPrompt._SECTION_RENDERERS. prompt_sections: list[str] = None @@ -296,6 +300,7 @@ def make_agent(self) -> Agent: prompt_txt=self.prompt_txt, save_dir=self.save_dir, action_parser_name=self.action_parser, + coord_scale=self.coord_scale, prompt_sections=self.prompt_sections, ) @@ -309,6 +314,7 @@ def __init__( max_retry: int = 3, save_dir: str = None, action_parser_name: str = "uitars", + coord_scale: int | None = None, prompt_sections: list[str] | None = None, ): logging.info("Initializing vllmAgent with flags: %s", asdict(flags)) @@ -318,7 +324,7 @@ def __init__( self.flags = flags self.action_set = flags.action.action_set.make_action_set() self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs) - self.action_parser = get_action_parser(action_parser_name) + self.action_parser = get_action_parser(action_parser_name, coord_scale) self.prompt_sections = ( list(prompt_sections) if prompt_sections is not None else None ) diff --git a/src/open_apps/agent/vLLM_prompt.py b/src/open_apps/agent/vLLM_prompt.py index 2691234e..65147d32 100644 --- a/src/open_apps/agent/vLLM_prompt.py +++ b/src/open_apps/agent/vLLM_prompt.py @@ -4,6 +4,7 @@ It is based on the dynamic_prompting module from the agentlab package. """ +import logging from dataclasses import dataclass from PIL import Image import numpy as np @@ -265,7 +266,14 @@ def __init__( if screenshot is not None and hasattr(screenshot, "shape"): self.viewport = (int(screenshot.shape[1]), int(screenshot.shape[0])) else: + # Only reachable with use_screenshot off; any rescaling coordinates + # would silently land in the wrong place under a non-1080p preset. self.viewport = (1920, 1080) + logging.warning( + "No screenshot in obs; falling back to a %s viewport for " + "coordinate rescaling.", + self.viewport, + ) self.action_prompt = ActionPrompt(action_set, action_flags=flags.action, concrete_ex_txt=prompt_txt.get("action_concrete_example"), diff --git a/tests/test_action_parsers.py b/tests/test_action_parsers.py index 959d0c2c..32263b43 100644 --- a/tests/test_action_parsers.py +++ b/tests/test_action_parsers.py @@ -6,7 +6,7 @@ import pytest from agentlab.llm.llm_utils import ParseError -from open_apps.agent.action_parsers import REGISTRY, get_action_parser +from open_apps.agent.action_parsers import REGISTRY, get_action_parser, rescale_xy from open_apps.agent.action_parsers.qwen3vl import Qwen3VLActionParser @@ -30,6 +30,54 @@ def test_get_action_parser_raises_on_unknown_name(): get_action_parser("bogus") +def test_action_parser_families_carry_their_own_coord_space(): + assert get_action_parser("uitars").coord_scale is None + assert get_action_parser("qwen3vl").coord_scale == 1000 + + +def test_coord_scale_argument_overrides_the_family_default(): + assert get_action_parser("uitars", 1024).coord_scale == 1024 + assert get_action_parser("qwen3vl", 1024).coord_scale == 1024 + # None means "keep the family default", not "raw pixels". + assert get_action_parser("qwen3vl", None).coord_scale == 1000 + + +# --------------------------------------------------------------------------- +# rescale_xy: the shared coordinate conversion +# --------------------------------------------------------------------------- + +def test_rescale_xy_passes_raw_pixels_through_when_scale_is_none(): + assert rescale_xy(612, 455, None, VIEWPORT) == (612, 455) + + +def test_rescale_xy_rounds_raw_pixels_to_int(): + assert rescale_xy(612.6, 455.4, None, VIEWPORT) == (613, 455) + + +def test_rescale_xy_maps_normalized_1000_space_to_viewport(): + assert rescale_xy(870, 940, 1000, VIEWPORT) == (1670, 1015) + assert rescale_xy(500, 500, 1000, VIEWPORT) == (960, 540) + + +def test_rescale_xy_maps_normalized_1024_space_to_viewport(): + # Gemma/PaliGemma-lineage bins are 0-1024. + assert rescale_xy(512, 512, 1024, (1280, 800)) == (640, 400) + + +def test_rescale_xy_scales_each_axis_by_its_own_viewport_dimension(): + # Same input coordinate, non-square viewport -> different pixel per axis. + assert rescale_xy(500, 500, 1000, (1280, 800)) == (640, 400) + + +def test_rescale_xy_preserves_sign_for_negative_deltas(): + assert rescale_xy(0, -500, 1000, VIEWPORT) == (0, -540) + + +def test_rescale_xy_treats_zero_scale_as_raw_pixels(): + # ``if coord_scale`` guards the division; 0 must not raise. + assert rescale_xy(100, 200, 0, VIEWPORT) == (100, 200) + + # --------------------------------------------------------------------------- # qwen3vl: prompts live in yaml, not the action_parser # --------------------------------------------------------------------------- @@ -186,3 +234,44 @@ def test_uitars_action_parser_parses_native_action_syntax(): assert out["action"] == "mouse_click(x=100, y=200)" assert out["think"] == "Click the Submit button." assert out["displayed_action"] == "click(point='100 200')" + + +# --------------------------------------------------------------------------- +# uitars grammar + a normalized coord space (the Gemma screenshot-only case) +# --------------------------------------------------------------------------- + +def test_uitars_grammar_rescales_clicks_when_coord_scale_is_set(): + a = get_action_parser("uitars", coord_scale=1000) + response = "tclick(point='(500,500)')" + out = a.parse(response, viewport=(1280, 800)) + assert out["action"] == "mouse_click(x=640, y=400)" + # The model-native text shown in history stays in the model's own space. + assert out["displayed_action"] == "click(point='(500,500)')" + + +def test_uitars_grammar_rescales_right_click_when_coord_scale_is_set(): + a = get_action_parser("uitars", coord_scale=1000) + response = "tright_single(point='(500,500)')" + out = a.parse(response, viewport=(1280, 800)) + assert out["action"] == "mouse_click(x=640, y=400, button='right')" + + +def test_uitars_grammar_rescales_scroll_magnitude_when_coord_scale_is_set(): + # The magnitude is read off the point, so it lives in the same space. + a = get_action_parser("uitars", coord_scale=1000) + response = "tscroll(direction='down', point='(500,500)')" + assert a.parse(response, viewport=(1280, 800))["action"] == "scroll(0, 400)" + + +def test_coord_scale_does_not_touch_bid_or_text_actions(): + a = get_action_parser("uitars", coord_scale=1000) + for action in ['click("96")', 'fill("92", "hello")', "scroll(0, 400)"]: + response = f"t{action}" + assert a.parse(response, viewport=(1280, 800))["action"] == action + + +def test_default_uitars_parser_still_passes_raw_pixels_through(): + """Regression guard: the default path must be byte-identical to before.""" + a = get_action_parser("uitars") + response = "tclick(point='(500,500)')" + assert a.parse(response, viewport=(1280, 800))["action"] == "mouse_click(x=500, y=500)" From 5f7535a09faee32f7ff676708043ee20c1812073 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 6 Aug 2026 14:56:03 -0400 Subject: [PATCH 07/24] [Improvement] Code Editor tasks, changes: 1. edit script task 1. create notes file task 1. documentation 1. tests --- config/tasks/original_tasks.yaml | 21 +++ docs/tasks.md | 39 ++++++ src/open_apps/tasks/__init__.py | 3 + src/open_apps/tasks/tasks.py | 90 +++++++++++++ tests/test_codeeditor_tasks.py | 213 +++++++++++++++++++++++++++++++ 5 files changed, 366 insertions(+) create mode 100644 tests/test_codeeditor_tasks.py diff --git a/config/tasks/original_tasks.yaml b/config/tasks/original_tasks.yaml index 27e0f653..99d7af8a 100644 --- a/config/tasks/original_tasks.yaml +++ b/config/tasks/original_tasks.yaml @@ -193,3 +193,24 @@ navigate_to_messenger: goal: Navigate to the Messenger app. source_app: start_page target_app: messages + +# ---- Code Editor tasks --------------------------------------------------- +# EditFileTask walks the Code Editor file tree (the /codeeditor_all JSON) +# and rewards once the target file's saved contents satisfy the content +# requirement AND differ from the initial state (so an unsaved / unchanged +# file earns no reward). Provide either `expected_content` (full-file match) +# or `required_fragment` (substring match). + +edit_script_add_header_comment: + _target_: open_apps.tasks.tasks.EditFileTask + goal: Open 'script.py' in the Code Editor, add the line '# Reviewed by Bob' as a + comment, and save the file. + file_path: script.py + required_fragment: "# Reviewed by Bob" + +create_notes_file_in_code_editor: + _target_: open_apps.tasks.tasks.EditFileTask + goal: "In the Code Editor, create a new file named 'notes.txt' containing the text + 'TODO: refactor' and save it." + file_path: notes.txt + expected_content: "TODO: refactor" diff --git a/docs/tasks.md b/docs/tasks.md index ccd20b49..7674b173 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -61,6 +61,45 @@ my_custom_task: Finally, ask your agent to solve the task by specifying `task_name=my_custom_task`. +### Example: editing a file in the Code Editor + +`EditFileTask` makes the Code Editor a first-class task app. Unlike the other +apps, the Code Editor state is excluded from the generic state comparison, so +`EditFileTask` implements its own reward: it walks the current file tree (from +the `/codeeditor_all` endpoint), finds the file at `file_path`, and checks its +saved contents. It also compares against the **initial** state so an unsaved or +unchanged file earns no reward. + +Provide either `required_fragment` (a substring that must appear in the file) or +`expected_content` (a whitespace-normalized full-file match): + +```yaml +edit_script_add_header_comment: + _target_: open_apps.tasks.tasks.EditFileTask + goal: Open 'script.py' in the Code Editor, add the line '# Reviewed by Bob' as a + comment, and save the file. + file_path: script.py + required_fragment: "# Reviewed by Bob" +``` + +The same class also covers creating a new file — if the file did not exist in +the initial state, any matching saved content counts as a change: + +```yaml +create_notes_file_in_code_editor: + _target_: open_apps.tasks.tasks.EditFileTask + goal: "In the Code Editor, create a new file named 'notes.txt' containing the text + 'TODO: refactor' and save it." + file_path: notes.txt + expected_content: "TODO: refactor" +``` + +Run it like any other task: + +```shell +uv run launch_agent.py agent=gemma-4 task_name=edit_script_add_header_comment +``` + ## Goal Variations Tasks come with **goal variations**: the same task with its goal reworded in a diff --git a/src/open_apps/tasks/__init__.py b/src/open_apps/tasks/__init__.py index 1da4d1cd..0030d33f 100644 --- a/src/open_apps/tasks/__init__.py +++ b/src/open_apps/tasks/__init__.py @@ -18,6 +18,7 @@ AddToDoTask, CompositeTask, DeleteToDoTask, + EditFileTask, MarkToDoDoneTask, NavigateToAppTask, RemoveEventTask, @@ -33,6 +34,7 @@ "AddToDoTask", "CompositeTask", "DeleteToDoTask", + "EditFileTask", "MarkToDoDoneTask", "NavigateToAppTask", "RemoveEventTask", @@ -58,6 +60,7 @@ "SendMessageTask": "messages", "SavePlaceTask": "map", "RemoveLandmarkTask": "map", + "EditFileTask": "codeeditor", } diff --git a/src/open_apps/tasks/tasks.py b/src/open_apps/tasks/tasks.py index 5984666a..b39b9d74 100644 --- a/src/open_apps/tasks/tasks.py +++ b/src/open_apps/tasks/tasks.py @@ -672,6 +672,7 @@ def check_if_task_is_complete( ) +@dataclass @dataclass class CompositeTask(Task): """A meta-task made of several sub-tasks that must *all* be satisfied. @@ -753,5 +754,94 @@ def check_if_task_is_complete( return app_state_comparison.compare() +@dataclass +class EditFileTask(Task): + """Edit (and save) a file in the Code Editor app. + + Because the Code Editor state is intentionally excluded from + ``AppStateComparison`` (see ``AppStateComparison.preprocess``), this + task implements its own reward logic that walks the Code Editor file + tree directly (the JSON returned by the ``/codeeditor_all`` endpoint). + + Reward = 1 once the file at ``file_path`` in the *current* Code Editor + state satisfies the content requirement **and** its content differs + from the *initial* Code Editor state. The latter check ensures an + unsaved / unchanged file does not earn reward. + + Provide exactly one content requirement: + * ``expected_content`` — the file's full contents must match this + (whitespace-normalized), or + * ``required_fragment`` — this text fragment must appear in the file. + + This class also handles the "create a new file" case: if the file did + not exist in the initial state, any matching current content counts as + a change. + """ + + file_path: str + expected_content: str | None = None + required_fragment: str | None = None + + @staticmethod + def _normalize(text: str) -> str: + # Normalize line endings and strip trailing whitespace per line so + # editor-added newlines / indentation noise don't cause spurious + # mismatches. + lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n") + return "\n".join(line.rstrip() for line in lines).strip() + + def _find_file_content(self, tree: dict, target_path: str) -> str | None: + """Return the content of the file at ``target_path`` in the file + tree, or ``None`` if no such file exists.""" + if not isinstance(tree, dict): + return None + for child in tree.get("children", []) or []: + if child.get("type") == "file" and child.get("path") == target_path: + return child.get("content", "") + if child.get("type") == "folder": + found = self._find_file_content(child, target_path) + if found is not None: + return found + return None + + def _content_matches(self, content: str) -> bool: + if self.expected_content is not None: + return self._normalize(content) == self._normalize(self.expected_content) + if self.required_fragment is not None: + return self._normalize(self.required_fragment) in self._normalize(content) + raise ValueError( + "EditFileTask requires either 'expected_content' or 'required_fragment'" + ) + + def check_if_task_is_complete( + self, initial_state: dict, current_state: dict, current_url: str | None = None + ) -> bool: + if isinstance(current_state, DictConfig): + current_state = OmegaConf.to_container(current_state, resolve=True) + if isinstance(initial_state, DictConfig): + initial_state = OmegaConf.to_container(initial_state, resolve=True) + + current_tree = current_state.get("codeeditor") or {} + current_content = self._find_file_content(current_tree, self.file_path) + if current_content is None: + # The target file does not exist in the Code Editor. + return False + + if not self._content_matches(current_content): + # Wrong file / wrong contents. + return False + + # Require the file to have actually changed vs. the initial state so + # that an unsaved or unchanged file does not earn reward. + initial_tree = initial_state.get("codeeditor") or {} + initial_content = self._find_file_content(initial_tree, self.file_path) + if initial_content is not None and self._normalize( + initial_content + ) == self._normalize(current_content): + return False + + return True + + if __name__ == "__main__": pass diff --git a/tests/test_codeeditor_tasks.py b/tests/test_codeeditor_tasks.py new file mode 100644 index 00000000..31d1b67a --- /dev/null +++ b/tests/test_codeeditor_tasks.py @@ -0,0 +1,213 @@ +""" +Copyright (c) Meta Platforms, Inc. and affiliates. +All rights reserved. +This source code is licensed under the license found in the +LICENSE file in the root directory of this source tree. +""" + +""" +Unit tests for the Code Editor EditFileTask reward logic. +""" + +from open_apps.tasks.tasks import EditFileTask +from open_apps.tasks import load_task + + +def make_state(files: dict[str, str]) -> dict: + """Build a cross-app state whose ``codeeditor`` value mirrors the file + tree returned by the ``/codeeditor_all`` endpoint. + + Args: + files: Mapping of file ``path`` (relative to the editor root) to + its text content. + """ + children = [] + for path, content in files.items(): + children.append( + { + "type": "file", + "name": path.split("/")[-1], + "path": path, + "content": content, + } + ) + return { + "codeeditor": {"type": "folder", "name": "codeeditor", "children": children} + } + + +SCRIPT_INITIAL = ( + "# Basic PyTorch tensor operation\n" + "import torch\n" + "\n" + "x = torch.randn(2, 3, 4)\n" +) + + +class TestEditFileTask: + def test_correct_file_and_contents(self): + """Editing the right file with the required fragment earns reward.""" + initial_state = make_state({"script.py": SCRIPT_INITIAL}) + current_state = make_state( + {"script.py": SCRIPT_INITIAL + "# Reviewed by Bob\n"} + ) + task = EditFileTask( + goal="Add a review comment to script.py", + file_path="script.py", + required_fragment="# Reviewed by Bob", + ) + assert task.check_if_task_is_complete(initial_state, current_state) + + def test_correct_contents_in_wrong_file(self): + """The required content in a different file does not earn reward.""" + initial_state = make_state( + {"script.py": SCRIPT_INITIAL, "other.py": "x = 1\n"} + ) + # The fragment ended up in other.py, not script.py. + current_state = make_state( + { + "script.py": SCRIPT_INITIAL, + "other.py": "x = 1\n# Reviewed by Bob\n", + } + ) + task = EditFileTask( + goal="Add a review comment to script.py", + file_path="script.py", + required_fragment="# Reviewed by Bob", + ) + assert not task.check_if_task_is_complete(initial_state, current_state) + + def test_unchanged_contents(self): + """A file whose contents did not change earns no reward, even if the + required fragment already happens to be present.""" + already_has_fragment = SCRIPT_INITIAL + "# Reviewed by Bob\n" + initial_state = make_state({"script.py": already_has_fragment}) + current_state = make_state({"script.py": already_has_fragment}) + task = EditFileTask( + goal="Add a review comment to script.py", + file_path="script.py", + required_fragment="# Reviewed by Bob", + ) + assert not task.check_if_task_is_complete(initial_state, current_state) + + def test_missing_fragment_fails(self): + """Saving the file without the required fragment earns no reward.""" + initial_state = make_state({"script.py": SCRIPT_INITIAL}) + current_state = make_state({"script.py": SCRIPT_INITIAL + "print(x)\n"}) + task = EditFileTask( + goal="Add a review comment to script.py", + file_path="script.py", + required_fragment="# Reviewed by Bob", + ) + assert not task.check_if_task_is_complete(initial_state, current_state) + + def test_expected_content_full_match(self): + """expected_content requires a whitespace-normalized full-file match.""" + initial_state = make_state({"notes.txt": "old\n"}) + current_state = make_state({"notes.txt": "TODO: refactor\n"}) + task = EditFileTask( + goal="Set the contents of notes.txt", + file_path="notes.txt", + expected_content="TODO: refactor", + ) + assert task.check_if_task_is_complete(initial_state, current_state) + + def test_expected_content_partial_does_not_match(self): + """A partial match must fail when expected_content is a full match.""" + initial_state = make_state({"notes.txt": "old\n"}) + current_state = make_state( + {"notes.txt": "TODO: refactor and more stuff\n"} + ) + task = EditFileTask( + goal="Set the contents of notes.txt", + file_path="notes.txt", + expected_content="TODO: refactor", + ) + assert not task.check_if_task_is_complete(initial_state, current_state) + + def test_create_new_file(self): + """Creating a new file that did not exist initially earns reward.""" + initial_state = make_state({"script.py": SCRIPT_INITIAL}) + current_state = make_state( + {"script.py": SCRIPT_INITIAL, "notes.txt": "TODO: refactor\n"} + ) + task = EditFileTask( + goal="Create notes.txt", + file_path="notes.txt", + expected_content="TODO: refactor", + ) + assert task.check_if_task_is_complete(initial_state, current_state) + + def test_target_file_absent(self): + """If the target file does not exist, the task is incomplete.""" + initial_state = make_state({"script.py": SCRIPT_INITIAL}) + current_state = make_state({"script.py": SCRIPT_INITIAL}) + task = EditFileTask( + goal="Create notes.txt", + file_path="notes.txt", + expected_content="TODO: refactor", + ) + assert not task.check_if_task_is_complete(initial_state, current_state) + + def test_nested_file_path(self): + """Files nested inside folders are located by their relative path.""" + nested = { + "codeeditor": { + "type": "folder", + "name": "codeeditor", + "children": [ + { + "type": "folder", + "name": "developing", + "children": [ + { + "type": "file", + "name": "simple_python.py", + "path": "developing/simple_python.py", + "content": '# beginner\nprint("Hello, World!")\n# done\n', + } + ], + } + ], + } + } + initial = { + "codeeditor": { + "type": "folder", + "name": "codeeditor", + "children": [ + { + "type": "folder", + "name": "developing", + "children": [ + { + "type": "file", + "name": "simple_python.py", + "path": "developing/simple_python.py", + "content": '# beginner\nprint("Hello, World!")\n', + } + ], + } + ], + } + } + task = EditFileTask( + goal="Add a comment to the nested file", + file_path="developing/simple_python.py", + required_fragment="# done", + ) + assert task.check_if_task_is_complete(initial, nested) + + +class TestEditFileTaskInstantiation: + def test_edit_task_instantiation(self): + task = load_task("edit_script_add_header_comment") + assert isinstance(task, EditFileTask) + assert task.file_path == "script.py" + assert task.required_fragment == "# Reviewed by Bob" + + def test_create_task_instantiation(self): + task = load_task("create_notes_file_in_code_editor") + assert isinstance(task, EditFileTask) + assert task.file_path == "notes.txt" + assert task.expected_content == "TODO: refactor" From a6c00c759aa14b125f0867abf88b8a4fc3a005e9 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 6 Aug 2026 15:01:01 -0400 Subject: [PATCH 08/24] Gemma 4 agent --- config/agent/gemma-4.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/agent/gemma-4.yaml diff --git a/config/agent/gemma-4.yaml b/config/agent/gemma-4.yaml new file mode 100644 index 00000000..e771d595 --- /dev/null +++ b/config/agent/gemma-4.yaml @@ -0,0 +1,7 @@ +defaults: +- default +- _self_ +model_name: google/gemma-4-E2B-it +model_pretty_name: Gemma-4-E2B-it +client_type: vllm +port: "8000" From 5461a381d625dd88a5a0901dce3a3dcc08054922 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 6 Aug 2026 17:26:15 -0400 Subject: [PATCH 09/24] [Fix] open_pages_urls return agent_info --- src/open_apps/launcher.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/open_apps/launcher.py b/src/open_apps/launcher.py index 715cc045..9bcf2700 100644 --- a/src/open_apps/launcher.py +++ b/src/open_apps/launcher.py @@ -311,8 +311,12 @@ def _log_agent_results_to_wandb(self, exp_record: dict, exp_result): [ i, str(step_info.action), - str(step_info.obs["open_pages_urls"]), - str(step_info.agent_info.get("think")), + str(step_info.obs.get("open_pages_urls") if step_info.obs else None), + str( + step_info.agent_info.get("think") + if step_info.agent_info + else None + ), ] for i, step_info in enumerate(exp_result.steps_info) ] From 569ad62a3f91d3dcf6832e9acc7f2a77d2dde342 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 7 Aug 2026 15:14:21 -0400 Subject: [PATCH 10/24] [Tweak] comment --- scripts/conduct_slurm.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/conduct_slurm.sh b/scripts/conduct_slurm.sh index af7c66d4..5f95e095 100755 --- a/scripts/conduct_slurm.sh +++ b/scripts/conduct_slurm.sh @@ -3,9 +3,8 @@ # SLURM wrapper around scripts/conduct.sh. # # Requests a CPU allocation, auto-discovers the node running vLLM (serving the -# target model on :8000), and launches the worker pool pointed at it. The eval -# job talks to Gemma over the cluster-internal network, so no SSH tunnel from a -# laptop is needed. +# target model on :8000), and launches the worker pool pointed at it. This assume +# the eval job is running on the same internal network as Gemme. # # Submit: # AGENTS=gemma-4-computer-use COUNT=1 sbatch scripts/conduct_slurm.sh From 2ba85f348bb4e706f36d5b272945e7d26afb80a7 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 7 Aug 2026 18:25:03 -0400 Subject: [PATCH 11/24] [Fix] drop deprecated OmegaConf.register_resolver The local 'now' resolver registration was dead code: launcher.py never imported datetime, so the lambda would NameError if it ran, yet runs work because Hydra registers its own 'now' resolver that shadows this one. Remove it to silence the register_resolver() deprecation warning. --- src/open_apps/launcher.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/open_apps/launcher.py b/src/open_apps/launcher.py index 9bcf2700..4f11fcc9 100644 --- a/src/open_apps/launcher.py +++ b/src/open_apps/launcher.py @@ -44,15 +44,10 @@ from open_apps.tasks.tasks import Task from open_apps.utils import merge_plus_keys -try: - # Register the custom 'now' resolver - OmegaConf.register_resolver( - "now", - lambda format_str="%Y-%m-%d_%H-%M-%S": datetime.now().strftime(format_str), - ) -except AssertionError: - # resolver already registered, ignore - pass +# Note: the "now" interpolation resolver (used as ${now:...} in the configs) +# is provided by Hydra's own setup at run/compose time, so we don't register +# one here. (A local registration previously lived here but was shadowed by +# Hydra's and never actually ran.) class OpenAppsLauncher: From 0c0bcf9ccf554b4ee02b1db7495642098d23434a Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 7 Aug 2026 19:58:27 -0400 Subject: [PATCH 12/24] [Cleanup] remove unused Gemma file --- config/agent/gemma-4.yaml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 config/agent/gemma-4.yaml diff --git a/config/agent/gemma-4.yaml b/config/agent/gemma-4.yaml deleted file mode 100644 index e771d595..00000000 --- a/config/agent/gemma-4.yaml +++ /dev/null @@ -1,7 +0,0 @@ -defaults: -- default -- _self_ -model_name: google/gemma-4-E2B-it -model_pretty_name: Gemma-4-E2B-it -client_type: vllm -port: "8000" From ff6750b7eabf8dfd40b32a4b586f4ff3758f768d Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 7 Aug 2026 20:53:13 -0400 Subject: [PATCH 13/24] [Improvement] Gemme 4 31B --- ...-use.yaml => gemma-4-2B-computer-use.yaml} | 2 +- config/agent/gemma-4-31B-computer-use.yaml | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) rename config/agent/{gemma-4-computer-use.yaml => gemma-4-2B-computer-use.yaml} (98%) create mode 100644 config/agent/gemma-4-31B-computer-use.yaml diff --git a/config/agent/gemma-4-computer-use.yaml b/config/agent/gemma-4-2B-computer-use.yaml similarity index 98% rename from config/agent/gemma-4-computer-use.yaml rename to config/agent/gemma-4-2B-computer-use.yaml index 28b2043c..a1a0b261 100644 --- a/config/agent/gemma-4-computer-use.yaml +++ b/config/agent/gemma-4-2B-computer-use.yaml @@ -4,7 +4,7 @@ defaults: # Must exactly match the model vLLM is serving (the --model / served_model_name). model_name: "google/gemma-4-E2B-it" -model_pretty_name: "gemma-4-computer-use" +model_pretty_name: "gemma-4-2B-computer-use" api_version: null client_type: "vllm" # For client_type=vllm the URL is built as http://${hostname}:${port}/v1 diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml new file mode 100644 index 00000000..aead3acf --- /dev/null +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -0,0 +1,79 @@ +defaults: + - default + - _self_ + +# Must exactly match the model vLLM is serving (the --model / served_model_name). +model_name: "google/gemma-4-31B-it" +model_pretty_name: "gemma-4-31B-computer-use" +api_version: null +client_type: "vllm" +# For client_type=vllm the URL is built as http://${hostname}:${port}/v1 +# (base_url is ignored). The vLLM node changes every SLURM allocation, so pass it +# at launch, e.g.: uv run launch_agent.py agent=gemma-4-31B-computer-use agent.hostname=h200-000-026 +hostname: null +port: "8000" +# vLLM does not check the key, but the OpenAI client requires a non-empty string. +api_key: "EMPTY" +temperature: 1 +max_tokens: 5000 +aws_access_key: null +aws_secret_key: null +aws_session_token: null +aws_region: us-west-2 + +custom_actions: +- mouse_click +- mouse_dblclick +- scroll +- mouse_move +- mouse_down +- mouse_up +- mouse_drag_and_drop +- mouse_upload_file +- keyboard_down +- keyboard_up +- keyboard_press +- keyboard_type +- keyboard_insert_text + +use_html: false +use_axtree: false +use_screenshot: true +save_som: false +extract_visible_tag: false +extract_clickable_tag: false +extract_coords: false +filter_visible_elements_only: false +use_focused_element: false +prompt_txt: + system_prompt: You are a GUI agent. You are given a task and your action history, + with screenshots. You need to perform the next action to complete the task. + output_format: ' + + + + + + + + + ' + + think_prompt: null + think_abstract_example: null + think_concrete_example: null + action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ + \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ + \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ + \ python string format. If you want to submit your input, use \\\\n at the end\ + \ of content. \nscroll(direction='down or up', point='(x, y)')\ + \ # Show more information on the `direction` side.\nwait() #Sleep\ + \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ + \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ + \ (with its target element) in one sentence in `Thought` part.\n" + action_abstract_example: 'type(content='''') + + ' + action_concrete_example: 'mouse_click(x=x, y=y) + + ' From 00c20bbceb89fc2c6a8aa6d6d933f76e305678c6 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 13 Aug 2026 18:29:19 -0400 Subject: [PATCH 14/24] [Fix] actions & results, changes: 1. add SOM 1. use axtree 1. use html 1. remove prompt_txt --- config/agent/gemma-4-31B-computer-use.yaml | 55 +++++++++++----------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml index aead3acf..0dc65912 100644 --- a/config/agent/gemma-4-31B-computer-use.yaml +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -36,8 +36,9 @@ custom_actions: - keyboard_type - keyboard_insert_text -use_html: false -use_axtree: false +use_html: true +use_axtree: true +use_som: True # Set-of-marks (visual annotation overlay) use_screenshot: true save_som: false extract_visible_tag: false @@ -45,35 +46,35 @@ extract_clickable_tag: false extract_coords: false filter_visible_elements_only: false use_focused_element: false -prompt_txt: - system_prompt: You are a GUI agent. You are given a task and your action history, - with screenshots. You need to perform the next action to complete the task. - output_format: ' +# prompt_txt: +# system_prompt: You are a GUI agent. You are given a task and your action history, +# with screenshots. You need to perform the next action to complete the task. +# output_format: ' - +# - +# - +# - - ' +# +# ' - think_prompt: null - think_abstract_example: null - think_concrete_example: null - action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ - \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ - \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ - \ python string format. If you want to submit your input, use \\\\n at the end\ - \ of content. \nscroll(direction='down or up', point='(x, y)')\ - \ # Show more information on the `direction` side.\nwait() #Sleep\ - \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ - \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ - \ (with its target element) in one sentence in `Thought` part.\n" - action_abstract_example: 'type(content='''') +# think_prompt: null +# think_abstract_example: null +# think_concrete_example: null +# action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ +# \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ +# \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ +# \ python string format. If you want to submit your input, use \\\\n at the end\ +# \ of content. \nscroll(direction='down or up', point='(x, y)')\ +# \ # Show more information on the `direction` side.\nwait() #Sleep\ +# \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ +# \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ +# \ (with its target element) in one sentence in `Thought` part.\n" +# action_abstract_example: 'type(content='''') - ' - action_concrete_example: 'mouse_click(x=x, y=y) +# ' +# action_concrete_example: 'mouse_click(x=x, y=y) - ' +# ' From 8d5944835d8aa903f6b43e56f4e4505d816c61fa Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 13 Aug 2026 19:49:29 -0400 Subject: [PATCH 15/24] [Fix] remove `use_som` (and comment) --- config/agent/gemma-4-31B-computer-use.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml index 0dc65912..44ed11ef 100644 --- a/config/agent/gemma-4-31B-computer-use.yaml +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -38,9 +38,8 @@ custom_actions: use_html: true use_axtree: true -use_som: True # Set-of-marks (visual annotation overlay) use_screenshot: true -save_som: false +save_som: true extract_visible_tag: false extract_clickable_tag: false extract_coords: false From 7ccdec2ede2de7a406c15581f8cc4e474f3df749 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 13 Aug 2026 20:03:22 -0400 Subject: [PATCH 16/24] [Fix] remove `custom_actions` --- config/agent/gemma-4-31B-computer-use.yaml | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml index 44ed11ef..824276c8 100644 --- a/config/agent/gemma-4-31B-computer-use.yaml +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -21,20 +21,20 @@ aws_secret_key: null aws_session_token: null aws_region: us-west-2 -custom_actions: -- mouse_click -- mouse_dblclick -- scroll -- mouse_move -- mouse_down -- mouse_up -- mouse_drag_and_drop -- mouse_upload_file -- keyboard_down -- keyboard_up -- keyboard_press -- keyboard_type -- keyboard_insert_text +# custom_actions: +# - mouse_click +# - mouse_dblclick +# - scroll +# - mouse_move +# - mouse_down +# - mouse_up +# - mouse_drag_and_drop +# - mouse_upload_file +# - keyboard_down +# - keyboard_up +# - keyboard_press +# - keyboard_type +# - keyboard_insert_text use_html: true use_axtree: true From 4c7d51b6c4e4e49dfe945610f9ed614becee74fa Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Thu, 13 Aug 2026 21:16:50 -0400 Subject: [PATCH 17/24] [Fix] code editor save method --- src/open_apps/apps/codeeditor_app/main.py | 151 ++++++++++++++-------- 1 file changed, 94 insertions(+), 57 deletions(-) diff --git a/src/open_apps/apps/codeeditor_app/main.py b/src/open_apps/apps/codeeditor_app/main.py index 5c3565a0..561b8caf 100644 --- a/src/open_apps/apps/codeeditor_app/main.py +++ b/src/open_apps/apps/codeeditor_app/main.py @@ -204,6 +204,40 @@ def newfile_index(current_path): i += 1 return i +def editor_binding(options_js: str) -> str: + """JS that binds the page-global ``editor`` used by Save / the selectors. + + With ``code_editor.highlight`` on, ``editor`` is a CodeMirror instance + wrapping the ``#editor`` textarea. With it off there is no CodeMirror on + the page (the CDN scripts are only added in the highlight branch of + ``set_environment``), so bind a small shim over the plain textarea that + exposes the handful of methods the page calls: ``getValue`` (Save), + ``setValue``, ``setOption`` (mode/theme selectors), and ``setSize``. + + Without the shim the emitted JS was + ``var editor = (document.getElementById('editor'), {...});`` — the comma + operator, which bound ``editor`` to the *options object*. Every + ``editor.getValue()`` then threw a TypeError, so the Save button silently + did nothing: no POST, no reload, no error modal. + """ + if app.config.code_editor.highlight: + return ( + "var editor = CodeMirror.fromTextArea(document.getElementById('editor'), " + f"{options_js});" + ) + return """ + var editorTextarea = document.getElementById('editor'); + var editor = { + getValue: function() { return editorTextarea.value; }, + setValue: function(value) { editorTextarea.value = value; }, + getOption: function() { return null; }, + setOption: function() {}, + setSize: function() {}, + refresh: function() {}, + focus: function() { editorTextarea.focus(); } + };""" + + def get_file_tree(path: str) -> Dict: """Recursively build a file tree structure""" # base_path = os.path.join(current_dir, "files") @@ -416,6 +450,28 @@ def index(): # files_root = f"{current_dir}/files/" files_root = current_dir file_tree = get_file_tree(files_root) + editor_options = f"""{{ + mode: '{app.config.code_editor.mode}', + theme: '{app.config.code_editor.theme}', + lineNumbers: true, + indentUnit: 4, + tabSize: 4, + indentWithTabs: false, + smartIndent: true, + lineWrapping: true, + extraKeys: {{ + "Tab": function(cm) {{ + if (cm.somethingSelected()) {{ + cm.indentSelection("add"); + }} else {{ + cm.replaceSelection(" ", "end", "+input"); + }} + }}, + "Shift-Tab": function(cm) {{ + cm.indentSelection("subtract"); + }} + }} + }}""" # by default, the main screen should display an empty code editor main_screen = Div(cls="w-5/6")( Div(cls="main-content p-4 rounded-lg styled-content")( @@ -484,28 +540,7 @@ def index(): disabled="disabled" ), Script(f""" - var editor = {'CodeMirror.fromTextArea' if app.config.code_editor.highlight else ''} (document.getElementById('editor'), {{ - mode: '{app.config.code_editor.mode}', - theme: '{app.config.code_editor.theme}', - lineNumbers: true, - indentUnit: 4, - tabSize: 4, - indentWithTabs: false, - smartIndent: true, - lineWrapping: true, - extraKeys: {{ - "Tab": function(cm) {{ - if (cm.somethingSelected()) {{ - cm.indentSelection("add"); - }} else {{ - cm.replaceSelection(" ", "end", "+input"); - }} - }}, - "Shift-Tab": function(cm) {{ - cm.indentSelection("subtract"); - }} - }} - }}); + {editor_binding(editor_options)} {f'editor.setSize("100%", "calc(100vh - 12rem)");' if app.config.code_editor.highlight else ''} """), ), @@ -535,6 +570,12 @@ def get(path: str): def get_folder(folder: str): """Handle folder view with empty editor""" side_bar = create_sidebar(folder) + editor_options = f"""{{ + mode: '{app.config.code_editor.mode}', + theme: '{app.config.code_editor.theme}', + lineNumbers: true, + readOnly: true + }}""" main_screen = Div(cls="w-5/6")( Div(cls="main-content p-4 rounded-lg styled-content")( Div(cls="flex justify-between items-center")( @@ -590,12 +631,7 @@ def get_folder(folder: str): disabled="disabled" ), Script(f""" - var editor = {'CodeMirror.fromTextArea' if app.config.code_editor.highlight else ''} (document.getElementById('editor'), {{ - mode: '{app.config.code_editor.mode}', - theme: '{app.config.code_editor.theme}', - lineNumbers: true, - readOnly: true - }}); + {editor_binding(editor_options)} {f'editor.setSize("100%", "calc(100vh - 12rem)");' if app.config.code_editor.highlight else ''} """), ), @@ -638,6 +674,35 @@ def get_file(file: str): # files_root = f"{current_dir}/files/" files_root = current_dir file_tree = get_file_tree(files_root) + editor_options = f"""{{ + mode: '{app.config.code_editor.mode}', + theme: '{app.config.code_editor.theme}', + lineNumbers: true, + indentUnit: 4, + tabSize: 4, + indentWithTabs: false, + smartIndent: true, + lineWrapping: true, + screenReaderLabel: 'Code editor', + inputStyle: 'contenteditable', + role: 'textbox', + 'aria-multiline': true, + 'aria-atomic': true, + 'aria-live': 'off', + announceMultiline: true, + extraKeys: {{ + "Tab": function(cm) {{ + if (cm.somethingSelected()) {{ + cm.indentSelection("add"); + }} else {{ + cm.replaceSelection(" ", "end", "+input"); + }} + }}, + "Shift-Tab": function(cm) {{ + cm.indentSelection("subtract"); + }} + }} + }}""" # same layout and sidebar as the main screen side_bar = create_sidebar(file) tab_bar = Div(cls="flex overflow-x-auto bg-gray-800 border-b border-gray-700")( @@ -867,35 +932,7 @@ def get_file(file: str): cls="sr-only" )(f"Code editor for editing {file}"), Script(f""" - var editor = {'CodeMirror.fromTextArea' if app.config.code_editor.highlight else ''} (document.getElementById('editor'), {{ - mode: '{app.config.code_editor.mode}', - theme: '{app.config.code_editor.theme}', - lineNumbers: true, - indentUnit: 4, - tabSize: 4, - indentWithTabs: false, - smartIndent: true, - lineWrapping: true, - screenReaderLabel: 'Code editor', - inputStyle: 'contenteditable', - role: 'textbox', - 'aria-multiline': true, - 'aria-atomic': true, - 'aria-live': 'off', - announceMultiline: true, - extraKeys: {{ - "Tab": function(cm) {{ - if (cm.somethingSelected()) {{ - cm.indentSelection("add"); - }} else {{ - cm.replaceSelection(" ", "end", "+input"); - }} - }}, - "Shift-Tab": function(cm) {{ - cm.indentSelection("subtract"); - }} - }} - }}); + {editor_binding(editor_options)} {f'editor.setSize("100%", "calc(100vh - 12rem)");' if app.config.code_editor.highlight else ''} """), ), From 8b23754a8ff2246783c844a65360b2cacaf5f9db Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 14 Aug 2026 11:27:56 -0400 Subject: [PATCH 18/24] [Experiment] bring clicks back for screenshot only usage --- config/agent/gemma-4-31B-computer-use.yaml | 82 +++++++++++----------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml index 824276c8..878c9826 100644 --- a/config/agent/gemma-4-31B-computer-use.yaml +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -21,23 +21,23 @@ aws_secret_key: null aws_session_token: null aws_region: us-west-2 -# custom_actions: -# - mouse_click -# - mouse_dblclick -# - scroll -# - mouse_move -# - mouse_down -# - mouse_up -# - mouse_drag_and_drop -# - mouse_upload_file -# - keyboard_down -# - keyboard_up -# - keyboard_press -# - keyboard_type -# - keyboard_insert_text +custom_actions: +- mouse_click +- mouse_dblclick +- scroll +- mouse_move +- mouse_down +- mouse_up +- mouse_drag_and_drop +- mouse_upload_file +- keyboard_down +- keyboard_up +- keyboard_press +- keyboard_type +- keyboard_insert_text -use_html: true -use_axtree: true +use_html: false +use_axtree: false use_screenshot: true save_som: true extract_visible_tag: false @@ -45,35 +45,35 @@ extract_clickable_tag: false extract_coords: false filter_visible_elements_only: false use_focused_element: false -# prompt_txt: -# system_prompt: You are a GUI agent. You are given a task and your action history, -# with screenshots. You need to perform the next action to complete the task. -# output_format: ' +prompt_txt: + system_prompt: You are a GUI agent. You are given a task and your action history, + with screenshots. You need to perform the next action to complete the task. + output_format: ' -# + -# + -# + -# -# ' + + ' -# think_prompt: null -# think_abstract_example: null -# think_concrete_example: null -# action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ -# \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ -# \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ -# \ python string format. If you want to submit your input, use \\\\n at the end\ -# \ of content. \nscroll(direction='down or up', point='(x, y)')\ -# \ # Show more information on the `direction` side.\nwait() #Sleep\ -# \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ -# \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ -# \ (with its target element) in one sentence in `Thought` part.\n" -# action_abstract_example: 'type(content='''') + think_prompt: null + think_abstract_example: null + think_concrete_example: null + action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ + \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ + \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ + \ python string format. If you want to submit your input, use \\\\n at the end\ + \ of content. \nscroll(direction='down or up', point='(x, y)')\ + \ # Show more information on the `direction` side.\nwait() #Sleep\ + \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ + \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ + \ (with its target element) in one sentence in `Thought` part.\n" + action_abstract_example: 'type(content='''') -# ' -# action_concrete_example: 'mouse_click(x=x, y=y) + ' + action_concrete_example: 'mouse_click(x=x, y=y) -# ' + ' From bfd9349ebd092fdce4bd501e72d0a577347bb331 Mon Sep 17 00:00:00 2001 From: Aaron Smulktis Date: Fri, 14 Aug 2026 12:34:00 -0400 Subject: [PATCH 19/24] [Improvement] Gemma screenshot only, changes: 1. add wait to custom_actions 1. improve prompt (w/bids, not coords) 1. screenshot config 1. agent config test 1. editor save test --- config/agent/gemma-4-31B-computer-use.yaml | 114 +++++++++++++++------ 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/config/agent/gemma-4-31B-computer-use.yaml b/config/agent/gemma-4-31B-computer-use.yaml index 878c9826..060f7896 100644 --- a/config/agent/gemma-4-31B-computer-use.yaml +++ b/config/agent/gemma-4-31B-computer-use.yaml @@ -1,3 +1,12 @@ +# Gemma 4 31B, screenshot-only via set-of-marks. +# +# The model sees ONLY the screenshot (no AXTree, no HTML), but the screenshot is +# annotated with numbered boxes (save_som: true -> ObsFlags.use_som), so it can +# target elements by bid instead of by pixel coordinate. Gemma is not trained +# for pixel-precise GUI grounding the way UI-TARS is, and nothing in this stack +# rescales coordinates for it (the default "uitars" action_parser passes raw +# pixels straight through), so reading a printed bid off the image is the +# reliable path. Pair with: browsergym_env_args=screenshot defaults: - default - _self_ @@ -14,40 +23,48 @@ hostname: null port: "8000" # vLLM does not check the key, but the OpenAI client requires a non-empty string. api_key: "EMPTY" -temperature: 1 +# Grounding wants determinism, not diversity. +temperature: 0 max_tokens: 5000 aws_access_key: null aws_secret_key: null aws_session_token: null aws_region: us-west-2 +# Bid-based actions only. Kept deliberately small: every action here is +# described in action_prompt below, and a smaller space is easier for a model +# that is not GUI-pretrained. All of these pass through flexible_parser +# untouched (the UI-TARS remaps in utils.uitars_parser only fire on +# click(point=/start_box=/x=, type(content=, scroll(direction=, right_single(, +# and hotkey(key=). custom_actions: -- mouse_click -- mouse_dblclick +- click +- fill +- select_option - scroll -- mouse_move -- mouse_down -- mouse_up -- mouse_drag_and_drop -- mouse_upload_file -- keyboard_down -- keyboard_up -- keyboard_press -- keyboard_type -- keyboard_insert_text +- noop use_html: false use_axtree: false use_screenshot: true -save_som: true +save_som: true # feeds ObsFlags.use_som -> the bid-annotated screenshot extract_visible_tag: false extract_clickable_tag: false extract_coords: false filter_visible_elements_only: false use_focused_element: false prompt_txt: - system_prompt: You are a GUI agent. You are given a task and your action history, - with screenshots. You need to perform the next action to complete the task. + system_prompt: |- + You are a GUI agent operating a web browser. At each step you are given a + task, your action history, and a screenshot of the current page. + + The screenshot is annotated with a set of marks: every interactive element + is outlined with a coloured box and labelled with a short id (its "bid"), + drawn at the corner of the box. Target elements by that id. Never guess + pixel coordinates -- there is no coordinate-based action available to you. + + Read the bid off the image carefully; it is the label attached to the box + around the element you want, not the element's visible text. output_format: ' @@ -60,20 +77,57 @@ prompt_txt: ' think_prompt: null - think_abstract_example: null + # Overridden: agentlab's default abstract example tells the model to compute + # coordinates, which contradicts the bid-only action space above. + think_abstract_example: | + + Think step by step. Describe what your previous action changed on the page, + name the element you need next, and read its bid off the annotated + screenshot. + think_concrete_example: null - action_prompt: "## Action Space\n\nmouse_click(x=x, y=y)\nmouse_dblclick(x=x,\ - \ y=y)\ntype(content='xxx') # Use escape characters \\\\', \\\ - \\\\\", and \\\\n in content part to ensure we can parse the content in normal\ - \ python string format. If you want to submit your input, use \\\\n at the end\ - \ of content. \nscroll(direction='down or up', point='(x, y)')\ - \ # Show more information on the `direction` side.\nwait() #Sleep\ - \ for 5s and take a screenshot to check for any changes.\n\n## Note\n- Use English\ - \ in `Thought` part.\n- Write a small plan and finally summarize your next action\ - \ (with its target element) in one sentence in `Thought` part.\n" - action_abstract_example: 'type(content='''') + action_prompt: | + ## Action Space - ' - action_concrete_example: 'mouse_click(x=x, y=y) + click("bid") + Click the element with this bid. Use for links, buttons, checkboxes, + tabs, files in a sidebar, and to focus a text field. - ' + fill("bid", "text") + Type text into the input, textarea or editor with this bid. This + REPLACES the whole current value, so when you are editing existing + content include the existing text plus your change in one call. + + select_option("bid", "option label") + Choose an option in a