miniapp: inline report after submit + standalone preview-report.html

REPORT RENDERER (podbor.js):
- New renderReport(ai, leadId) function — beautiful inline report after submit success
- Shows by_category with up to 5 models per category
- Model card: photo (88x88), brand · name, price range, rating + reviews + stores
- Highlights (with tech translations), pros (green), cons (orange)
- External links to WB / Я.Маркет / OZON / DNS (when enriched data present)
- Comparison table per category (accordion details)
- Total price block (dark theme contrast)
- Warnings block (when AI returns concerns)

CSS (podbor.css):
- .report-* classes: head, summary, cat, model, links, compare, total, warnings
- Editorial Calm palette — walnut accents, paper bg, Newsreader for titles
- Responsive: model card grid 88px image + 1fr body
- Placeholder gradient when no image (camera emoji)

STANDALONE PREVIEW (preview-report.html):
- Mock AI response with 3 fridges + 2 hobs
- Same render logic, runs without backend
- Visit: https://wasrusgen.github.io/zov-tech/preview-report.html

NEXT: integrate proxy6 token → real photos/prices instead of placeholders
This commit is contained in:
wasrusgen 2026-05-11 12:26:58 +03:00
parent 82425dbd88
commit c2be5e846f
4 changed files with 774 additions and 10 deletions

View File

@ -1119,3 +1119,292 @@
line-height: 1.3;
}
.rev-val .muted { color: var(--muted); font-weight: 400; }
/* ============================================================
Inline-отчёт после отправки подбора
============================================================ */
.report {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 18px;
}
.report-head .kicker {
margin-bottom: 4px;
}
.report-summary {
font-size: 14px;
line-height: 1.5;
color: var(--ink-2);
margin: 6px 0 0;
padding: 12px 14px;
background: var(--warm);
border-radius: 12px;
border-left: 3px solid var(--accent-2);
}
.report-cat {
display: flex;
flex-direction: column;
gap: 12px;
}
.report-cat-head {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 22px;
line-height: 1;
letter-spacing: -0.01em;
color: var(--ink);
margin: 0;
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--line);
}
.report-cat-icon {
width: 24px;
height: 24px;
display: grid;
place-items: center;
color: var(--accent-2);
}
.report-cat-icon svg { width: 22px; height: 22px; }
.report-models {
display: flex;
flex-direction: column;
gap: 12px;
}
.report-model {
display: grid;
grid-template-columns: 88px 1fr;
gap: 12px;
padding: 12px;
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
position: relative;
}
.report-model-img {
width: 88px;
height: 88px;
background: var(--warm);
border-radius: 10px;
overflow: hidden;
display: grid;
place-items: center;
}
.report-model-img img {
width: 100%;
height: 100%;
object-fit: contain;
background: #fff;
}
.report-model-img.placeholder {
background: repeating-linear-gradient(45deg, var(--warm), var(--warm) 5px, #F0E8D5 5px, #F0E8D5 10px);
}
.report-model-img.placeholder::after {
content: "📷";
font-size: 24px;
opacity: 0.4;
}
.report-model-body {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.report-model-brand {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.report-model-name {
font-family: var(--font-sans);
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--ink);
line-height: 1.2;
margin-bottom: 2px;
}
.report-model-meta {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.06em;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
margin: 2px 0;
}
.report-model-meta .rating { color: var(--accent-2); font-weight: 500; }
.report-model-price {
font-family: var(--font-display);
font-style: italic;
font-size: 17px;
color: var(--ink);
margin: 4px 0;
letter-spacing: -0.01em;
}
.report-model-price strong { font-style: normal; font-weight: 600; }
.report-model-price .muted { font-style: normal; font-size: 13px; color: var(--muted); }
.report-highlights {
font-size: 12px;
line-height: 1.4;
color: var(--ink-2);
margin-top: 2px;
}
.report-pros {
font-size: 12px;
line-height: 1.4;
color: #2A6B3F;
margin-top: 2px;
}
.report-cons {
font-size: 12px;
line-height: 1.4;
color: #8A3E2A;
margin-top: 2px;
}
.report-links {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.report-link {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent-2);
text-decoration: none;
border: 1px solid var(--accent-2);
padding: 4px 9px;
border-radius: var(--r-pill);
transition: background 0.12s;
}
.report-link:active { background: var(--warm); }
/* Сравнительная таблица — accordion */
.report-compare {
background: #fff;
border: 1px solid var(--line);
border-radius: 14px;
margin-top: 4px;
overflow: hidden;
}
.report-compare summary {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
padding: 12px 14px;
cursor: pointer;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.report-compare summary::-webkit-details-marker { display: none; }
.report-compare summary::after {
content: "↓";
font-size: 12px;
color: var(--muted);
transition: transform 0.2s;
}
.report-compare[open] summary::after { transform: rotate(180deg); }
.report-compare table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.report-compare th,
.report-compare td {
padding: 8px 10px;
text-align: left;
border-top: 1px solid var(--line);
vertical-align: top;
}
.report-compare th {
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
background: var(--warm);
}
.report-compare td strong { font-weight: 600; color: var(--ink); }
/* Итого */
.report-total {
padding: 14px 16px;
background: var(--ink);
color: var(--paper);
border-radius: 14px;
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.report-total .lbl {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
opacity: 0.7;
}
.report-total strong {
font-family: var(--font-display);
font-style: italic;
font-size: 20px;
font-weight: 500;
letter-spacing: -0.01em;
}
.report-total .status {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
margin-left: auto;
}
.report-warnings {
background: #F5E1DC;
border: 1px solid #C7705A;
border-radius: 12px;
padding: 10px 14px;
color: #8A3E2A;
font-size: 13px;
line-height: 1.5;
}
.report-warnings > div { margin: 2px 0; }

View File

@ -1143,16 +1143,25 @@ const Podbor = (function () {
if (data.error) {
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
} else {
result.innerHTML = `
// Успех + красивый inline-отчёт под кнопкой
const headSuccess = `
<div class="success">
<div class="success-icon">${ICONS.check}</div>
<div>
<div class="success-title">Подбор отправлен в чат бота</div>
<div class="success-sub">Лид #${(data.id || "").slice(0, 6)} · откройте Telegram</div>
<div class="success-title">Подбор готов</div>
<div class="success-sub">Лид #${(data.id || "").slice(0, 6)} · также отправлен в Telegram</div>
</div>
</div>
`;
result.innerHTML = headSuccess;
// Рендер отчёта (если AI вернул by_category)
if (data.ai) {
const reportNode = renderReport(data.ai, data.id || "");
result.appendChild(reportNode);
}
haptic && haptic("success");
// Скроллим к отчёту
setTimeout(() => result.scrollIntoView({ behavior: "smooth", block: "start" }), 100);
}
} catch (e) {
result.innerHTML = `<div class="error">Сеть: ${e.message}</div>`;
@ -1161,6 +1170,164 @@ const Podbor = (function () {
btn.textContent = "Отправить ещё раз";
}
/* ===================== Отчёт (inline, в шаге summary) ===================== */
function renderReport(ai, leadId) {
const summary = ai.summary || "";
const byCat = ai.by_category || {};
const total = ai.total_price_estimate_rub || {};
const budgetStatus = ai.budget_status || "";
const warnings = ai.warnings || [];
const wrap = el(`<section class="report"></section>`);
// Шапка
wrap.appendChild(el(`
<div class="report-head">
<div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div>
${summary ? `<p class="report-summary">${_esc(summary)}</p>` : ""}
</div>
`));
// Категории
for (const [catKey, catData] of Object.entries(byCat)) {
const catMeta = PODBOR_CATEGORIES.find(c => c.key === catKey);
const catLabel = catMeta?.label || catKey;
const catIcon = catMeta?.icon;
const models = (catData && catData.models) || [];
if (!models.length) continue;
const catNode = el(`
<div class="report-cat">
<h3 class="report-cat-head">
<span class="report-cat-icon">${(catIcon && ICONS[catIcon]) || ""}</span>
${_esc(catLabel)}
</h3>
<div class="report-models"></div>
</div>
`);
const modelsWrap = catNode.querySelector(".report-models");
for (const m of models) {
modelsWrap.appendChild(_renderModelCard(m));
}
// Сравнение
if (models.length >= 2) {
modelsWrap.appendChild(_renderCompareTable(models));
}
wrap.appendChild(catNode);
}
// Итого
if (total && (total.min || total.max)) {
const tmin = total.min, tmax = total.max;
const range = (tmin && tmax && tmin !== tmax)
? `${formatRub(tmin)}${formatRub(tmax)}`
: `${formatRub(tmin || tmax)}`;
wrap.appendChild(el(`
<div class="report-total">
<span class="lbl">ИТОГО</span>
<strong>${range}</strong>
${budgetStatus ? `<span class="status">${_esc(budgetStatus)}</span>` : ""}
</div>
`));
}
// Предупреждения
if (warnings.length) {
const wn = el(`<div class="report-warnings"></div>`);
warnings.forEach(w => wn.appendChild(el(`<div>⚠️ ${_esc(w)}</div>`)));
wrap.appendChild(wn);
}
return wrap;
}
function _renderModelCard(m) {
const enriched = m.enriched || {};
const pMin = m.price_min_rub || enriched.price_min_rub;
const pMax = m.price_max_rub || enriched.price_max_rub;
const img = enriched.image_url;
const rating = enriched.rating_max;
const reviews = enriched.reviews_total;
const stores = enriched.stores_count;
const priceHtml = (pMin && pMax && pMin !== pMax)
? `<strong>${formatRub(pMin)}</strong> — <strong>${formatRub(pMax)}</strong> ₽`
: pMin ? `<strong>${formatRub(pMin)}</strong> ₽`
: `<span class="muted">цена уточняется</span>`;
const metaParts = [];
if (rating) metaParts.push(`<span class="rating">★ ${Number(rating).toFixed(1)}</span>`);
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</span>`);
const links = [];
if (enriched.wb && enriched.wb.url) links.push({ label: "Wildberries", url: enriched.wb.url });
if (enriched.yamarket && enriched.yamarket.url) links.push({ label: "Я.Маркет", url: enriched.yamarket.url });
if (enriched.ozon && enriched.ozon.url) links.push({ label: "OZON", url: enriched.ozon.url });
if (enriched.dns && enriched.dns.url) links.push({ label: "DNS", url: enriched.dns.url });
const card = el(`
<article class="report-model">
<div class="report-model-img${img ? "" : " placeholder"}">${img ? `<img src="${_esc(img)}" alt="" loading="lazy">` : ""}</div>
<div class="report-model-body">
<div class="report-model-brand">${_esc(m.brand || "")}</div>
<div class="report-model-name">${_esc(m.model || "")}</div>
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
<div class="report-model-price">${priceHtml}</div>
${(m.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_esc).join(" · ")}</div>` : ""}
${(m.pros || []).length ? `<div class="report-pros">⊕ ${m.pros.slice(0, 3).map(_esc).join(" · ")}</div>` : ""}
${(m.cons || []).length ? `<div class="report-cons">⊖ ${m.cons.slice(0, 2).map(_esc).join(" · ")}</div>` : ""}
${links.length ? `
<div class="report-links">
${links.map(l => `<a href="${_esc(l.url)}" target="_blank" rel="noopener noreferrer" class="report-link">${l.label} →</a>`).join("")}
</div>
` : ""}
</div>
</article>
`);
return card;
}
function _renderCompareTable(models) {
const rows = models.map(m => {
const e = m.enriched || {};
const p = m.price_min_rub || e.price_min_rub;
return `
<tr>
<td><strong>${_esc(m.brand || "")}</strong> ${_esc(m.model || "")}</td>
<td>${p ? formatRub(p) + " ₽" : "—"}</td>
<td>${e.reviews_total || "—"}</td>
<td>${e.rating_max ? Number(e.rating_max).toFixed(1) : "—"}</td>
</tr>
`;
}).join("");
return el(`
<details class="report-compare">
<summary>Сравнить модели</summary>
<table>
<thead>
<tr><th>Модель</th><th>Цена от</th><th>Отзывов</th><th></th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
</details>
`);
}
function _esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/* ===================== Helpers ===================== */
function bindInputs(node) {

View File

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

308
miniapp/preview-report.html Normal file
View File

@ -0,0 +1,308 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#FBF7F0">
<title>Превью отчёта · ЗОВ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&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="assets/styles.css?v=20260511e">
<link rel="stylesheet" href="assets/podbor.css?v=20260511e">
<style>
body { padding: 24px 16px; background: var(--paper); }
.wrap { max-width: 720px; margin: 0 auto; }
.preview-kicker {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 6px;
}
.preview-title {
font-family: var(--font-display);
font-style: italic;
font-size: 32px;
line-height: 1.05;
letter-spacing: -0.02em;
margin: 0 0 8px;
color: var(--ink);
}
.preview-lede {
color: var(--muted);
font-size: 14px;
margin: 0 0 24px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="preview-kicker">Preview · Inline Report v1</div>
<h1 class="preview-title">Отчёт<br><em style="color: var(--accent-2)">после подбора</em></h1>
<p class="preview-lede">Как будет выглядеть страница для менеджера и клиента после нажатия «Отправить». Mock-данные.</p>
<div id="reportRoot"></div>
</div>
<script src="assets/icons.js?v=20260511e"></script>
<script src="assets/podbor.config.js?v=20260511e"></script>
<script>
/* Mock-данные — имитируем AI-ответ с DNS+WB+Я.Маркет обогащением */
const MOCK_AI = {
summary: "Подобран комплект для семьи 3 человек, бюджет средний. Холодильник Bosch с NoFrost + индукционная варочная Bosch и духовка с конвекцией. Все три прибора одной марки для дизайн-единства.",
by_category: {
fridge: {
models: [
{
brand: "Bosch", model: "Serie 4 KGN39NW00R",
price_min_rub: 79990, price_max_rub: 92000,
highlights: ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],
pros: ["тихий 38дБ", "класс A++", "стеклянные полки"],
cons: ["глубина 660мм — на 60мм больше стандартной ниши"],
tier: "middle",
enriched: {
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Bosch",
price_min_rub: 79990, price_max_rub: 92000,
rating_max: 4.7, reviews_total: 1242, stores_count: 12,
wb: { url: "https://www.wildberries.ru/catalog/123/detail.aspx" },
yamarket: { url: "https://market.yandex.ru/product--bosch-kgn39/123" },
ozon: { url: "https://www.ozon.ru/product/bosch-kgn39-123" }
}
},
{
brand: "Liebherr", model: "CNd 5223",
price_min_rub: 109900, price_max_rub: 124000,
highlights: ["BioFresh (зона свежести)", "NoFrost", "SoftClose"],
pros: ["премиум-качество сборки", "очень тихий 34дБ", "10 лет гарантии на компрессор"],
cons: ["цена выше среднего на +15%"],
tier: "premium",
enriched: {
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr",
price_min_rub: 109900, price_max_rub: 124000,
rating_max: 4.9, reviews_total: 873, stores_count: 8,
wb: { url: "#" }, yamarket: { url: "#" }
}
},
{
brand: "Indesit", model: "ITS 4180 W",
price_min_rub: 39990,
highlights: ["LowFrost (размораживать только мороз)", "класс A+"],
pros: ["доступная цена", "компактные габариты"],
cons: ["без инвертора", "шум 42дБ"],
tier: "budget",
enriched: {
image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Indesit",
price_min_rub: 39990, price_max_rub: 44900,
rating_max: 4.3, reviews_total: 2100, stores_count: 15,
dns: { url: "#" }, ozon: { url: "#" }
}
}
]
},
hob: {
models: [
{
brand: "Bosch", model: "PUE611BB5E",
price_min_rub: 59900, price_max_rub: 68000,
highlights: ["Индукция", "PowerBoost (форсаж — кипятит за минуту)", "TouchSelect (плавная регулировка)"],
pros: ["4 индукционные зоны", "безопасность для детей", "класс A"],
cons: ["требует индукционной посуды"],
tier: "middle",
enriched: {
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Bosch+PUE",
rating_max: 4.6, reviews_total: 845, stores_count: 10,
wb: { url: "#" }, yamarket: { url: "#" }
}
},
{
brand: "Siemens", model: "EH675FJC1E",
price_min_rub: 79900, price_max_rub: 89000,
highlights: ["Индукция", "FlexZone (объединение зон)", "Hob2Hood (управление вытяжкой)"],
pros: ["варочная управляет вытяжкой", "5 зон + Flex", "8-летняя гарантия"],
cons: ["цена премиум-сегмента"],
tier: "premium",
enriched: {
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Siemens",
rating_max: 4.8, reviews_total: 432, stores_count: 6,
yamarket: { url: "#" }, ozon: { url: "#" }
}
}
]
}
},
total_price_estimate_rub: { min: 280000, max: 420000 },
budget_status: "в_рамках",
warnings: [
"Холодильник Bosch KGN39 имеет глубину 660мм — уточнить нишу клиента (стандарт 600мм)"
]
};
/* ---- mini-копия renderReport из podbor.js, чтобы превью работало автономно ---- */
function el(html) {
const t = document.createElement("template");
t.innerHTML = html.trim();
return t.content.firstChild;
}
function _esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatRub(n) {
if (!n) return "—";
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function renderReport(ai, leadId) {
const summary = ai.summary || "";
const byCat = ai.by_category || {};
const total = ai.total_price_estimate_rub || {};
const budgetStatus = ai.budget_status || "";
const warnings = ai.warnings || [];
const wrap = el(`<section class="report"></section>`);
wrap.appendChild(el(`
<div class="report-head">
<div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div>
${summary ? `<p class="report-summary">${_esc(summary)}</p>` : ""}
</div>
`));
for (const [catKey, catData] of Object.entries(byCat)) {
const catMeta = (window.PODBOR_CATEGORIES || []).find(c => c.key === catKey);
const catLabel = catMeta?.label || catKey;
const catIcon = catMeta?.icon;
const models = (catData && catData.models) || [];
if (!models.length) continue;
const catNode = el(`
<div class="report-cat">
<h3 class="report-cat-head">
<span class="report-cat-icon">${(catIcon && window.ICONS && window.ICONS[catIcon]) || ""}</span>
${_esc(catLabel)}
</h3>
<div class="report-models"></div>
</div>
`);
const modelsWrap = catNode.querySelector(".report-models");
for (const m of models) {
modelsWrap.appendChild(_renderModelCard(m));
}
if (models.length >= 2) {
modelsWrap.appendChild(_renderCompareTable(models));
}
wrap.appendChild(catNode);
}
if (total && (total.min || total.max)) {
const tmin = total.min, tmax = total.max;
const range = (tmin && tmax && tmin !== tmax)
? `${formatRub(tmin)} — ${formatRub(tmax)} ₽`
: `${formatRub(tmin || tmax)} ₽`;
wrap.appendChild(el(`
<div class="report-total">
<span class="lbl">ИТОГО</span>
<strong>${range}</strong>
${budgetStatus ? `<span class="status">${_esc(budgetStatus)}</span>` : ""}
</div>
`));
}
if (warnings.length) {
const wn = el(`<div class="report-warnings"></div>`);
warnings.forEach(w => wn.appendChild(el(`<div>⚠️ ${_esc(w)}</div>`)));
wrap.appendChild(wn);
}
return wrap;
}
function _renderModelCard(m) {
const enriched = m.enriched || {};
const pMin = m.price_min_rub || enriched.price_min_rub;
const pMax = m.price_max_rub || enriched.price_max_rub;
const img = enriched.image_url;
const rating = enriched.rating_max;
const reviews = enriched.reviews_total;
const stores = enriched.stores_count;
const priceHtml = (pMin && pMax && pMin !== pMax)
? `<strong>${formatRub(pMin)}</strong><strong>${formatRub(pMax)}</strong> ₽`
: pMin ? `<strong>${formatRub(pMin)}</strong> ₽`
: `<span class="muted">цена уточняется</span>`;
const metaParts = [];
if (rating) metaParts.push(`<span class="rating">★ ${Number(rating).toFixed(1)}</span>`);
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</span>`);
const links = [];
if (enriched.wb && enriched.wb.url) links.push({ label: "Wildberries", url: enriched.wb.url });
if (enriched.yamarket && enriched.yamarket.url) links.push({ label: "Я.Маркет", url: enriched.yamarket.url });
if (enriched.ozon && enriched.ozon.url) links.push({ label: "OZON", url: enriched.ozon.url });
if (enriched.dns && enriched.dns.url) links.push({ label: "DNS", url: enriched.dns.url });
return el(`
<article class="report-model">
<div class="report-model-img${img ? "" : " placeholder"}">${img ? `<img src="${_esc(img)}" alt="" loading="lazy">` : ""}</div>
<div class="report-model-body">
<div class="report-model-brand">${_esc(m.brand || "")}</div>
<div class="report-model-name">${_esc(m.model || "")}</div>
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
<div class="report-model-price">${priceHtml}</div>
${(m.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_esc).join(" · ")}</div>` : ""}
${(m.pros || []).length ? `<div class="report-pros">⊕ ${m.pros.slice(0, 3).map(_esc).join(" · ")}</div>` : ""}
${(m.cons || []).length ? `<div class="report-cons">⊖ ${m.cons.slice(0, 2).map(_esc).join(" · ")}</div>` : ""}
${links.length ? `
<div class="report-links">
${links.map(l => `<a href="${_esc(l.url)}" target="_blank" rel="noopener noreferrer" class="report-link">${l.label} →</a>`).join("")}
</div>
` : ""}
</div>
</article>
`);
}
function _renderCompareTable(models) {
const rows = models.map(m => {
const e = m.enriched || {};
const p = m.price_min_rub || e.price_min_rub;
return `
<tr>
<td><strong>${_esc(m.brand || "")}</strong> ${_esc(m.model || "")}</td>
<td>${p ? formatRub(p) + " ₽" : "—"}</td>
<td>${e.reviews_total || "—"}</td>
<td>${e.rating_max ? Number(e.rating_max).toFixed(1) : "—"}</td>
</tr>
`;
}).join("");
return el(`
<details class="report-compare" open>
<summary>Сравнить модели</summary>
<table>
<thead>
<tr><th>Модель</th><th>Цена от</th><th>Отзывов</th><th></th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
</details>
`);
}
document.getElementById("reportRoot").appendChild(renderReport(MOCK_AI, "ABC12345"));
</script>
</body>
</html>