user feedback batch: model count, specs, manual link, dimensions, export

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
This commit is contained in:
wasrusgen 2026-05-11 17:11:30 +03:00
parent 7f417da7e0
commit ef500fa446
6 changed files with 359 additions and 11 deletions

View File

@ -93,7 +93,7 @@ SYSTEM_PROMPT_PICKER = (
" «AquaStop (защита от протечек)»\n" " «AquaStop (защита от протечек)»\n"
" «Инвертор (тише и экономия ~30% электричества)»\n\n" " «Инвертор (тише и экономия ~30% электричества)»\n\n"
"═══ ФОРМАТ ОТВЕТА ═══\n" "═══ ФОРМАТ ОТВЕТА ═══\n"
"Возвращай **35 моделей по КАЖДОЙ категории** (не одну!) — для клиента это выбор.\n" "Количество моделей по категории определяется параметром `checklist.model_count` (3 / 5 / 7) — соблюдай!\n"
"Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\n" "Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\n"
"По КАЖДОЙ категории напиши `analysis` — обзор: какие компромиссы, на что обратить внимание.\n" "По КАЖДОЙ категории напиши `analysis` — обзор: какие компромиссы, на что обратить внимание.\n"
"Валидный JSON без markdown, без ```:\n" "Валидный JSON без markdown, без ```:\n"
@ -109,10 +109,19 @@ SYSTEM_PROMPT_PICKER = (
' "price_min_rub": 79990,\n' ' "price_min_rub": 79990,\n'
' "price_max_rub": 92000,\n' ' "price_max_rub": 92000,\n'
' "search_query": "Haier C4F744CMG холодильник",\n' ' "search_query": "Haier C4F744CMG холодильник",\n'
' "manual_search_query": "Haier C4F744CMG manual инструкция pdf",\n'
' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],\n' ' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],\n'
' "pros": ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "класс A++, экономия ~30% против A+", "большой объём 463 л против 380 л у конкурентов в той же ценовой категории"],\n' ' "pros": ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "класс A++, экономия ~30% против A+", "большой объём 463 л против 380 л у конкурентов в той же ценовой категории"],\n'
' "cons": ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить нишу клиента", "нет зоны свежести BioFresh — в этом плане Liebherr ровно вдвое лучше"],\n' ' "cons": ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить нишу клиента", "нет зоны свежести BioFresh — в этом плане Liebherr ровно вдвое лучше"],\n'
' "reasoning": "Лучший выбор по цена/качество в этом бюджете. Тише и больше чем Bosch в той же цене, но без премиум-зоны свежести.",\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' ' "tier": "middle",\n'
' "match_score": 0.92\n' ' "match_score": 0.92\n'
" }\n" " }\n"
@ -132,13 +141,19 @@ SYSTEM_PROMPT_PICKER = (
"4. **Cons обязательны**: даже у лучших моделей есть недостатки. Если cons пусто — модель не выбрана. Конкретные минусы: габарит больше ниши, шумнее на 2 дБ, без какой-то функции, цена выше на N%, длительная гарантия только N лет.\n" "4. **Cons обязательны**: даже у лучших моделей есть недостатки. Если cons пусто — модель не выбрана. Конкретные минусы: габарит больше ниши, шумнее на 2 дБ, без какой-то функции, цена выше на N%, длительная гарантия только N лет.\n"
"5. **Reasoning**: 1 предложение «почему именно эта модель в этом наборе» — позиционирование относительно других в выдаче.\n" "5. **Reasoning**: 1 предложение «почему именно эта модель в этом наборе» — позиционирование относительно других в выдаче.\n"
"6. **search_query**: точная строка для поиска (бренд + индекс + слово категория). AI агент будет парсить маркетплейсы по этой строке — не указывай лишнего.\n" "6. **search_query**: точная строка для поиска (бренд + индекс + слово категория). AI агент будет парсить маркетплейсы по этой строке — не указывай лишнего.\n"
"7. Бренд-стратегия 'single'ВСЕ models из одной марки.\n" "7. **manual_search_query**: строка для Google-поиска инструкции, в формате «<brand> <model> manual инструкция pdf»\n"
"8. price_min_rub/price_max_rub — диапазон по разным магазинам (если не уверен — один и тот же)." "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, 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?}.""" """Вызов GigaChat. Возвращает {json, text, tokens, model, error?}."""
cfg = get_config() cfg = get_config()
try: try:

View File

@ -111,6 +111,14 @@ const PODBOR_PICK_STRATEGIES = [
{ key: "style", label: "Стилевая согласованность", hint: "единый дизайн-язык всей техники" }, { 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): Новая схема (иерархический wizard):

View File

@ -1353,6 +1353,85 @@
color: var(--walnut); 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 { .report-cat-analysis {
font-size: 13.5px; font-size: 13.5px;
line-height: 1.5; line-height: 1.5;
@ -1654,3 +1733,53 @@
line-height: 1.5; line-height: 1.5;
} }
.report-warnings > div { margin: 2px 0; } .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;
}

View File

@ -39,6 +39,7 @@ const Podbor = (function () {
budget_preset: "", // 'luxe'|'premium'|'middle'|'budget'|'exact' budget_preset: "", // 'luxe'|'premium'|'middle'|'budget'|'exact'
price_ranges: {}, // только если budget_preset === 'exact' price_ranges: {}, // только если budget_preset === 'exact'
pick_strategies: [], // ['reviews','balance','tech',...] — multi pick_strategies: [], // ['reviews','balance','tech',...] — multi
model_count: "5", // '3' | '5' | '7' — сколько моделей в каждой категории
infra: { stove: "", vent: "" }, infra: { stove: "", vent: "" },
notes: "", 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(` 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>
@ -1066,6 +1075,15 @@ const Podbor = (function () {
</section> </section>
`); `);
node.appendChild(grid); node.appendChild(grid);
const countHead = el(`
<div class="block">
<div class="block-head">Сколько моделей предложить</div>
</div>
`);
countHead.appendChild(countGrid);
node.appendChild(countHead);
const cta = el(` const cta = el(`
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-secondary" data-go="budget">Назад</button> <button class="btn-secondary" data-go="budget">Назад</button>
@ -1356,9 +1374,99 @@ const Podbor = (function () {
wrap.appendChild(wn); wrap.appendChild(wn);
} }
// Кнопки экспорта в конце
const exportNode = el(`
<div class="report-export">
<div class="report-export-head">Сохранить отчёт</div>
<div class="report-export-buttons">
<button class="btn-export" id="exportHtml"> Скачать HTML</button>
<button class="btn-export" id="exportPrint">🖨 Печать PDF</button>
</div>
<div class="report-export-hint">HTML удобно отправить клиенту в мессенджер · PDF для печати или вложения</div>
</div>
`);
exportNode.querySelector("#exportHtml").addEventListener("click", () => _exportReportHtml(ai, leadId));
exportNode.querySelector("#exportPrint").addEventListener("click", () => _exportReportPrint(wrap, leadId));
wrap.appendChild(exportNode);
return wrap; 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 = `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Подбор техники · ${leadId.slice(0,8)}</title>
${stylesheets.join("\n")}
<style>
body { background: #FBF7F0; padding: 20px; max-width: 900px; margin: 0 auto; }
.report-export { display: none; }
.podbor-header, .podbor-progress, .cat-strip, #submitResult > .success { display: none; }
</style>
</head>
<body>
<h1 style="font-family: 'Newsreader', serif; font-style: italic; color: #1F1A14;">Подбор техники · клиент: ${_esc(state.client_name || "—")}</h1>
${reportEl.outerHTML}
<footer style="margin-top: 40px; padding: 20px; border-top: 1px solid #ddd; font-size: 11px; color: #888; font-family: 'JetBrains Mono', monospace;">
Отчёт сгенерирован ${new Date().toLocaleString("ru-RU")} · ZOV Tech Picker · Lead ID: ${leadId}
</footer>
</body>
</html>`;
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(`<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Подбор техники · ${leadId.slice(0,8)} · Печать</title>
${stylesheets.join("\n")}
<style>
body { background: #fff; padding: 20px; max-width: 900px; margin: 0 auto; }
.report-export, .podbor-header, .podbor-progress, .cat-strip { display: none; }
@media print {
body { padding: 10px; }
.report-model { page-break-inside: avoid; }
.report-cat { page-break-after: auto; }
}
</style>
</head>
<body>
<h1 style="font-family: 'Newsreader', serif; font-style: italic;">Подбор техники · ${_esc(state.client_name || "—")}</h1>
${reportEl.outerHTML}
<script>setTimeout(() => window.print(), 500);<\/script>
</body>
</html>`);
w.document.close();
haptic && haptic("success");
}
function _renderModelCard(m) { function _renderModelCard(m) {
const enriched = m.enriched || {}; const enriched = m.enriched || {};
const pMin = enriched.price_min_rub || m.price_min_rub; const pMin = enriched.price_min_rub || m.price_min_rub;
@ -1414,8 +1522,12 @@ const Podbor = (function () {
</div> </div>
` : ""} ` : ""}
${_renderSpecsBlock(m.specs || {})}
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""} ${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""}
${_renderUtilityLinks(m)}
${sourceLinks.length ? ` ${sourceLinks.length ? `
<div class="report-links"> <div class="report-links">
<div class="report-links-head">${sourceLinks.length} ${_pluralStores(sourceLinks.length)} нашли товар:</div> <div class="report-links-head">${sourceLinks.length} ${_pluralStores(sourceLinks.length)} нашли товар:</div>
@ -1428,6 +1540,53 @@ const Podbor = (function () {
return card; return card;
} }
/* Спеки технические: габариты, объём, шум, класс энергии, цвет.
Габариты критично для проектирования кухни. */
function _renderSpecsBlock(specs) {
if (!specs || Object.keys(specs).length === 0) return "";
const items = [];
if (specs.dimensions_mm) items.push({ label: "Габариты ШхГxВ", value: `${specs.dimensions_mm} мм`, highlight: true });
if (specs.volume_l) items.push({ label: "Объём", value: `${specs.volume_l} л` });
if (specs.weight_kg) items.push({ label: "Вес", value: `${specs.weight_kg} кг` });
if (specs.noise_db) items.push({ label: "Шум", value: `${specs.noise_db} дБ` });
if (specs.energy_class) items.push({ label: "Класс энергии", value: specs.energy_class });
if (specs.color) items.push({ label: "Цвет / материал", value: specs.color });
if (!items.length) return "";
return `
<div class="report-specs">
<div class="report-specs-head">Характеристики</div>
<div class="report-specs-grid">
${items.map(i => `
<div class="spec-item${i.highlight ? " highlight" : ""}">
<div class="spec-label">${_esc(i.label)}</div>
<div class="spec-value">${_esc(i.value)}</div>
</div>
`).join("")}
</div>
</div>
`;
}
/* Кнопки "Инструкция" (Google поиск) + "Габариты" (если есть в specs). */
function _renderUtilityLinks(m) {
const buttons = [];
const manualQuery = m.manual_search_query
|| `${m.brand || ""} ${m.model || ""} manual инструкция pdf`.trim();
if (manualQuery && manualQuery.length > 5) {
const url = `https://www.google.com/search?q=${encodeURIComponent(manualQuery)}`;
buttons.push(`<a href="${url}" target="_blank" rel="noopener noreferrer" class="util-link util-link--manual">📄 Инструкция</a>`);
}
const dims = (m.specs || {}).dimensions_mm;
if (dims) {
const url = `https://www.google.com/search?q=${encodeURIComponent(`${m.brand} ${m.model} габариты схема установки`)}`;
buttons.push(`<a href="${url}" target="_blank" rel="noopener noreferrer" class="util-link util-link--dims">📐 Схема установки</a>`);
}
if (!buttons.length) return "";
return `<div class="report-util-links">${buttons.join("")}</div>`;
}
function _pluralStores(n) { function _pluralStores(n) {
const last = n % 10, lastTwo = n % 100; const last = n % 10, lastTwo = n % 100;
if (lastTwo >= 11 && lastTwo <= 14) return "магазинов"; if (lastTwo >= 11 && lastTwo <= 14) return "магазинов";

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=20260511j"> <link rel="stylesheet" href="assets/styles.css?v=20260511k">
<link rel="stylesheet" href="assets/podbor.css?v=20260511j"> <link rel="stylesheet" href="assets/podbor.css?v=20260511k">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
@ -21,10 +21,10 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
</main> </main>
<script src="assets/icons.js?v=20260511j"></script> <script src="assets/icons.js?v=20260511k"></script>
<script src="assets/podbor.config.js?v=20260511j"></script> <script src="assets/podbor.config.js?v=20260511k"></script>
<script src="assets/podbor.picts.js?v=20260511j"></script> <script src="assets/podbor.picts.js?v=20260511k"></script>
<script src="assets/podbor.js?v=20260511j"></script> <script src="assets/podbor.js?v=20260511k"></script>
<script src="assets/app.js?v=20260511j"></script> <script src="assets/app.js?v=20260511k"></script>
</body> </body>
</html> </html>

View File

@ -63,6 +63,8 @@ const MOCK_AI = {
pros: ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "большой объём 463 л против 380 л у Bosch в этой цене", "инвертор + класс A++ — экономия ~30% против A+ моделей", "10 лет гарантии на компрессор"], pros: ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "большой объём 463 л против 380 л у Bosch в этой цене", "инвертор + класс A++ — экономия ~30% против A+ моделей", "10 лет гарантии на компрессор"],
cons: ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить на замере", "нет зоны свежести BioFresh — в этом плане Liebherr заметно лучше"], cons: ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить на замере", "нет зоны свежести BioFresh — в этом плане Liebherr заметно лучше"],
reasoning: "Лучший по цена/качество в среднем сегменте. Тише и больше Bosch в той же цене, но без премиум-зоны свежести.", 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", tier: "middle",
enriched: { enriched: {
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier", image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier",
@ -81,6 +83,8 @@ const MOCK_AI = {
pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"], pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"],
cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"], cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"],
reasoning: "Премиум-выбор для тех, кому важна зона свежести и тишина. Переплата ~50% за бренд и BioFresh.", 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", tier: "premium",
enriched: { enriched: {
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr", image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr",
@ -97,6 +101,8 @@ const MOCK_AI = {
pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"], pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"],
cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"], cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"],
reasoning: "Страховочный бюджет-вариант если клиент не хочет переплачивать. Российский, доступный, надёжный.", 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", tier: "budget",
enriched: { enriched: {
image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa", image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa",
@ -301,6 +307,33 @@ function _renderModelCard(m) {
return "магазинов"; 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 ? `
<div class="report-specs">
<div class="report-specs-head">Характеристики</div>
<div class="report-specs-grid">
${specsItems.map(i => `<div class="spec-item${i.highlight ? " highlight" : ""}"><div class="spec-label">${_esc(i.label)}</div><div class="spec-value">${_esc(i.value)}</div></div>`).join("")}
</div>
</div>` : "";
// Utility links
const utilButtons = [];
if (m.manual_search_query) {
utilButtons.push(`<a href="https://www.google.com/search?q=${encodeURIComponent(m.manual_search_query)}" target="_blank" class="util-link util-link--manual">📄 Инструкция</a>`);
}
if (specs.dimensions_mm) {
utilButtons.push(`<a href="https://www.google.com/search?q=${encodeURIComponent((m.brand || '') + ' ' + (m.model || '') + ' габариты схема установки')}" target="_blank" class="util-link util-link--dims">📐 Схема установки</a>`);
}
const utilHtml = utilButtons.length ? `<div class="report-util-links">${utilButtons.join("")}</div>` : "";
return el(` return el(`
<article class="report-model"> <article class="report-model">
<div class="report-model-img${img ? "" : " placeholder"}">${img ? `<img src="${_esc(img)}" alt="" loading="lazy">` : ""}</div> <div class="report-model-img${img ? "" : " placeholder"}">${img ? `<img src="${_esc(img)}" alt="" loading="lazy">` : ""}</div>
@ -325,8 +358,12 @@ function _renderModelCard(m) {
</div> </div>
` : ""} ` : ""}
${specsHtml}
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""} ${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""}
${utilHtml}
${sourceLinks.length ? ` ${sourceLinks.length ? `
<div class="report-links"> <div class="report-links">
<div class="report-links-head">${sourceLinks.length} ${_plural(sourceLinks.length)} нашли товар:</div> <div class="report-links-head">${sourceLinks.length} ${_plural(sourceLinks.length)} нашли товар:</div>