fix(podbor): HTML AI output + home button on all steps

- AI prompt: use HTML tags (b, em, br, li) instead of markdown
- renderReport: _ai() helper renders AI text as innerHTML (safe, backend-controlled)
- Header: added podbor-home button (top-right) → goes to main menu from any step
- After successful submit: show "← Вернуться в главное меню" button immediately
- Fixes: no way to leave podbor after result was received

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-19 08:02:09 +03:00
parent 3f1531f7ca
commit f1b7f71337
4 changed files with 62 additions and 20 deletions

View File

@ -128,7 +128,12 @@ SYSTEM_PROMPT_PICKER = (
"Количество моделей по категории определяется параметром `checklist.model_count` (3 / 5 / 7) — соблюдай!\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"
"Для текстовых полей (summary, analysis, reasoning, элементы pros[], cons[], highlights[], next_steps[]) используй HTML-разметку:\n"
" <b>число или ключевой термин</b> — выделение, <br> — перенос строки, <em> — курсив.\n"
" НЕ используй markdown (**текст**, *текст*, ## заголовки) — только HTML.\n"
" pros/cons/highlights — массивы строк с HTML внутри.\n\n"
"Структура ответа:\n"
"{\n" "{\n"
' "summary": "2-3 предложения общего вывода: что подобрали, почему этот набор, на чём сэкономили / куда вложились",\n' ' "summary": "2-3 предложения общего вывода: что подобрали, почему этот набор, на чём сэкономили / куда вложились",\n'
' "by_category": {\n' ' "by_category": {\n'

View File

@ -10,7 +10,8 @@
margin-bottom: var(--s4); margin-bottom: var(--s4);
} }
.podbor-back { .podbor-back,
.podbor-home {
width: 28px; width: 28px;
height: 28px; height: 28px;
display: grid; display: grid;
@ -19,7 +20,11 @@
cursor: pointer; cursor: pointer;
} }
.podbor-back svg { width: 20px; height: 20px; } .podbor-back svg,
.podbor-home svg { width: 20px; height: 20px; }
.podbor-home { color: var(--muted); opacity: 0.7; transition: opacity .15s; }
.podbor-home:hover { opacity: 1; }
.podbor-title { .podbor-title {
font-family: var(--font-mono); font-family: var(--font-mono);

View File

@ -97,24 +97,31 @@ const Podbor = (function () {
/* ===================== Header & progress ===================== */ /* ===================== Header & progress ===================== */
function _goHome() {
location.hash = "";
if (typeof routeByHash === "function") routeByHash();
}
function renderHeader() { function renderHeader() {
const h = el(` const h = el(`
<header class="podbor-header"> <header class="podbor-header">
<button class="podbor-back" aria-label="Назад">${ICONS.arrow_left}</button> <button class="podbor-back" aria-label="Назад">${ICONS.arrow_left}</button>
<div class="podbor-title">Подбор техники</div> <div class="podbor-title">Подбор техники</div>
<div style="width:28px"></div> <button class="podbor-home" aria-label="Главное меню" title="Главное меню">${ICONS.home || "🏠"}</button>
</header> </header>
`); `);
h.querySelector(".podbor-back").addEventListener("click", () => { h.querySelector(".podbor-back").addEventListener("click", () => {
const idx = STEPS.indexOf(currentStep); const idx = STEPS.indexOf(currentStep);
if (idx <= 0) { if (idx <= 0) {
// Выход из подбора в главный экран кабинета — без перезагрузки (иначе сплэш мигает) _goHome();
location.hash = "";
if (typeof routeByHash === "function") routeByHash();
} else { } else {
go(STEPS[idx - 1]); go(STEPS[idx - 1]);
} }
}); });
h.querySelector(".podbor-home").addEventListener("click", () => {
haptic && haptic("impact");
_goHome();
});
return h; return h;
} }
@ -1277,6 +1284,17 @@ const Podbor = (function () {
</div> </div>
`; `;
result.innerHTML = headSuccess; result.innerHTML = headSuccess;
// Кнопка "Вернуться в главное" сразу после успеха
const homeBtn = el(`
<div style="margin:12px 0;">
<button class="btn-secondary" style="width:100%;"> Вернуться в главное меню</button>
</div>
`);
homeBtn.querySelector("button").addEventListener("click", () => {
haptic && haptic("impact");
_goHome();
});
result.appendChild(homeBtn);
// Рендер отчёта (если AI вернул by_category) // Рендер отчёта (если AI вернул by_category)
if (data.ai) { if (data.ai) {
const reportNode = renderReport(data.ai, data.id || ""); const reportNode = renderReport(data.ai, data.id || "");
@ -1305,12 +1323,14 @@ const Podbor = (function () {
const wrap = el(`<section class="report"></section>`); const wrap = el(`<section class="report"></section>`);
// Шапка // Шапка
wrap.appendChild(el(` const headNode = el(`<div class="report-head"><div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div></div>`);
<div class="report-head"> if (summary) {
<div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div> const sumP = document.createElement("p");
${summary ? `<p class="report-summary">${_esc(summary)}</p>` : ""} sumP.className = "report-summary";
</div> sumP.innerHTML = _ai(summary);
`)); headNode.appendChild(sumP);
}
wrap.appendChild(headNode);
// Категории // Категории
for (const [catKey, catData] of Object.entries(byCat)) { for (const [catKey, catData] of Object.entries(byCat)) {
@ -1327,9 +1347,12 @@ 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>` : ""} ${catAnalysis ? `<div class="report-cat-analysis"></div>` : ""}
</div> </div>
`); `);
if (catAnalysis) {
catNode.querySelector(".report-cat-analysis").innerHTML = _ai(catAnalysis);
}
// Сравнение цен — основной блок, всегда вверху // Сравнение цен — основной блок, всегда вверху
const matrixNode = _renderPriceMatrix(models); const matrixNode = _renderPriceMatrix(models);
@ -1513,25 +1536,25 @@ ${reportEl.outerHTML}
<div class="report-model-name">${_esc(m.model || "")}</div> <div class="report-model-name">${_esc(m.model || "")}</div>
${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(_ai).join(" · ")}</div>` : ""}
${(m.pros || []).length ? ` ${(m.pros || []).length ? `
<div class="report-pros-block"> <div class="report-pros-block">
<div class="pc-head">Плюсы</div> <div class="pc-head">Плюсы</div>
<ul class="pc-list">${m.pros.slice(0, 4).map(p => `<li>${_esc(p)}</li>`).join("")}</ul> <ul class="pc-list">${m.pros.slice(0, 4).map(p => `<li>${_ai(p)}</li>`).join("")}</ul>
</div> </div>
` : ""} ` : ""}
${(m.cons || []).length ? ` ${(m.cons || []).length ? `
<div class="report-cons-block"> <div class="report-cons-block">
<div class="pc-head">Минусы</div> <div class="pc-head">Минусы</div>
<ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_esc(c)}</li>`).join("")}</ul> <ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_ai(c)}</li>`).join("")}</ul>
</div> </div>
` : ""} ` : ""}
${_renderSpecsBlock(m.specs || {})} ${_renderSpecsBlock(m.specs || {})}
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""} ${m.reasoning ? `<div class="report-reasoning">💡 ${_ai(m.reasoning)}</div>` : ""}
${_renderUtilityLinks(m)} ${_renderUtilityLinks(m)}
@ -1725,6 +1748,15 @@ ${reportEl.outerHTML}
.replace(/'/g, "&#39;"); .replace(/'/g, "&#39;");
} }
/* AI-generated text trusted backend output, render as HTML.
Strip script/on* to be safe, but allow <b><em><ul><li><br>. */
function _ai(s) {
if (s == null) return "";
return String(s)
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/\son\w+\s*=/gi, " data-stripped=");
}
/* ===================== Helpers ===================== */ /* ===================== Helpers ===================== */
function bindInputs(node) { function bindInputs(node) {

View File

@ -13,7 +13,7 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Manrope: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&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Manrope: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&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&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=20260518o"> <link rel="stylesheet" href="assets/styles.css?v=20260518o">
<link rel="stylesheet" href="assets/podbor.css?v=20260517j"> <link rel="stylesheet" href="assets/podbor.css?v=20260519a">
</head> </head>
<body> <body>
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск --> <!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
@ -39,7 +39,7 @@
<script src="assets/icons.js?v=20260516h"></script> <script src="assets/icons.js?v=20260516h"></script>
<script src="assets/podbor.config.js?v=20260516h"></script> <script src="assets/podbor.config.js?v=20260516h"></script>
<script src="assets/podbor.picts.js?v=20260516h"></script> <script src="assets/podbor.picts.js?v=20260516h"></script>
<script src="assets/podbor.js?v=20260517d"></script> <script src="assets/podbor.js?v=20260519a"></script>
<script src="assets/clients.js?v=20260518e"></script> <script src="assets/clients.js?v=20260518e"></script>
<script src="assets/zamer-picts.js?v=20260516h"></script> <script src="assets/zamer-picts.js?v=20260516h"></script>
<script src="assets/measurements.js?v=20260518f"></script> <script src="assets/measurements.js?v=20260518f"></script>