feat(bte panel): real SKU dropdown + contextual CMO chat handoff
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:
parent
3fa980583d
commit
b75fbf48ae
@ -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 ────────────────────────────────────────────────────────
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user