1/** 2 * ЧаÑ-конÑÑлÑÑÐ°Ð½Ñ komplektadr.ru â клиенÑ. 3 * ÐÑÑÑ Ð½Ð° ÑеÑвеÑе: /local/js/kadr_chat_widget.js (подклÑÑаеÑÑÑ Ð¸Ð· chat_widget_inject.php). 4 * 5 * ÐдÑÑ window.KADR_CW = { sessid, cartCount, version } из инÑекÑии. 6 * СеÑвеÑнÑе ÑоÑки: /local/ajax/chat_agent.php (мозг; stream=1 â NDJSON-ÑÑаÑÑÑÑ + Ñинал), 7 * /local/ajax/transcribe.php (голоÑ), /local/ajax/add2basket_by_id.php (кнопка «в коÑзинÑ»). 8 * ÐÑÑоÑÐ¸Ñ Ð´Ð¸Ð°Ð»Ð¾Ð³Ð° живÑÑ Ð² sessionStorage и ÑлÑÑÑÑ Ñ ÐºÐ°Ð¶Ð´Ñм запÑоÑом (ÑеÑÐ²ÐµÑ stateless). 9 * 10 * v20: живÑе ÑÑаÑÑÑÑ ÑÑапов, оÑенки ð/ð (kadr_widget_feedback), бейдж «ÐÑовеÑено: â¦Â», 11 * FAB-пилÑÐ»Ñ Ñ ÑекÑÑом + мобилÑнÑй ÑизеÑ, конÑекÑÑнÑе ÑипÑ, волна запиÑи голоÑа, 12 * ÑазвоÑÐ¾Ñ Ð¿Ð°Ð½ÐµÐ»Ð¸, кнопка «вниз» + ÑмнÑй авÑоÑкÑолл, ÑвоÑаÑивание длиннÑÑ Ð¾ÑвеÑов, 13 * каÑÑÑÐµÐ»Ñ ÐºÐ°ÑÑоÑек, ÑкÑим на мобилÑном, вибÑаÑиÑ. 14 * v25: воÑонка в Я.ÐеÑÑÐ¸ÐºÑ (Ñели kcw_*): impression, invite_show, open(source), 15 * close(guardrail), first_message(input/chip), product_click, add_to_cart. 16 */ 17(function () { 18 'use strict'; 19 20 var CFG = window.KADR_CW || {}; 21 if (!CFG.sessid || document.getElementById('kcw-root')) return; 22 23 var API_CHAT = '/local/ajax/chat_agent.php'; 24 var API_VOICE = '/local/ajax/transcribe.php'; 25 var API_CART = '/local/ajax/add2basket_by_id.php'; 26 var STORE_KEY = 'kcw_state_v1'; 27 var GREETING = 'ÐдÑавÑÑвÑйÑе! Я ÐÐ-помоÑник KomplektADR.ru. ÐÐ¾Ð¼Ð¾Ð³Ñ Ð¿Ð¾Ð´Ð¾Ð±ÑаÑÑ Ð¾ÑнаÑение Ð´Ð»Ñ Ð¿ÐµÑевозки опаÑнÑÑ Ð³ÑÑзов, подÑÐºÐ°Ð¶Ñ ÑÐµÐ½Ñ Ð¸ налиÑие, ÑобеÑÑ ÐºÐ¾ÑÐ·Ð¸Ð½Ñ Ð¿Ð¾ ваÑÐµÐ¼Ñ ÑпиÑкÑ. Ðожно пиÑаÑÑ ÑекÑÑом или надикÑоваÑÑ Ð³Ð¾Ð»Ð¾Ñом.'; 28 var FAB_LABEL = 'СпÑоÑиÑÑ ÐÐ'; 29 var TEASER_TEXT = 'ÐдÑавÑÑвÑйÑе! ÐодбеÑÑ ÐºÐ¾Ð¼Ð¿Ð»ÐµÐºÑ ADR и оÑвеÑÑ Ð½Ð° вопÑоÑÑ Ð¿Ð¾ пеÑевозке опаÑнÑÑ Ð³ÑÑзов.'; 30 // ÑипÑ: ÑÑабилÑнÑй id Ð´Ð»Ñ Ð°Ð½Ð°Ð»Ð¸Ñики (chip_id) + ÑекÑÑ; ÑекÑÑ Ð¼Ð¾Ð¶Ð½Ð¾ менÑÑÑ, id â Ð½ÐµÑ 31 var CHIPS_BASE = [ 32 { id: 'base_unknown_cargo', t: 'Ðе знаÑ, ÑÑо Ð¿Ð¾Ð²ÐµÐ·Ñ â ÑÑо нÑжно маÑине?' }, 33 { id: 'base_price_class3', t: 'СколÑко ÑÑÐ¾Ð¸Ñ ÐºÐ¾Ð¼Ð¿Ð»ÐµÐºÑ ADR 3 клаÑÑа?' }, 34 { id: 'base_gazel_plate', t: 'ÐÐ°ÐºÐ°Ñ ÑаблиÑка нÑжна на ÐазелÑ?' }, 35 { id: 'base_invoice_inn', t: 'Ðак бÑÑÑÑо полÑÑиÑÑ ÑÑÑÑ Ð¿Ð¾ ÐÐÐ?' } 36 ]; 37 // Ñип под ÑекÑÑÑÑ ÑÑÑаниÑÑ â пеÑÑонализаÑÐ¸Ñ ÑÑаÑÑовÑÑ Ð¿Ð¾Ð´Ñказок 38 function contextChip() { 39 var p = location.pathname; 40 // ÑÑаÑÑи клаÑÑов ADR в блоге (adr-9-klass-txt и Ñ.п.) â главнÑй Ð²Ñ Ð¾Ð´ Ñ Ð¿Ð¾Ð¸Ñка 41 var mCls = p.match(/^\/blog\/.*adr-(\d+)-klass/); 42 if (mCls) return { id: 'article_adr_class_' + mCls[1], t: 'ЧÑо нÑжно Ð´Ð»Ñ Ð¿ÐµÑевозки гÑÑза клаÑÑа ' + mCls[1] + '?' }; 43 if (p.indexOf('/catalog/komplekty-adr') === 0) return { id: 'ctx_kit_contents', t: 'ЧÑо Ð²Ñ Ð¾Ð´Ð¸Ñ Ð² ÐºÐ¾Ð¼Ð¿Ð»ÐµÐºÑ ADR?' }; 44 if (/tablichk|znak|markirovka|nakleyk/i.test(p)) return { id: 'ctx_signs_pick', t: 'ÐодобÑаÑÑ Ð·Ð½Ð°ÐºÐ¸ опаÑноÑÑи на маÑинÑ' }; 45 if (/ognetushitel/i.test(p)) return { id: 'ctx_extinguishers', t: 'Ðакие огнеÑÑÑиÑели нÑÐ¶Ð½Ñ Ð¿Ð¾ ÐÐÐÐÐ?' }; 46 if (p.indexOf('osnashchenie-ts-po-trebovaniyam-tr-ts') !== -1) return { id: 'ctx_trts', t: 'Чем ТРТС 018/2011 оÑлиÑаеÑÑÑ Ð¾Ñ ÐÐÐÐÐ?' }; 47 if (p === '/un' || p.indexOf('/un/') === 0) return { id: 'ctx_un_more', t: 'ЧÑо еÑÑ Ð¿Ð¾Ð½Ð°Ð´Ð¾Ð±Ð¸ÑÑÑ Ð´Ð»Ñ Ð¼Ð¾ÐµÐ³Ð¾ гÑÑза?' }; 48 return null; 49 } 50 51 var reducedMotion = false;
52 try { reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) {} 53 54 // ---------- аналиÑика воÑонки (Я.ÐеÑÑика 21641209, Ñели kcw_*) ---------- 55 // СÑÑÑÑик opt-out (invis-counter.php): гÑÑзиÑÑÑ Ñ Ð²ÑÐµÑ , кÑоме Ñвно оÑказавÑÐ¸Ñ ÑÑ Ð² 56 // кÑки-баннеÑе; пÑовеÑка ym() покÑÑÐ²Ð°ÐµÑ Ð¾Ñказ и блокиÑовÑики ÑÑекеÑов. 57 var YM_ID = 21641209; 58 var AB_VARIANT = 'baseline'; // ÑÑап 2 (A/B ÑекÑÑа пилÑли): 'control' | 'test' 59 var IS_MOBILE = false; 60 try { IS_MOBILE = window.matchMedia('(max-width: 899px)').matches; } catch (e) {} 61 function pageType() { 62 var p = location.pathname; 63 if (p.indexOf('/blog/') === 0) return 'article'; 64 if (p.indexOf('/catalog/') === 0) return 'catalog'; 65 if (p === '/un' || p.indexOf('/un/') === 0) return 'un'; 66 return 'other'; 67 } 68 function track(goal, params) { 69 try { 70 if (typeof ym !== 'function') return; 71 var p = { variant: AB_VARIANT, page_type: pageType(), device: IS_MOBILE ? 'mobile' : 'desktop', path: location.pathname.slice(0, 120) }; 72 if (params) { 73 for (var k in params) { 74 if (Object.prototype.hasOwnProperty.call(params, k) && params[k] != null) p[k] = params[k]; 75 } 76 } 77 ym(YM_ID, 'reachGoal', 'kcw_' + goal, p); 78 } catch (e) {} 79 } 80 // оÑкÑда пÑиÑÑл ÑекÑÑ Ð² поле ввода â Ð´Ð»Ñ Ð¿Ð°ÑамеÑÑа input Ñ first_message 81 var inputSource = 'typed'; 82 var inputChipId = null; 83 84 // лÑÐ³ÐºÐ°Ñ Ð²Ð¸Ð±ÑаÑÐ¸Ñ Ð½Ð° ÑелеÑоне (Android; iOS молÑа игноÑиÑÑеÑ) 85 function buzz(ms) { 86 try { if (navigator.vibrate) navigator.vibrate(ms); } catch (e) {} 87 } 88 89 // ---------- ÑоÑÑоÑние ---------- 90 var state = { transcript: [], history: [], lastProducts: [], cartCount: 0, open: false }; 91 try { 92 var saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); 93 if (saved && saved.transcript) state = saved; 94 } catch (e) {} 95 state.open = false; 96 // ÑеалÑное колиÑеÑÑво в коÑзине Ñ ÑеÑвеÑа â иÑÑоÑник пÑÐ°Ð²Ð´Ñ Ð¿Ñи каждой загÑÑзке ÑÑÑаниÑÑ 97 if (typeof CFG.cartCount === 'number') state.cartCount = CFG.cartCount; 98 99 function persist() { 100 try { 101 sessionStorage.setItem(STORE_KEY, JSON.stringify({ 102 transcript: state.transcript.slice(-40), 103 history: state.history.slice(-10), 104 lastProducts: state.lastProducts, 105 cartCount: state.cartCount 106 })); 107 } catch (e) {} 108 } 109 110 // ---------- ÑÑилиÑÑ ---------- 111 function el(tag, cls, parent) { 112 var node = document.createElement(tag); 113 if (cls) node.className = cls; 114 if (parent) parent.appendChild(node); 115 return node; 116 } 117 function esc(s) { 118 return String(s == null ? '' : s) 119 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 120 .replace(/"/g, '"').replace(/'/g, '''); 121 } 122 // ÑÑÑлки кликабелÑÐ½Ñ ÑолÑко на Ñвой домен 123 function linkify(s) { 124 return esc(s).replace(/https:\/\/komplektadr\.ru[^\s<>"')<]*/g, function (u) { 125 var trail = ''; 126 while (/[.,;:!?]$/.test(u)) { trail = u.slice(-1) + trail; u = u.slice(0, -1); } 127 return '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' + u + '</a>' + trail; 128 }); 129 } 130 131 // мини-markdown Ð´Ð»Ñ Ð¾ÑвеÑов боÑа: [ÑекÑÑ](url Ñвой домен), **жиÑнÑй**, ÑпиÑки «- » и «1. », абзаÑÑ 132 function mdInline(s) { 133 s = s.replace(/\[([^\]]+)\]\((https:\/\/komplektadr\.ru[^)\s]*)\)/g, 134 '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'); 135 s = s.replace(/(^|[\s(])(https:\/\/komplektadr\.ru[^\s<>"')]+)/g, function (m, pre, u) { 136 var trail = ''; 137 while (/[.,;:!?]$/.test(u)) { trail = u.slice(-1) + trail; u = u.slice(0, -1); } 138 return pre + '<a href="' + u + '" target="_blank" rel="noopener noreferrer">' + u + '</a>' + trail; 139 }); 140 s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>'); 141 return s; 142 } 143 function md(text) { 144 var lines = esc(text).split(/\r?\n/); 145 var html = ''; 146 var list = null; // 'ul' | 'ol' 147 var para = []; 148 function flushPara() { 149 if (para.length) { html += '<p>
149' + para.map(mdInline).join('<br>') + '</p>'; para = []; } 150 } 151 function flushList() { 152 if (list) { html += '</' + list + '>'; list = null; } 153 } 154 for (var i = 0; i < lines.length; i++) { 155 var line = lines[i].trim(); 156 var mUl = line.match(/^[-â¢]\s+(.*)/); 157 var mOl = line.match(/^\d+[.)]\s+(.*)/); 158 if (mUl || mOl) { 159 flushPara(); 160 var want = mUl ? 'ul' : 'ol'; 161 if (list !== want) { flushList(); html += '<' + want + '>'; list = want; } 162 html += '<li>' + mdInline((mUl || mOl)[1]) + '</li>'; 163 } else if (line === '') { 164 flushPara(); 165 flushList(); 166 } else { 167 flushList(); 168 para.push(line); 169 } 170 } 171 flushPara(); 172 flushList(); 173 return html; 174 } 175 function fmtPrice(p) { 176 if (p == null || isNaN(p)) return ''; 177 return String(Math.round(p)).replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' â½'; 178 } 179 function svg(pathHtml, cls) { 180 var NS = 'http://www.w3.org/2000/svg'; 181 var node = document.createElementNS(NS, 'svg'); 182 node.setAttribute('viewBox', '0 0 24 24'); 183 if (cls) node.setAttribute('class', cls); 184 node.innerHTML = pathHtml; 185 return node; 186 } 187 var IC = { 188 chat: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>', 189 close: '<path d="M18 6 6 18M6 6l12 12"/>', 190 mic: '<path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v1a7 7 0 0 1-14 0v-1M12 18v4"/>', 191 send: '<path d="m5 12 14-7-4 7 4 7-14-7z"/><path d="M5 12h9"/>', 192 cart: '<circle cx="9" cy="20" r="1.6"/><circle cx="17" cy="20" r="1.6"/><path d="M3 3h2.2l2.3 11.6a1.4 1.4 0 0 0 1.4 1.1h7.9a1.4 1.4 0 0 0 1.4-1.1L20 7H6"/>', 193 spark: '<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/>', 194 link: '<path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/>', 195 check: '<path d="M20 6 9 17l-5-5"/>', 196 down: '<path d="M12 5v14M5 12l7 7 7-7"/>', 197 expand: '<path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/>', 198 shrink: '<path d="M9 9H5M9 9V5M9 9 3.5 3.5M15 9h4M15 9V5M15 9l5.5-5.5M9 15H5M9 15v4M9 15l-5.5 5.5M15 15h4M15 15v4M15 15l5.5 5.5"/>', 199 thumbUp: '<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>', 200 thumbDown: '<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zM17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/>' 201 }; 202 203 // ---------- DOM ---------- 204 var root = el('div', null, document.body); 205 root.id = 'kcw-root'; 206 207 // кнопка-пилÑÐ»Ñ Ð·Ð°Ð¿ÑÑка: иконка + подпиÑÑ (подпиÑÑ ÑкÑÑÑа, когда ÑÐ°Ñ Ð¾ÑкÑÑÑ / на ÑелеÑоне) 208 var fab = el('button', 'kcw-fab', root); 209 fab.type = 'button'; 210 fab.setAttribute('aria-label', 'ÐÐ-помоÑник KomplektADR.ru'); 211 var fabIc = el('span', 'kcw-fab-ic', fab); 212 fabIc.appendChild(svg(IC.chat, 'kcw-ic-chat')); 213 fabIc.appendChild(svg(IC.close, 'kcw-ic-close')); 214 var fabLabel = el('span', 'kcw-fab-label', fab); 215 fabLabel.textContent = FAB_LABEL; 216 var fabBadge = el('span', 'kcw-fab-badge', fab); 217 218 // заÑемнение ÑÑÑаниÑÑ Ð¿Ð¾Ð´ мобилÑной ÑÑоÑкой (клик â закÑÑÑÑ) 219 var scrim = el('div', 'kcw-scrim', root); 220 221 var panel = el('div', 'kcw-panel', root); 222 panel.setAttribute('role', 'dialog'); 223 panel.setAttribute('aria-label', 'ÐÐ-помоÑник KomplektADR.ru'); 224 225 var head = el('div', 'kcw-head', panel); 226 var avatar = el('div', 'kcw-avatar', head); 227 // Ñмблема: ÑиÑменнÑй логоÑип-ÑÑмка ADR (favicon ÑайÑа, векÑоÑнÑй) + живое «Ðл-колÑÑо (CSS) 228 var avatarImg = el('img', 'kcw-avatar-img', avatar); 229 avatarImg.src = '/favicon.svg'; 230 avatarImg.alt = ''; 231 var headTxt = el('div', 'kcw-head-txt', head); 232 el('div', 'kcw-title', headTxt).textContent = 'ÐРпомоÑник KomplektADR.ru'; 233 el('div', 'kcw-status', headTxt).textContent = 'онлайн'; 234 var cartLink = el('a', 'kcw-cart-link', head); 235 cartLink.href = '/basket/'; 236 cartLink.target = '_blank'; 237 cartLink.rel = 'noopener';
238 cartLink.title = 'ÐоÑзина'; 239 cartLink.appendChild(svg(IC.cart)); 240 var cartBadge = el('span', 'kcw-cart-badge', cartLink); 241 var wideBtn = el('button', 'kcw-cart-link kcw-btn-wide', head); 242 wideBtn.type = 'button'; 243 wideBtn.title = 'РазвеÑнÑÑÑ'; 244 wideBtn.setAttribute('aria-label', 'РазвеÑнÑÑÑ Ð¾ÐºÐ½Ð¾ ÑаÑа'); 245 wideBtn.appendChild(svg(IC.expand, 'kcw-ic-exp')); 246 wideBtn.appendChild(svg(IC.shrink, 'kcw-ic-shr')); 247 var closeBtn = el('button', 'kcw-cart-link kcw-close', head); 248 closeBtn.type = 'button'; 249 closeBtn.title = 'ÐакÑÑÑÑ'; 250 closeBtn.setAttribute('aria-label', 'ÐакÑÑÑÑ ÑаÑ'); 251 closeBtn.appendChild(svg(IC.close)); 252 253 var body = el('div', 'kcw-body', panel); 254 body.setAttribute('role', 'log'); 255 body.setAttribute('aria-live', 'polite'); 256 257 // плаваÑÑÐ°Ñ ÐºÐ½Ð¾Ð¿ÐºÐ° «к поÑÐ»ÐµÐ´Ð½ÐµÐ¼Ñ ÑообÑениÑ» 258 var downBtn = el('button', 'kcw-down', panel); 259 downBtn.type = 'button'; 260 downBtn.title = 'РпоÑÐ»ÐµÐ´Ð½ÐµÐ¼Ñ ÑообÑениÑ'; 261 downBtn.appendChild(svg(IC.down)); 262 263 var toast = el('div', 'kcw-toast', panel); 264 265 var foot = el('div', 'kcw-foot', panel); 266 var inrow = el('div', 'kcw-inrow', foot); 267 var input = el('textarea', 'kcw-input', inrow); 268 input.rows = 1; 269 input.placeholder = 'СпÑоÑиÑе или пÑодикÑÑйÑеâ¦'; 270 input.setAttribute('aria-label', 'СообÑение конÑÑлÑÑанÑÑ'); 271 // визÑализаÑÐ¸Ñ Ð·Ð°Ð¿Ð¸Ñи голоÑа: [Ã] [ÐапиÑÑ⦠0:07 ââ ââ] â показÑваеÑÑÑ Ð²Ð¼ÐµÑÑо Ð¿Ð¾Ð»Ñ Ð²Ð²Ð¾Ð´Ð° 272 var cancelBtn = el('button', 'kcw-btn-cancel', inrow); 273 cancelBtn.type = 'button'; 274 cancelBtn.title = 'ÐÑмениÑÑ Ð·Ð°Ð¿Ð¸ÑÑ'; 275 cancelBtn.appendChild(svg(IC.close)); 276 var wave = el('div', 'kcw-wave', inrow); 277 var waveLabel = el('span', 'kcw-wave-label', wave); 278 waveLabel.textContent = 'ÐапиÑÑâ¦'; 279 var waveTime = el('span', 'kcw-wave-time', wave); 280 var waveCanvas = el('canvas', 'kcw-wave-c', wave); 281 var micBtn = el('button', 'kcw-btn-mic', inrow); 282 micBtn.type = 'button'; 283 micBtn.title = 'ÐолоÑовой ввод'; 284 micBtn.appendChild(svg(IC.mic)); 285 var sendBtn = el('button', 'kcw-btn-send', inrow); 286 sendBtn.type = 'button'; 287 sendBtn.title = 'ÐÑпÑавиÑÑ'; 288 sendBtn.appendChild(svg(IC.send)); 289 el('div', 'kcw-hint', foot).textContent = 'ÐÐ-помоÑник Ð¼Ð¾Ð¶ÐµÑ Ð¾ÑибаÑÑÑÑ â ÑÐµÐ½Ñ Ð¸ налиÑие пÑовеÑÑйÑе Ñ Ð¼ÐµÐ½ÐµÐ´Ð¶ÐµÑов.' + (CFG.version ? ' · v' + CFG.version : ''); 290 291 // мобилÑнÑй ÑизеÑ: пÑиглаÑение над кнопкой (деÑкÑоп â¥900px оÑкÑÑÐ²Ð°ÐµÑ Ð¿Ð°Ð½ÐµÐ»Ñ Ñам) 292 var teaser = null; 293 function hideTeaser(remember) { 294 if (!teaser) return; 295 teaser.classList.remove('kcw-on'); 296 if (remember !== false) { 297 try { sessionStorage.setItem('kcw_teaser', '1'); } catch (e) {} 298 } 299 } 300 try { 301 if (window.matchMedia('(max-width: 899px)').matches 302 && !sessionStorage.getItem('kcw_auto_open') && !sessionStorage.getItem('kcw_teaser')) { 303 teaser = el('div', 'kcw-teaser', root); 304 el('div', 'kcw-teaser-txt', teaser).textContent = TEASER_TEXT; 305 var teaserClose = el('button', 'kcw-teaser-close', teaser); 306 teaserClose.type = 'button'; 307 teaserClose.setAttribute('aria-label', 'СкÑÑÑÑ Ð¿Ð¾Ð´ÑказкÑ'); 308 teaserClose.appendChild(svg(IC.close)); 309 teaser.addEventListener('click', function (ev) { 310 if (teaserClose.contains(ev.target)) return; 311 hideTeaser(); 312 toggle(true, 'teaser'); 313 }); 314 teaserClose.addEventListener('click', function (ev) { 315 ev.stopPropagation(); 316 hideTeaser(); // закÑÑл Ñам â болÑÑе не показÑваем в ÑÑой ÑеÑÑии 317 track('invite_close', { invite: 'teaser' }); 318 }); 319 setTimeout(function () { 320 if (teaser && !state.open) { 321 teaser.classList.add('kcw-on'); 322 track('invite_show', { invite: 'teaser' }); 323 } 324 }, 6000); 325 setTimeout(function () { hideTeaser(false); }, 34000); // ÑÐ¸Ñ Ð¾ ÑбÑаÑÑ; на ÑледÑÑÑей ÑÑÑаниÑе можно Ñнова 326 } 327 } catch (e) {} 328 329 // ---------- пÑокÑÑÑка ---------- 330 function nearBottom() { 331 return body.scrollHeight - body.scrollTop - body.clientHeight < 90; 332 } 333 // ÑмнÑй авÑоÑкÑолл: не дÑÑгаем ленÑÑ, еÑли Ñеловек ÑиÑÐ°ÐµÑ Ð²ÑÑе; вмеÑÑо ÑÑого â кнопка Ñ ÑоÑкой
334 function scrollDown(force) { 335 if (force || nearBottom()) { 336 body.scrollTop = body.scrollHeight; 337 downBtn.classList.remove('kcw-new'); 338 } else { 339 downBtn.classList.add('kcw-new'); 340 } 341 updateDown(); 342 } 343 function updateDown() { 344 var gap = body.scrollHeight - body.scrollTop - body.clientHeight; 345 if (gap > 240) { 346 downBtn.classList.add('kcw-show'); 347 } else { 348 downBtn.classList.remove('kcw-show'); 349 downBtn.classList.remove('kcw-new'); 350 } 351 } 352 var scrollRaf = 0; 353 body.addEventListener('scroll', function () { 354 if (scrollRaf) return; 355 scrollRaf = requestAnimationFrame(function () { scrollRaf = 0; updateDown(); }); 356 }, { passive: true }); 357 downBtn.addEventListener('click', function () { 358 try { 359 body.scrollTo({ top: body.scrollHeight, behavior: reducedMotion ? 'auto' : 'smooth' }); 360 } catch (e) { 361 body.scrollTop = body.scrollHeight; 362 } 363 downBtn.classList.remove('kcw-new'); 364 }); 365 366 // ---------- оÑÑиÑовка ---------- 367 // длиннÑе «пÑоÑÑÑни» ÑвоÑаÑиваем: видно пеÑвÑе ~5 ÑкÑанов ÑÑÑок, далÑÑе «ÐоказаÑÑ Ð¿Ð¾Ð»Ð½Ð¾ÑÑÑÑ» 368 function maybeClamp(msg, item) { 369 if (item.checkout) return; // внÑÑÑи Ð¼Ð¾Ð¶ÐµÑ Ð±ÑÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ° оÑоÑÐ¼Ð»ÐµÐ½Ð¸Ñ â не пÑÑÑем 370 if (msg.scrollHeight > 620) { 371 msg.classList.add('kcw-clamp'); 372 var more = el('button', 'kcw-more', msg); 373 more.type = 'button'; 374 more.textContent = 'ÐоказаÑÑ Ð¿Ð¾Ð»Ð½Ð¾ÑÑÑÑ'; 375 more.addEventListener('click', function () { 376 msg.classList.remove('kcw-clamp'); 377 more.remove(); 378 }); 379 } 380 } 381 382 // ÑÑÑока под оÑвеÑом: «ÐÑовеÑено: â¦Â» + оÑенки ð/ð 383 function renderMeta(item) { 384 var meta = el('div', 'kcw-meta', body); 385 if (item.src && item.src.length) { 386 var src = el('span', 'kcw-src', meta); 387 src.appendChild(svg(IC.check)); 388 src.appendChild(document.createTextNode('ÐÑовеÑено: ' + item.src.join(' · '))); 389 } 390 var fb = el('span', 'kcw-fb', meta); 391 var up = el('button', 'kcw-fb-btn kcw-fb-up', fb); 392 up.type = 'button'; 393 up.title = 'ÐолезнÑй оÑвеÑ'; 394 up.setAttribute('aria-label', 'ÐолезнÑй оÑвеÑ'); 395 up.appendChild(svg(IC.thumbUp)); 396 var dn = el('button', 'kcw-fb-btn kcw-fb-dn', fb); 397 dn.type = 'button'; 398 dn.title = 'ÐÐ»Ð¾Ñ Ð¾Ð¹ оÑвеÑ'; 399 dn.setAttribute('aria-label', 'ÐÐ»Ð¾Ñ Ð¾Ð¹ оÑвеÑ'); 400 dn.appendChild(svg(IC.thumbDown)); 401 function apply() { 402 fb.classList.toggle('kcw-voted', !!item.fb); 403 up.classList.toggle('kcw-sel', item.fb === 1); 404 dn.classList.toggle('kcw-sel', item.fb === -1); 405 } 406 function vote(v) { 407 if (item.fb === v) return; 408 item.fb = v; 409 apply(); 410 persist(); 411 buzz(8); 412 postForm(API_CHAT, { sessid: CFG.sessid, cw_action: 'feedback', log_id: item.log, rating: v }, 413 function () {}, function () {}); 414 showToast(v === 1 ? 'СпаÑибо за оÑенкÑ!' : 'СпаÑибо, ÑÑÑÑм!'); 415 } 416 up.addEventListener('click', function () { vote(1); }); 417 dn.addEventListener('click', function () { vote(-1); }); 418 apply(); 419 } 420 421 // каÑÑоÑки ÑоваÑов Ñ ÐºÐ½Ð¾Ð¿ÐºÐ¾Ð¹ «РкоÑзинÑ»; set = { items, added } â ÑоÑÑоÑние кнопок ÑÐ¾Ñ ÑанÑеÑÑÑ Ð² persist() 422 function renderCards(set) { 423 var wrap = el('div', 'kcw-cards', body); 424 var many = (set.items || []).length >= 3; 425 if (many) wrap.classList.add('kcw-carousel'); // 3+ ÑоваÑов â гоÑизонÑалÑÐ½Ð°Ñ Ð»ÐµÐ½Ñа 426 (set.items || []).forEach(function (p) { 427 var card = el('div', 'kcw-card', wrap); 428 if (p.picture) { 429 var img = el('img', 'kcw-card-img', card); 430 img.src = p.picture; 431 img.alt = ''; 432 img.loading = 'lazy'; 433 } else { 434 el('div', 'kcw-card-img kcw-noimg', card).textContent = 'ð¦'; 435 } 436 var info = el('div', 'kcw-card-info', card); 437 var name = el('a', 'kcw-card-name', info); 438 name.textContent = p.name || ''; 439 if (p.url) { 440 name.href = p.url; 441 name.target = '_blank'; 442 name.rel = 'noopener'; 443 name.addEventListener('click', function () { track('product_click', { product_id: p.id }); }); 444 } 445 if (p.variant) el('div', 'kcw-card-variant', info).textContent = p.variant; 446 if (p.price) el('div', 'kcw-card-price', info).textContent = fmtPrice(p.price); 447 var btn = el('button', 'kcw-card-btn', card); 448 btn.type = 'button'; 449 if (set.added && set.added[p.id]) { 450 btn.textContent = 'â РкоÑзине'; 451 btn.className += ' kcw-done'; 452 } else {
453 btn.textContent = 'РкоÑзинÑ'; 454 btn.addEventListener('click', function () { cardAddToCart(p, btn, set); }); 455 } 456 }); 457 } 458 459 function renderItem(item) { 460 if (item.k === 'user' || item.k === 'bot') { 461 var msg = el('div', 'kcw-msg ' + (item.k === 'user' ? 'kcw-msg-user' : 'kcw-msg-bot'), body); 462 msg.innerHTML = item.k === 'bot' ? md(item.t) : linkify(item.t); 463 // кнопка «к оÑоÑмлениÑ» â внÑÑÑи пÑзÑÑÑ Ð¾ÑвеÑа, поÑле ÑекÑÑа 464 if (item.k === 'bot' && item.checkout) { 465 var cbtn = el('a', 'kcw-msg-btn', msg); 466 cbtn.href = item.checkout; 467 cbtn.target = '_blank'; 468 cbtn.rel = 'noopener'; 469 cbtn.appendChild(svg(IC.cart)); 470 cbtn.appendChild(document.createTextNode('ÐеÑейÑи к оÑоÑÐ¼Ð»ÐµÐ½Ð¸Ñ Ð·Ð°ÐºÐ°Ð·Ð°')); 471 } 472 if (item.k === 'bot') { 473 maybeClamp(msg, item); 474 // каÑÑоÑки â ÑÑÐ°Ð·Ñ Ð¿Ð¾Ð´ ÑекÑÑом (оÑÐ²ÐµÑ Ð¾Ð±ÑÑно заканÑиваеÑÑÑ Ð¿Ñедложением кÑпиÑÑ), оÑенки â под каÑÑоÑками 475 (item.cards || []).forEach(renderCards); 476 if (item.log) renderMeta(item); 477 } 478 return; 479 } 480 if (item.k === 'products') { // ÑÑаÑÑе ÑÐ¾Ñ ÑанÑннÑе диалоги: каÑÑоÑки оÑделÑнÑм ÑлеменÑом над оÑвеÑом 481 renderCards(item); 482 return; 483 } 484 if (item.k === 'service' || item.k === 'pagelink' || item.k === 'checkout') { 485 var link = el('a', 'kcw-service', body); 486 link.href = item.url; 487 link.target = '_blank'; 488 link.rel = 'noopener noreferrer'; 489 link.appendChild(svg(item.k === 'checkout' ? IC.cart : IC.link)); 490 var txt = el('div', null, link); 491 el('div', 'kcw-service-title', txt).textContent = item.title || item.url; 492 el('div', 'kcw-service-sub', txt).textContent = item.k === 'service' ? 'ÐеÑплаÑнÑй ÑеÑÐ²Ð¸Ñ â оÑкÑÑÑÑ' 493 : (item.k === 'checkout' ? 'ÐаннÑе Ñже Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ñ â оÑÑанеÑÑÑ Ð¿Ð¾Ð´ÑвеÑдиÑÑ' : 'ÐÑкÑÑÑÑ ÑÑÑаниÑÑ'); 494 return; 495 } 496 } 497 498 function push(item) { 499 state.transcript.push(item); 500 renderItem(item); 501 persist(); 502 scrollDown(item.k === 'user'); // ÑÐ²Ð¾Ñ ÑообÑение â вÑегда вниз; оÑвеÑÑ Ð½Ðµ дÑÑгаÑÑ ÑиÑаÑÑего 503 } 504 505 function renderAll() { 506 body.innerHTML = ''; 507 if (!state.transcript.length) { 508 push({ k: 'bot', t: GREETING }); 509 } else { 510 state.transcript.forEach(renderItem); 511 } 512 // ÑипÑ-подÑказки â пока полÑзоваÑÐµÐ»Ñ Ð½Ð¸Ñего не напиÑал; пеÑвÑй â под ÑекÑÑÑÑ ÑÑÑаниÑÑ 513 if (!state.history.length) { 514 var chips = el('div', 'kcw-chips', body); 515 var ctx = contextChip(); 516 var list = ctx ? [ctx].concat(CHIPS_BASE.slice(0, 3)) : CHIPS_BASE.slice(0); 517 list.forEach(function (q) { 518 var chip = el('button', 'kcw-chip', chips); 519 chip.type = 'button'; 520 chip.textContent = q.t; 521 chip.addEventListener('click', function () { 522 input.value = q.t; 523 inputSource = 'chip'; 524 inputChipId = q.id; 525 send(); 526 }); 527 }); 528 } 529 scrollDown(true); 530 } 531 532 function setCart(n) { 533 if (n == null || isNaN(n)) return; 534 state.cartCount = Math.max(0, Math.round(n)); 535 [cartBadge, fabBadge].forEach(function (b) { 536 b.textContent = state.cartCount > 99 ? '99+' : String(state.cartCount); 537 b.className = b.className.replace(' kcw-on', '') + (state.cartCount > 0 ? ' kcw-on' : ''); 538 }); 539 persist(); 540 } 541 542 var toastTimer = null; 543 function showToast(text) { 544 toast.textContent = text; 545 toast.classList.add('kcw-on'); 546 clearTimeout(toastTimer); 547 toastTimer = setTimeout(function () { toast.classList.remove('kcw-on'); }, 2600); 548 } 549 550 // ---------- индикаÑÐ¾Ñ Ð½Ð°Ð±Ð¾Ñа + живой ÑÑаÑÑÑ ÑÑапа ---------- 551 var typingNode = null; 552 var typingTxt = null; 553 function typing(on) { 554 if (on && !typingNode) { 555 typingNode = el('div', 'kcw-typing', body); 556 typingNode.innerHTML = '<i></i><i></i><i></i>'; 557 typingTxt = el('span', 'kcw-typing-txt', typingNode); 558 scrollDown(true); 559 } else if (!on && typingNode) { 560 typingNode.remove(); 561 typingNode = null; 562 typingTxt = null; 563 } 564 } 565 function setTypingStatus(text) { 566 if (!typingTxt || typingTxt.textContent === text) return; 567 typingTxt.textContent = text; 568 typingTxt.classList.remove('kcw-fade'); 569 void typingTxt.offsetWidth; // пеÑезапÑÑк анимаÑии поÑÐ²Ð»ÐµÐ½Ð¸Ñ 570 typingTxt.classList.add('kcw-fade'); 571 scrollDown(false); 572 } 573 574 // ---------- ÑеÑÑ ---------- 575 function postForm(url, fields, done, fail) { 576 var fd = new FormData();
577 Object.keys(fields).forEach(function (key) { fd.append(key, fields[key]); }); 578 fetch(url, { method: 'POST', body: fd, credentials: 'same-origin' }) 579 .then(function (r) { return r.json(); }) 580 .then(done) 581 .catch(fail); 582 } 583 584 var sending = false; 585 function send() { 586 var text = input.value.trim(); 587 if (!text || sending) return; 588 // подÑказки ÑбиÑаем пÑи ÐЮÐÐРоÑпÑавке (не ÑолÑко по ÐºÐ»Ð¸ÐºÑ Ð½Ð° Ñип) 589 var chipsNode = body.querySelector('.kcw-chips'); 590 if (chipsNode) chipsNode.remove(); 591 sending = true; 592 sendBtn.disabled = true; 593 buzz(10); 594 input.value = ''; 595 autosize(); 596 // пеÑвое ÑообÑение за ÑеÑÑÐ¸Ñ â главнÑй KPI воÑонки 597 try { 598 if (!sessionStorage.getItem('kcw_msg_sent')) { 599 sessionStorage.setItem('kcw_msg_sent', '1'); 600 track('first_message', { input: inputSource, chip_id: inputChipId }); 601 } 602 } catch (e) {} 603 inputSource = 'typed'; 604 inputChipId = null; 605 push({ k: 'user', t: text }); 606 state.history.push({ role: 'user', content: text }); 607 state.history = state.history.slice(-10); 608 persist(); 609 typing(true); 610 root.classList.add('kcw-busy'); // ÑÑкоÑÑÐµÑ Â«Ðл-колÑÑо на аваÑаÑе 611 612 // поÑоковÑй запÑоÑ: NDJSON-ÑÑÑоки {"s":"ÐÑÑ Ð² каÑалогеâ¦"} + ÑиналÑнÑй обÑÐµÐºÑ Ð¾ÑвеÑа 613 var fd = new FormData(); 614 fd.append('sessid', CFG.sessid); 615 fd.append('messages', JSON.stringify(state.history)); 616 fd.append('context_products', JSON.stringify(state.lastProducts || [])); 617 fd.append('stream', '1'); 618 619 fetch(API_CHAT, { method: 'POST', body: fd, credentials: 'same-origin' }) 620 .then(function (r) { 621 if (r.body && window.TextDecoder) { 622 var reader = r.body.getReader(); 623 var dec = new TextDecoder(); 624 var buf = ''; 625 var last = null; 626 function pump() { 627 return reader.read().then(function (chunk) { 628 if (chunk.done) { 629 var tail = buf.trim(); 630 if (tail) { 631 try { 632 var o = JSON.parse(tail); 633 if (o && !o.s) last = o; 634 } catch (e) {} 635 } 636 return last; 637 } 638 buf += dec.decode(chunk.value, { stream: true }); 639 var i; 640 while ((i = buf.indexOf('\n')) >= 0) { 641 var line = buf.slice(0, i).trim(); 642 buf = buf.slice(i + 1); 643 if (!line) continue; 644 try { 645 var obj = JSON.parse(line); 646 if (obj && obj.s) setTypingStatus(obj.s); 647 else last = obj; 648 } catch (e) {} 649 } 650 return pump(); 651 }); 652 } 653 return pump(); 654 } 655 // ÑÑаÑÑй бÑаÑÐ·ÐµÑ Ð±ÐµÐ· поÑокового ÑÑÐµÐ½Ð¸Ñ â ÑазбиÑаем оÑÐ²ÐµÑ Ñеликом 656 return r.text().then(function (t) { 657 var last = null; 658 t.split('\n').forEach(function (line) { 659 line = line.trim(); 660 if (!line) return; 661 try { 662 var o = JSON.parse(line); 663 if (o && !o.s) last = o; 664 } catch (e) {} 665 }); 666 return last; 667 }); 668 }) 669 .then(function (res) { finishSend(res, false); }) 670 .catch(function () { finishSend(null, true); }); 671 } 672 673 function finishSend(res, netFail) { 674 // еÑли Ñеловек бÑл Ð²Ð½Ð¸Ð·Ñ Ð´Ð¾ вÑÑавки паÑки (каÑÑоÑки + оÑвеÑ) â докÑÑÑиваем поÑле Ð½ÐµÑ Ñеликом; 675 // пеÑÐ²Ð°Ñ Ð¶Ðµ вÑÑавка Ð´ÐµÐ»Ð°ÐµÑ nearBottom() ложнÑм, поÑÑÐ¾Ð¼Ñ ÑеÑаем заÑанее 676 var stick = nearBottom(); 677 typing(false); 678 sending = false; 679 sendBtn.disabled = false; 680 root.classList.remove('kcw-busy'); 681 if (!res || !res.success) { 682 push({ 683 k: 'bot', 684 t: netFail 685 ? 'ÐÐµÑ ÑвÑзи Ñ ÑеÑвеÑом. ÐÑовеÑÑÑе инÑеÑÐ½ÐµÑ Ð¸ попÑобÑйÑе еÑÑ Ñаз.'
686 : ((res && res.message) || 'ЧÑо-Ñо поÑло не Ñак, попÑобÑйÑе еÑÑ Ñаз.') 687 }); 688 return; 689 } 690 var checkoutUrl = null; 691 var cardSets = []; // каÑÑоÑки â ÐÐРоÑвеÑом: оÑÐ²ÐµÑ Ð·Ð°ÐºÐ°Ð½ÑиваеÑÑÑ Ð¿Ñедложением кÑпиÑÑ, кнопка «РкоÑзинÑ» ÑÑÐ°Ð·Ñ Ð¿Ð¾Ð´ ним 692 (res.actions || []).forEach(function (a) { 693 if (a.type === 'products' && a.items && a.items.length) { 694 cardSets.push(a.items); 695 } else if (a.type === 'cart') { 696 if (a.cart_count != null) setCart(a.cart_count); 697 if (a.added > 0) { 698 showToast('Ðобавлено в коÑзинÑ: ' + a.added); 699 buzz(15); 700 track('add_to_cart', { via: 'chat', count: a.added }); 701 } 702 } else if (a.type === 'open_page' && a.url) { 703 push({ k: 'pagelink', url: a.url, title: a.url.replace('https://komplektadr.ru', 'komplektadr.ru') }); 704 try { window.open(a.url, '_blank', 'noopener,noreferrer'); } catch (e) {} 705 } else if (a.type === 'service' && a.url) { 706 push({ k: 'service', url: a.url, title: a.title || a.url }); 707 } else if (a.type === 'checkout_ready' && a.url) { 708 checkoutUrl = a.url; // кнопка вÑÑÑаиваеÑÑÑ Ð² пÑзÑÑÑ Ð¾ÑвеÑа ниже 709 } 710 }); 711 var cards = cardSets.map(function (items) { return { items: items, added: {} }; }); 712 if (res.reply) { 713 push({ 714 k: 'bot', 715 t: res.reply, 716 checkout: checkoutUrl, 717 cards: cards.length ? cards : null, 718 log: res.log_id || null, 719 src: (res.sources && res.sources.length) ? res.sources : null 720 }); 721 state.history.push({ role: 'assistant', content: res.reply }); 722 state.history = state.history.slice(-10); 723 } else { 724 if (checkoutUrl) push({ k: 'checkout', url: checkoutUrl, title: 'ÐеÑейÑи к оÑоÑÐ¼Ð»ÐµÐ½Ð¸Ñ Ð·Ð°ÐºÐ°Ð·Ð°' }); 725 cards.forEach(function (set) { 726 push({ k: 'products', items: set.items, added: set.added }); 727 }); 728 } 729 if (res.last_products && res.last_products.length) { 730 state.lastProducts = res.last_products; 731 } 732 persist(); 733 if (stick) scrollDown(true); 734 input.focus(); 735 } 736 737 // кнопка «в коÑзинÑ» на каÑÑоÑке â напÑÑмÑÑ Ð² гоÑовÑй ÑндпоинÑ, без LLM-Ñ Ð¾Ð´Ð° 738 function cardAddToCart(product, btn, item) { 739 btn.disabled = true; 740 postForm(API_CART, { 741 sessid: CFG.sessid, 742 // Ñ ÑоваÑов Ñ ÑоÑговÑми пÑедложениÑми в коÑÐ·Ð¸Ð½Ñ Ð¸Ð´ÑÑ Ð¢Ð (basket_id Ñ ÑеÑвеÑа) 743 products: JSON.stringify([{ id: product.basket_id || product.id, quantity: 1 }]) 744 }, function (res) { 745 if (res && res.success) { 746 btn.textContent = 'â РкоÑзине'; 747 btn.classList.add('kcw-done'); 748 item.added = item.added || {}; 749 item.added[product.id] = true; 750 setCart(state.cartCount + 1); 751 showToast('Ðобавлено в коÑзинÑ'); 752 buzz(15); 753 track('add_to_cart', { via: 'card', product_id: product.id }); 754 persist(); 755 } else { 756 btn.disabled = false; 757 showToast('Ðе ÑдалоÑÑ Ð´Ð¾Ð±Ð°Ð²Ð¸ÑÑ'); 758 } 759 }, function () { 760 btn.disabled = false; 761 showToast('ÐÐµÑ ÑвÑзи Ñ ÑеÑвеÑом'); 762 }); 763 } 764 765 // ---------- Ð³Ð¾Ð»Ð¾Ñ ---------- 766 var recorder = null; 767 var recChunks = []; 768 var recStopTimer = null; 769 var recCancelled = false; 770 var audioCtx = null; 771 var waveRaf = 0; 772 var waveTimer = null; 773 var micSupported = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia && window.MediaRecorder); 774 if (!micSupported) micBtn.style.display = 'none'; 775 776 function pickMime() { 777 var types = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus']; 778 for (var i = 0; i < types.length; i++) { 779 if (MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(types[i])) return types[i]; 780 } 781 return ''; 782 } 783 784 // Ð¶Ð¸Ð²Ð°Ñ Ð²Ð¾Ð»Ð½Ð° запиÑи Ñ Ð¼Ð¸ÐºÑоÑона (WebAudio); пÑи reduced-motion â ÑолÑко ÑÐ°Ð¹Ð¼ÐµÑ 785 function startWave(stream) { 786 var t0 = Date.now(); 787 waveTime.textContent = '0:00'; 788 waveTimer = setInterval(function () { 789 var s = Math.floor((Date.now() - t0) / 1000); 790 waveTime.textContent = Math.floor(s / 60) + ':' + ('0' + (s % 60)).slice(-2); 791 }, 300); 792 var AC = window.AudioContext || window.webkitAudioContext; 793 if (!AC || reducedMotion) return; 794 try { 795 audioCtx = new AC(); 796 var srcNode = audioCtx.createMediaStreamSource(stream); 797 var analyser = audioCtx.createAnalyser(); 798 analyser.fftSize = 128; 799 srcNode.connect(analyser); 800 var data = new Uint8Array(analyser.frequencyBinCount); 801 var dpr = window.devicePixelRatio || 1; 802 var ctx2d = waveCanvas.getContext('2d'); 803 function draw() { 804 waveRaf = requestAnimationFrame(draw); 805 var w = waveCanvas.clientWidth; 806 var h = waveCanvas.clientHeight; 807 if (!w || !ctx2d) return; 808 if (waveCanvas.width !== Math.round(w * dpr)) { 809 waveCanvas.width = Math.round(w * dpr); 810 waveCanvas.height = Math.round(h * dpr); 811 } 812 analyser.getByteFrequencyData(data); 813 ctx2d.clearRect(0, 0, waveCanvas.width, waveCanvas.height); 814 ctx2d.fillStyle = '#e0402c'; 815 var bars = 24; 816 var gap = 3 * dpr; 817 var bw = (waveCanvas.width - gap * (bars - 1)) / bars; 818 for (var i = 0; i < bars; i++) { 819 var v = data[Math.floor(i * data.length / bars / 1.6)] / 255; 820 var bh = Math.max(3 * dpr, v * waveCanvas.height * 0.95); 821 var x = i * (bw + gap); 822 var y = (waveCanvas.height - bh) / 2; 823 if (ctx2d.roundRect) { 824 ctx2d.beginPath(); 825 ctx2d.roundRect(x, y, bw, bh, bw / 2); 826 ctx2d.fill(); 827 } else { 828 ctx2d.fillRect(x, y, bw, bh); 829 } 830 } 831 } 832 draw(); 833 } catch (e) {} 834 } 835 function stopWave() { 836 clearInterval(waveTimer); 837 waveTimer = null; 838 if (waveRaf) cancelAnimationFrame(waveRaf); 839 waveRaf = 0; 840 if (audioCtx) {
841 try { audioCtx.close(); } catch (e) {} 842 audioCtx = null; 843 } 844 try { 845 var c = waveCanvas.getContext('2d'); 846 if (c) c.clearRect(0, 0, waveCanvas.width, waveCanvas.height); 847 } catch (e) {} 848 } 849 850 function startRec() { 851 navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { 852 var mime = pickMime(); 853 recorder = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined); 854 recChunks = []; 855 recCancelled = false; 856 recorder.ondataavailable = function (ev) { 857 if (ev.data && ev.data.size) recChunks.push(ev.data); 858 }; 859 recorder.onstop = function () { 860 stream.getTracks().forEach(function (t) { t.stop(); }); 861 micBtn.classList.remove('kcw-rec'); 862 inrow.classList.remove('kcw-rec-on'); 863 stopWave(); 864 var blob = new Blob(recChunks, { type: recorder.mimeType || 'audio/webm' }); 865 recorder = null; 866 if (recCancelled) { recCancelled = false; return; } // оÑменили â ниÑего не ÑлÑм 867 if (blob.size < 1200) return; // ÑлÑÑайнÑй клик â ÑиÑина 868 micBtn.classList.add('kcw-busy'); 869 var fd = new FormData(); 870 fd.append('sessid', CFG.sessid); 871 fd.append('audio', blob, 'voice'); 872 fetch(API_VOICE, { method: 'POST', body: fd, credentials: 'same-origin' }) 873 .then(function (r) { return r.json(); }) 874 .then(function (res) { 875 micBtn.classList.remove('kcw-busy'); 876 if (res && res.success && res.text) { 877 input.value = (input.value ? input.value + ' ' : '') + res.text; 878 inputSource = 'voice'; 879 inputChipId = null; 880 autosize(); 881 input.focus(); 882 } else { 883 showToast((res && res.message) || 'Ðе ÑаÑÑлÑÑал â попÑобÑйÑе еÑÑ Ñаз'); 884 } 885 }) 886 .catch(function () { 887 micBtn.classList.remove('kcw-busy'); 888 showToast('ÐÐµÑ ÑвÑзи Ñ ÑеÑвеÑом'); 889 }); 890 }; 891 recorder.start(); 892 micBtn.classList.add('kcw-rec'); 893 inrow.classList.add('kcw-rec-on'); 894 startWave(stream); 895 buzz(10); 896 micBtn.title = 'ÐÑÑановиÑÑ Ð·Ð°Ð¿Ð¸ÑÑ'; 897 recStopTimer = setTimeout(stopRec, 60000); 898 }).catch(function () { 899 showToast('ÐÐµÑ Ð´Ð¾ÑÑÑпа к микÑоÑонÑ'); 900 }); 901 } 902 903 function stopRec() { 904 clearTimeout(recStopTimer); 905 micBtn.title = 'ÐолоÑовой ввод'; 906 if (recorder && recorder.state !== 'inactive') recorder.stop(); 907 } 908 909 micBtn.addEventListener('click', function () { 910 if (recorder) stopRec(); 911 else startRec(); 912 }); 913 cancelBtn.addEventListener('click', function () { 914 recCancelled = true; 915 stopRec(); 916 }); 917 918 // ---------- ввод ---------- 919 function autosize() { 920 input.style.height = 'auto'; 921 input.style.height = Math.min(input.scrollHeight, 108) + 'px'; 922 input.style.overflowY = input.scrollHeight > 108 ? 'auto' : 'hidden'; 923 } 924 input.addEventListener('input', function () { 925 // Ñеловек пÑÐ°Ð²Ð¸Ñ ÑекÑÑ ÑÑками â иÑÑоÑник ввода Ñнова «клавиаÑÑÑа» 926 inputSource = 'typed'; 927 inputChipId = null; 928 autosize(); 929 }); 930 input.addEventListener('keydown', function (ev) { 931 if (ev.key === 'Enter' && !ev.shiftKey) { 932 ev.preventDefault(); 933 send(); 934 } 935 }); 936 sendBtn.addEventListener('click', send); 937 938 // ---------- оÑкÑÑÑие/закÑÑÑие ---------- 939 var openSource = null; 940 var openedAt = 0; 941 function toggle(open, source) { 942 var was = state.open; 943 state.open = open == null ? !state.open : open; 944 root.classList.toggle('kcw-open', state.open); 945 fab.setAttribute('aria-expanded', state.open ? 'true' : 'false'); 946 // на ÑелеÑоне под ÑÑоÑкой блокиÑÑем пÑокÑÑÑÐºÑ ÑÑÑаниÑÑ (ÑкÑим + lock) 947 var sheet = window.matchMedia('(max-width: 520px)').matches; 948 document.documentElement.classList.toggle('kcw-lock', state.open && sheet); 949 if (state.open && !was) { 950 openSource = source || 'fab'; 951 openedAt = Date.now(); 952 track('open', { source: openSource }); 953 }
953 else if (!state.open && was) { 954 // guardrail: бÑÑÑÑÑе закÑÑÑÐ¸Ñ Ð¿Ð¾Ñле auto-open Ð²Ð¸Ð´Ð½Ñ Ð¿Ð¾ open_source + secs 955 track('close', { open_source: openSource, secs: openedAt ? Math.round((Date.now() - openedAt) / 1000) : null }); 956 } 957 if (state.open) { 958 hideTeaser(); 959 try { sessionStorage.setItem('kcw_auto_open', '1'); } catch (e) {} 960 renderAll(); 961 if (window.matchMedia('(min-width: 521px)').matches) { 962 setTimeout(function () { input.focus(); }, 350); 963 } 964 } else if (recorder) { 965 stopRec(); 966 } 967 } 968 fab.addEventListener('click', function () { toggle(); }); 969 closeBtn.addEventListener('click', function () { toggle(false); }); 970 scrim.addEventListener('click', function () { toggle(false); }); 971 972 // ÑазвоÑÐ¾Ñ Ð¿Ð°Ð½ÐµÐ»Ð¸ на деÑкÑопе (Intercom-паÑÑеÑн); вÑÐ±Ð¾Ñ Ð¿Ð¾Ð¼Ð½Ð¸Ð¼ в ÑеÑÑии 973 function setWide(v) { 974 root.classList.toggle('kcw-wide', !!v); 975 wideBtn.title = v ? 'СвеÑнÑÑÑ' : 'РазвеÑнÑÑÑ'; 976 try { 977 if (v) sessionStorage.setItem('kcw_wide', '1'); 978 else sessionStorage.removeItem('kcw_wide'); 979 } catch (e) {} 980 } 981 wideBtn.addEventListener('click', function () { 982 setWide(!root.classList.contains('kcw-wide')); 983 }); 984 try { if (sessionStorage.getItem('kcw_wide')) setWide(true); } catch (e) {} 985 986 // авÑо-ÑазвоÑаÑивание пÑи пеÑвом Ð·Ð°Ñ Ð¾Ð´Ðµ (как Ñ Ð¶Ð¸Ð²ÑÑ ÑаÑов â ÑилÑно Ð¿Ð¾Ð´Ð½Ð¸Ð¼Ð°ÐµÑ Ð²Ð¾Ð²Ð»ÐµÑÑнноÑÑÑ): 987 // один Ñаз за ÑеÑÑÐ¸Ñ Ð±ÑаÑзеÑа, ÑолÑко деÑкÑоп, ÑеÑез 6 ÑекÑнд поÑле загÑÑзки 988 try { 989 if (!sessionStorage.getItem('kcw_auto_open') && window.matchMedia('(min-width: 900px)').matches) { 990 setTimeout(function () { 991 if (!state.open && !sessionStorage.getItem('kcw_auto_open')) { 992 try { sessionStorage.setItem('kcw_auto_open', '1'); } catch (e) {} 993 toggle(true, 'auto'); 994 } 995 }, 6000); 996 } 997 } catch (e) {} 998 document.addEventListener('keydown', function (ev) { 999 if (ev.key === 'Escape' && state.open) toggle(false); 1000 }); 1001 1002 // ---------- Ñазводка Ñ Ð»Ð¸Ð¿ÐºÐ¸Ð¼Ð¸ нижними панелÑми ÑайÑа ---------- 1003 // СеÑÐ²Ð¸Ñ /un/ (.unx-cartbar «ÐобавиÑÑ Ð² коÑзинÑ») и кÑки-Ð±Ð°Ð½Ð½ÐµÑ Ð¿ÐµÑекÑÑвалиÑÑ 1004 // кнопкой ÑаÑа. ÐеÑÑем видимÑе fixed-панели Ñ Ð½Ð¸Ð·Ð° ÑкÑана и поднимаем Ð²Ð¸Ð´Ð¶ÐµÑ 1005 // на Ð¸Ñ Ð²ÑÑоÑÑ ÑеÑез CSS-пеÑеменнÑÑ --kcw-lift (на html â ÐµÑ Ð²Ð¸Ð´Ð¸Ñ Ð¸ a.scroll-to-top). 1006 var LIFT_SELECTORS = ['.unx-cartbar', '#kadr-cookie-banner']; 1007 var liftLast = -1; 1008 function updLift() { 1009 var vh = window.innerHeight; 1010 var lift = 0; 1011 for (var i = 0; i < LIFT_SELECTORS.length; i++) { 1012 var n = document.querySelector(LIFT_SELECTORS[i]); 1013 if (!n) continue; 1014 var cs = getComputedStyle(n); 1015 if (cs.position !== 'fixed' || cs.display === 'none' || cs.visibility === 'hidden') continue; 1016 var r = n.getBoundingClientRect(); 1017 if (r.height < 20 || r.top >= vh) continue; // ÑпÑÑÑана или ÑÐµÑ Ð°Ð»Ð° за низ ÑкÑана 1018 var overlap = Math.min(vh - r.top, 160); // наÑколÑко Ð·Ð°Ð½Ð¸Ð¼Ð°ÐµÑ Ð½Ð¸Ð· (Ñ Ð¿Ð¾Ñолком) 1019 if (overlap > lift) lift = overlap; 1020 } 1021 lift = lift ? Math.round(lift + 10) : 0; 1022 if (lift !== liftLast) { 1023 liftLast = lift; 1024 document.documentElement.style.setProperty('--kcw-lift', lift + 'px'); 1025 } 1026 } 1027 var liftRaf = 0; 1028 function liftOnScroll() { 1029 if (liftRaf) return; 1030 liftRaf = requestAnimationFrame(function () { liftRaf = 0; updLift(); }); 1031 } 1032 updLift(); 1033 window.addEventListener('scroll', liftOnScroll, { passive: true }); 1034 window.addEventListener('resize', liftOnScroll); 1035 setInterval(updLift, 900); // Ð»Ð¾Ð²Ð¸Ñ Ð¿Ð¾Ñвление/закÑÑÑие панелей без ÑобÑÑий (кÑки-Ð±Ð°Ð½Ð½ÐµÑ Ð¸ Ñ.п.) 1036 1037 setCart(state.cartCount); 1038 track('impression'); // FAB оÑÑиÑован на ÑÑой ÑÑÑаниÑе â знаменаÑÐµÐ»Ñ Ð²Ð¾Ñонки 1039})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.