feat(podbor): drop niches; price-range from-to per cat; ventilation Y/N; priorities multi-select; brand tiers as color (no labels)

This commit is contained in:
wasrusgen 2026-05-09 15:10:23 +03:00
parent 571297c017
commit 129046de07
4 changed files with 241 additions and 171 deletions

View File

@ -19,19 +19,6 @@ const PODBOR_BUDGET_TIERS = [
{ key: "budget", label: "Бюджет", hint: "только нужное" }, { key: "budget", label: "Бюджет", hint: "только нужное" },
]; ];
const PODBOR_FAMILY = [
{ key: "single", label: "1 взрослый" },
{ key: "couple", label: "Пара" },
{ key: "family", label: "Семья с детьми" },
{ key: "multigen", label: "2+ поколения" },
];
const PODBOR_COOKING = [
{ key: "daily", label: "Ежедневно" },
{ key: "weekly", label: "35 раз в неделю" },
{ key: "rare", label: "По выходным или реже" },
];
const PODBOR_INFRA = { const PODBOR_INFRA = {
stove: [ stove: [
{ key: "induction", label: "Индукция / 380 В" }, { key: "induction", label: "Индукция / 380 В" },
@ -40,19 +27,19 @@ const PODBOR_INFRA = {
{ key: "any", label: "Не знаю / любой" }, { key: "any", label: "Не знаю / любой" },
], ],
vent: [ vent: [
{ key: "shaft", label: "Шахта вентиляции есть" }, { key: "yes", label: "Да — есть выводы в вентиляцию" },
{ key: "no_shaft", label: "Только рециркуляция" }, { key: "no", label: "Нет — рециркуляция с угольным фильтром" },
{ key: "unknown", label: "Не знаю" }, { key: "unknown", label: "Не знаю — менеджер уточнит" },
], ],
}; };
const PODBOR_TECHNIQUES = [ const PODBOR_PRIORITIES = [
{ key: "bake", label: "Выпечка" }, { key: "balance", label: "Цена / качество" },
{ key: "steam", label: "На пару" }, { key: "reviews", label: "Отзывы" },
{ key: "grill", label: "Гриль" }, { key: "popular", label: "Популярность бренда" },
{ key: "wok", label: "Wok / стир-фрай" }, { key: "design", label: "Дизайн и цвет" },
{ key: "low_t", label: "Низкотемпературное" }, { key: "tech", label: "Технологичность" },
{ key: "smart", label: "Умные режимы / Smart" }, { key: "service", label: "Сервис и гарантия" },
]; ];
/* Бренды для каждой категории для чипов с тирами. /* Бренды для каждой категории для чипов с тирами.

View File

@ -258,6 +258,72 @@
text-align: right; color: var(--ink); text-align: right; color: var(--ink);
} }
/* ----- Price range (от — до по категориям) ----- */
.price-list { display: flex; flex-direction: column; gap: var(--s3); }
.price-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.price-label {
font-family: var(--font-ui);
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
}
.price-inputs {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.price-inputs input {
flex: 1;
min-width: 0;
width: 100%;
font-family: var(--font-mono);
font-size: 13px;
padding: 8px 10px;
text-align: center;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-tag);
color: var(--ink);
}
.price-inputs .dash {
font-family: var(--font-mono);
color: var(--muted);
flex-shrink: 0;
}
.price-inputs .rub {
font-family: var(--font-mono);
font-size: 12px;
color: var(--muted);
flex-shrink: 0;
padding-left: 2px;
}
.price-total {
margin-top: var(--s2);
padding-top: var(--s2);
border-top: 1px solid var(--line);
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.10em;
text-transform: uppercase;
color: var(--ink-2);
text-align: right;
}
.price-total strong { font-family: var(--font-ui); font-size: 14px; color: var(--ink); font-weight: 600; }
.price-total.muted { color: var(--muted); }
/* ----- Option chips ----- */ /* ----- Option chips ----- */
.opt-list { display: flex; flex-wrap: wrap; gap: 6px; } .opt-list { display: flex; flex-wrap: wrap; gap: 6px; }
@ -283,49 +349,86 @@
} }
/* ----- Brand chips ----- */ /* ----- Brand chips ----- */
.tier-row { display: flex; flex-direction: column; gap: 6px; padding: 8px 0; border-top: 1px solid var(--line); } /* Тиры (Premium/Middle/Budget) различаются цветом, без явных текстовых ярлыков.
.tier-row:first-child { border-top: none; padding-top: 0; } Внутренне храним tier для аналитики «температуры» клиента. */
.tier-label { .brand-chips {
font-family: var(--font-mono); display: flex;
font-size: 9.5px; flex-wrap: wrap;
font-weight: 500; gap: 6px;
letter-spacing: 0.14em; padding: 6px 0;
text-transform: uppercase; border-top: 1px dashed var(--line);
color: var(--muted);
} }
.brand-chips { display: flex; flex-wrap: wrap; gap: 6px; } .brand-chips:first-of-type {
border-top: none;
padding-top: 0;
}
.chip { .chip {
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 12.5px; font-size: 12.5px;
font-weight: 500; font-weight: 500;
padding: 6px 10px; padding: 6px 11px;
border-radius: var(--r-tag); border-radius: var(--r-tag);
border: 1px solid var(--line-strong); border: 1px solid;
background: var(--paper);
color: var(--muted);
cursor: pointer; cursor: pointer;
transition: all 0.12s; transition: all 0.12s;
position: relative; position: relative;
} }
.chip.status-preferred { /* Базовый цвет покоя по тирам — без слов, только тёплый/холодный оттенок */
background: var(--accent-2); .chip.tier-premium {
background: var(--paper);
color: var(--accent-2); /* walnut */
border-color: rgba(107, 74, 43, 0.35);
}
.chip.tier-middle {
background: var(--paper);
color: var(--ink-2);
border-color: var(--line-strong);
}
.chip.tier-budget {
background: var(--paper);
color: var(--muted);
border-color: var(--line);
}
/* Состояние preferred — заливка цветом тира */
.chip.tier-premium.status-preferred {
background: var(--accent-2); /* walnut */
color: var(--paper); color: var(--paper);
border-color: var(--accent-2); border-color: var(--accent-2);
font-weight: 600; font-weight: 600;
} }
.chip.tier-middle.status-preferred {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
font-weight: 600;
}
.chip.tier-budget.status-preferred {
background: var(--muted);
color: var(--paper);
border-color: var(--muted);
font-weight: 600;
}
.chip.status-preferred::before { content: "★ "; } .chip.status-preferred::before { content: "★ "; }
/* Состояние acceptable — обводка цветом тира, прозрачная заливка */
.chip.status-acceptable { .chip.status-acceptable {
background: var(--paper-2); background: var(--paper-2);
color: var(--ink);
border-color: var(--accent-2);
} }
.chip.tier-premium.status-acceptable { border-color: var(--accent-2); color: var(--accent-2); }
.chip.tier-middle.status-acceptable { border-color: var(--ink); color: var(--ink); }
.chip.tier-budget.status-acceptable { border-color: var(--muted); color: var(--ink-2); }
.chip.status-acceptable::before { content: "✓ "; } .chip.status-acceptable::before { content: "✓ "; }
/* ----- Summary ----- */ /* ----- Summary ----- */

View File

@ -3,8 +3,8 @@
============================================================ */ ============================================================ */
const Podbor = (function () { const Podbor = (function () {
const STORAGE_KEY = "zov-podbor-v1"; const STORAGE_KEY = "zov-podbor-v2";
const STEPS = ["intro", "categories", "context", "infra", "scenario", "brands", "summary"]; const STEPS = ["intro", "categories", "pricing", "infra", "priorities", "brands", "summary"];
let state = loadState(); let state = loadState();
let root = null; let root = null;
@ -23,13 +23,11 @@ const Podbor = (function () {
client_name: "", client_name: "",
client_phone: "", client_phone: "",
address: "", address: "",
budget_total: "",
categories: [], // ['fridge','hob',...] categories: [], // ['fridge','hob',...]
niches: {}, // { fridge:{w,h,d}, hob:{w,d}, oven:{w,h,d}, dw:{w,h,d} } price_ranges: {}, // { fridge: { from: 50000, to: 120000 }, ... }
budget_by_cat: {},// { fridge:80000, hob:50000, ... }
infra: { stove: "", vent: "" }, infra: { stove: "", vent: "" },
scenario: { family: "", cooking: "", techniques: [], guests: "" }, priorities: [], // ['balance','reviews',...]
brands: {}, // { fridge: {Bosch:'preferred', Liebherr:'preferred'}, ... } brands: {}, // { fridge: {Bosch:'preferred',...}, ... }
notes: "", notes: "",
}; };
} }
@ -72,9 +70,9 @@ const Podbor = (function () {
switch (currentStep) { switch (currentStep) {
case "intro": screen.appendChild(renderIntro()); break; case "intro": screen.appendChild(renderIntro()); break;
case "categories": screen.appendChild(renderCategories()); break; case "categories": screen.appendChild(renderCategories()); break;
case "context": screen.appendChild(renderContext()); break; case "pricing": screen.appendChild(renderPricing()); break;
case "infra": screen.appendChild(renderInfra()); break; case "infra": screen.appendChild(renderInfra()); break;
case "scenario": screen.appendChild(renderScenario()); break; case "priorities": screen.appendChild(renderPriorities()); break;
case "brands": screen.appendChild(renderBrands()); break; case "brands": screen.appendChild(renderBrands()); break;
case "summary": screen.appendChild(renderSummary()); break; case "summary": screen.appendChild(renderSummary()); break;
} }
@ -107,7 +105,7 @@ const Podbor = (function () {
const idx = STEPS.indexOf(currentStep); const idx = STEPS.indexOf(currentStep);
const total = STEPS.length; const total = STEPS.length;
const pct = Math.round(((idx + 1) / total) * 100); const pct = Math.round(((idx + 1) / total) * 100);
const labels = ["Старт", "Категории", "Контекст", "Инфра", "Сценарий", "Бренды", "Подбор"]; const labels = ["Старт", "Категории", "Цена", "Инфра", "Приоритеты", "Бренды", "Подбор"];
return el(` return el(`
<div class="podbor-progress"> <div class="podbor-progress">
<div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div> <div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div>
@ -124,7 +122,7 @@ const Podbor = (function () {
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2> <h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2>
<p class="lede">7 коротких шагов. Указываем категории, бюджет, инфраструктуру и предпочтения. Дальше AI сам соберёт предложение.</p> <p class="lede">7 коротких шагов. Категории, ценовой коридор, инфраструктура и предпочтения. AI соберёт предложение.</p>
<div class="form-row"> <div class="form-row">
<label class="field"> <label class="field">
@ -132,15 +130,11 @@ const Podbor = (function () {
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова"> <input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
</label> </label>
</div> </div>
<div class="form-row two-col"> <div class="form-row">
<label class="field"> <label class="field">
<span class="field-label">Телефон</span> <span class="field-label">Телефон</span>
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ..."> <input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ...">
</label> </label>
<label class="field">
<span class="field-label">Бюджет на технику, </span>
<input type="number" data-bind="budget_total" value="${state.budget_total || ""}" placeholder="например 350000">
</label>
</div> </div>
<div class="podbor-cta-row"> <div class="podbor-cta-row">
@ -189,86 +183,86 @@ const Podbor = (function () {
return node; return node;
} }
/* ===================== Step: context (ниши + бюджет по категориям) ===================== */ /* ===================== Step: pricing (ценовой коридор по категориям) ===================== */
function renderContext() { function renderPricing() {
const builtinCats = ["fridge", "hob", "oven", "dw"]; // встройка if (!state.categories.length) {
const niches = builtinCats.filter(c => state.categories.includes(c)).map(c => { return el(`
<section class="podbor-step">
<div class="empty">Сначала выберите категории.</div>
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="categories">Назад</button>
</div>
</section>
`);
}
// Подсчёт суммы коридоров
let totalFrom = 0, totalTo = 0;
state.categories.forEach(c => {
const r = state.price_ranges[c] || {};
if (r.from) totalFrom += parseInt(r.from, 10) || 0;
if (r.to) totalTo += parseInt(r.to, 10) || 0;
});
const rows = state.categories.map(c => {
const cat = PODBOR_CATEGORIES.find(x => x.key === c); const cat = PODBOR_CATEGORIES.find(x => x.key === c);
const n = state.niches[c] || {}; const r = state.price_ranges[c] || {};
return ` return `
<div class="niche-row"> <div class="price-row">
<div class="niche-label">${cat.label}</div> <div class="price-label">${cat.label}</div>
<div class="niche-inputs"> <div class="price-inputs">
<input type="number" data-niche="${c}.w" value="${n.w || ""}" placeholder="Ш"> <input type="number" inputmode="numeric" data-price="${c}.from" value="${r.from || ""}" placeholder="от">
<input type="number" data-niche="${c}.h" value="${n.h || ""}" placeholder="В"> <span class="dash"></span>
<input type="number" data-niche="${c}.d" value="${n.d || ""}" placeholder="Г"> <input type="number" inputmode="numeric" data-price="${c}.to" value="${r.to || ""}" placeholder="до">
<span class="rub"></span>
</div> </div>
</div> </div>
`; `;
}).join(""); }).join("");
const budgets = state.categories.map(c => { const totalLine = (totalFrom || totalTo)
const cat = PODBOR_CATEGORIES.find(x => x.key === c); ? `<div class="price-total">Итого: <strong>${formatRub(totalFrom)}${formatRub(totalTo)} ₽</strong></div>`
const v = state.budget_by_cat[c] || ""; : `<div class="price-total muted">Сумма посчитается автоматически</div>`;
return `
<label class="field-inline">
<span>${cat.label}</span>
<input type="number" data-budget="${c}" value="${v}" placeholder="₽">
</label>
`;
}).join("");
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Размеры<br><span class="accent">и бюджет</span></h2> <h2 class="display-title">Ценовой<br><span class="accent">коридор</span></h2>
<p class="lede">Если планируется встройка укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.</p> <p class="lede">«От До» по каждой категории. AI подберёт варианты, которые попадают в коридор и совокупно укладываются в общий бюджет клиента.</p>
${niches ? `
<div class="block"> <div class="block">
<div class="block-head">Ниши под встройку, мм</div> <div class="block-head">По категориям, </div>
<div class="niche-list">${niches}</div> <div class="price-list">${rows}</div>
${totalLine}
</div> </div>
` : ""}
${budgets ? `
<div class="block">
<div class="block-head">Бюджет по категориям, </div>
<div class="budget-list">${budgets}</div>
</div>
` : ""}
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-secondary" data-go="categories">Назад</button> <button class="btn-secondary" data-go="categories">Назад</button>
<button class="btn-primary" data-go="infra">Дальше</button> <button class="btn-primary" data-go="infra">Дальше</button>
</div> </div>
</section> </section>
`); `);
node.querySelectorAll("[data-niche]").forEach(inp => { node.querySelectorAll("[data-price]").forEach(inp => {
inp.addEventListener("input", e => { inp.addEventListener("input", e => {
const [cat, dim] = e.target.dataset.niche.split("."); const [cat, key] = e.target.dataset.price.split(".");
const next = { ...state.niches, [cat]: { ...(state.niches[cat] || {}), [dim]: e.target.value } }; const next = { ...state.price_ranges, [cat]: { ...(state.price_ranges[cat] || {}), [key]: e.target.value } };
update({ niches: next }); update({ price_ranges: next });
}); render();
});
node.querySelectorAll("[data-budget]").forEach(inp => {
inp.addEventListener("input", e => {
const cat = e.target.dataset.budget;
const next = { ...state.budget_by_cat, [cat]: e.target.value };
update({ budget_by_cat: next });
}); });
}); });
bindNav(node); bindNav(node);
return node; return node;
} }
function formatRub(n) {
if (!n) return "—";
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
/* ===================== Step: infra ===================== */ /* ===================== Step: infra ===================== */
function renderInfra() { function renderInfra() {
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2> <h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
<p class="lede">Газ или электрика главный вопрос для варочной. Шахта вентиляции для вытяжки.</p> <p class="lede">Газ или электрика определит тип варочной (индукция / стеклокерамика / газ). Подключение вытяжки нужны ли выводы или угольный фильтр.</p>
<div class="block"> <div class="block">
<div class="block-head">Подключение варочной</div> <div class="block-head">Подключение варочной</div>
<div class="opt-list"> <div class="opt-list">
@ -278,16 +272,17 @@ const Podbor = (function () {
</div> </div>
</div> </div>
<div class="block"> <div class="block">
<div class="block-head">Вентиляция для вытяжки</div> <div class="block-head">Вытяжка внутридомовая вентиляция?</div>
<div class="opt-list"> <div class="opt-list">
${PODBOR_INFRA.vent.map(o => ` ${PODBOR_INFRA.vent.map(o => `
<button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button> <button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button>
`).join("")} `).join("")}
</div> </div>
<div class="hint">Если «Нет» менеджер закладывает угольный фильтр. Если «Да» заранее планируем выводы.</div>
</div> </div>
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-secondary" data-go="context">Назад</button> <button class="btn-secondary" data-go="pricing">Назад</button>
<button class="btn-primary" data-go="scenario">Дальше</button> <button class="btn-primary" data-go="priorities">Дальше</button>
</div> </div>
</section> </section>
`); `);
@ -301,60 +296,34 @@ const Podbor = (function () {
return node; return node;
} }
/* ===================== Step: scenario ===================== */ /* ===================== Step: priorities (что важно при выборе) ===================== */
function renderScenario() { function renderPriorities() {
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Сценарий<br><span class="accent">использования</span></h2> <h2 class="display-title">Что важно<br><span class="accent">при выборе?</span></h2>
<p class="lede">Семья с детьми готовит иначе, чем пара. AI учтёт это в подборе.</p> <p class="lede">Бюджет уже задал коридор. Здесь что AI должен использовать как тай-брейк, когда варианты примерно равны по цене.</p>
<div class="block"> <div class="block">
<div class="block-head">Состав семьи</div> <div class="block-head">Приоритеты</div>
<div class="opt-list"> <div class="opt-list">
${PODBOR_FAMILY.map(o => ` ${PODBOR_PRIORITIES.map(o => `
<button class="opt${state.scenario.family === o.key ? " on" : ""}" data-scenario="family" data-val="${o.key}">${o.label}</button> <button class="opt${(state.priorities || []).includes(o.key) ? " on" : ""}" data-pri="${o.key}">${o.label}</button>
`).join("")} `).join("")}
</div> </div>
<div class="hint">Можно несколько · в порядке выбора</div>
</div> </div>
<div class="block">
<div class="block-head">Частота готовки</div>
<div class="opt-list">
${PODBOR_COOKING.map(o => `
<button class="opt${state.scenario.cooking === o.key ? " on" : ""}" data-scenario="cooking" data-val="${o.key}">${o.label}</button>
`).join("")}
</div>
</div>
<div class="block">
<div class="block-head">Любимые техники приготовления</div>
<div class="opt-list">
${PODBOR_TECHNIQUES.map(o => `
<button class="opt${(state.scenario.techniques || []).includes(o.key) ? " on" : ""}" data-tech="${o.key}">${o.label}</button>
`).join("")}
</div>
<div class="hint">Можно несколько</div>
</div>
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-secondary" data-go="infra">Назад</button> <button class="btn-secondary" data-go="infra">Назад</button>
<button class="btn-primary" data-go="brands">Дальше</button> <button class="btn-primary" data-go="brands">Дальше</button>
</div> </div>
</section> </section>
`); `);
node.querySelectorAll("[data-scenario]").forEach(b => { node.querySelectorAll("[data-pri]").forEach(b => {
b.addEventListener("click", () => { b.addEventListener("click", () => {
update({ scenario: { ...state.scenario, [b.dataset.scenario]: b.dataset.val } }); const cur = state.priorities || [];
render(); const key = b.dataset.pri;
});
});
node.querySelectorAll("[data-tech]").forEach(b => {
b.addEventListener("click", () => {
const cur = state.scenario.techniques || [];
const key = b.dataset.tech;
const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key]; const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
update({ scenario: { ...state.scenario, techniques: next } }); update({ priorities: next });
render(); render();
}); });
}); });
@ -372,22 +341,20 @@ const Podbor = (function () {
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
const brands = PODBOR_BRANDS[catKey] || { premium: [], middle: [], budget: [] }; const brands = PODBOR_BRANDS[catKey] || { premium: [], middle: [], budget: [] };
const catState = state.brands[catKey] || {}; const catState = state.brands[catKey] || {};
const tierBlock = (tier) => ` // Тиры остаются в данных (для аналитики «температуры» клиента),
<div class="tier-row"> // но визуально просто разный цветовой оттенок чипа — без явного ярлыка.
<div class="tier-label">${PODBOR_BUDGET_TIERS.find(t => t.key === tier).label}</div> const tierGroup = (tier) => `
<div class="brand-chips"> <div class="brand-chips brand-tier-${tier}">
${(brands[tier] || []).map(b => { ${(brands[tier] || []).map(b => {
const status = catState[b] || "none"; const status = catState[b] || "none";
return `<button class="chip status-${status}" data-cat="${catKey}" data-brand="${b}">${b}</button>`; return `<button class="chip tier-${tier} status-${status}" data-cat="${catKey}" data-brand="${b}" data-tier="${tier}">${b}</button>`;
}).join("")} }).join("")}
</div> </div>
</div>
`; `;
return ` return `
<div class="block"> <div class="block">
<div class="block-head">${cat.label}</div> <div class="block-head">${cat.label}</div>
<div class="hint"> Тап предпочтительно · Двойной допустимо · Третий снять</div> ${tierGroup("premium")}${tierGroup("middle")}${tierGroup("budget")}
${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")}
</div> </div>
`; `;
}).join(""); }).join("");
@ -395,10 +362,10 @@ const Podbor = (function () {
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2> <h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
<p class="lede">Какие марки уважаете, какие допустимы. AI сначала пробует preferred.</p> <p class="lede">Тап предпочтительно. Дабл допустимо. Третий снять. AI сначала пробует , потом .</p>
${blocks} ${blocks}
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-secondary" data-go="scenario">Назад</button> <button class="btn-secondary" data-go="priorities">Назад</button>
<button class="btn-primary" data-go="summary">Дальше</button> <button class="btn-primary" data-go="summary">Дальше</button>
</div> </div>
</section> </section>
@ -422,17 +389,30 @@ const Podbor = (function () {
/* ===================== Step: summary + submit ===================== */ /* ===================== Step: summary + submit ===================== */
function renderSummary() { function renderSummary() {
let totalFrom = 0, totalTo = 0;
state.categories.forEach(c => {
const r = state.price_ranges[c] || {};
totalFrom += parseInt(r.from || "0", 10) || 0;
totalTo += parseInt(r.to || "0", 10) || 0;
});
const totalRange = (totalFrom || totalTo)
? `${formatRub(totalFrom)}${formatRub(totalTo)}`
: "—";
const priorityLabels = (state.priorities || [])
.map(k => PODBOR_PRIORITIES.find(p => p.key === k)?.label)
.filter(Boolean).join(" · ");
const node = el(` const node = el(`
<section class="podbor-step"> <section class="podbor-step">
<h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2> <h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2>
<p class="lede">Проверьте и отправьте AI вернёт предложение в чат с ботом.</p> <p class="lede">Проверьте и отправьте AI вернёт предложение в чат с ботом.</p>
<div class="block summary-block"> <div class="block summary-block">
<div class="kv"><span>Клиент</span><strong>${state.client_name || ""}</strong></div> <div class="kv"><span>Клиент</span><strong>${state.client_name || ""}</strong></div>
<div class="kv"><span>Бюджет</span><strong>${state.budget_total ? state.budget_total + " " : ""}</strong></div>
<div class="kv"><span>Категорий</span><strong>${state.categories.length}</strong></div> <div class="kv"><span>Категорий</span><strong>${state.categories.length}</strong></div>
<div class="kv"><span>Семья</span><strong>${PODBOR_FAMILY.find(f => f.key === state.scenario.family)?.label || ""}</strong></div> <div class="kv"><span>Ценовой коридор</span><strong>${totalRange}</strong></div>
<div class="kv"><span>Готовка</span><strong>${PODBOR_COOKING.find(f => f.key === state.scenario.cooking)?.label || ""}</strong></div>
<div class="kv"><span>Подключение</span><strong>${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || ""}</strong></div> <div class="kv"><span>Подключение</span><strong>${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || ""}</strong></div>
<div class="kv"><span>Вентиляция</span><strong>${PODBOR_INFRA.vent.find(f => f.key === state.infra.vent)?.label || ""}</strong></div>
<div class="kv"><span>Приоритеты</span><strong>${priorityLabels || ""}</strong></div>
</div> </div>
<label class="field"> <label class="field">

View File

@ -12,8 +12,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260509l"> <link rel="stylesheet" href="assets/styles.css?v=20260509m">
<link rel="stylesheet" href="assets/podbor.css?v=20260509l"> <link rel="stylesheet" href="assets/podbor.css?v=20260509m">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
@ -21,9 +21,9 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
</main> </main>
<script src="assets/icons.js?v=20260509l"></script> <script src="assets/icons.js?v=20260509m"></script>
<script src="assets/podbor.config.js?v=20260509l"></script> <script src="assets/podbor.config.js?v=20260509m"></script>
<script src="assets/podbor.js?v=20260509l"></script> <script src="assets/podbor.js?v=20260509m"></script>
<script src="assets/app.js?v=20260509l"></script> <script src="assets/app.js?v=20260509m"></script>
</body> </body>
</html> </html>