svrnty-hermes-webui-plugin/static/bte.js
Svrnty 0b19fdd7d0
Some checks failed
plugin-tests / test (push) Failing after 5s
feat(plugin): Adwright + BTE Command Center panels (v0.4.0)
Two new tool panels surfaced inside hermes-webui via inject_script/
inject_stylesheet. Both vanilla JS + CSS, no frameworks, WebUI CSS-vars
only (no hardcoded colors), light/dark inherits free.

## Adwright panel (static/adwright.{js,css} + routes/adwright.py)

5 tabs: Overview · Cycles · Audience · Targeting · Connections.
Layout: 60/40 panel/chat split via CSS :has() selector.
Always-visible, soft-disabled when active profile isn't `cmo*`.

Action wiring (READ path — agent-mediated per governance):
1. Panel button → fires custom event
2. Handler synthesizes /adwright <cmd> chat message
3. Posts via existing btnSend pathway → message visible in chat
4. CMO sees + calls mcp_adwright_<tool>
5. Panel polls /api/adwright/last-panel-update for structured payload
6. Mock payload returned v1; real session-DB reader plugs in when
   adwright-mcp gains writer

Connections WRITE path (governance exception, NO secrets in chat):
- POST /api/adwright/provision-creds with form fields
- Plugin invokes credctl set <key> via stdin (value never on argv)
- Allowlist enforced (defense-in-depth on key names)
- Auth-gated by WebUI session cookie

Skin: .svrnty-aw-* class prefix, window.SvrntyAdwright JS namespace,
guard against double-load, scoped MutationObserver.

## BTE Command Center panel (static/bte.{js,css} + routes/bte_proxy.py)

Content-mode pills (Polished/UGC/Photorealistic/Artistic) × media toggle
(Image/Video — Video disabled v1 pending Phase 4e) × recipe family picker
(Hero Shot/Lifestyle Shot/Photoshoot/Recipe Sheet/Montage Catalog) per
canonical PLANB-RECIPE-TAXONOMY. SKU picker, variant stepper 1-12,
single/batch toggle, [Generate] button.

Asset grid with streaming thumbnails, asset detail (full-res + rate +
comment + "Use in Adwright cycle" deep link). Embedded CMO chat right rail
for re-orienting generations ("make next batch warmer / less white space").

BTE proxy route (/api/bte/proxy) with whitelisted paths
(requestPhotoshoot, assetGrid, recipeStats, assets/{id}/thumb, etc.)
prevents browser-side CORS to BTE :6001.

Skin: .svrnty-bte-* class prefix, window.SvrntyBTE JS namespace.

## Wiring

manifest_version: 0.2.0 → 0.4.0
assets registered:
- /plugins/svrnty/adwright.{js,css} + static/adwright/
- /plugins/svrnty/bte.{js,css} + static/bte/
routes registered:
- GET /api/adwright/last-panel-update (panel update channel)
- POST /api/adwright/provision-creds (governance-exception write)
- GET/POST /api/bte/proxy (BTE REST proxy with allowlist)

Karpathy 4 rules: agents reported every deviation with rationale (Python
venv interp for hermes mcp add, missing aggregate connections-status RPC
composed from two verifies, mock panel-update v1 with locked frontend
protocol so real session-DB reader is a drop-in swap), verified asset
serving + plugin route registration before claiming complete, surfaced
open questions instead of silently choosing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:12:27 -04:00

551 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// svrnty-bte: BTE Command Center panel — brand asset gen + rate + iterate.
// IIFE, idempotent, namespace window.SvrntyBTE. Class prefix .svrnty-bte-*.
// Per COMMAND-CENTER-PRD §3 + PLANB-RECIPE-TAXONOMY.md §5.
(function () {
"use strict";
if (window.__svrntyBteLoaded) return;
window.__svrntyBteLoaded = true;
// ── Taxonomy (mirror of sot/07-BRAND/PLANB-RECIPE-TAXONOMY.md) ─────────────
const MODES = [
{ slug: "polished", label: "Polished" },
{ slug: "ugc", label: "UGC" },
{ slug: "photoreal", label: "Photorealistic" },
{ slug: "artistic", label: "Artistic" },
];
const MEDIA = [
{ slug: "image", label: "Image", enabled: true },
{ slug: "video", label: "Video (soon)", enabled: false },
];
const FAMILIES = [
{ slug: "hero-shot", label: "Hero Shot" },
{ slug: "lifestyle-shot", label: "Lifestyle Shot" },
{ slug: "photoshoot", label: "Photoshoot" },
{ slug: "recipe-sheet", label: "Recipe Sheet" },
{ slug: "montage-catalog", label: "Montage Catalog" },
];
// Placeholder SKUs — to be replaced with Woo catalog when piped in.
const PLACEHOLDER_SKUS = [
{ id: "42", name: "Poulet tao (placeholder)" },
{ id: "43", name: "Boeuf bourguignon (placeholder)" },
{ id: "44", name: "Saumon teriyaki (placeholder)" },
{ id: "45", name: "Tofu sauté (placeholder)" },
];
const PROXY_BASE = "/api/bte/proxy?path=";
const POLL_INTERVAL_MS = 2000;
// ── State ────────────────────────────────────────────────────────────────
const state = {
brand: "planb",
mode: "polished",
media: "image",
family: null,
skuId: null,
variants: 4,
batch: false,
runId: null,
assets: [], // [{id, thumbUrl, lifecycle, ratingCount, meanScore, recipeSlug, ...}]
selected: null, // asset id
pollTimer: null,
log: [],
};
// ── Public namespace ─────────────────────────────────────────────────────
const SvrntyBTE = {
open: () => _openOverlay(),
close: () => _closeOverlay(),
state,
};
window.SvrntyBTE = SvrntyBTE;
// ── Init: inject launcher button when DOM ready ─────────────────────────
function _init() {
_installLauncher();
// Some WebUI flows rebuild body content; re-install on DOM mutations.
const observer = new MutationObserver(() => _installLauncher());
observer.observe(document.body, { childList: true, subtree: false });
}
function _installLauncher() {
if (document.querySelector(".svrnty-bte-launcher")) return;
const btn = document.createElement("button");
btn.className = "svrnty-bte-launcher";
btn.type = "button";
btn.title = "Open BTE Command Center";
btn.innerHTML =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<rect x="3" y="3" width="7" height="7" rx="1"/>' +
'<rect x="14" y="3" width="7" height="7" rx="1"/>' +
'<rect x="3" y="14" width="7" height="7" rx="1"/>' +
'<rect x="14" y="14" width="7" height="7" rx="1"/>' +
'</svg>BTE Command Center';
btn.addEventListener("click", _openOverlay);
document.body.appendChild(btn);
}
// ── Overlay lifecycle ────────────────────────────────────────────────────
function _openOverlay() {
let overlay = document.getElementById("svrntyBteOverlay");
if (!overlay) {
overlay = _buildOverlay();
document.body.appendChild(overlay);
}
overlay.hidden = false;
_refreshGrid();
}
function _closeOverlay() {
const overlay = document.getElementById("svrntyBteOverlay");
if (overlay) overlay.hidden = true;
if (state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
}
}
// ── Overlay DOM build ────────────────────────────────────────────────────
function _buildOverlay() {
const root = document.createElement("div");
root.id = "svrntyBteOverlay";
root.className = "svrnty-bte-overlay";
root.appendChild(_buildToolbar());
const body = document.createElement("div");
body.className = "svrnty-bte-body";
body.appendChild(_buildMain());
body.appendChild(_buildRail());
root.appendChild(body);
return root;
}
function _buildToolbar() {
const bar = document.createElement("div");
bar.className = "svrnty-bte-toolbar";
const title = document.createElement("span");
title.className = "svrnty-bte-toolbar-title";
title.textContent = "BTE Command Center";
bar.appendChild(title);
// Brand selector (single-option for now — Plan B)
bar.appendChild(_labeled("Brand", _select("svrntyBteBrand",
[{ value: "planb", label: "Plan B" }],
state.brand, (v) => { state.brand = v; })));
// Content mode pills
bar.appendChild(_pillGroup("Mode", "svrnty-bte-pill-mode", MODES, state.mode, (v) => {
state.mode = v; _updateToolbarPressed(); _updateGenerateEnabled();
}));
// Media toggle
bar.appendChild(_pillGroup("Media", "svrnty-bte-pill-media",
MEDIA.map((m) => ({ slug: m.slug, label: m.label, disabled: !m.enabled })),
state.media, (v) => {
state.media = v; _updateToolbarPressed(); _updateGenerateEnabled();
}));
// Recipe family pills
bar.appendChild(_pillGroup("Recipe", "svrnty-bte-pill-family", FAMILIES, state.family, (v) => {
state.family = v; _updateToolbarPressed(); _updateGenerateEnabled(); _refreshGrid();
}));
// SKU dropdown
const skuOpts = [{ value: "", label: "— pick SKU —" }].concat(
PLACEHOLDER_SKUS.map((s) => ({ value: s.id, label: s.name })));
bar.appendChild(_labeled("SKU", _select("svrntyBteSku", skuOpts, state.skuId || "", (v) => {
state.skuId = v || null; _updateGenerateEnabled();
})));
// Variants stepper
const stepper = document.createElement("input");
stepper.type = "number";
stepper.min = "1"; stepper.max = "12"; stepper.value = String(state.variants);
stepper.className = "svrnty-bte-stepper";
stepper.addEventListener("change", () => {
const n = parseInt(stepper.value, 10);
state.variants = isNaN(n) ? 1 : Math.max(1, Math.min(12, n));
stepper.value = String(state.variants);
});
bar.appendChild(_labeled("Variants", stepper));
// Batch toggle
bar.appendChild(_pillGroup("Run", "svrnty-bte-pill-batch",
[{ slug: "single", label: "Single" }, { slug: "batch", label: "Batch" }],
state.batch ? "batch" : "single",
(v) => { state.batch = (v === "batch"); }));
// Generate button
const gen = document.createElement("button");
gen.id = "svrntyBteGenerate";
gen.className = "svrnty-bte-generate";
gen.type = "button";
gen.textContent = "Generate";
gen.disabled = true;
gen.addEventListener("click", _onGenerate);
bar.appendChild(gen);
// Close
const close = document.createElement("button");
close.className = "svrnty-bte-close";
close.type = "button";
close.textContent = "Close";
close.addEventListener("click", _closeOverlay);
bar.appendChild(close);
return bar;
}
function _buildMain() {
const main = document.createElement("div");
main.className = "svrnty-bte-main";
main.id = "svrntyBteMain";
main.innerHTML =
'<div id="svrntyBteBanner"></div>' +
'<div class="svrnty-bte-empty" id="svrntyBteEmpty">' +
'Pick a mode, recipe family, and SKU to begin. Recent renders for the selected cell will appear here.' +
'</div>' +
'<div class="svrnty-bte-grid" id="svrntyBteGrid"></div>' +
'<div id="svrntyBteDetail"></div>';
return main;
}
function _buildRail() {
const rail = document.createElement("div");
rail.className = "svrnty-bte-rail";
rail.innerHTML =
'<div class="svrnty-bte-rail-section">' +
'<h4>CMO chat</h4>' +
'<p>Tell CMO what to change about the next batch — warmer light, less white space, different framing — then re-run.</p>' +
'<button class="svrnty-bte-cmo-link" id="svrntyBteCmoOpen" type="button">Open CMO chat →</button>' +
'</div>' +
'<div class="svrnty-bte-rail-section">' +
'<h4>Run status</h4>' +
'<div class="svrnty-bte-run-log" id="svrntyBteRunLog">No active run.</div>' +
'</div>' +
'<div class="svrnty-bte-rail-section">' +
'<h4>Selected cell</h4>' +
'<p id="svrntyBteCellLabel">—</p>' +
'</div>';
// CMO chat link: simplest honest path — switch the WebUI rail to the chat
// panel so JP can talk to whichever profile is active. Real CMO-scoped
// iframe deferred (PRD §6.5 option A — Phase E).
setTimeout(() => {
const cmoBtn = document.getElementById("svrntyBteCmoOpen");
if (cmoBtn) cmoBtn.addEventListener("click", _openCmoChat);
}, 0);
return rail;
}
// ── DOM helpers ──────────────────────────────────────────────────────────
function _labeled(label, child) {
const wrap = document.createElement("div");
wrap.className = "svrnty-bte-toolbar-group";
const lab = document.createElement("span");
lab.className = "svrnty-bte-toolbar-label";
lab.textContent = label;
wrap.appendChild(lab);
wrap.appendChild(child);
return wrap;
}
function _select(id, options, value, onChange) {
const sel = document.createElement("select");
sel.id = id;
sel.className = "svrnty-bte-select";
options.forEach((o) => {
const opt = document.createElement("option");
opt.value = o.value;
opt.textContent = o.label;
if (o.value === value) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener("change", () => onChange(sel.value));
return sel;
}
function _pillGroup(label, className, items, currentValue, onPick) {
const wrap = document.createElement("div");
wrap.className = "svrnty-bte-toolbar-group";
const lab = document.createElement("span");
lab.className = "svrnty-bte-toolbar-label";
lab.textContent = label;
wrap.appendChild(lab);
items.forEach((it) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "svrnty-bte-pill " + className;
btn.dataset.value = it.slug;
btn.textContent = it.label;
btn.setAttribute("aria-pressed", it.slug === currentValue ? "true" : "false");
if (it.disabled) btn.disabled = true;
btn.addEventListener("click", () => {
if (btn.disabled) return;
onPick(it.slug);
});
wrap.appendChild(btn);
});
return wrap;
}
function _updateToolbarPressed() {
const sync = (cls, val) => {
document.querySelectorAll("." + cls).forEach((b) => {
b.setAttribute("aria-pressed", b.dataset.value === val ? "true" : "false");
});
};
sync("svrnty-bte-pill-mode", state.mode);
sync("svrnty-bte-pill-media", state.media);
sync("svrnty-bte-pill-family", state.family);
sync("svrnty-bte-pill-batch", state.batch ? "batch" : "single");
_renderCellLabel();
}
function _updateGenerateEnabled() {
const gen = document.getElementById("svrntyBteGenerate");
if (!gen) return;
gen.disabled = !(state.mode && state.media && state.family && state.skuId);
}
function _renderCellLabel() {
const el = document.getElementById("svrntyBteCellLabel");
if (!el) return;
if (!state.family) { el.textContent = "—"; return; }
el.textContent = `${state.family}__${state.mode}__${state.media}`;
}
// ── Generate flow ────────────────────────────────────────────────────────
function _onGenerate() {
if (!state.family || !state.skuId) return;
const sku = PLACEHOLDER_SKUS.find((s) => s.id === state.skuId);
const recipeLabel = `${state.family}__${state.mode}__${state.media}`;
const body = {
brandId: state.brand,
recipeSlug: state.family,
recipeLabel: recipeLabel,
items: [{ offeringId: parseInt(state.skuId, 10), offeringName: sku ? sku.name : state.skuId }],
variantsPerScenario: state.variants,
tags: { source: "svrnty-bte-panel" },
};
_log(`POST requestPhotoshoot (${recipeLabel}, sku=${state.skuId}, variants=${state.variants})`);
_proxyPost("/api/command/requestPhotoshoot", body)
.then((resp) => {
if (resp.status === 404 || resp.status === 501) {
_banner("BTE endpoint /api/command/requestPhotoshoot not yet implemented — see COMMAND-CENTER-PRD §5.8 (status: implementing).", "warn");
_log(`${resp.status} (endpoint not yet built)`);
return;
}
if (!resp.ok) {
_banner(`BTE returned ${resp.status}: ${resp.bodyText.slice(0, 200)}`, "warn");
_log(`${resp.status} ${resp.bodyText.slice(0, 80)}`);
return;
}
const json = _safeJson(resp.bodyText) || {};
state.runId = json.runId || json.run_id || null;
_log(`← runId=${state.runId || "(none)"}, assetIds=${(json.assetIds || []).length}`);
_banner(`Run ${state.runId || "started"} — polling grid every ${POLL_INTERVAL_MS / 1000}s.`, "info");
_startPolling();
})
.catch((e) => {
_banner(`Proxy error: ${e.message}`, "warn");
_log(`× ${e.message}`);
});
}
// ── Grid refresh + polling ───────────────────────────────────────────────
function _refreshGrid() {
if (!state.family) return;
const filters = {
brandId: state.brand,
recipeSlug: state.family,
lifecycle: ["approved", "generating", "evaluating"],
};
if (state.runId) filters.runId = state.runId;
const body = {
filters: filters,
page: 1,
pageSize: 24,
sort: "-created_at",
};
_proxyPost("/api/query/assetGrid", body)
.then((resp) => {
if (resp.status === 404 || resp.status === 501) {
state.assets = [];
_renderGrid();
_banner("BTE endpoint /api/query/assetGrid not yet implemented (PRD §5.4 status: implementing). Grid will populate once endpoint ships.", "warn");
return;
}
if (!resp.ok) {
_log(`assetGrid ← ${resp.status}`);
return;
}
const json = _safeJson(resp.bodyText) || { items: [] };
state.assets = json.items || [];
_renderGrid();
// Stop polling when no rendering asset remains.
const stillInFlight = state.assets.some((a) => a.lifecycle === "generating" || a.lifecycle === "evaluating");
if (!stillInFlight && state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
_log("polling stopped — all renders complete");
}
})
.catch((e) => _log(`assetGrid × ${e.message}`));
}
function _startPolling() {
if (state.pollTimer) clearInterval(state.pollTimer);
_refreshGrid();
state.pollTimer = setInterval(_refreshGrid, POLL_INTERVAL_MS);
}
function _renderGrid() {
const grid = document.getElementById("svrntyBteGrid");
const empty = document.getElementById("svrntyBteEmpty");
if (!grid || !empty) return;
if (!state.assets.length) {
grid.innerHTML = "";
empty.style.display = "";
return;
}
empty.style.display = "none";
grid.innerHTML = state.assets.map((a) => {
const thumb = a.thumbUrl
? `<img src="${PROXY_BASE}${encodeURIComponent(a.thumbUrl)}" alt="">`
: '<span>no thumb</span>';
const score = (a.meanScore != null) ? ` · ★${a.meanScore.toFixed(1)}` : "";
const sel = (a.id === state.selected) ? " svrnty-bte-card-selected" : "";
return (
`<div class="svrnty-bte-card${sel}" data-id="${_esc(a.id)}">` +
`<div class="svrnty-bte-card-thumb">${thumb}</div>` +
`<div class="svrnty-bte-card-meta">` +
`<span class="svrnty-bte-status svrnty-bte-status-${_esc(a.lifecycle || "generating")}">${_esc(a.lifecycle || "—")}</span>` +
`<span>${a.ratingCount || 0}r${score}</span>` +
`</div>` +
`</div>`
);
}).join("");
grid.querySelectorAll(".svrnty-bte-card").forEach((card) => {
card.addEventListener("click", () => _selectAsset(card.dataset.id));
});
}
function _selectAsset(id) {
state.selected = id;
const asset = state.assets.find((a) => a.id === id);
_renderGrid();
_renderDetail(asset);
}
function _renderDetail(asset) {
const host = document.getElementById("svrntyBteDetail");
if (!host) return;
if (!asset) { host.innerHTML = ""; return; }
const thumb = asset.thumbUrl
? `<img src="${PROXY_BASE}${encodeURIComponent(asset.thumbUrl)}" alt="">`
: "no preview";
host.innerHTML =
'<div class="svrnty-bte-detail">' +
`<div class="svrnty-bte-detail-preview">${thumb}</div>` +
'<dl class="svrnty-bte-detail-meta">' +
`<dt>asset id</dt><dd>${_esc(asset.id)}</dd>` +
`<dt>recipe</dt><dd>${_esc(asset.recipeSlug || "—")} v${asset.recipeVersion || "?"}</dd>` +
`<dt>lifecycle</dt><dd>${_esc(asset.lifecycle || "—")}</dd>` +
`<dt>ratings</dt><dd>${asset.ratingCount || 0} · mean ${asset.meanScore != null ? asset.meanScore.toFixed(2) : "—"}</dd>` +
'<dt>rate</dt><dd>' +
'<div class="svrnty-bte-rate-row">' +
`<button class="svrnty-bte-rate-btn svrnty-bte-rate-btn-up" data-v="1" data-id="${_esc(asset.id)}" type="button">+1</button>` +
`<button class="svrnty-bte-rate-btn svrnty-bte-rate-btn-down" data-v="-1" data-id="${_esc(asset.id)}" type="button">1</button>` +
'<select class="svrnty-bte-select" id="svrntyBteScore">' +
'<option value="">— score —</option>' +
'<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>' +
'</select>' +
'</div>' +
`<textarea class="svrnty-bte-comment" id="svrntyBteComment" placeholder="Comment (optional) — e.g. 'too plastic', 'needs warmer light'"></textarea>` +
'</dd>' +
'</dl>' +
'</div>';
host.querySelectorAll(".svrnty-bte-rate-btn").forEach((btn) => {
btn.addEventListener("click", () => _rateAsset(btn.dataset.id, parseInt(btn.dataset.v, 10)));
});
}
function _rateAsset(id, verdict) {
const scoreSel = document.getElementById("svrntyBteScore");
const commentEl = document.getElementById("svrntyBteComment");
const body = {
assetId: id,
verdict: verdict > 0 ? "accept" : "reject",
score: scoreSel && scoreSel.value ? parseInt(scoreSel.value, 10) : null,
comment: commentEl ? (commentEl.value || "").trim() : "",
ratedBy: localStorage.getItem("svrnty-bte.rater") || "jp",
};
_log(`POST rateAsset ${id} ${body.verdict}`);
_proxyPost("/api/command/rateAsset", body)
.then((resp) => {
if (resp.status === 404 || resp.status === 501) {
_banner("BTE /api/command/rateAsset not yet implemented (PRD §7).", "warn");
return;
}
if (!resp.ok) {
_log(`rateAsset ← ${resp.status}`);
return;
}
_log(`rateAsset ← ok`);
_refreshGrid();
})
.catch((e) => _log(`rateAsset × ${e.message}`));
}
// ── CMO chat handoff ─────────────────────────────────────────────────────
function _openCmoChat() {
// Honest minimum: surface the WebUI chat panel (whichever profile is
// active). Full CMO-scoped iframe / component reuse is COMMAND-CENTER-PRD
// §6.5 Phase E — not v1 of this panel.
_closeOverlay();
if (typeof window.switchPanel === "function") {
try { window.switchPanel("chat", { fromRailClick: true }); } catch (_) { /* ignore */ }
}
}
// ── Proxy helpers ────────────────────────────────────────────────────────
function _proxyPost(btePath, body) {
return fetch(PROXY_BASE + encodeURIComponent(btePath), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(_responseEnvelope);
}
function _responseEnvelope(r) {
return r.text().then((t) => ({ ok: r.ok, status: r.status, bodyText: t }));
}
function _safeJson(s) {
try { return JSON.parse(s); } catch (_) { return null; }
}
// ── UI utils ─────────────────────────────────────────────────────────────
function _banner(text, kind) {
const host = document.getElementById("svrntyBteBanner");
if (!host) return;
host.innerHTML = `<div class="svrnty-bte-banner svrnty-bte-banner-${kind === "warn" ? "warn" : "info"}">${_esc(text)}</div>`;
}
function _log(line) {
state.log.unshift(`${new Date().toLocaleTimeString()} ${line}`);
state.log = state.log.slice(0, 50);
const el = document.getElementById("svrntyBteRunLog");
if (el) el.textContent = state.log.join("\n");
}
function _esc(s) {
return String(s == null ? "" : s).replace(/[<>&"]/g, (c) =>
({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[c]));
}
// ── Boot ─────────────────────────────────────────────────────────────────
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", _init);
} else {
_init();
}
})();