feat(bte panel): real SKU dropdown + contextual CMO chat handoff
Some checks failed
plugin-tests / test (push) Failing after 6s
upstream-drift / drift (push) Failing after 6s

Tranche C — BTE basics.

1. SKU dropdown now sources from Adwright's TopRecipes (top-selling
   offerings) via /api/adwright/last-panel-update?tool=adwright_list_recipes
   instead of bundled PLACEHOLDER_SKUS. Falls back to placeholders on
   any failure. state.skus drives both the toolbar select build and the
   name lookups in _openCmoChat / requestPhotoshoot. _refreshSkuOptions
   rewrites the <option> set in place so the dropdown updates without
   rebuilding the toolbar.

2. _openCmoChat now pre-fills the chat textarea with the operator's
   current BTE setup (brand, mode, media, recipe family, SKU, variants,
   selected asset) so they don't retype context every iteration.
   Switch + setTimeout(200) lets the chat panel mount before set value
   + dispatch input event. Doesn't auto-send — operator reviews then
   hits send themselves.

C3 (seed assets) closed without code: BTE backend at localhost:6001
already serves 10 real assets (1 evaluated with brandFit 4.2 ★3.8,
2 approved with hasImageData=true, 5 requested). Original "placeholder"
complaint was about the SKU dropdown, not the asset grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Svrnty 2026-05-24 18:02:45 -04:00
parent 3fa980583d
commit b75fbf48ae

View File

@ -48,6 +48,10 @@
selected: null, // asset id selected: null, // asset id
pollTimer: null, pollTimer: null,
log: [], log: [],
// SKU options surfaced in the toolbar dropdown. Defaults to the bundled
// placeholders until _fetchSkuOptions replaces them with real top-selling
// offerings from Adwright's TopRecipes RPC.
skus: PLACEHOLDER_SKUS.slice(),
}; };
// ── Public namespace ───────────────────────────────────────────────────── // ── Public namespace ─────────────────────────────────────────────────────
@ -91,6 +95,37 @@
} }
overlay.hidden = false; overlay.hidden = false;
_refreshGrid(); _refreshGrid();
_fetchSkuOptions();
}
// Pull top-selling offerings from Adwright (TopRecipes) so the SKU
// dropdown reflects what's actually shipping, not the bundled PLACEHOLDER
// list. Best-effort — falls through to placeholders on any failure.
function _fetchSkuOptions() {
fetch("/api/adwright/last-panel-update?session_id=&since=0&tool=adwright_list_recipes")
.then((r) => r.json())
.then((r) => {
const recipes = r && r.update && r.update.payload && r.update.payload.recipes;
if (!Array.isArray(recipes) || !recipes.length) return;
state.skus = recipes.map((rec) => ({
id: String(rec.product_id || rec.id || ""),
name: rec.name || "",
})).filter((s) => s.id && s.name);
_refreshSkuOptions();
})
.catch(() => { /* fallback: keep placeholders */ });
}
function _refreshSkuOptions() {
const sel = document.getElementById("svrntyBteSku");
if (!sel) return;
const current = sel.value;
let html = '<option value="">— pick SKU —</option>';
state.skus.forEach((s) => {
html += '<option value="' + _esc(s.id) + '">' + _esc(s.name) + "</option>";
});
sel.innerHTML = html;
if (current) sel.value = current;
} }
function _closeOverlay() { function _closeOverlay() {
@ -148,9 +183,10 @@
state.family = v; _updateToolbarPressed(); _updateGenerateEnabled(); _refreshGrid(); state.family = v; _updateToolbarPressed(); _updateGenerateEnabled(); _refreshGrid();
})); }));
// SKU dropdown // SKU dropdown — populated from state.skus (placeholder at boot,
// replaced by real top-selling offerings via _fetchSkuOptions).
const skuOpts = [{ value: "", label: "— pick SKU —" }].concat( const skuOpts = [{ value: "", label: "— pick SKU —" }].concat(
PLACEHOLDER_SKUS.map((s) => ({ value: s.id, label: s.name }))); state.skus.map((s) => ({ value: s.id, label: s.name })));
bar.appendChild(_labeled("SKU", _select("svrntyBteSku", skuOpts, state.skuId || "", (v) => { bar.appendChild(_labeled("SKU", _select("svrntyBteSku", skuOpts, state.skuId || "", (v) => {
state.skuId = v || null; _updateGenerateEnabled(); state.skuId = v || null; _updateGenerateEnabled();
}))); })));
@ -315,7 +351,7 @@
// ── Generate flow ──────────────────────────────────────────────────────── // ── Generate flow ────────────────────────────────────────────────────────
function _onGenerate() { function _onGenerate() {
if (!state.family || !state.skuId) return; if (!state.family || !state.skuId) return;
const sku = PLACEHOLDER_SKUS.find((s) => s.id === state.skuId); const sku = state.skus.find((s) => s.id === state.skuId);
const recipeLabel = `${state.family}__${state.mode}__${state.media}`; const recipeLabel = `${state.family}__${state.mode}__${state.media}`;
const body = { const body = {
brandId: state.brand, brandId: state.brand,
@ -496,13 +532,35 @@
// ── CMO chat handoff ───────────────────────────────────────────────────── // ── CMO chat handoff ─────────────────────────────────────────────────────
function _openCmoChat() { function _openCmoChat() {
// Honest minimum: surface the WebUI chat panel (whichever profile is // Pre-fill the chat textarea with BTE state context so the operator
// active). Full CMO-scoped iframe / component reuse is COMMAND-CENTER-PRD // doesn't have to retype "I'm in BTE, brand Plan B, mode Polished, …".
// §6.5 Phase E — not v1 of this panel. // Don't auto-send — user reviews + edits + hits send. Full CMO-scoped
// iframe is COMMAND-CENTER-PRD §6.5 Phase E (not v1).
const modeL = MODES.find((m) => m.slug === state.mode);
const familyL = FAMILIES.find((f) => f.slug === state.family);
const sku = state.skus.find((s) => s.id === state.skuId);
const lines = ["I'm working in BTE Command Center. Help me iterate."];
lines.push("- Brand: " + state.brand);
if (modeL) lines.push("- Mode: " + modeL.label);
lines.push("- Media: " + state.media);
if (familyL) lines.push("- Recipe family: " + familyL.label);
if (sku) lines.push("- SKU: " + sku.name);
lines.push("- Variants: " + state.variants);
if (state.selected) lines.push("- Selected asset: " + state.selected);
const prompt = lines.join("\n") + "\n\nWhat should I change next?";
_closeOverlay(); _closeOverlay();
if (typeof window.switchPanel === "function") { if (typeof window.switchPanel === "function") {
try { window.switchPanel("chat", { fromRailClick: true }); } catch (_) { /* ignore */ } try { window.switchPanel("chat", { fromRailClick: true }); } catch (_) { /* ignore */ }
} }
// Defer pre-fill so the chat panel + #msg textarea mount first.
setTimeout(() => {
const msg = document.getElementById("msg");
if (!msg) return;
msg.value = prompt;
try { msg.dispatchEvent(new Event("input", { bubbles: true })); } catch (_) {}
try { msg.focus(); } catch (_) {}
}, 200);
} }
// ── Proxy helpers ──────────────────────────────────────────────────────── // ── Proxy helpers ────────────────────────────────────────────────────────