mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 18:04:47 +00:00
feat(podbor iter2): per-category detail menu with primary params + accordion 'Подробнее' for tech features; updated AI prompt to require feature explanations
This commit is contained in:
parent
129046de07
commit
cc28984122
@ -494,25 +494,44 @@ function synthesizeManagerFromUser(user) {
|
||||
|
||||
const SYSTEM_PROMPT_PICKER = (
|
||||
"Ты — эксперт-консультант по подбору кухонной техники для фабрики мебели «ЗОВ».\n" +
|
||||
"Помогаешь менеджерам салонов быстро согласовать с клиентом комплект техники.\n\n" +
|
||||
"Принципы:\n" +
|
||||
"1. Физические ограничения важнее эстетики. Если ниша 600×1850×600 — не предлагай 700×2000×650.\n" +
|
||||
"2. Уважай бюджет. Лимит в категории — не превышай >10%.\n" +
|
||||
"3. Уважай предпочтения по брендам: сначала preferred (★), потом alternative (✓).\n" +
|
||||
"4. Сценарий использования. Семья с детьми = простой UI, защита от детей. Выпечка = пар + конвекция.\n" +
|
||||
"5. Инфраструктура. Газ исключает индукцию. Нет шахты = только рециркуляция.\n" +
|
||||
"6. По каждой позиции: модель (линейка), цена, 2-3 преимущества под клиента, 1 предупреждение.\n\n" +
|
||||
"Помогаешь менеджерам салонов согласовать с клиентом комплект техники.\n\n" +
|
||||
"Принципы подбора:\n" +
|
||||
"1. Уважай ценовой коридор. У каждой категории есть `price_ranges.{cat}.from..to` — попадай в него (±5% допустимо).\n" +
|
||||
"2. Уважай предпочтения по брендам: сначала preferred (★), потом acceptable (✓), потом нейтральные альтернативы.\n" +
|
||||
"3. Учитывай инфраструктуру: газ исключает индукцию (если клиент не готов менять подключение); нет вентиляции (`vent: no`) = только рециркуляция с угольным фильтром.\n" +
|
||||
"4. Учитывай приоритеты выбора (`priorities`): «цена/качество» → балансные модели; «отзывы» → проверенные хиты; «дизайн и цвет» → подбирай эстетику; «технологичность» → топовые фичи.\n" +
|
||||
"5. Если клиент явно отметил features в `per_cat.{cat}.features` — обязательно ставь модели с этими фичами.\n" +
|
||||
"6. ВАЖНО: каждую тех. фичу в highlights ОБЯЗАТЕЛЬНО объясняй простым языком в скобках. Менеджер и клиент не обязаны знать что такое NoFrost / FlexZone / 4D HotAir.\n\n" +
|
||||
"Примеры пояснений (используй такой стиль):\n" +
|
||||
" «NoFrost (не нужно размораживать вручную)»\n" +
|
||||
" «PowerBoost (форсаж — кипятит за минуту)»\n" +
|
||||
" «FlexZone (объединяет зоны под большую сковороду)»\n" +
|
||||
" «Hob2Hood (вытяжка автоматически работает синхронно с варочной)»\n" +
|
||||
" «4D HotAir (конвекция с 4 сторон — равномерное запекание)»\n" +
|
||||
" «Термощуп (готовит до точной температуры medium / well-done)»\n" +
|
||||
" «AquaStop (защита от протечек — машина сама перекроет воду)»\n" +
|
||||
" «SoftClose (дверца закрывается плавно, без хлопка)»\n" +
|
||||
" «Инвертор (тише и экономия ~30% электричества)»\n\n" +
|
||||
"Формат ответа — валидный JSON без markdown:\n" +
|
||||
"{\n" +
|
||||
' "summary": "...", \n' +
|
||||
' "items": [{"category":"fridge","brand":"Bosch","model":"Serie 4 60см","price_rub":79990,' +
|
||||
'"size_mm":{"w":600,"h":2030,"d":660},"fits_niche":true,"highlights":["NoFrost","инвертор"],' +
|
||||
'"caveats":"Глубина 660мм","match_score":0.92}],\n' +
|
||||
' "summary": "1-2 предложения общего вывода",\n' +
|
||||
' "items": [{\n' +
|
||||
' "category": "fridge",\n' +
|
||||
' "brand": "Bosch",\n' +
|
||||
' "model": "Serie 4 KGN39LB35R",\n' +
|
||||
' "price_rub": 79990,\n' +
|
||||
' "highlights": ["NoFrost (не нужно размораживать вручную)", "Инвертор (тише и экономия ~30%)", "Зона свежести (овощи дольше хрустящие)"],\n' +
|
||||
' "caveats": "Глубина 660мм — на 60мм больше стандартной ниши",\n' +
|
||||
' "match_score": 0.92,\n' +
|
||||
' "tier_signal": "middle"\n' +
|
||||
' }],\n' +
|
||||
' "total_price_rub": 350000,\n' +
|
||||
' "budget_status": "в_рамках|превышение|значительно_ниже",\n' +
|
||||
' "client_temperature": "premium|middle|budget|mixed",\n' +
|
||||
' "warnings": [],\n' +
|
||||
' "next_steps": []\n' +
|
||||
"}\n\n" +
|
||||
"tier_signal — твоя оценка позиционирования модели. client_temperature — общий типаж клиента по сумме выбора (для аналитики).\n" +
|
||||
"Не выдумывай несуществующие артикулы — указывай линейку (Bosch Serie 4 60см)."
|
||||
);
|
||||
|
||||
|
||||
@ -42,6 +42,223 @@ const PODBOR_PRIORITIES = [
|
||||
{ key: "service", label: "Сервис и гарантия" },
|
||||
];
|
||||
|
||||
/* Параметры по категориям: Главное (всегда видно) + Подробнее (свёрнуто) */
|
||||
const PODBOR_PARAMS = {
|
||||
fridge: {
|
||||
primary: [
|
||||
{ key: "type", label: "Тип", options: [
|
||||
{ key: "two_chamber", label: "Двухкамерный" },
|
||||
{ key: "sbs", label: "Side-by-side" },
|
||||
{ key: "french", label: "French Door" },
|
||||
{ key: "column", label: "Колонна (встр.)" },
|
||||
{ key: "combi", label: "Комбинированный" },
|
||||
]},
|
||||
{ key: "width", label: "Ширина, см", options: [
|
||||
{ key: "54", label: "54" }, { key: "60", label: "60" },
|
||||
{ key: "70", label: "70" }, { key: "75", label: "75" }, { key: "91", label: "91" },
|
||||
]},
|
||||
{ key: "volume", label: "Объём, л", options: [
|
||||
{ key: "to300", label: "до 300" },
|
||||
{ key: "300-450", label: "300–450" },
|
||||
{ key: "450-600", label: "450–600" },
|
||||
{ key: "600+", label: "600+" },
|
||||
]},
|
||||
{ key: "color", label: "Цвет", options: [
|
||||
{ key: "white", label: "Белый" },
|
||||
{ key: "inox", label: "Нерж. сталь" },
|
||||
{ key: "black", label: "Чёрный" },
|
||||
{ key: "anthracite", label: "Антрацит" },
|
||||
{ key: "builtin", label: "Под фасад" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "nofrost", label: "NoFrost", hint: "не нужно размораживать вручную" },
|
||||
{ key: "inverter", label: "Инвертор", hint: "тише и экономичнее на ~30%" },
|
||||
{ key: "freshzone", label: "Зона свежести", hint: "овощи и зелень дольше хрустящие" },
|
||||
{ key: "silent", label: "≤40 дБ", hint: "почти не слышно ночью" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "управление с телефона" },
|
||||
{ key: "ice", label: "Лёдогенератор", hint: "автоматически делает кубики" },
|
||||
],
|
||||
},
|
||||
hob: {
|
||||
primary: [
|
||||
{ key: "heat", label: "Тип нагрева", options: [
|
||||
{ key: "induction", label: "Индукция" },
|
||||
{ key: "hi_light", label: "Hi-Light (стеклокерамика)" },
|
||||
{ key: "gas", label: "Газ" },
|
||||
{ key: "domino", label: "Domino (модульная)" },
|
||||
]},
|
||||
{ key: "width", label: "Ширина, см", options: [
|
||||
{ key: "30", label: "30" }, { key: "45", label: "45" },
|
||||
{ key: "60", label: "60" }, { key: "80", label: "80" }, { key: "90", label: "90" },
|
||||
]},
|
||||
{ key: "zones", label: "Число зон", options: [
|
||||
{ key: "2", label: "2" }, { key: "3", label: "3" },
|
||||
{ key: "4", label: "4" }, { key: "5", label: "5" },
|
||||
]},
|
||||
{ key: "color", label: "Цвет", options: [
|
||||
{ key: "black", label: "Чёрный" }, { key: "white", label: "Белый" },
|
||||
{ key: "frameless", label: "Без рамки" }, { key: "inox", label: "Нерж. сталь" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "boost", label: "PowerBoost", hint: "форсаж — кипятит за минуту" },
|
||||
{ key: "flex", label: "FlexZone", hint: "объединяет зоны под большую сковороду" },
|
||||
{ key: "hob2hood", label: "Hob2Hood", hint: "вытяжка автоматически следит за варочной" },
|
||||
{ key: "child_lock", label: "Защита от детей", hint: "блокировка панели" },
|
||||
],
|
||||
},
|
||||
oven: {
|
||||
primary: [
|
||||
{ key: "config", label: "Конфигурация", options: [
|
||||
{ key: "compact_combi", label: "Компакт + СВЧ" },
|
||||
{ key: "full_60", label: "Полный 60 см" },
|
||||
{ key: "xl_90", label: "XL 90 см" },
|
||||
{ key: "two_separate", label: "2 отдельных прибора" },
|
||||
]},
|
||||
{ key: "color", label: "Цвет", options: [
|
||||
{ key: "black", label: "Чёрный" },
|
||||
{ key: "inox", label: "Нерж. сталь" },
|
||||
{ key: "white", label: "Белый" },
|
||||
{ key: "blackglass", label: "Чёрное стекло" },
|
||||
{ key: "anthracite", label: "Антрацит" },
|
||||
]},
|
||||
{ key: "cleaning", label: "Очистка", options: [
|
||||
{ key: "hydro", label: "Гидролиз" },
|
||||
{ key: "pyro", label: "Пиролиз" },
|
||||
{ key: "eco", label: "Eco / каталитическая" },
|
||||
{ key: "aqua", label: "Aqua" },
|
||||
{ key: "std", label: "Стандарт" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "4d", label: "4D HotAir", hint: "конвекция с 4 сторон — равномерное запекание" },
|
||||
{ key: "steam", label: "Пар", hint: "хлеб с румяной корочкой, мясо без пересушки" },
|
||||
{ key: "probe", label: "Термощуп", hint: "готовит до точной температуры (medium / well-done)" },
|
||||
{ key: "autopilot", label: "Автопилот", hint: "выбираешь блюдо — духовка сама ставит режим" },
|
||||
{ key: "softclose", label: "SoftClose", hint: "дверца закрывается плавно" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "следишь за приготовлением с телефона" },
|
||||
],
|
||||
},
|
||||
dw: {
|
||||
primary: [
|
||||
{ key: "width", label: "Ширина, см", options: [
|
||||
{ key: "45", label: "45" }, { key: "60", label: "60" },
|
||||
]},
|
||||
{ key: "mount", label: "Монтаж", options: [
|
||||
{ key: "full_built_in", label: "Полная встройка (под фасад)" },
|
||||
{ key: "partial", label: "Частичная встройка" },
|
||||
{ key: "freestanding", label: "Отдельная" },
|
||||
]},
|
||||
{ key: "settings", label: "Комплектов", options: [
|
||||
{ key: "8-9", label: "8–9 (для 2–3 человек)" },
|
||||
{ key: "10-11", label: "10–11 (семья 3–4)" },
|
||||
{ key: "12-14", label: "12–14 (большая семья)" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "aquastop", label: "AquaStop", hint: "защита от протечек — машина сама перекроет воду" },
|
||||
{ key: "tray", label: "3-й лоток", hint: "отдельная полка для столовых приборов" },
|
||||
{ key: "autoopen", label: "AutoOpen", hint: "приоткрывает дверь после мойки — сухая посуда" },
|
||||
{ key: "silent", label: "≤44 дБ", hint: "можно мыть ночью, не слышно" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "уведомление на телефон когда готово" },
|
||||
],
|
||||
},
|
||||
hood: {
|
||||
primary: [
|
||||
{ key: "type", label: "Тип", options: [
|
||||
{ key: "inclined", label: "Наклонная" },
|
||||
{ key: "t_shape", label: "Т-образная" },
|
||||
{ key: "dome", label: "Купольная" },
|
||||
{ key: "built_in", label: "Встроенная" },
|
||||
{ key: "telescopic", label: "Телескопическая" },
|
||||
{ key: "island", label: "Островная" },
|
||||
]},
|
||||
{ key: "width", label: "Ширина, см", options: [
|
||||
{ key: "50", label: "50" }, { key: "60", label: "60" },
|
||||
{ key: "80", label: "80" }, { key: "90", label: "90" },
|
||||
]},
|
||||
{ key: "color", label: "Цвет", options: [
|
||||
{ key: "inox", label: "Нерж. сталь" },
|
||||
{ key: "black", label: "Чёрный" },
|
||||
{ key: "white", label: "Белый" },
|
||||
{ key: "black_glass", label: "Чёрное стекло" },
|
||||
]},
|
||||
{ key: "mode", label: "Режим работы", options: [
|
||||
{ key: "exhaust", label: "Только отвод (вентиляция)" },
|
||||
{ key: "recirc", label: "Только рециркуляция (фильтр)" },
|
||||
{ key: "combi", label: "Оба режима" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "hi_perf", label: "Производительность 600+ м³/ч", hint: "сильно тянет — для большой кухни / wok" },
|
||||
{ key: "perimeter", label: "Периметральная вытяжка", hint: "тянет с краёв — больше пара захватывает" },
|
||||
{ key: "low_noise", label: "Тихая работа ≤50 дБ", hint: "не оглушает за столом" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "автоматическая работа в паре с варочной" },
|
||||
],
|
||||
},
|
||||
microwave: {
|
||||
primary: [
|
||||
{ key: "type", label: "Размещение", options: [
|
||||
{ key: "builtin", label: "Встроенная" },
|
||||
{ key: "freestanding", label: "Отдельная" },
|
||||
]},
|
||||
{ key: "volume", label: "Объём, л", options: [
|
||||
{ key: "to20", label: "до 20" },
|
||||
{ key: "20-25", label: "20–25" },
|
||||
{ key: "25+", label: "25+" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "grill", label: "Гриль", hint: "запекает корочку сверху" },
|
||||
{ key: "convection", label: "Конвекция", hint: "работает как маленькая духовка" },
|
||||
{ key: "inverter", label: "Инвертор", hint: "плавная мощность — не пересушивает" },
|
||||
],
|
||||
},
|
||||
coffee: {
|
||||
primary: [
|
||||
{ key: "type", label: "Размещение", options: [
|
||||
{ key: "builtin", label: "Встроенная" },
|
||||
{ key: "freestanding", label: "Отдельная" },
|
||||
]},
|
||||
{ key: "tech", label: "Тип", options: [
|
||||
{ key: "auto_grinder", label: "Автомат с кофемолкой" },
|
||||
{ key: "capsule", label: "Капсульная" },
|
||||
{ key: "manual", label: "Рожковая (бариста)" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "milk", label: "Капучинатор", hint: "автоматическое латте/капучино" },
|
||||
{ key: "profiles", label: "Профили", hint: "у каждого свой размер/крепость" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "управление с телефона" },
|
||||
],
|
||||
},
|
||||
washer: {
|
||||
primary: [
|
||||
{ key: "type", label: "Размещение", options: [
|
||||
{ key: "builtin", label: "Встроенная" },
|
||||
{ key: "freestanding", label: "Отдельная" },
|
||||
]},
|
||||
{ key: "load", label: "Загрузка, кг", options: [
|
||||
{ key: "to6", label: "до 6" },
|
||||
{ key: "6-8", label: "6–8" },
|
||||
{ key: "8-10", label: "8–10" },
|
||||
{ key: "10+", label: "10+" },
|
||||
]},
|
||||
{ key: "depth", label: "Глубина", options: [
|
||||
{ key: "slim", label: "Slim (до 45 см)" },
|
||||
{ key: "standard", label: "Стандарт (60 см)" },
|
||||
]},
|
||||
],
|
||||
features: [
|
||||
{ key: "steam", label: "Пар", hint: "освежает без стирки, убивает аллергены" },
|
||||
{ key: "dry", label: "Сушка", hint: "достал — и сразу в шкаф" },
|
||||
{ key: "silent", label: "≤50 дБ", hint: "ночная стирка не разбудит" },
|
||||
{ key: "smart", label: "Smart / Wi-Fi", hint: "запуск с телефона, уведомления" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/* Бренды для каждой категории — для чипов с тирами.
|
||||
Сокращённый набор; полный список можно расширить из исходного HTML. */
|
||||
const PODBOR_BRANDS = {
|
||||
|
||||
@ -543,3 +543,250 @@
|
||||
}
|
||||
|
||||
.empty { text-align: center; color: var(--muted); padding: var(--s7) 0; }
|
||||
|
||||
/* ============================================================
|
||||
Detail menu — list of categories with status
|
||||
============================================================ */
|
||||
.detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s2);
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s3);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-card);
|
||||
padding: var(--s3) var(--s4);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detail-card:active { background: var(--paper-2); }
|
||||
|
||||
.detail-card.done { border-color: var(--accent-2); }
|
||||
|
||||
.detail-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-icon svg { width: 22px; height: 22px; stroke-width: 1.4; }
|
||||
|
||||
.detail-card.done .detail-icon { color: var(--accent-2); }
|
||||
|
||||
.detail-text { flex: 1; min-width: 0; }
|
||||
|
||||
.detail-name {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.detail-sum {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.detail-card.done .detail-sum { color: var(--ink-2); text-transform: none; letter-spacing: 0; font-family: var(--font-ui); font-size: 12.5px; }
|
||||
|
||||
.detail-status {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.detail-status svg { width: 18px; height: 18px; }
|
||||
|
||||
.detail-card.done .detail-status {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: var(--accent-2);
|
||||
color: var(--paper);
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
|
||||
.detail-card.done .detail-status svg { width: 14px; height: 14px; stroke-width: 2.5; }
|
||||
|
||||
/* ============================================================
|
||||
Category detail screen
|
||||
============================================================ */
|
||||
.cat-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s3);
|
||||
margin-bottom: var(--s4);
|
||||
}
|
||||
|
||||
.cat-detail-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.cat-detail-icon svg { width: 26px; height: 26px; stroke-width: 1.4; }
|
||||
|
||||
.cat-detail-title {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.param-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: var(--s3) 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.param-group:first-of-type {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Accordion "Подробнее"
|
||||
============================================================ */
|
||||
.accordion-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: var(--s3) var(--s4);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-card);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accordion-chev {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted);
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
|
||||
.accordion-chev svg { width: 16px; height: 16px; transform: rotate(90deg); transition: transform 0.18s; }
|
||||
.accordion-chev.open svg { transform: rotate(-90deg); }
|
||||
|
||||
.accordion-body {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height 0.25s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.accordion-body.open {
|
||||
max-height: 2000px;
|
||||
padding-top: var(--s3);
|
||||
}
|
||||
|
||||
.accordion-body > * + * { margin-top: var(--s3); }
|
||||
|
||||
/* ============================================================
|
||||
Feature card (in accordion)
|
||||
============================================================ */
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 22px;
|
||||
gap: var(--s2);
|
||||
align-items: center;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-tag);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.feature:active { background: var(--paper-2); }
|
||||
.feature.on {
|
||||
border-color: var(--accent-2);
|
||||
background: var(--paper-2);
|
||||
}
|
||||
|
||||
.feature-name {
|
||||
grid-column: 1 / 2;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.feature-hint {
|
||||
grid-column: 1 / 2;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.3;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.feature-tick {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 3;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-pill);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.feature.on .feature-tick {
|
||||
background: var(--accent-2);
|
||||
color: var(--paper);
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
|
||||
.feature.on .feature-tick svg { width: 12px; height: 12px; stroke-width: 2.5; }
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
============================================================ */
|
||||
|
||||
const Podbor = (function () {
|
||||
const STORAGE_KEY = "zov-podbor-v2";
|
||||
const STEPS = ["intro", "categories", "pricing", "infra", "priorities", "brands", "summary"];
|
||||
const STORAGE_KEY = "zov-podbor-v3";
|
||||
const STEPS = ["intro", "categories", "detail", "pricing", "infra", "priorities", "brands", "summary"];
|
||||
|
||||
// Внутренний sub-state для шага «detail»: 'menu' | 'cat:<key>'
|
||||
let detailView = "menu";
|
||||
|
||||
let state = loadState();
|
||||
let root = null;
|
||||
@ -24,6 +27,7 @@ const Podbor = (function () {
|
||||
client_phone: "",
|
||||
address: "",
|
||||
categories: [], // ['fridge','hob',...]
|
||||
per_cat: {}, // { fridge: { params: {type:'sbs',...}, features: ['nofrost'], notes: '' }, ... }
|
||||
price_ranges: {}, // { fridge: { from: 50000, to: 120000 }, ... }
|
||||
infra: { stove: "", vent: "" },
|
||||
priorities: [], // ['balance','reviews',...]
|
||||
@ -54,6 +58,7 @@ const Podbor = (function () {
|
||||
function go(step) {
|
||||
if (!STEPS.includes(step)) return;
|
||||
currentStep = step;
|
||||
detailView = "menu"; // на любой переход detail возвращается в меню
|
||||
render();
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
haptic && haptic("impact");
|
||||
@ -70,6 +75,7 @@ const Podbor = (function () {
|
||||
switch (currentStep) {
|
||||
case "intro": screen.appendChild(renderIntro()); break;
|
||||
case "categories": screen.appendChild(renderCategories()); break;
|
||||
case "detail": screen.appendChild(renderDetail()); break;
|
||||
case "pricing": screen.appendChild(renderPricing()); break;
|
||||
case "infra": screen.appendChild(renderInfra()); break;
|
||||
case "priorities": screen.appendChild(renderPriorities()); break;
|
||||
@ -105,7 +111,7 @@ const Podbor = (function () {
|
||||
const idx = STEPS.indexOf(currentStep);
|
||||
const total = STEPS.length;
|
||||
const pct = Math.round(((idx + 1) / total) * 100);
|
||||
const labels = ["Старт", "Категории", "Цена", "Инфра", "Приоритеты", "Бренды", "Подбор"];
|
||||
const labels = ["Старт", "Категории", "Параметры", "Цена", "Инфра", "Приоритеты", "Бренды", "Подбор"];
|
||||
return el(`
|
||||
<div class="podbor-progress">
|
||||
<div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div>
|
||||
@ -165,7 +171,7 @@ const Podbor = (function () {
|
||||
<div class="cat-grid">${grid}</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="intro">Назад</button>
|
||||
<button class="btn-primary" data-go="context">Дальше</button>
|
||||
<button class="btn-primary" data-go="detail">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
@ -183,6 +189,191 @@ const Podbor = (function () {
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: detail — menu + per-category sub-screen ===================== */
|
||||
|
||||
function isCategoryFilled(catKey) {
|
||||
const cat = state.per_cat[catKey];
|
||||
if (!cat || !cat.params) return false;
|
||||
const params = PODBOR_PARAMS[catKey]?.primary || [];
|
||||
return params.every(p => cat.params[p.key]);
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
if (!state.categories.length) {
|
||||
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>
|
||||
`);
|
||||
}
|
||||
if (detailView !== "menu" && detailView.startsWith("cat:")) {
|
||||
const catKey = detailView.slice(4);
|
||||
return renderCategoryDetail(catKey);
|
||||
}
|
||||
return renderDetailMenu();
|
||||
}
|
||||
|
||||
function renderDetailMenu() {
|
||||
const cards = state.categories.map(catKey => {
|
||||
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
|
||||
const filled = isCategoryFilled(catKey);
|
||||
const summary = filled ? buildPerCatSummary(catKey) : "Заполнить параметры";
|
||||
return `
|
||||
<button class="detail-card${filled ? " done" : ""}" data-cat="${catKey}">
|
||||
<div class="detail-icon">${ICONS[cat.icon] || ""}</div>
|
||||
<div class="detail-text">
|
||||
<div class="detail-name">${cat.label}</div>
|
||||
<div class="detail-sum">${summary}</div>
|
||||
</div>
|
||||
<div class="detail-status">${filled ? ICONS.check : ICONS.chevron}</div>
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Параметры<br><span class="accent">по категориям</span></h2>
|
||||
<p class="lede">Только главное: тип, размер, цвет. Технические фичи — в «Подробнее ↓», по желанию.</p>
|
||||
<div class="detail-list">${cards}</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||
<button class="btn-primary" data-go="pricing">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll(".detail-card").forEach(c => {
|
||||
c.addEventListener("click", () => {
|
||||
detailView = "cat:" + c.dataset.cat;
|
||||
render();
|
||||
});
|
||||
});
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function buildPerCatSummary(catKey) {
|
||||
const cat = state.per_cat[catKey];
|
||||
if (!cat || !cat.params) return "—";
|
||||
const params = PODBOR_PARAMS[catKey]?.primary || [];
|
||||
const labels = params
|
||||
.map(p => {
|
||||
const opt = p.options.find(o => o.key === cat.params[p.key]);
|
||||
return opt ? opt.label : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return labels.join(" · ") || "—";
|
||||
}
|
||||
|
||||
function renderCategoryDetail(catKey) {
|
||||
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
|
||||
const config = PODBOR_PARAMS[catKey];
|
||||
if (!config) {
|
||||
return el(`<section class="podbor-step"><div class="empty">Параметры для «${cat?.label}» ещё не описаны.</div></section>`);
|
||||
}
|
||||
const catState = state.per_cat[catKey] || { params: {}, features: [], notes: "" };
|
||||
const isExpanded = catState._expanded || false;
|
||||
|
||||
const primaryHtml = config.primary.map(p => {
|
||||
const cur = catState.params?.[p.key] || "";
|
||||
return `
|
||||
<div class="param-group">
|
||||
<div class="param-label">${p.label}</div>
|
||||
<div class="opt-list">
|
||||
${p.options.map(o => `
|
||||
<button class="opt${cur === o.key ? " on" : ""}" data-param="${p.key}" data-val="${o.key}">${o.label}</button>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const featuresHtml = config.features.map(f => {
|
||||
const on = (catState.features || []).includes(f.key);
|
||||
return `
|
||||
<button class="feature${on ? " on" : ""}" data-feat="${f.key}">
|
||||
<div class="feature-name">${f.label}</div>
|
||||
<div class="feature-hint">${f.hint}</div>
|
||||
<div class="feature-tick">${on ? ICONS.check : ""}</div>
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step podbor-cat-detail">
|
||||
<header class="cat-detail-header">
|
||||
<button class="podbor-back" aria-label="К меню">${ICONS.arrow_left}</button>
|
||||
<div class="cat-detail-icon">${ICONS[cat.icon] || ""}</div>
|
||||
<h2 class="cat-detail-title">${cat.label}</h2>
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
<div class="block-head">Главное</div>
|
||||
${primaryHtml}
|
||||
</div>
|
||||
|
||||
<button class="accordion-head" data-toggle="exp">
|
||||
<span>Подробнее</span>
|
||||
<span class="accordion-chev${isExpanded ? " open" : ""}">${ICONS.chevron}</span>
|
||||
</button>
|
||||
<div class="accordion-body${isExpanded ? " open" : ""}">
|
||||
<div class="hint">Технические фичи — необязательно. Если не отметите, AI выберет сам и пояснит в подборе.</div>
|
||||
<div class="feature-list">${featuresHtml}</div>
|
||||
<label class="field">
|
||||
<span class="field-label">Заметки по этой категории</span>
|
||||
<textarea data-bind="cat_notes" rows="2" placeholder="Что-то особенное?">${catState.notes || ""}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" id="catBack">К списку</button>
|
||||
<button class="btn-primary" id="catSave">Сохранить</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
|
||||
// Главное — radio
|
||||
node.querySelectorAll("[data-param]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" };
|
||||
cs.params = { ...(cs.params || {}), [b.dataset.param]: b.dataset.val };
|
||||
update({ per_cat: { ...state.per_cat, [catKey]: cs } });
|
||||
render();
|
||||
});
|
||||
});
|
||||
// Features — toggle
|
||||
node.querySelectorAll("[data-feat]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" };
|
||||
const cur = cs.features || [];
|
||||
cs.features = cur.includes(b.dataset.feat) ? cur.filter(x => x !== b.dataset.feat) : [...cur, b.dataset.feat];
|
||||
update({ per_cat: { ...state.per_cat, [catKey]: cs } });
|
||||
render();
|
||||
});
|
||||
});
|
||||
// Accordion
|
||||
node.querySelector("[data-toggle='exp']").addEventListener("click", () => {
|
||||
const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" };
|
||||
cs._expanded = !cs._expanded;
|
||||
update({ per_cat: { ...state.per_cat, [catKey]: cs } });
|
||||
render();
|
||||
});
|
||||
// Notes
|
||||
const ta = node.querySelector("textarea[data-bind='cat_notes']");
|
||||
if (ta) ta.addEventListener("input", e => {
|
||||
const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" };
|
||||
cs.notes = e.target.value;
|
||||
update({ per_cat: { ...state.per_cat, [catKey]: cs } });
|
||||
});
|
||||
// Back / save → menu
|
||||
node.querySelector(".podbor-back").addEventListener("click", () => { detailView = "menu"; render(); });
|
||||
node.querySelector("#catBack").addEventListener("click", () => { detailView = "menu"; render(); });
|
||||
node.querySelector("#catSave").addEventListener("click", () => { detailView = "menu"; render(); haptic && haptic("success"); });
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: pricing (ценовой коридор по категориям) ===================== */
|
||||
|
||||
function renderPricing() {
|
||||
@ -191,7 +382,7 @@ const Podbor = (function () {
|
||||
<section class="podbor-step">
|
||||
<div class="empty">Сначала выберите категории.</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||
<button class="btn-secondary" data-go="detail">Назад</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
@ -234,7 +425,7 @@ const Podbor = (function () {
|
||||
${totalLine}
|
||||
</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||
<button class="btn-secondary" data-go="detail">Назад</button>
|
||||
<button class="btn-primary" data-go="infra">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<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">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260509m">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260509m">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260509n">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260509n">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
@ -21,9 +21,9 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js?v=20260509m"></script>
|
||||
<script src="assets/podbor.config.js?v=20260509m"></script>
|
||||
<script src="assets/podbor.js?v=20260509m"></script>
|
||||
<script src="assets/app.js?v=20260509m"></script>
|
||||
<script src="assets/icons.js?v=20260509n"></script>
|
||||
<script src="assets/podbor.config.js?v=20260509n"></script>
|
||||
<script src="assets/podbor.js?v=20260509n"></script>
|
||||
<script src="assets/app.js?v=20260509n"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user