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"
"Каждая модель ДОЛЖНА содержать аналитику: pros (минимум 3), cons (минимум 2), почему выбрана, с чем сравнивать.\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"
' "summary": "2-3 предложения общего вывода: что подобрали, почему этот набор, на чём сэкономили / куда вложились",\n'
' "by_category": {\n'

View File

@ -10,7 +10,8 @@
margin-bottom: var(--s4);
}
.podbor-back {
.podbor-back,
.podbor-home {
width: 28px;
height: 28px;
display: grid;
@ -19,7 +20,11 @@
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 {
font-family: var(--font-mono);

View File

@ -97,24 +97,31 @@ const Podbor = (function () {
/* ===================== Header & progress ===================== */
function _goHome() {
location.hash = "";
if (typeof routeByHash === "function") routeByHash();
}
function renderHeader() {
const h = el(`
<header class="podbor-header">
<button class="podbor-back" aria-label="Назад">${ICONS.arrow_left}</button>
<div class="podbor-title">Подбор техники</div>
<div style="width:28px"></div>
<button class="podbor-home" aria-label="Главное меню" title="Главное меню">${ICONS.home || "🏠"}</button>
</header>
`);
h.querySelector(".podbor-back").addEventListener("click", () => {
const idx = STEPS.indexOf(currentStep);
if (idx <= 0) {
// Выход из подбора в главный экран кабинета — без перезагрузки (иначе сплэш мигает)
location.hash = "";
if (typeof routeByHash === "function") routeByHash();
_goHome();
} else {
go(STEPS[idx - 1]);
}
});
h.querySelector(".podbor-home").addEventListener("click", () => {
haptic && haptic("impact");
_goHome();
});
return h;
}
@ -1277,6 +1284,17 @@ const Podbor = (function () {
</div>
`;
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)
if (data.ai) {
const reportNode = renderReport(data.ai, data.id || "");
@ -1305,12 +1323,14 @@ const Podbor = (function () {
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>
`));
const headNode = el(`<div class="report-head"><div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div></div>`);
if (summary) {
const sumP = document.createElement("p");
sumP.className = "report-summary";
sumP.innerHTML = _ai(summary);
headNode.appendChild(sumP);
}
wrap.appendChild(headNode);
// Категории
for (const [catKey, catData] of Object.entries(byCat)) {
@ -1327,9 +1347,12 @@ const Podbor = (function () {
<span class="report-cat-icon">${(catIcon && ICONS[catIcon]) || ""}</span>
${_esc(catLabel)}
</h3>
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
${catAnalysis ? `<div class="report-cat-analysis"></div>` : ""}
</div>
`);
if (catAnalysis) {
catNode.querySelector(".report-cat-analysis").innerHTML = _ai(catAnalysis);
}
// Сравнение цен — основной блок, всегда вверху
const matrixNode = _renderPriceMatrix(models);
@ -1513,25 +1536,25 @@ ${reportEl.outerHTML}
<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.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_ai).join(" · ")}</div>` : ""}
${(m.pros || []).length ? `
<div class="report-pros-block">
<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>
` : ""}
${(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>
<ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_ai(c)}</li>`).join("")}</ul>
</div>
` : ""}
${_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)}
@ -1725,6 +1748,15 @@ ${reportEl.outerHTML}
.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 ===================== */
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">
<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/podbor.css?v=20260517j">
<link rel="stylesheet" href="assets/podbor.css?v=20260519a">
</head>
<body>
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
@ -39,7 +39,7 @@
<script src="assets/icons.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.js?v=20260517d"></script>
<script src="assets/podbor.js?v=20260519a"></script>
<script src="assets/clients.js?v=20260518e"></script>
<script src="assets/zamer-picts.js?v=20260516h"></script>
<script src="assets/measurements.js?v=20260518f"></script>