From ef500fa446cc0a35535a32b5b478d06f9b9ec4e3 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 11 May 2026 17:11:30 +0300 Subject: [PATCH] user feedback batch: model count, specs, manual link, dimensions, export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. MODEL COUNT SELECTOR (strategy step): - new PODBOR_MODEL_COUNTS [3/5/7] - state.model_count default '5' - UI on strategy page with description (быстро/оптимально/максимум) 2. AI PROMPT EXPANDED: - new field: manual_search_query — for Google search instruction PDF - new specs object per model: dimensions_mm/volume_l/weight_kg/noise_db/energy_class/color - 'specs ОБЯЗАТЕЛЬНЫ для проектирования кухни' explicit rule - reads checklist.model_count to determine how many models per category - max_tokens 4000 → 8000 (room for richer responses) 3. MODEL CARD RICHER: - _renderSpecsBlock — characteristics in 2-col grid, dimensions highlighted - _renderUtilityLinks — Google search buttons for инструкция (PDF) + Схема установки - Specs critical for ZOV kitchen design (manager needs to verify niche fits) 4. EXPORT BUTTONS: - 'Скачать HTML' — generates standalone HTML with inline styles, downloads as file - 'Печать → PDF' — opens new window with cleaned layout + auto-prints - User can save as PDF via system print dialog 5. PREVIEW updated with realistic specs/manual_query for all 3 fridges --- backend-py/app/ai.py | 23 ++++- miniapp/assets/podbor.config.js | 8 ++ miniapp/assets/podbor.css | 129 ++++++++++++++++++++++++++ miniapp/assets/podbor.js | 159 ++++++++++++++++++++++++++++++++ miniapp/index.html | 14 +-- miniapp/preview-report.html | 37 ++++++++ 6 files changed, 359 insertions(+), 11 deletions(-) diff --git a/backend-py/app/ai.py b/backend-py/app/ai.py index acebc07..ddacf59 100644 --- a/backend-py/app/ai.py +++ b/backend-py/app/ai.py @@ -93,7 +93,7 @@ SYSTEM_PROMPT_PICKER = ( " «AquaStop (защита от протечек)»\n" " «Инвертор (тише и экономия ~30% электричества)»\n\n" "═══ ФОРМАТ ОТВЕТА ═══\n" - "Возвращай **3–5 моделей по КАЖДОЙ категории** (не одну!) — для клиента это выбор.\n" + "Количество моделей по категории определяется параметром `checklist.model_count` (3 / 5 / 7) — соблюдай!\n" "Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\n" "По КАЖДОЙ категории напиши `analysis` — обзор: какие компромиссы, на что обратить внимание.\n" "Валидный JSON без markdown, без ```:\n" @@ -109,10 +109,19 @@ SYSTEM_PROMPT_PICKER = ( ' "price_min_rub": 79990,\n' ' "price_max_rub": 92000,\n' ' "search_query": "Haier C4F744CMG холодильник",\n' + ' "manual_search_query": "Haier C4F744CMG manual инструкция pdf",\n' ' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],\n' ' "pros": ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "класс A++, экономия ~30% против A+", "большой объём 463 л против 380 л у конкурентов в той же ценовой категории"],\n' ' "cons": ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить нишу клиента", "нет зоны свежести BioFresh — в этом плане Liebherr ровно вдвое лучше"],\n' ' "reasoning": "Лучший выбор по цена/качество в этом бюджете. Тише и больше чем Bosch в той же цене, но без премиум-зоны свежести.",\n' + ' "specs": {\n' + ' "dimensions_mm": "595×660×2000",\n' + ' "weight_kg": 75,\n' + ' "volume_l": 463,\n' + ' "noise_db": 36,\n' + ' "energy_class": "A++",\n' + ' "color": "Нержавеющая сталь"\n' + ' },\n' ' "tier": "middle",\n' ' "match_score": 0.92\n' " }\n" @@ -132,13 +141,19 @@ SYSTEM_PROMPT_PICKER = ( "4. **Cons обязательны**: даже у лучших моделей есть недостатки. Если cons пусто — модель не выбрана. Конкретные минусы: габарит больше ниши, шумнее на 2 дБ, без какой-то функции, цена выше на N%, длительная гарантия только N лет.\n" "5. **Reasoning**: 1 предложение «почему именно эта модель в этом наборе» — позиционирование относительно других в выдаче.\n" "6. **search_query**: точная строка для поиска (бренд + индекс + слово категория). AI агент будет парсить маркетплейсы по этой строке — не указывай лишнего.\n" - "7. Бренд-стратегия 'single' — ВСЕ models из одной марки.\n" - "8. price_min_rub/price_max_rub — диапазон по разным магазинам (если не уверен — один и тот же)." + "7. **manual_search_query**: строка для Google-поиска инструкции, в формате « manual инструкция pdf»\n" + "8. **specs ОБЯЗАТЕЛЬНЫ для проектирования кухни**:\n" + " - `dimensions_mm` — габариты ШхГxВ в мм (это критично для дизайна ниш в кухне ЗОВ)\n" + " - `weight_kg`, `volume_l` (для холодильников/духовок/ПММ), `noise_db`, `energy_class` ('A+++', 'A++', 'A+', 'A', 'B')\n" + " - `color` — основной цвет/материал\n" + "9. **Количество моделей в каждой категории = `checklist.model_count`** (3 или 5 или 7). Меньше не возвращай. Если AI не уверен в N-й модели — добавь её всё равно из доступных в РФ.\n" + "10. Бренд-стратегия 'single' — ВСЕ models из одной марки.\n" + "11. price_min_rub/price_max_rub — диапазон по разным магазинам (если не уверен — один и тот же)." ) def call_ai(user_prompt: str, system_prompt: str | None = None, - temperature: float = 0.3, max_tokens: int = 4000) -> dict[str, Any]: + temperature: float = 0.3, max_tokens: int = 8000) -> dict[str, Any]: """Вызов GigaChat. Возвращает {json, text, tokens, model, error?}.""" cfg = get_config() try: diff --git a/miniapp/assets/podbor.config.js b/miniapp/assets/podbor.config.js index 5fc9228..e4d3b2d 100644 --- a/miniapp/assets/podbor.config.js +++ b/miniapp/assets/podbor.config.js @@ -111,6 +111,14 @@ const PODBOR_PICK_STRATEGIES = [ { key: "style", label: "Стилевая согласованность", hint: "единый дизайн-язык всей техники" }, ]; +/* Сколько моделей предлагать в каждой категории. + Меньше = быстрее, больше = больше выбора, но AI ответ дольше и нагрузка на парсеры. */ +const PODBOR_MODEL_COUNTS = [ + { key: "3", label: "3 модели", hint: "быстро · базовый выбор" }, + { key: "5", label: "5 моделей", hint: "оптимально · хороший баланс", recommended: true }, + { key: "7", label: "7 моделей", hint: "максимум · долго" }, +]; + /* Параметры по категориям. ---------------------------------------------------------- Новая схема (иерархический wizard): diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index a4c99f0..a714a63 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -1353,6 +1353,85 @@ color: var(--walnut); } +/* Технические характеристики */ +.report-specs { + margin-top: 10px; + background: #FFFCF6; + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px 12px; +} +.report-specs-head { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 6px; +} +.report-specs-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px 10px; +} +.spec-item { + display: flex; + flex-direction: column; + gap: 1px; + padding: 4px 0; +} +.spec-item.highlight { + grid-column: 1 / -1; + border-bottom: 1px dashed var(--accent-2); + padding-bottom: 6px; + margin-bottom: 4px; +} +.spec-label { + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); +} +.spec-value { + font-family: var(--font-sans); + font-size: 13px; + font-weight: 500; + color: var(--ink); +} +.spec-item.highlight .spec-value { + font-size: 15px; + font-weight: 600; + color: var(--accent-2); +} + +/* Утилитарные ссылки — инструкция, схема установки */ +.report-util-links { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.util-link { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink); + background: var(--warm); + border: 1px solid var(--line); + padding: 5px 10px; + border-radius: var(--r-pill); + text-decoration: none; + transition: background 0.12s; +} +.util-link:active { background: #EAD9B6; } +.util-link--manual { border-color: #6B4A2B; } +.util-link--dims { border-color: var(--accent-2); color: var(--accent-2); } + .report-cat-analysis { font-size: 13.5px; line-height: 1.5; @@ -1654,3 +1733,53 @@ line-height: 1.5; } .report-warnings > div { margin: 2px 0; } + +/* Экспорт отчёта */ +.report-export { + margin-top: 16px; + padding: 16px; + background: var(--warm); + border: 1px dashed var(--walnut); + border-radius: var(--r); +} +.report-export-head { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--walnut); + margin-bottom: 10px; +} +.report-export-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.btn-export { + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + padding: 10px 16px; + border-radius: 10px; + background: #fff; + border: 1px solid var(--walnut); + color: var(--walnut); + cursor: pointer; + transition: all 0.12s; + flex: 1; + min-width: 140px; +} +.btn-export:active { + background: var(--walnut); + color: var(--paper); + transform: scale(0.97); +} +.report-export-hint { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.06em; + color: var(--muted); + margin-top: 8px; + line-height: 1.4; +} diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index b085f4d..d985425 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -39,6 +39,7 @@ const Podbor = (function () { budget_preset: "", // 'luxe'|'premium'|'middle'|'budget'|'exact' price_ranges: {}, // только если budget_preset === 'exact' pick_strategies: [], // ['reviews','balance','tech',...] — multi + model_count: "5", // '3' | '5' | '7' — сколько моделей в каждой категории infra: { stove: "", vent: "" }, notes: "", }; @@ -1059,6 +1060,14 @@ const Podbor = (function () { } ); + // Количество моделей в категории + const mc = state.model_count || "5"; + const countGrid = renderPinCards( + PODBOR_MODEL_COUNTS, + o => (mc === o.key ? "on" : ""), + key => { update({ model_count: key }); render(); } + ); + const node = el(`

Стратегия
подбора

@@ -1066,6 +1075,15 @@ const Podbor = (function () {
`); node.appendChild(grid); + + const countHead = el(` +
+
Сколько моделей предложить
+
+ `); + countHead.appendChild(countGrid); + node.appendChild(countHead); + const cta = el(`
@@ -1356,9 +1374,99 @@ const Podbor = (function () { wrap.appendChild(wn); } + // Кнопки экспорта в конце + const exportNode = el(` +
+
Сохранить отчёт
+
+ + +
+
HTML удобно отправить клиенту в мессенджер · PDF — для печати или вложения
+
+ `); + exportNode.querySelector("#exportHtml").addEventListener("click", () => _exportReportHtml(ai, leadId)); + exportNode.querySelector("#exportPrint").addEventListener("click", () => _exportReportPrint(wrap, leadId)); + wrap.appendChild(exportNode); + return wrap; } + /* Экспорт: HTML файл с inline стилями и данными — скачивается */ + function _exportReportHtml(ai, leadId) { + // Создаём stand-alone HTML вокруг текущего DOM отчёта + const reportEl = document.querySelector(".report"); + if (!reportEl) return; + // Копируем все стили со страницы (link + style) + const stylesheets = []; + document.querySelectorAll("link[rel='stylesheet'], style").forEach(s => stylesheets.push(s.outerHTML)); + const html = ` + + + +Подбор техники · ${leadId.slice(0,8)} +${stylesheets.join("\n")} + + + +

Подбор техники · клиент: ${_esc(state.client_name || "—")}

+${reportEl.outerHTML} +
+ Отчёт сгенерирован ${new Date().toLocaleString("ru-RU")} · ZOV Tech Picker · Lead ID: ${leadId} +
+ +`; + const blob = new Blob([html], { type: "text/html;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `podbor-${leadId.slice(0,8)}-${(state.client_name || "client").replace(/\s+/g, "_")}.html`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + haptic && haptic("success"); + } + + /* Экспорт: открываем системный print диалог — пользователь сохраняет как PDF */ + function _exportReportPrint(reportNode, leadId) { + // Открываем новое окно с чистой версткой отчёта только + const reportEl = document.querySelector(".report"); + if (!reportEl) return; + const stylesheets = []; + document.querySelectorAll("link[rel='stylesheet'], style").forEach(s => stylesheets.push(s.outerHTML)); + const w = window.open("", "_blank"); + if (!w) { alert("Браузер заблокировал окно. Разрешите всплывающие окна."); return; } + w.document.write(` + + + +Подбор техники · ${leadId.slice(0,8)} · Печать +${stylesheets.join("\n")} + + + +

Подбор техники · ${_esc(state.client_name || "—")}

+${reportEl.outerHTML} + - - + +
@@ -21,10 +21,10 @@
- - - - - + + + + + diff --git a/miniapp/preview-report.html b/miniapp/preview-report.html index b0fdc21..ca0d4bf 100644 --- a/miniapp/preview-report.html +++ b/miniapp/preview-report.html @@ -63,6 +63,8 @@ const MOCK_AI = { pros: ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "большой объём 463 л против 380 л у Bosch в этой цене", "инвертор + класс A++ — экономия ~30% против A+ моделей", "10 лет гарантии на компрессор"], cons: ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить на замере", "нет зоны свежести BioFresh — в этом плане Liebherr заметно лучше"], reasoning: "Лучший по цена/качество в среднем сегменте. Тише и больше Bosch в той же цене, но без премиум-зоны свежести.", + specs: { dimensions_mm: "595×660×2000", volume_l: 463, weight_kg: 75, noise_db: 36, energy_class: "A++", color: "Нержавеющая сталь" }, + manual_search_query: "Haier C4F744CMG manual инструкция pdf", tier: "middle", enriched: { image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier", @@ -81,6 +83,8 @@ const MOCK_AI = { pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"], cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"], reasoning: "Премиум-выбор для тех, кому важна зона свежести и тишина. Переплата ~50% за бренд и BioFresh.", + specs: { dimensions_mm: "597×675×2010", volume_l: 372, weight_kg: 89, noise_db: 34, energy_class: "A+++", color: "Нержавеющая сталь SmartSteel" }, + manual_search_query: "Liebherr CNd 5223 manual инструкция pdf", tier: "premium", enriched: { image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr", @@ -97,6 +101,8 @@ const MOCK_AI = { pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"], cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"], reasoning: "Страховочный бюджет-вариант если клиент не хочет переплачивать. Российский, доступный, надёжный.", + specs: { dimensions_mm: "580×600×1450", volume_l: 240, weight_kg: 52, noise_db: 42, energy_class: "A+", color: "Белый" }, + manual_search_query: "Бирюса M124 инструкция pdf", tier: "budget", enriched: { image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa", @@ -301,6 +307,33 @@ function _renderModelCard(m) { return "магазинов"; } + // Specs block + const specs = m.specs || {}; + const specsItems = []; + if (specs.dimensions_mm) specsItems.push({ label: "Габариты ШхГxВ", value: specs.dimensions_mm + " мм", highlight: true }); + if (specs.volume_l) specsItems.push({ label: "Объём", value: specs.volume_l + " л" }); + if (specs.weight_kg) specsItems.push({ label: "Вес", value: specs.weight_kg + " кг" }); + if (specs.noise_db) specsItems.push({ label: "Шум", value: specs.noise_db + " дБ" }); + if (specs.energy_class) specsItems.push({ label: "Класс энергии", value: specs.energy_class }); + if (specs.color) specsItems.push({ label: "Цвет / материал", value: specs.color }); + const specsHtml = specsItems.length ? ` +
+
Характеристики
+
+ ${specsItems.map(i => `
${_esc(i.label)}
${_esc(i.value)}
`).join("")} +
+
` : ""; + + // Utility links + const utilButtons = []; + if (m.manual_search_query) { + utilButtons.push(`📄 Инструкция`); + } + if (specs.dimensions_mm) { + utilButtons.push(`📐 Схема установки`); + } + const utilHtml = utilButtons.length ? `` : ""; + return el(`
${img ? `` : ""}
@@ -325,8 +358,12 @@ function _renderModelCard(m) { ` : ""} + ${specsHtml} + ${m.reasoning ? `
💡 ${_esc(m.reasoning)}
` : ""} + ${utilHtml} + ${sourceLinks.length ? `