mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-04-28 03:30:23 +00:00
browser-agent: selectable _model_config preset for browser runs
This PR keeps the Browser Agent runtime behavior as-is and only adds in the model-preset option for browser runs (highly requested by our users). The Browser Agent can now use either: - the effective Main Model from `_model_config`, or - one saved `_model_config` preset dedicated to browser tasks - this PR brings back LLM customization for Browser Agent plugin, but without over engineering. Model presets-only, not custom provider + LLM, like we have in Email Integration. - created a separate `browser-agent-store.js` page store to remove JS from within x-data in the HTML markup of main.html
This commit is contained in:
parent
071194281c
commit
d30a565549
6 changed files with 363 additions and 105 deletions
35
plugins/_browser_agent/api/model_preset.py
Normal file
35
plugins/_browser_agent/api/model_preset.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from helpers.api import ApiHandler, Request, Response
|
||||
|
||||
from plugins._browser_agent.helpers.model_preset import (
|
||||
get_browser_model_preset_name,
|
||||
save_browser_model_preset_name,
|
||||
)
|
||||
from plugins._model_config.helpers import model_config
|
||||
|
||||
|
||||
class ModelPreset(ApiHandler):
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
action = str(input.get("action", "get") or "get").strip().lower()
|
||||
|
||||
if action == "get":
|
||||
return {
|
||||
"ok": True,
|
||||
"preset_name": get_browser_model_preset_name(),
|
||||
}
|
||||
|
||||
if action not in {"set", "clear"}:
|
||||
return Response(status=400, response=f"Unknown action: {action}")
|
||||
|
||||
preset_name = ""
|
||||
if action == "set":
|
||||
preset_name = str(input.get("preset_name", "") or "").strip()
|
||||
if not preset_name:
|
||||
return Response(status=400, response="Missing preset_name")
|
||||
if not model_config.get_preset_by_name(preset_name):
|
||||
return Response(status=404, response=f"Preset '{preset_name}' not found")
|
||||
|
||||
save_browser_model_preset_name(preset_name)
|
||||
return {
|
||||
"ok": True,
|
||||
"preset_name": preset_name,
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
import importlib.metadata
|
||||
|
||||
from helpers.api import ApiHandler, Request, Response
|
||||
from plugins._browser_agent.helpers.model_preset import (
|
||||
get_browser_model_preset_options,
|
||||
resolve_browser_model_selection,
|
||||
)
|
||||
from plugins._browser_agent.helpers.playwright import (
|
||||
get_playwright_binary,
|
||||
get_playwright_cache_dir,
|
||||
)
|
||||
from plugins._model_config.helpers.model_config import get_chat_model_config
|
||||
|
||||
|
||||
class Status(ApiHandler):
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
cfg = get_chat_model_config()
|
||||
selection = resolve_browser_model_selection()
|
||||
cfg = selection["config"]
|
||||
binary = get_playwright_binary()
|
||||
|
||||
browser_use_ok = False
|
||||
|
|
@ -26,7 +30,12 @@ class Status(ApiHandler):
|
|||
|
||||
return {
|
||||
"plugin": "_browser_agent",
|
||||
"model_source": "Main Model via _model_config",
|
||||
"model_source": selection["source_label"],
|
||||
"model_source_kind": selection["source_kind"],
|
||||
"selected_preset_name": selection["selected_preset_name"],
|
||||
"preset_status": selection["preset_status"],
|
||||
"preset_warning": selection["warning"],
|
||||
"available_presets": get_browser_model_preset_options(),
|
||||
"model": {
|
||||
"provider": cfg.get("provider", ""),
|
||||
"name": cfg.get("name", ""),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import models
|
|||
from browser_use.llm import ChatGoogle, ChatOpenRouter
|
||||
|
||||
from plugins._browser_agent.helpers import browser_use_monkeypatch
|
||||
from plugins._browser_agent.helpers import model_preset
|
||||
from plugins._browser_agent.helpers import browser_use_openrouter_compat
|
||||
from plugins._browser_agent.helpers import browser_use_output_sanitize
|
||||
|
||||
|
|
@ -151,11 +152,11 @@ def build_browser_model_from_config(
|
|||
def build_browser_model_for_agent(agent=None) -> BrowserCompatibleChatWrapper:
|
||||
"""Build and return the browser-use adapter using chat model config."""
|
||||
from plugins._model_config.helpers.model_config import (
|
||||
get_chat_model_config,
|
||||
build_model_config,
|
||||
)
|
||||
import models
|
||||
|
||||
cfg = get_chat_model_config(agent)
|
||||
|
||||
selection = model_preset.resolve_browser_model_selection(agent)
|
||||
cfg = selection["config"]
|
||||
mc = build_model_config(cfg, models.ModelType.CHAT)
|
||||
return build_browser_model_from_config(mc)
|
||||
|
|
|
|||
122
plugins/_browser_agent/helpers/model_preset.py
Normal file
122
plugins/_browser_agent/helpers/model_preset.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from helpers import plugins as plugin_helpers
|
||||
from plugins._model_config.helpers import model_config
|
||||
|
||||
|
||||
MODEL_PRESET_KEY = "model_preset"
|
||||
|
||||
|
||||
def get_browser_model_preset_name(agent=None) -> str:
|
||||
config = plugin_helpers.get_plugin_config("_browser_agent", agent=agent) or {}
|
||||
return str(config.get(MODEL_PRESET_KEY, "") or "").strip()
|
||||
|
||||
|
||||
def get_browser_model_preset_options(agent=None) -> list[dict[str, Any]]:
|
||||
selected_name = get_browser_model_preset_name(agent)
|
||||
options: list[dict[str, Any]] = []
|
||||
found_selected = False
|
||||
|
||||
for preset in model_config.get_presets():
|
||||
name = str(preset.get("name", "") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
if name == selected_name:
|
||||
found_selected = True
|
||||
chat_cfg = preset.get("chat", {}) if isinstance(preset, dict) else {}
|
||||
if not isinstance(chat_cfg, dict):
|
||||
chat_cfg = {}
|
||||
provider = str(chat_cfg.get("provider", "") or "").strip()
|
||||
model_name = str(chat_cfg.get("name", "") or "").strip()
|
||||
summary = " / ".join(part for part in (provider, model_name) if part)
|
||||
options.append(
|
||||
{
|
||||
"name": name,
|
||||
"label": name,
|
||||
"missing": False,
|
||||
"summary": summary,
|
||||
}
|
||||
)
|
||||
|
||||
if selected_name and not found_selected:
|
||||
options.append(
|
||||
{
|
||||
"name": selected_name,
|
||||
"label": f"{selected_name} (missing)",
|
||||
"missing": True,
|
||||
"summary": "",
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def resolve_browser_model_selection(agent=None) -> dict[str, Any]:
|
||||
preset_name = get_browser_model_preset_name(agent)
|
||||
if preset_name:
|
||||
preset = model_config.get_preset_by_name(preset_name)
|
||||
if isinstance(preset, dict):
|
||||
chat_cfg = preset.get("chat", {})
|
||||
if isinstance(chat_cfg, dict) and (
|
||||
str(chat_cfg.get("provider", "") or "").strip()
|
||||
or str(chat_cfg.get("name", "") or "").strip()
|
||||
):
|
||||
return {
|
||||
"config": chat_cfg,
|
||||
"source_kind": "preset",
|
||||
"source_label": f"Preset '{preset_name}' via _model_config",
|
||||
"selected_preset_name": preset_name,
|
||||
"preset_status": "active",
|
||||
"warning": "",
|
||||
}
|
||||
return {
|
||||
"config": model_config.get_chat_model_config(agent),
|
||||
"source_kind": "main",
|
||||
"source_label": "Main Model via _model_config",
|
||||
"selected_preset_name": preset_name,
|
||||
"preset_status": "invalid",
|
||||
"warning": (
|
||||
f"Configured browser preset '{preset_name}' does not define a chat model. "
|
||||
"Falling back to the Main Model."
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"config": model_config.get_chat_model_config(agent),
|
||||
"source_kind": "main",
|
||||
"source_label": "Main Model via _model_config",
|
||||
"selected_preset_name": preset_name,
|
||||
"preset_status": "missing",
|
||||
"warning": (
|
||||
f"Configured browser preset '{preset_name}' was not found. "
|
||||
"Falling back to the Main Model."
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"config": model_config.get_chat_model_config(agent),
|
||||
"source_kind": "main",
|
||||
"source_label": "Main Model via _model_config",
|
||||
"selected_preset_name": "",
|
||||
"preset_status": "none",
|
||||
"warning": "",
|
||||
}
|
||||
|
||||
|
||||
def save_browser_model_preset_name(preset_name: str) -> None:
|
||||
normalized = str(preset_name or "").strip()
|
||||
config = plugin_helpers.get_plugin_config("_browser_agent") or {}
|
||||
|
||||
if normalized:
|
||||
config[MODEL_PRESET_KEY] = normalized
|
||||
else:
|
||||
config.pop(MODEL_PRESET_KEY, None)
|
||||
|
||||
plugin_helpers.save_plugin_config(
|
||||
"_browser_agent",
|
||||
project_name="",
|
||||
agent_profile="",
|
||||
settings=config,
|
||||
)
|
||||
56
plugins/_browser_agent/webui/browser-agent-store.js
Normal file
56
plugins/_browser_agent/webui/browser-agent-store.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { createStore } from "/js/AlpineStore.js";
|
||||
import { callJsonApi } from "/js/api.js";
|
||||
|
||||
const STATUS_API = "/plugins/_browser_agent/status";
|
||||
const MODEL_PRESET_API = "/plugins/_browser_agent/model_preset";
|
||||
|
||||
const model = {
|
||||
loading: true,
|
||||
savingPreset: false,
|
||||
error: "",
|
||||
status: null,
|
||||
|
||||
async openModelSettings() {
|
||||
await import("/components/plugins/plugin-settings-store.js");
|
||||
await $store.pluginSettingsPrototype.openConfig("_model_config");
|
||||
},
|
||||
|
||||
async refreshStatus() {
|
||||
this.status = await callJsonApi(STATUS_API, {});
|
||||
},
|
||||
|
||||
async savePreset(presetName) {
|
||||
this.savingPreset = true;
|
||||
try {
|
||||
await callJsonApi(MODEL_PRESET_API, {
|
||||
action: presetName ? "set" : "clear",
|
||||
preset_name: presetName || "",
|
||||
});
|
||||
this.error = "";
|
||||
await this.refreshStatus();
|
||||
} catch (error) {
|
||||
this.error = error instanceof Error ? error.message : String(error);
|
||||
await this.refreshStatus();
|
||||
} finally {
|
||||
this.savingPreset = false;
|
||||
}
|
||||
},
|
||||
|
||||
async onOpen() {
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
|
||||
try {
|
||||
await this.refreshStatus();
|
||||
} catch (error) {
|
||||
this.status = null;
|
||||
this.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
cleanup() {},
|
||||
};
|
||||
|
||||
export const store = createStore("browserAgentPage", model);
|
||||
|
|
@ -2,120 +2,130 @@
|
|||
<head>
|
||||
<title>Browser Agent</title>
|
||||
<script type="module">
|
||||
import { callJsonApi } from "/js/api.js";
|
||||
import "/components/plugins/list/pluginListStore.js";
|
||||
|
||||
globalThis.browserAgentStatusApi = { callJsonApi };
|
||||
import { store } from "/plugins/_browser_agent/webui/browser-agent-store.js";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
x-data="{
|
||||
loading: true,
|
||||
error: '',
|
||||
status: null,
|
||||
async init() {
|
||||
try {
|
||||
this.status = await browserAgentStatusApi.callJsonApi('/plugins/_browser_agent/status', {});
|
||||
} catch (error) {
|
||||
this.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
x-init="init()"
|
||||
class="browser-agent-page"
|
||||
>
|
||||
<div class="section-title">Browser Agent</div>
|
||||
<div class="section-description">
|
||||
Built-in browser automation plugin backed by `browser-use` and Playwright.
|
||||
Model selection stays in `_model_config`; the browser agent follows the effective Main Model.
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card" x-show="loading">
|
||||
<div class="status-row">
|
||||
<span class="material-symbols-outlined spinning">progress_activity</span>
|
||||
<span>Loading browser status...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card error" x-show="!loading && error">
|
||||
<div class="field-title">Status check failed</div>
|
||||
<div class="field-description" x-text="error"></div>
|
||||
</div>
|
||||
|
||||
<template x-if="!loading && status">
|
||||
<div class="browser-agent-grid">
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Model Source</div>
|
||||
<div class="field-description" x-text="status.model_source"></div>
|
||||
<div x-data>
|
||||
<template x-if="$store.browserAgentPage">
|
||||
<div
|
||||
x-create="$store.browserAgentPage.onOpen()"
|
||||
x-destroy="$store.browserAgentPage.cleanup()"
|
||||
class="browser-agent-page"
|
||||
>
|
||||
<div class="section-title">Browser Agent</div>
|
||||
<div class="section-description">
|
||||
Built-in browser automation plugin backed by `browser-use` and Playwright.
|
||||
Model selection stays in `_model_config`; the browser agent can follow the effective Main Model or use one saved preset just for browser tasks.
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Resolved Main Model</div>
|
||||
<div class="browser-agent-card" x-show="$store.browserAgentPage.loading">
|
||||
<div class="status-row">
|
||||
<span class="status-key">Provider</span>
|
||||
<span class="status-value" x-text="status.model.provider || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Model</span>
|
||||
<span class="status-value" x-text="status.model.name || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Vision</span>
|
||||
<span class="status-badge" :class="status.model.vision ? 'ok' : 'warn'" x-text="status.model.vision ? 'Enabled' : 'Disabled'"></span>
|
||||
<span class="material-symbols-outlined spinning">progress_activity</span>
|
||||
<span>Loading browser status...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Playwright Runtime</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Binary</span>
|
||||
<span class="status-badge" :class="status.playwright.binary_found ? 'ok' : 'fail'" x-text="status.playwright.binary_found ? 'Found' : 'Missing'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Cache</span>
|
||||
<span class="status-value mono" x-text="status.playwright.cache_dir"></span>
|
||||
</div>
|
||||
<div class="status-row" x-show="status.playwright.binary_path">
|
||||
<span class="status-key">Path</span>
|
||||
<span class="status-value mono" x-text="status.playwright.binary_path"></span>
|
||||
</div>
|
||||
<div class="field-description" x-show="!status.playwright.binary_found">
|
||||
Docker images ship the Playwright Chromium shell preinstalled. In local development, the first run installs it on demand via <span class="mono">ensure_playwright_binary()</span> if missing.
|
||||
</div>
|
||||
<div class="browser-agent-card error" x-show="!$store.browserAgentPage.loading && $store.browserAgentPage.error">
|
||||
<div class="field-title">Status check failed</div>
|
||||
<div class="field-description" x-text="$store.browserAgentPage.error"></div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">browser-use</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Import</span>
|
||||
<span class="status-badge" :class="status.browser_use.import_ok ? 'ok' : 'fail'" x-text="status.browser_use.import_ok ? 'Ready' : 'Error'"></span>
|
||||
<template x-if="!$store.browserAgentPage.loading && $store.browserAgentPage.status">
|
||||
<div class="browser-agent-grid">
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Model Source</div>
|
||||
<div class="field-description" x-text="$store.browserAgentPage.status.model_source"></div>
|
||||
<div class="field-description" x-show="$store.browserAgentPage.status.preset_warning" x-text="$store.browserAgentPage.status.preset_warning"></div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Resolved Browser Model</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Provider</span>
|
||||
<span class="status-value" x-text="$store.browserAgentPage.status.model.provider || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Model</span>
|
||||
<span class="status-value" x-text="$store.browserAgentPage.status.model.name || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Vision</span>
|
||||
<span class="status-badge" :class="$store.browserAgentPage.status.model.vision ? 'ok' : 'warn'" x-text="$store.browserAgentPage.status.model.vision ? 'Enabled' : 'Disabled'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Browser Model Preset</div>
|
||||
<div class="field-description">
|
||||
Pick an optional `_model_config` preset for browser-only runs. Leave it empty to keep using the effective Main Model.
|
||||
</div>
|
||||
<label class="browser-agent-select-label" for="browser-agent-preset-select">Preset</label>
|
||||
<select
|
||||
id="browser-agent-preset-select"
|
||||
class="browser-agent-select"
|
||||
:disabled="$store.browserAgentPage.savingPreset"
|
||||
x-model="$store.browserAgentPage.status.selected_preset_name"
|
||||
@change="$store.browserAgentPage.savePreset($store.browserAgentPage.status.selected_preset_name)"
|
||||
>
|
||||
<option value="">Use Main Model</option>
|
||||
<template x-for="preset in $store.browserAgentPage.status.available_presets" :key="preset.name">
|
||||
<option :value="preset.name" x-text="preset.label"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="field-description" x-show="$store.browserAgentPage.savingPreset">Saving browser preset...</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Playwright Runtime</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Binary</span>
|
||||
<span class="status-badge" :class="$store.browserAgentPage.status.playwright.binary_found ? 'ok' : 'fail'" x-text="$store.browserAgentPage.status.playwright.binary_found ? 'Found' : 'Missing'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Cache</span>
|
||||
<span class="status-value mono" x-text="$store.browserAgentPage.status.playwright.cache_dir"></span>
|
||||
</div>
|
||||
<div class="status-row" x-show="$store.browserAgentPage.status.playwright.binary_path">
|
||||
<span class="status-key">Path</span>
|
||||
<span class="status-value mono" x-text="$store.browserAgentPage.status.playwright.binary_path"></span>
|
||||
</div>
|
||||
<div class="field-description" x-show="!$store.browserAgentPage.status.playwright.binary_found">
|
||||
Docker images ship the Playwright Chromium shell preinstalled. In local development, the first run installs it on demand via <span class="mono">ensure_playwright_binary()</span> if missing.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">browser-use</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Import</span>
|
||||
<span class="status-badge" :class="$store.browserAgentPage.status.browser_use.import_ok ? 'ok' : 'fail'" x-text="$store.browserAgentPage.status.browser_use.import_ok ? 'Ready' : 'Error'"></span>
|
||||
</div>
|
||||
<div class="status-row" x-show="$store.browserAgentPage.status.browser_use.version">
|
||||
<span class="status-key">Version</span>
|
||||
<span class="status-value" x-text="$store.browserAgentPage.status.browser_use.version"></span>
|
||||
</div>
|
||||
<div class="field-description mono" x-show="$store.browserAgentPage.status.browser_use.error" x-text="$store.browserAgentPage.status.browser_use.error"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-row" x-show="status.browser_use.version">
|
||||
<span class="status-key">Version</span>
|
||||
<span class="status-value" x-text="status.browser_use.version"></span>
|
||||
</div>
|
||||
<div class="field-description mono" x-show="status.browser_use.error" x-text="status.browser_use.error"></div>
|
||||
</template>
|
||||
|
||||
<div class="browser-agent-actions">
|
||||
<button
|
||||
class="btn btn-field"
|
||||
@click="$store.browserAgentPage.openModelSettings()"
|
||||
>
|
||||
Open Model Settings
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
|
||||
Open Presets
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
|
||||
Open API Keys
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="browser-agent-actions">
|
||||
<button
|
||||
class="btn btn-field"
|
||||
@click="$store.pluginListStore.openPluginConfig({ name: '_model_config', has_config_screen: true })"
|
||||
>
|
||||
Open Model Settings
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
|
||||
Open Presets
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
|
||||
Open API Keys
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -151,6 +161,26 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.browser-agent-select-label {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.browser-agent-select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.browser-agent-select:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
@ -199,6 +229,11 @@
|
|||
font-family: var(--font-mono);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
option {
|
||||
background: var(--color-input);
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue