mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 16:44:48 +00:00
ai+report: deeper analysis — required pros/cons, category insights, source visibility
AI PROMPT (ai.py): - Requires minimum 3 pros + 2 cons per model with NUMBERS (36 dB, 463 L, A++, не 'тихий/большой') - New field 'reasoning' — 1-sentence why-this-model justification - New per-category 'analysis' — 2-3 sentences about trade-offs - Strict rules: no fake article numbers, account for parallel-import price markup - Russian market 2026 awareness: Haier/Korting up, Bosch/Siemens ⚠ TELEGRAM FORMAT (main.py): - Renders category analysis as italic prelude - Lists pros/cons as bullet lists (up to 4 pros, 3 cons) - Shows '🛒 Нашли в: OZON · Citilink · WB' line listing successful sources - Rating + reviews + stores count line: '📊 ★ 4.7 · 1242 отзыв. · 12 магаз.' - Direct link to best store: '🔗 Открыть в магазине' WB PARSER: - Generates 3 query variants per request: full → brand+model → model only - Increases hit rate when AI search_query is too verbose - First non-empty variant wins MINIAPP REPORT (podbor.js + podbor.css): - Category analysis block above models (italic, walnut left-border) - Pros block: green tinted bg, bullet list, header 'Плюсы' - Cons block: terracotta tinted bg, bullet list, header 'Минусы' - Reasoning chip: 💡 italic in warm background - Source badges with per-store price '<store> · 89 990 ₽' - Color-coded source links: OZON blue, Citilink yellow, WB pink, Я.Маркет red, DNS orange - 'X магазинов нашли товар' header + plural fix - '— не найден' fallback if 0 sources PREVIEW (preview-report.html): - Mock updated with Haier as flagship (more relevant for 2026 RF) - Shows analysis, reasoning, source spread (4 stores with different prices)
This commit is contained in:
parent
4b04f2de54
commit
ca342c0641
@ -94,21 +94,25 @@ SYSTEM_PROMPT_PICKER = (
|
|||||||
" «Инвертор (тише и экономия ~30% электричества)»\n\n"
|
" «Инвертор (тише и экономия ~30% электричества)»\n\n"
|
||||||
"═══ ФОРМАТ ОТВЕТА ═══\n"
|
"═══ ФОРМАТ ОТВЕТА ═══\n"
|
||||||
"Возвращай **3–5 моделей по КАЖДОЙ категории** (не одну!) — для клиента это выбор.\n"
|
"Возвращай **3–5 моделей по КАЖДОЙ категории** (не одну!) — для клиента это выбор.\n"
|
||||||
|
"Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\n"
|
||||||
|
"По КАЖДОЙ категории напиши `analysis` — обзор: какие компромиссы, на что обратить внимание.\n"
|
||||||
"Валидный JSON без markdown, без ```:\n"
|
"Валидный JSON без markdown, без ```:\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
' "summary": "1-2 предложения общего вывода",\n'
|
' "summary": "2-3 предложения общего вывода: что подобрали, почему этот набор, на чём сэкономили / куда вложились",\n'
|
||||||
' "by_category": {\n'
|
' "by_category": {\n'
|
||||||
' "fridge": {\n'
|
' "fridge": {\n'
|
||||||
|
' "analysis": "2-3 предложения: какие компромиссы в этой категории, какие модели для каких сценариев, на что смотреть при финальном выборе",\n'
|
||||||
' "models": [\n'
|
' "models": [\n'
|
||||||
' {\n'
|
' {\n'
|
||||||
' "brand": "Bosch",\n'
|
' "brand": "Haier",\n'
|
||||||
' "model": "Serie 4 KGN39NW00R",\n'
|
' "model": "C4F744CMG",\n'
|
||||||
' "price_min_rub": 79990,\n'
|
' "price_min_rub": 79990,\n'
|
||||||
' "price_max_rub": 92000,\n'
|
' "price_max_rub": 92000,\n'
|
||||||
' "search_query": "Bosch Serie 4 KGN39NW00R холодильник",\n'
|
' "search_query": "Haier C4F744CMG холодильник",\n'
|
||||||
' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и экономия ~30%)"],\n'
|
' "highlights": ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],\n'
|
||||||
' "pros": ["тихий 38дБ", "класс A++", "стеклянные полки"],\n'
|
' "pros": ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "класс A++, экономия ~30% против A+", "большой объём 463 л против 380 л у конкурентов в той же ценовой категории"],\n'
|
||||||
' "cons": ["глубина 660мм — на 60мм больше ниши"],\n'
|
' "cons": ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить нишу клиента", "нет зоны свежести BioFresh — в этом плане Liebherr ровно вдвое лучше"],\n'
|
||||||
|
' "reasoning": "Лучший выбор по цена/качество в этом бюджете. Тише и больше чем Bosch в той же цене, но без премиум-зоны свежести.",\n'
|
||||||
' "tier": "middle",\n'
|
' "tier": "middle",\n'
|
||||||
' "match_score": 0.92\n'
|
' "match_score": 0.92\n'
|
||||||
" }\n"
|
" }\n"
|
||||||
@ -119,13 +123,17 @@ SYSTEM_PROMPT_PICKER = (
|
|||||||
' "budget_status": "в_рамках|превышение|значительно_ниже",\n'
|
' "budget_status": "в_рамках|превышение|значительно_ниже",\n'
|
||||||
' "client_temperature": "premium|middle|budget|mixed",\n'
|
' "client_temperature": "premium|middle|budget|mixed",\n'
|
||||||
' "warnings": [],\n'
|
' "warnings": [],\n'
|
||||||
' "next_steps": []\n'
|
' "next_steps": ["рекомендации для менеджера: что уточнить с клиентом, что проверить на замере"]\n'
|
||||||
"}\n\n"
|
"}\n\n"
|
||||||
"ВАЖНО:\n"
|
"═══ КРИТИЧНО ═══\n"
|
||||||
"- Не выдумывай артикулы — указывай реальные линейки/индексы (Bosch Serie 4 KGN39NW00R, не «Bosch X-200»)\n"
|
"1. **Реальные модели**: артикулы должны существовать в природе (Haier C4F744CMG, Bosch Serie 4 KGN39NW00R, Liebherr CNd 5223 — НЕ «Bosch X-200» и НЕ «Haier выгодный»).\n"
|
||||||
"- `search_query` — точная строка для поиска модели на маркетплейсе (бренд + индекс + категория)\n"
|
"2. **РЕАЛИИ РФ 2026**: Bosch/Siemens/Miele идут параллельным импортом — их цена в РФ выше официальных на 15-30%. Учитывай это.\n"
|
||||||
"- Если клиент выбрал brand_strategy='single' с конкретной маркой — ВСЕ models в каждой категории должны быть из этой марки\n"
|
"3. **Pros с числами**: НЕ «тихий» — а «36 дБ». НЕ «энергоэффективный» — а «класс A++, ~30% экономии». НЕ «вместительный» — а «463 л».\n"
|
||||||
"- price_min_rub / price_max_rub — диапазон цен по разным магазинам (если не уверен — поставь один и тот же)"
|
"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 — диапазон по разным магазинам (если не уверен — один и тот же)."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -474,21 +474,55 @@ def _format_podbor_for_telegram(ai_result: dict[str, Any], client_name: str, lea
|
|||||||
for cat_key, cat_data in by_cat.items():
|
for cat_key, cat_data in by_cat.items():
|
||||||
cat_label = _CAT_LABELS.get(cat_key, cat_key.upper())
|
cat_label = _CAT_LABELS.get(cat_key, cat_key.upper())
|
||||||
lines.append(f"━━━ <b>{cat_label}</b> ━━━")
|
lines.append(f"━━━ <b>{cat_label}</b> ━━━")
|
||||||
|
# Анализ категории от AI
|
||||||
|
analysis = (cat_data or {}).get("analysis")
|
||||||
|
if analysis:
|
||||||
|
lines.append(f"<i>{analysis}</i>")
|
||||||
|
lines.append("")
|
||||||
models = (cat_data or {}).get("models") or []
|
models = (cat_data or {}).get("models") or []
|
||||||
for i, m in enumerate(models, 1):
|
for i, m in enumerate(models, 1):
|
||||||
lines.append(f"<b>{i}. {m.get('brand', '')} {m.get('model', '')}</b>")
|
lines.append(f"<b>{i}. {m.get('brand', '')} {m.get('model', '')}</b>")
|
||||||
pmin = m.get("price_min_rub")
|
# Цены и магазины из enrichment
|
||||||
pmax = m.get("price_max_rub")
|
enriched = m.get("enriched") or {}
|
||||||
|
pmin = enriched.get("price_min_rub") or m.get("price_min_rub")
|
||||||
|
pmax = enriched.get("price_max_rub") or m.get("price_max_rub")
|
||||||
if pmin and pmax and pmin != pmax:
|
if pmin and pmax and pmin != pmax:
|
||||||
lines.append(f"💰 {_format_price(pmin)} — {_format_price(pmax)} ₽")
|
lines.append(f"💰 {_format_price(pmin)} — {_format_price(pmax)} ₽")
|
||||||
elif pmin:
|
elif pmin:
|
||||||
lines.append(f"💰 {_format_price(pmin)} ₽")
|
lines.append(f"💰 {_format_price(pmin)} ₽")
|
||||||
|
# Отзывы и рейтинг (если есть)
|
||||||
|
rating = enriched.get("rating_max")
|
||||||
|
reviews = enriched.get("reviews_total")
|
||||||
|
meta_parts = []
|
||||||
|
if rating: meta_parts.append(f"★ {rating:.1f}")
|
||||||
|
if reviews: meta_parts.append(f"{reviews} отзыв.")
|
||||||
|
stores = enriched.get("stores_count")
|
||||||
|
if stores: meta_parts.append(f"{stores} магаз.")
|
||||||
|
if meta_parts:
|
||||||
|
lines.append("📊 " + " · ".join(meta_parts))
|
||||||
|
# Источники где нашли товар
|
||||||
|
sources_found = [
|
||||||
|
src.upper() for src in ("ozon", "citilink", "wb", "yamarket", "dns")
|
||||||
|
if enriched.get(src)
|
||||||
|
]
|
||||||
|
if sources_found:
|
||||||
|
lines.append(f"🛒 Нашли в: {' · '.join(sources_found)}")
|
||||||
if m.get("highlights"):
|
if m.get("highlights"):
|
||||||
lines.append("✓ " + ", ".join(m["highlights"]))
|
lines.append("✓ " + ", ".join(m["highlights"]))
|
||||||
if m.get("pros"):
|
if m.get("pros"):
|
||||||
lines.append("⊕ " + "; ".join(m["pros"][:3]))
|
lines.append("<b>⊕ Плюсы:</b>")
|
||||||
|
for p in m["pros"][:4]:
|
||||||
|
lines.append(f" • {p}")
|
||||||
if m.get("cons"):
|
if m.get("cons"):
|
||||||
lines.append("⊖ " + "; ".join(m["cons"][:2]))
|
lines.append("<b>⊖ Минусы:</b>")
|
||||||
|
for c in m["cons"][:3]:
|
||||||
|
lines.append(f" • {c}")
|
||||||
|
if m.get("reasoning"):
|
||||||
|
lines.append(f"<i>💡 {m['reasoning']}</i>")
|
||||||
|
# Ссылка на «лучший» магазин
|
||||||
|
best_url = enriched.get("best_url")
|
||||||
|
if best_url:
|
||||||
|
lines.append(f"🔗 <a href=\"{best_url}\">Открыть в магазине</a>")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -40,7 +40,44 @@ _HEADERS = {
|
|||||||
|
|
||||||
def search_wb(query: str, limit: int = 3, timeout: float = 12.0,
|
def search_wb(query: str, limit: int = 3, timeout: float = 12.0,
|
||||||
max_retries: int = 2) -> list[dict[str, Any]]:
|
max_retries: int = 2) -> list[dict[str, Any]]:
|
||||||
"""WB через прямой JSON API. Делает экспоненциальный backoff при 429."""
|
"""WB через прямой JSON API. Делает экспоненциальный backoff при 429.
|
||||||
|
|
||||||
|
Пробует несколько вариантов запроса (full → brand+model → brand only)
|
||||||
|
чтобы повысить вероятность найти товар."""
|
||||||
|
# Генерируем варианты запросов от точного к широкому
|
||||||
|
queries = _generate_query_variants(query)
|
||||||
|
for q in queries:
|
||||||
|
results = _search_wb_one(q, limit=limit, timeout=timeout, max_retries=max_retries)
|
||||||
|
if results:
|
||||||
|
return results
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_query_variants(query: str) -> list[str]:
|
||||||
|
"""Из 'Bosch Serie 4 KGN39NW00R холодильник' делаем варианты:
|
||||||
|
1. Bosch Serie 4 KGN39NW00R холодильник
|
||||||
|
2. Bosch KGN39NW00R
|
||||||
|
3. KGN39NW00R
|
||||||
|
4. Bosch holodilnik
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
variants = [query]
|
||||||
|
parts = query.split()
|
||||||
|
# Находим модель-индекс (с цифрами и буквами)
|
||||||
|
model_idx = None
|
||||||
|
for p in parts:
|
||||||
|
if re.search(r"\d", p) and re.search(r"[a-zA-Z]", p) and len(p) >= 4:
|
||||||
|
model_idx = p
|
||||||
|
break
|
||||||
|
brand = parts[0] if parts else ""
|
||||||
|
if brand and model_idx:
|
||||||
|
variants.append(f"{brand} {model_idx}")
|
||||||
|
variants.append(model_idx)
|
||||||
|
return list(dict.fromkeys(variants)) # дедуп с сохранением порядка
|
||||||
|
|
||||||
|
|
||||||
|
def _search_wb_one(query: str, limit: int, timeout: float, max_retries: int) -> list[dict[str, Any]]:
|
||||||
|
"""Один запрос к WB API."""
|
||||||
import time
|
import time
|
||||||
params = {**_DEFAULT_PARAMS, "query": query}
|
params = {**_DEFAULT_PARAMS, "query": query}
|
||||||
|
|
||||||
|
|||||||
@ -1288,6 +1288,89 @@
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Блоки плюсов/минусов с маркированными списками */
|
||||||
|
.report-pros-block, .report-cons-block {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.report-pros-block {
|
||||||
|
background: rgba(42, 107, 63, 0.08);
|
||||||
|
border-left: 2px solid #2A6B3F;
|
||||||
|
}
|
||||||
|
.report-cons-block {
|
||||||
|
background: rgba(138, 62, 42, 0.08);
|
||||||
|
border-left: 2px solid #8A3E2A;
|
||||||
|
}
|
||||||
|
.pc-head {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.report-pros-block .pc-head { color: #2A6B3F; }
|
||||||
|
.report-cons-block .pc-head { color: #8A3E2A; }
|
||||||
|
.pc-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.pc-list li { margin: 2px 0; }
|
||||||
|
|
||||||
|
.report-reasoning {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--warm);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--walnut);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-cat-analysis {
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #FFFCF6;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-left: 3px solid var(--accent-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-links-head {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.report-links-empty {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цветовая дифференциация бейджей магазинов */
|
||||||
|
.report-link--ozon { border-color: #0044CC; color: #0044CC; }
|
||||||
|
.report-link--citilink { border-color: #FFBA00; color: #B57E00; }
|
||||||
|
.report-link--wb { border-color: #CB11AB; color: #CB11AB; }
|
||||||
|
.report-link--yamarket { border-color: #FC3F1D; color: #FC3F1D; }
|
||||||
|
.report-link--dns { border-color: #FA9300; color: #C26C00; }
|
||||||
|
|
||||||
.report-links {
|
.report-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@ -1255,6 +1255,7 @@ const Podbor = (function () {
|
|||||||
const catLabel = catMeta?.label || catKey;
|
const catLabel = catMeta?.label || catKey;
|
||||||
const catIcon = catMeta?.icon;
|
const catIcon = catMeta?.icon;
|
||||||
const models = (catData && catData.models) || [];
|
const models = (catData && catData.models) || [];
|
||||||
|
const catAnalysis = (catData && catData.analysis) || "";
|
||||||
if (!models.length) continue;
|
if (!models.length) continue;
|
||||||
|
|
||||||
const catNode = el(`
|
const catNode = el(`
|
||||||
@ -1263,6 +1264,7 @@ const Podbor = (function () {
|
|||||||
<span class="report-cat-icon">${(catIcon && ICONS[catIcon]) || ""}</span>
|
<span class="report-cat-icon">${(catIcon && ICONS[catIcon]) || ""}</span>
|
||||||
${_esc(catLabel)}
|
${_esc(catLabel)}
|
||||||
</h3>
|
</h3>
|
||||||
|
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
|
||||||
<div class="report-models"></div>
|
<div class="report-models"></div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@ -1307,8 +1309,8 @@ const Podbor = (function () {
|
|||||||
|
|
||||||
function _renderModelCard(m) {
|
function _renderModelCard(m) {
|
||||||
const enriched = m.enriched || {};
|
const enriched = m.enriched || {};
|
||||||
const pMin = m.price_min_rub || enriched.price_min_rub;
|
const pMin = enriched.price_min_rub || m.price_min_rub;
|
||||||
const pMax = m.price_max_rub || enriched.price_max_rub;
|
const pMax = enriched.price_max_rub || m.price_max_rub;
|
||||||
const img = enriched.image_url;
|
const img = enriched.image_url;
|
||||||
const rating = enriched.rating_max;
|
const rating = enriched.rating_max;
|
||||||
const reviews = enriched.reviews_total;
|
const reviews = enriched.reviews_total;
|
||||||
@ -1324,11 +1326,17 @@ const Podbor = (function () {
|
|||||||
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
|
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
|
||||||
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</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 });
|
const sourcesData = [
|
||||||
if (enriched.yamarket && enriched.yamarket.url) links.push({ label: "Я.Маркет", url: enriched.yamarket.url });
|
{ key: "ozon", label: "OZON", item: enriched.ozon },
|
||||||
if (enriched.ozon && enriched.ozon.url) links.push({ label: "OZON", url: enriched.ozon.url });
|
{ key: "citilink", label: "Citilink", item: enriched.citilink },
|
||||||
if (enriched.dns && enriched.dns.url) links.push({ label: "DNS", url: enriched.dns.url });
|
{ key: "wb", label: "Wildberries", item: enriched.wb },
|
||||||
|
{ key: "yamarket", label: "Я.Маркет", item: enriched.yamarket },
|
||||||
|
{ key: "dns", label: "DNS", item: enriched.dns },
|
||||||
|
];
|
||||||
|
const sourceLinks = sourcesData
|
||||||
|
.filter(s => s.item && s.item.url)
|
||||||
|
.map(s => `<a href="${_esc(s.item.url)}" target="_blank" rel="noopener noreferrer" class="report-link report-link--${s.key}">${s.label}${s.item.price_min_rub ? ` · ${formatRub(s.item.price_min_rub)} ₽` : ""}</a>`);
|
||||||
|
|
||||||
const card = el(`
|
const card = el(`
|
||||||
<article class="report-model">
|
<article class="report-model">
|
||||||
@ -1339,19 +1347,43 @@ const Podbor = (function () {
|
|||||||
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
|
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
|
||||||
<div class="report-model-price">${priceHtml}</div>
|
<div class="report-model-price">${priceHtml}</div>
|
||||||
${(m.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_esc).join(" · ")}</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>` : ""}
|
${(m.pros || []).length ? `
|
||||||
${links.length ? `
|
<div class="report-pros-block">
|
||||||
<div class="report-links">
|
<div class="pc-head">Плюсы</div>
|
||||||
${links.map(l => `<a href="${_esc(l.url)}" target="_blank" rel="noopener noreferrer" class="report-link">${l.label} →</a>`).join("")}
|
<ul class="pc-list">${m.pros.slice(0, 4).map(p => `<li>${_esc(p)}</li>`).join("")}</ul>
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
|
|
||||||
|
${(m.cons || []).length ? `
|
||||||
|
<div class="report-cons-block">
|
||||||
|
<div class="pc-head">Минусы</div>
|
||||||
|
<ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_esc(c)}</li>`).join("")}</ul>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
|
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""}
|
||||||
|
|
||||||
|
${sourceLinks.length ? `
|
||||||
|
<div class="report-links">
|
||||||
|
<div class="report-links-head">${sourceLinks.length} ${_pluralStores(sourceLinks.length)} нашли товар:</div>
|
||||||
|
${sourceLinks.join("")}
|
||||||
|
</div>
|
||||||
|
` : `<div class="report-links-empty">— не найден в подключённых магазинах</div>`}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`);
|
`);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _pluralStores(n) {
|
||||||
|
const last = n % 10, lastTwo = n % 100;
|
||||||
|
if (lastTwo >= 11 && lastTwo <= 14) return "магазинов";
|
||||||
|
if (last === 1) return "магазин";
|
||||||
|
if (last >= 2 && last <= 4) return "магазина";
|
||||||
|
return "магазинов";
|
||||||
|
}
|
||||||
|
|
||||||
function _renderCompareTable(models) {
|
function _renderCompareTable(models) {
|
||||||
const rows = models.map(m => {
|
const rows = models.map(m => {
|
||||||
const e = m.enriched || {};
|
const e = m.enriched || {};
|
||||||
|
|||||||
@ -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=20260511g">
|
<link rel="stylesheet" href="assets/styles.css?v=20260511h">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511g">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260511h">
|
||||||
</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=20260511g"></script>
|
<script src="assets/icons.js?v=20260511h"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260511g"></script>
|
<script src="assets/podbor.config.js?v=20260511h"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260511g"></script>
|
<script src="assets/podbor.picts.js?v=20260511h"></script>
|
||||||
<script src="assets/podbor.js?v=20260511g"></script>
|
<script src="assets/podbor.js?v=20260511h"></script>
|
||||||
<script src="assets/app.js?v=20260511g"></script>
|
<script src="assets/app.js?v=20260511h"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -51,24 +51,27 @@
|
|||||||
<script>
|
<script>
|
||||||
/* Mock-данные — имитируем AI-ответ с DNS+WB+Я.Маркет обогащением */
|
/* Mock-данные — имитируем AI-ответ с DNS+WB+Я.Маркет обогащением */
|
||||||
const MOCK_AI = {
|
const MOCK_AI = {
|
||||||
summary: "Подобран комплект для семьи 3 человек, бюджет средний. Холодильник Bosch с NoFrost + индукционная варочная Bosch и духовка с конвекцией. Все три прибора одной марки для дизайн-единства.",
|
summary: "Подобран комплект Haier для семьи из 3 человек, бюджет средний. Холодильник с NoFrost + индукционная варочная Haier и компактная духовка с конвекцией. Все три прибора одной марки для дизайн-единства и тише в эксплуатации.",
|
||||||
by_category: {
|
by_category: {
|
||||||
fridge: {
|
fridge: {
|
||||||
|
analysis: "В этом бюджете 2026 года Haier и Korting вытеснили Bosch по соотношению цена/качество. Haier C4F744CMG — оптимальный двухкамерный по тишине и объёму, Liebherr — премиум-вариант с зоной свежести, Бирюса — бюджетная страховка.",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
brand: "Bosch", model: "Serie 4 KGN39NW00R",
|
brand: "Haier", model: "C4F744CMG",
|
||||||
price_min_rub: 79990, price_max_rub: 92000,
|
price_min_rub: 89990, price_max_rub: 105000,
|
||||||
highlights: ["NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)"],
|
highlights: ["Total NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)", "Класс A++ (~30% экономии)"],
|
||||||
pros: ["тихий 38дБ", "класс A++", "стеклянные полки"],
|
pros: ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "большой объём 463 л против 380 л у Bosch в этой цене", "инвертор + класс A++ — экономия ~30% против A+ моделей", "10 лет гарантии на компрессор"],
|
||||||
cons: ["глубина 660мм — на 60мм больше стандартной ниши"],
|
cons: ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить на замере", "нет зоны свежести BioFresh — в этом плане Liebherr заметно лучше"],
|
||||||
|
reasoning: "Лучший по цена/качество в среднем сегменте. Тише и больше Bosch в той же цене, но без премиум-зоны свежести.",
|
||||||
tier: "middle",
|
tier: "middle",
|
||||||
enriched: {
|
enriched: {
|
||||||
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Bosch",
|
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier",
|
||||||
price_min_rub: 79990, price_max_rub: 92000,
|
price_min_rub: 89990, price_max_rub: 105000,
|
||||||
rating_max: 4.7, reviews_total: 1242, stores_count: 12,
|
rating_max: 4.7, reviews_total: 1242, stores_count: 12,
|
||||||
wb: { url: "https://www.wildberries.ru/catalog/123/detail.aspx" },
|
citilink: { url: "https://www.citilink.ru/product/haier-123/", price_min_rub: 92000 },
|
||||||
yamarket: { url: "https://market.yandex.ru/product--bosch-kgn39/123" },
|
ozon: { url: "https://www.ozon.ru/product/haier-c4f744cmg/", price_min_rub: 89990 },
|
||||||
ozon: { url: "https://www.ozon.ru/product/bosch-kgn39-123" }
|
wb: { url: "https://www.wildberries.ru/catalog/123/detail.aspx", price_min_rub: 94500 },
|
||||||
|
yamarket: { url: "https://market.yandex.ru/product--haier-c4f744cmg/123", price_min_rub: 105000 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -184,12 +187,14 @@ function renderReport(ai, leadId) {
|
|||||||
const models = (catData && catData.models) || [];
|
const models = (catData && catData.models) || [];
|
||||||
if (!models.length) continue;
|
if (!models.length) continue;
|
||||||
|
|
||||||
|
const catAnalysis = (catData && catData.analysis) || "";
|
||||||
const catNode = el(`
|
const catNode = el(`
|
||||||
<div class="report-cat">
|
<div class="report-cat">
|
||||||
<h3 class="report-cat-head">
|
<h3 class="report-cat-head">
|
||||||
<span class="report-cat-icon">${(catIcon && window.ICONS && window.ICONS[catIcon]) || ""}</span>
|
<span class="report-cat-icon">${(catIcon && window.ICONS && window.ICONS[catIcon]) || ""}</span>
|
||||||
${_esc(catLabel)}
|
${_esc(catLabel)}
|
||||||
</h3>
|
</h3>
|
||||||
|
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
|
||||||
<div class="report-models"></div>
|
<div class="report-models"></div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@ -248,11 +253,24 @@ function _renderModelCard(m) {
|
|||||||
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
|
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
|
||||||
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</span>`);
|
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</span>`);
|
||||||
|
|
||||||
const links = [];
|
const sourcesData = [
|
||||||
if (enriched.wb && enriched.wb.url) links.push({ label: "Wildberries", url: enriched.wb.url });
|
{ key: "ozon", label: "OZON", item: enriched.ozon },
|
||||||
if (enriched.yamarket && enriched.yamarket.url) links.push({ label: "Я.Маркет", url: enriched.yamarket.url });
|
{ key: "citilink", label: "Citilink", item: enriched.citilink },
|
||||||
if (enriched.ozon && enriched.ozon.url) links.push({ label: "OZON", url: enriched.ozon.url });
|
{ key: "wb", label: "Wildberries", item: enriched.wb },
|
||||||
if (enriched.dns && enriched.dns.url) links.push({ label: "DNS", url: enriched.dns.url });
|
{ key: "yamarket", label: "Я.Маркет", item: enriched.yamarket },
|
||||||
|
{ key: "dns", label: "DNS", item: enriched.dns },
|
||||||
|
];
|
||||||
|
const sourceLinks = sourcesData
|
||||||
|
.filter(s => s.item && s.item.url)
|
||||||
|
.map(s => `<a href="${_esc(s.item.url)}" target="_blank" rel="noopener noreferrer" class="report-link report-link--${s.key}">${s.label}${s.item.price_min_rub ? ` · ${formatRub(s.item.price_min_rub)} ₽` : ""}</a>`);
|
||||||
|
|
||||||
|
function _plural(n) {
|
||||||
|
const last = n % 10, lastTwo = n % 100;
|
||||||
|
if (lastTwo >= 11 && lastTwo <= 14) return "магазинов";
|
||||||
|
if (last === 1) return "магазин";
|
||||||
|
if (last >= 2 && last <= 4) return "магазина";
|
||||||
|
return "магазинов";
|
||||||
|
}
|
||||||
|
|
||||||
return el(`
|
return el(`
|
||||||
<article class="report-model">
|
<article class="report-model">
|
||||||
@ -263,13 +281,29 @@ function _renderModelCard(m) {
|
|||||||
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
|
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
|
||||||
<div class="report-model-price">${priceHtml}</div>
|
<div class="report-model-price">${priceHtml}</div>
|
||||||
${(m.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_esc).join(" · ")}</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>` : ""}
|
${(m.pros || []).length ? `
|
||||||
${links.length ? `
|
<div class="report-pros-block">
|
||||||
<div class="report-links">
|
<div class="pc-head">Плюсы</div>
|
||||||
${links.map(l => `<a href="${_esc(l.url)}" target="_blank" rel="noopener noreferrer" class="report-link">${l.label} →</a>`).join("")}
|
<ul class="pc-list">${m.pros.slice(0, 4).map(p => `<li>${_esc(p)}</li>`).join("")}</ul>
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
|
|
||||||
|
${(m.cons || []).length ? `
|
||||||
|
<div class="report-cons-block">
|
||||||
|
<div class="pc-head">Минусы</div>
|
||||||
|
<ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_esc(c)}</li>`).join("")}</ul>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
|
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""}
|
||||||
|
|
||||||
|
${sourceLinks.length ? `
|
||||||
|
<div class="report-links">
|
||||||
|
<div class="report-links-head">${sourceLinks.length} ${_plural(sourceLinks.length)} нашли товар:</div>
|
||||||
|
${sourceLinks.join("")}
|
||||||
|
</div>
|
||||||
|
` : `<div class="report-links-empty">— не найден в подключённых магазинах</div>`}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`);
|
`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user