PageSourceSearch

https://masm.gov.mn/chatbot/widget/masm-chat.js

js masm.gov.mn collected 2026-10-06 10:59:57 UTC 68,208 bytes, 1,511 lines download raw bytes

1/*!
2 * MASM чат виджет — masm.gov.mn сайтад суулгах чат цонх.
3 *
4 * Суулгах:
5 *   <script src="/chatbot/widget/masm-chat.js" defer></script>
6 *
7 * Тохиргоо (script таг дээрх data-* атрибутууд, бүгд заавал биш):
8 *   data-api         API-ийн хаяг. Өгөөгүй бол скриптийн байрлалаас автоматаар олно.
9 *   data-title       Цонхны гарчиг
10 *   data-subtitle    Гарчгийн доорх жижиг мөр
11 *   data-accent      Үндсэн өнгө (HEX). Бусад өнгө үүнээс тооцоологдоно.
12 *   data-greeting    Эхний мэндчилгээ
13 *   data-suggestions Санал болгох асуултууд, "|"-ээр тусгаарлана
14 *   data-position    "right" (өгөгдмөл) эсвэл "left"
15 *   data-tooltip     Нээх товч дээр хулгана очиход гарах бичиг, мөрийг "|"-ээр тусгаарлана
16 *   data-logo        Цонхны толгойн лого (зургийн URL). "none" — логогүй
17 *   data-sound       "off" бол илгээх дуу анхнаасаа унтраалттай (хэрэглэгч толгойноос асаана)
18 *   data-sources     "on" бол хариултын доор эх сурвалжийн жагсаалт гаргана (одоогоор өгөгдмөл "off")
19 *
20 * Бүх бүтэц Shadow DOM дотор байрлана — сайтын CSS виджетэд, виджетийн CSS
21 * сайтад нөлөөлөхгүй. Хар загварыг (dark mode) системийн тохиргооноос дагана.
22 */
23(function () {
24  "use strict";
25
26  if (window.__masmChatLoaded) return;
27  window.__masmChatLoaded = true;
28
29  // Нээх товчны буланд харагдах хувилбар — шинэ хувилбар гаргахад энд солино.
30  var VERSION = "1.1.2";
31
32  var script = document.currentScript ||
33    document.querySelector('script[src*="masm-chat.js"]');
34
35  function attr(name, fallback) {
36    var value = script && script.getAttribute("data-" + name);
37    return value === null || value === undefined || value === "" ? fallback : value;
38  }
39
40  // API-ийн хаягийг скриптийн src-ээс гаргана: .../widget/masm-chat.js -> ...
41  function defaultApi() {
42    try {
43      var url = new URL(script.src, location.href);
44      return url.origin + url.pathname.replace(/\/widget\/[^/]*$/, "");
45    } catch (e) {
46      return location.origin;
47    }
48  }
49
50  var CFG = {
51    api: attr("api", defaultApi()).replace(/\/$/, ""),
52    title: attr("title", "MASM туслах"),
53    subtitle: attr("subtitle", "Албан ёсны эх сурвалжид тулгуурлана"),
54    accent: attr("accent", "#001f60"),   // masm.gov.mn-ийн үндсэн хар хөх
55    greeting: attr("greeting",
56      "Стандарт хэмжил зүйн газрын AI агент таныг сонсож байна."),
57    // Анхнаасаа жишээ асуултгүй — хэрэгтэй бол data-suggestions-ээр өгнө.
58    suggestions: attr("suggestions", "")
59      .split("|").map(function (s) { return s.trim(); }).filter(Boolean),
60    position: attr("position", "right"),
61    // Хариултын доорх эх сурвалжийн жагсаалт. Түр нуусан — буцааж нээхдээ
62    // өгөгдмөлийг "on" болгох эсвэл script таг дээр data-sources="on" өгнө.
63    sources: attr("sources", "off") === "on",
64    // "none" гэвэл лого ашиглахгүй (бамбайн дүрс)
65    logo: (function (v) { return v === "none" ? "" : v; })(
66      attr("logo", "https://masm.gov.mn/images/favoicon/favicon-32x32.png")),
67    tooltip: attr("tooltip", "Стандарт хэмжил зүйн газрын ухаалаг хөтөч|Masm AI Chatbot")
68      .split("|").map(function (s) { return s.trim(); }).filter(Boolean),
69    // Чат хаалттай үед товчны дээр байнга харагдах мэндчилгээ. Эхний мөр нь
70    // гарчиг, дараагийнх нь догол мөрүүд. "off" гэвэл огт гаргахгүй.
71    hello: (function (v) { return v === "off" ? [] : v.split("|"); })(
72      attr("hello", "Сайн байна уу.|Би Стандарт хэмжил зүйн гаÐ
72·Ñ€Ñ‹Ð½ хиймэл оюунт "
73                    + "чатбот байна.|Танд юугаар туслах вэ?"))
74      .map(function (s) { return s.trim(); }).filter(Boolean)
75  };
76
77  // --- Өнгөний туслахууд: accent-аас бүрэн палитр тооцоолно ---
78  function hexToRgb(hex) {
79    var h = String(hex).replace("#", "").trim();
80    if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
81    var n = parseInt(h, 16);
82    if (isNaN(n) || h.length !== 6) return { r: 27, g: 79, b: 156 };
83    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
84  }
85  function clamp(v) { return Math.max(0, Math.min(255, Math.round(v))); }
86  function shade(hex, amount) {
87    // amount < 0 бол бараан, > 0 бол цайвар
88    var c = hexToRgb(hex);
89    var t = amount < 0 ? 0 : 255;
90    var p = Math.abs(amount);
91    return "#" + [c.r, c.g, c.b].map(function (v) {
92      return clamp((t - v) * p + v).toString(16).padStart(2, "0");
93    }).join("");
94  }
95  function rgba(hex, alpha) {
96    var c = hexToRgb(hex);
97    return "rgba(" + c.r + "," + c.g + "," + c.b + "," + alpha + ")";
98  }
99
100  // localStorage — хөтчөө хааж нээсэн ч яриа сэргэнэ (сервер 30 хоног хадгална).
101  // Өмнөх хувилбар sessionStorage ашигладаг байсан тул тэндээс нэг удаа шилжүүлнэ.
102  var SESSION_KEY = "masm-chat-session";
103  var sessionId = null;
104  function saveSession(id) {
105    try {
106      if (id) localStorage.setItem(SESSION_KEY, id);
107      else localStorage.removeItem(SESSION_KEY);
108      sessionStorage.removeItem(SESSION_KEY);
109    } catch (e) { /* хаалттай */ }
110  }
111  try {
112    sessionId = localStorage.getItem(SESSION_KEY) || sessionStorage.getItem(SESSION_KEY);
113    if (sessionId) saveSession(sessionId);
114  } catch (e) { /* хаалттай */ }
115
116  var STYLE = `
117  :host { all: initial; }
118  *, *::before, *::after { box-sizing: border-box; }
119
120  .root {
121    /* --- өнгөний токенууд (JS-ээс утга онооно) --- */
122    --ink:        #141821;
123    --ink-soft:   #5b6472;
124    --ink-faint:  #8d95a3;
125    --surface:    #ffffff;
126    --canvas:     #f7f8fb;
127    --line:       #e7eaf0;
128    --line-soft:  #f0f2f6;
129    --bot-bubble: #ffffff;
130    --shadow-sm:  0 1px 2px rgba(16,20,28,.06);
131    --shadow-md:  0 4px 14px rgba(16,20,28,.09), 0 1px 3px rgba(16,20,28,.06);
132    --shadow-lg:  0 18px 50px -12px rgba(16,20,28,.32), 0 6px 18px -8px rgba(16,20,28,.18);
133    /* accent-ын уншигдах хувилбар — цайвар / хар дэвсгэрт тус тусдаа (JS тооцоолно).
134       Inline style @media-г дардаг тул JS эдгээрийг шууд биш --a-*-l/-d-ээр онооно. */
135    --accent-text: var(--a-text-l);
136    --accent-ring: var(--a-ring-l);
137    --accent-tint: var(--a-tint-l);
138    --accent-dot:  var(--a-dot-l);
139
140    position: fixed; bottom: 24px; z-index: 2147483000;
141    font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
142    font-size: 15px; line-height: 1.6;
143    color: var(--ink);
144    -webkit-font-smoothing: antialiased;
145  }
146  .root.right { right: 24px; }
147  .root.left  { left: 24px; }
148
149  /* Хар горим — хар хөх биш, төвийг сахисан хар саарал (R=G=B). */
150  @media (prefers-color-scheme: dark) {
151    .root {
152      --ink:        #ececec;
153      --ink-soft:   #b4b4b4;
154      --ink-faint:  #8a8a8a;
155      --surface:    #242424;
156      --canvas:     #2b2b2b;
157      --line:       #3d3d3d;
158      --line-soft:  #333333;
159      --bot-bubble: #3a3a3a;
160      --shadow-lg:  0 18px 50px -12px rgba(0,0,0,.6), 0 6px 18px -8px rgba(0,0,0,.4);
161      /* #001f60 шиг бараан accent хар саарал дээр харагдахгүй — цайвар хувилбар */
162      --accent-text: var(--a-text-d);
163      --accent-ring: var(--a-ring-d);
164      --accent-tint: var(--a-tint-d);
165      --accent-dot:  var(--a-dot-d);
166    }
167  }
168
169  /* ─────────────  Нээх товч  ───────────── */
170  .launcher {
171    position: relative;
172    width: 60px; height: 60px; border: 0; border-radius: 50%;
173    cursor: pointer; padding: 0;
174    background: linear-gradient(145deg, var(--accent-lift), var(--accent-deep));
175    color: #fff;
176    box-shadow: 0 10px 26px -6px var(--accent-glow), 0 2px 6px rgba(16,20,28,.18);
177    display: flex; align-items: center; justify-content: center;
178    transition: transform .22s cubic-bezier(.34,1.3,.64,1), box-shadow .22s ease;
179  }
180  .launcher:hover { transform: translateY(-2px) scale(1.04);
181    box-shadow: 0 16px 34px -8px var(--accent-glow), 0 3px 8px rgba(16,20,28,.2); }
182  .launcher:active { transform: translateY(0) scale(.97); }
183  .launcher:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 3px; }
184  .launcher svg { width: 25px; height: 25px; fill: currentColor; }
185
186  /* хувилбарын тэмдэг — нээх товчны баруун дээд буланд */
187  .launcher .ver {
188    position: absolute; top: -5px; right: -8px;
189    padding: 1px 6px; border-radius: 999px;
190    background: #f5c542; color: #001f60; border: 2px solid #fff;
191    font: 700 10px/1.35 "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
192    letter-spacing: .2px; white-space: nowrap; pointer-events: none;
193    box-shadow: 0 2px 6px rgba(16,20,28,.28);
194  }
195
196  /* ── AI чатбот дүрс (нээх товч) ── */
197  .launcher svg.bot { width: 34px; height: 34px; overflow: visible; }
198  .bot .face  { fill: #fff; }
199  .bot .stem  { stroke: #fff; stroke-width: 1.6; stroke-linecap: round; }
200  .bot .ant   { fill: #fff; transform-box: fill-box; transform-origin: center;
201                animation: bot-ant 1.8s ease-in-out infinite; }
202  .bot .eyes rect { fill: var(--accent-deep); }
203  .bot .eyes  { transform-box: fill-box; transform-origin: center;
204                animation: bot-blink 4.2s ease-in-out infinite; }
205  .bot .mouth { fill: none; stroke: var(--accent-deep); stroke-width: 1.5; stroke-linecap: round; }
206  .bot .spark { fill: #f5c542; transform-box: fill-box; transform-origin: center;
207                animation: bot-spark 2.6s ease-in-out infinite; }
208  .bot .float { animation: bot-float 3.2s ease-in-out infinite; }
209  .launcher:hover .bot .eyes { animation-duration: 1.2s; }   /* хулгана очиход "анхаарна" */
210  @keyframes bot-blink { 0%, 90%, 100% { transform: scaleY(1); } 94% { transform: scaleY(.12); } }
211  @keyframes bot-ant   { 0%, 100% { opacity: 1; transform: scale(1); }
212                         50% { opacity: .55; transform: scale(.72); } }
213  @keyframes bot-spark { 0%, 100% { opacity: 1; transform: scale(1) rotate(0deg); }
214                         50% { opacity: .35; transform: scale(.55) rotate(45deg); } }
215  @keyframes bot-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.4px); } }
216
217  /* анхаарал татах зөөлөн цацраг — нэг л удаа */
218  .launcher::after {
219    content: ""; position: absolute; inset: 0; border-radius: 50%;
220    border: 2px solid var(--accent-lift); opacity: 0;
221    animation: ping 2.6s cubic-bezier(0,0,.2,1) 1.2s 3;
222  }
223  @keyframes ping {
224    0%   { transform: scale(1);    opacity: .55; }
225    70%  { transform: scale(1.55); opacity: 0; }
226    100% { transform: scale(1.55); opacity: 0; }
227  }
228  .launcher.seen::after { animation: none; }
229
230  /* нээх товчны хажууд гарах тайлбар — зөвхөн хулганатай төхөөрөмж, эсвэл
231     гараар (Tab) очиход. Утсан дээр дэлгэцээс халиж, товчийг хаах тул гаргахгүй. */
232  .tip {
233    position: absolute; top: 50%;
234    padding: 7px 12px; border-radius: 8px;
235    background: var(--ink); color: var(--surface);
236    font-size: 13px; line-height: 1.35; font-weight: 500; white-space: nowrap;
237    box-shadow: var(--shadow-md);
238    pointer-events: none; opacity: 0; visibility: hidden;
239    transform: translateY(-50%) translateX(4px);
240    transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
241  }
242  .tip span { display: block; }
243  .tip span + span { font-size: 12px; font-weight: 400; opacity: .75; }
244  .root.right .tip { right: calc(100% + 12px); }
245  .root.left  .tip { left: calc(100% + 12px); transform: translateY(-50%) translateX(-4px); }
246  .tip::after {
247    content: ""; position: absolute; top: 50%; width: 8px; height: 8px;
248    background: inherit; transform: translateY(-50%) rotate(45deg);
249  }
250  .root.right .tip::after { right: -4px; }
251  .root.left  .tip::after { left: -4px; }
252  @media (hover: hover) {
253    .launcher:hover + .tip { opacity: 1; visibility: visible;
254      transform: translateY(-50%); transition-delay: .12s, .12s, 0s; }
255  }
256  .launcher:focus-visible + .tip { opacity: 1; visibility: visible; transform: translateY(-50%); }
257  .launcher[hidden] + .tip { display: none; }
258
259  /* ─────────  Мэндчилгээний бөмбөлөг (чат хаалттай үед товчны дээр)  ───────── */
260  .hello {
261    position: absolute; bottom: calc(100% + 14px);
262    width: 268px; max-width: calc(100vw - 40px);
263    padding: 13px 34px 14px 15px;
264    background: var(--surface); color: var(--ink);
265    border: 1px solid var(--line); border-radius: 14px;
266    box-shadow: var(--shadow-lg);
267    font-size: 13.5px; line-height: 1.55; text-align: left;
268    cursor: pointer;
269    opacity: 0; visibility: hidden; transform: translateY(6px);
270    transition: opacity .28s ease, transform .28s ease, visibility 0s linear .28s;
271  }
272  .root.right .hello { right: 0; }
273  .root.left  .hello { left: 0; }
274  .hello.on { opacity: 1; visibility: visible; transform: translateY(0); transition-delay: 0s; }
275  .hello::after {
276    content: ""; position: absolute; bottom: -6px; width: 11px; height: 11px;
277    background: var(--surface); border-right: 1px solid var(--line);
278    border-bottom: 1px solid var(--line); transform: rotate(45deg);
279  }
280  .root.right .hello::after { right: 22px; }
281  .root.left  .hello::after { left: 22px; }
282  .hello .t { font-weight: 650; margin-bottom: 6px; }
283  .hello .t i { font-style: normal; margin-right: 6px; }
284  .hello p { margin: 0 0 7px; }
285  .hello p:last-child { margin-bottom: 0; color: var(--ink-soft); }
286  .hello .x {
287    position: absolute; top: 6px; right: 6px;
288    width: 26px;
288 height: 26px; padding: 0; border: 0; border-radius: 7px;
289    background: none; color: var(--ink-faint); cursor: pointer;
290    font: 400 17px/1 "Segoe UI", system-ui, sans-serif;
291  }
292  .hello .x:hover { background: var(--line-soft); color: var(--ink); }
293  .hello .x:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 1px; }
294  /* Бөмбөлөг гарсан үед hover-ийн tooltip давхарлахгүй (.tip нь DOM-д өмнө
295     байдаг тул дүү сонгогч ажиллахгүй — root дээрх ангиар удирдана) */
296  .root.greeting .tip { display: none; }
297
298  /* ─────────────  Цонх  ───────────── */
299  .panel {
300    width: 396px; height: 600px; max-height: calc(100vh - 56px);
301    background: var(--surface); border-radius: 18px; overflow: hidden;
302    box-shadow: var(--shadow-lg);
303    border: 1px solid var(--line);
304    display: flex; flex-direction: column;
305    transform-origin: bottom right;
306    animation: rise .24s cubic-bezier(.22,1,.36,1);
307  }
308  .root.left .panel { transform-origin: bottom left; }
309  @keyframes rise {
310    from { opacity: 0; transform: translateY(14px) scale(.965); }
311    to   { opacity: 1; transform: translateY(0)    scale(1); }
312  }
313
314  /* ─────────────  Толгой  ───────────── */
315  .head {
316    flex: none; position: relative;
317    padding: 15px 16px;
318    background: linear-gradient(135deg, var(--accent-lift) 0%, var(--accent-deep) 100%);
319    color: #fff;
320    display: flex; align-items: center; gap: 11px;
321  }
322  .head::after {
323    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
324    background: rgba(255,255,255,.14);
325  }
326  .avatar {
327    width: 36px; height: 36px; flex: none; border-radius: 11px;
328    background: rgba(255,255,255,.16);
329    border: 1px solid rgba(255,255,255,.22);
330    display: flex; align-items: center; justify-content: center;
331  }
332  .avatar svg { width: 19px; height: 19px; fill: #fff; }
333  /* Байгууллагын лого — алтлаг тэмдэг хар хөх дээр бүдэг тул цагаан дугуйд */
334  .avatar.logo { border-radius: 50%; background: #fff; border: 0;
335                 box-shadow: 0 0 0 2px rgba(255,255,255,.28), 0 2px 6px rgba(0,0,0,.25); }
336  .avatar.logo img { width: 30px; height: 30px; display: block; }  .head .meta { min-width: 0; flex: 1; }
337  .head .t {
338    font-weight: 600; font-size: 15px; letter-spacing: -.01em;
339    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
340  }
341  .head .sub {
342    font-size: 11.5px; opacity: .82;
342 margin-top: 1px;
343    display: flex; align-items: center; gap: 5px;
344    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
345  }
346  .dot {
347    width: 6px; height: 6px; flex: none; border-radius: 50%;
348    background: #46d38a; box-shadow: 0 0 0 2px rgba(70,211,138,.28);
349  }
350  .head .acts { display: flex; gap: 2px; flex: none; }
351  .head button {
352    background: transparent; border: 0; color: #fff; cursor: pointer;
353    width: 32px; height: 32px; border-radius: 9px; opacity: .82;
354    display: flex; align-items: center; justify-content: center;
355    transition: background .15s ease, opacity .15s ease;
356  }
357  .head button svg { width: 16px; height: 16px; fill: currentColor; }
358  .head button:hover { background: rgba(255,255,255,.17); opacity: 1; }
359  .head button:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }
360
361  /* ─────────────  Яриа  ───────────── */
362  /* ─────────────  Таб (Асуулт хариулт / Стандарт хайх)  ───────────── */
363  .tabs {
364    display: flex; background: var(--surface);
365    border-bottom: 1px solid var(--line); padding: 0 8px;
366  }
367  .tab {
368    flex: 1; border: 0; background: none; cursor: pointer;
369    padding: 11px 8px 9px; font: inherit; font-size: 13.5px; font-weight: 600;
370    color: var(--ink-soft); border-bottom: 2px solid transparent; margin-bottom: -1px;
371  }
372  .tab:hover { color: var(--ink); }
373  .tab.on { color: var(--accent-text); border-bottom-color: var(--accent-text); }
374  .tab:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: -3px; }
375
376  /* ─────────────  Стандарт хайх  ───────────── */
377  .std {
378    flex: 1; display: flex; flex-direction: column; min-height: 0;
379    background: var(--canvas);
380  }
381  .std-box { padding: 12px 14px 8px; background: var(--surface);
382             border-bottom: 1px solid var(--line-soft); }
383  .std-q {
384    width: 100%; box-sizing: border-box; font: inherit; font-size: 14px;
385    padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
386    background: var(--canvas); color: var(--ink);
387  }
388  .std-q:focus { outline: 2px solid var(--accent-ring); border-color: var(--accent-text); }
389  .std-info { padding: 9px 16px 0; font-size: 12.5px; color: var(--ink-soft); }
390  .std-list {
391    flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 10px 14px 14px;
392    scrollbar-width: thin; scrollbar-color: var(--line) transparent;
393  }
394  .std-list::-webkit-scrollbar { width: 8px; }
395  .std-list::-webkit-scrollbar-thumb {
396    background: var(--line); border-radius: 99px; border: 2px solid var(--canvas);
397  }
398  .std-item {
399    background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
400    padding: 11px 13px; margin-bottom: 9px; box-shadow: var(--shadow-sm);
401  }
402  .std-item .num {
403    display: inline-block; font-size: 12px; font-weight: 700;
404    color: var(--accent-text); background: var(--accent-tint);
405    border-radius: 999px; padding: 2px 9px; margin-bottom: 5px;
406  }
407  .std-item .ttl { font-size: 13.5px; line-height: 1.5; margin-bottom: 5px; }
408  .std-item .meta2 { font-size: 12px; color: var(--ink-soft); line-height: 1.45; }
409  .std-item .free { color: #1f7a42; font-weight: 600; }
410  .std-item .go {
411    display: inline-flex; align-items: center; gap: 6px; margin-top: 9px;
412    padding: 6px 12px; border: 1px solid var(--accent-ring); border-radius: 999px;
413    background: var(--accent-tint); color: var(--accent-text);
414    font-size: 12.5px; font-weight: 600; text-decoration: none;
415  }
416  .std-item .go:hover { filter: brightness(.97); }
417  .std-item .go svg { width: 12px; height: 12px; fill: currentColor; }
418
419  .log {
420    flex: 1; overflow-y: auto; overscroll-behavior: contain;
421    padding: 18px 16px 8px; background: var(--canvas);
422    scrollbar-width: thin; scrollbar-color: var(--line) transparent;
423  }
424  .log::-webkit-scrollbar { width: 8px; }
425  .log::-webkit-scrollbar-thumb {
426    background: var(--line); border-radius: 99px;
427    border: 2px solid var(--canvas);
428  }
429  .log::-webkit-scrollbar-track { background: transparent; }
430
431  .msg { margin-bottom: 14px; display: flex; animation: pop .26s cubic-bezier(.22,1,.36,1); }
432  .msg.user { justify-content: flex-end; }
433  @keyframes pop {
434    from { opacity: 0; transform: translateY(7px); }
435    to   { opacity: 1; transform: translateY(0); }
436  }
437
438  .bubble {
439    max-width: 86%; padding: 11px 14px;
440    white-space: pre-wrap;
440 overflow-wrap: anywhere;
441    font-size: 14.5px; line-height: 1.62;
442  }
443  .msg.bot .bubble {
444    background: var(--bot-bubble); color: var(--ink);
445    border: 1px solid var(--line);
446    border-radius: 16px 16px 16px 5px;
447    box-shadow: var(--shadow-sm);
448  }
449  .msg.user .bubble {
450    background: linear-gradient(145deg, var(--accent-lift), var(--accent-deep));
451    color: #fff; border-radius: 16px 16px 5px 16px;
452    box-shadow: 0 3px 10px -3px var(--accent-glow);
453  }
454  .msg.err .bubble {
455    background: var(--err-bg); border: 1px solid var(--err-line);
456    color: var(--err-ink); border-radius: 16px 16px 16px 5px;
457  }
458  .bubble strong, .bubble b { font-weight: 650; }
459  /* Хариулт дотор гарсан холбоосыг "Дэлгэрэнгүй" товч болгож харуулна */
460  .bubble a.link-btn {
461    display: inline-flex; align-items: center; gap: 6px;
462    margin: 4px 0 2px; padding: 5px 11px;
463    border: 1px solid var(--accent-ring); border-radius: 999px;
464    background: var(--accent-tint); color: var(--accent-text);
465    font-size: 13px; font-weight: 600; line-height: 1.2;
466    text-decoration: none; white-space: nowrap;
467  }
468  .bubble a.link-btn:hover { background: var(--accent-soft, var(--accent-tint)); filter: brightness(0.97); }
469  .bubble a.link-btn:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
470  .bubble a.link-btn .ico { display: inline-flex; }
471  .bubble a.link-btn .ico svg { width: 13px; height: 13px; }
472
473  /* ─────────────  Эх сурвалж  ───────────── */
474  /* Хариултыг дуугаар уншуулах товч — хөөсний баруун дээд буланд, зөвхөн дүрс */
475  .bubble.has-speak { position: relative; padding-right: 40px; }
476  .speak {
477    position: absolute; top: 7px; right: 7px;
478    display: inline-flex; align-items: center; justify-content: center;
479    width: 26px; height: 26px; padding: 0; border-radius: 999px; cursor: pointer;
480    border: 1px solid transparent; background: transparent; color: var(--ink-soft);
481    transition: color .15s ease, border-color .15s ease, background .15s ease;
482  }
483  .speak svg { width: 15px; height: 15px; fill: currentColor; flex: none; }
484  .speak:hover { color: var(--accent-text); border-color: var(--accent-ring); }
485  .speak.on { color: var(--accent-text); border-color: var(--accent-ring); background: var(--accent-tint); }
486  .speak.on svg { animation: speak-pulse 1s ease-in-out infinite; }
487  @keyframes speak-pulse { 50% { opacity: .45; } }
488  .speak:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
489
490  .srcs { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line-soft); }
491  .srcs .lbl {
492    font-size: 10.5px; font-weight: 600; letter-spacing: .07em;
493    text-transform: uppercase; color: var(--ink-faint); margin-bottom: 7px;
494  }
495  .srcs a {
496    display: flex; align-items: flex-start; gap: 8px;
497    padding: 7px 9px; margin-bottom: 4px; border-radius: 9px;
498    background: var(--canvas); border: 1px solid var(--line-soft);
499    color: var(--ink-soft); text-decoration: none;
500    font-size: 12.5px; line-height: 1.45;
501    transition: border-color .15s ease, background .15s ease, transform .15s ease;
502  }
503  .srcs a:hover {
504    border-color: var(--accent-ring); background: var(--accent-tint);
505    color: var(--accent-text); transform: translateX(2px);
506  }
507  .srcs a:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 1px; }
508  .srcs .ico { flex: none; display: flex; margin-top: 2px; opacity: .65; }
509  .srcs .ico svg { width: 13px; height: 13px; fill: currentColor; }
510  .srcs .nm { min-width: 0; }
511
512  /* ─────────────  Цаг үеийн мэдээ (2 баганатай зурагтай карт)  ───────────── */
513  .bubble.has-news { width: 86%; }
514  .news { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; white-space: normal; }
515  .news a {
516    display: flex; flex-direction: column; min-width: 0; overflow: hidden;
517    border: 1px solid var(--line-soft); border-radius: 10px; background: var(--canvas);
518    color: var(--ink); text-decoration: none;
519    transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
520  }
521  .news a:hover { border-color: var(--accent-ring); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
522  .news a:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 1px; }
523  .news img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: var(--line-soft); }
524  .news .nw-b { padding: 6px 8px 8px; }
525  .news .nw-d { font-size: 11px; color: var(--ink-faint); }
526  .news .nw-t {
527    font-size: 12.5px; line-height: 1.35; font-weight: 600; margin-top: 2px;
528    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
529  }
530
531  /* ─────────────  Санал болгох асуултууд  ───────────── */
532  .chips { display: flex; flex-wrap: wrap; gap: 7px; margin: 2px 0 16px 0; }
533  .chips button {
534    font: inherit; font-size: 13px; line-height: 1.4;
535    padding: 8px 13px; border-radius: 99px; cursor: pointer;
536    background: var(--surface); color: var(--accent-text);
537    border: 1px solid var(--accent-ring);
538    transition: background .15s ease, transform .15s ease, box-shadow .15s ease;
539  }
540  .chips button:hover {
541    background: var(--accent-tint); transform: translateY(-1px);
542    box-shadow: var(--shadow-sm);
543  }
544  .chips button:active { transform: translateY(0); }
545  .chips button:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
546
547  /* ─────────────  Ачаалж буй байдал  ───────────── */
548  .prog { display: flex; align-items: center; gap: 10px; color: var(--ink-soft); font-size: 13.5px; }
549  .prog .txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
550  .typing { display: flex; gap: 4px; align-items: center; flex: none; }
551  .typing i {
552    width: 6px; height: 6px; border-radius: 50%; background: var(--accent-dot);
553    display: block; animation: bounce 1.25s ease-in-out infinite;
554  }
555  .typing i:nth-child(2) { animation-delay: .16s; }
556  .typing i:nth-child(3) { animation-delay: .32s; }
557  @keyframes bounce {
558    0%, 60%, 100% { transform: translateY(0);    opacity: .4; }
559    30%           { transform: translateY(-5px); opacity: 1; }
560  }
561  .caret {
562    display: inline-block; width: 2px; height: 1.05em; margin-left: 2px;
563    background: var(--accent-dot); border-radius: 1px;
564    vertical-align: text-bottom; animation: blink 1.1s steps(2, start) infinite;
565  }
566  @keyframes blink { 50% { opacity: 0; } }
567
568  /* ─────────────  Оруулах хэсэг  ───────────── */
569  .foot {
570    flex: none; border-top: 1px solid var(--line);
571    padding: 11px 12px 12px; background: var(--surface);
572  }
573  .foot form {
574    display: flex; align-items: flex-end; gap: 8px;
575    background: var(--canvas); border: 1px solid var(--line);
576    border-radius: 14px; padding: 5px 5px 5px 6px;
577    transition: border-color .15s ease, box-shadow .15s ease;
578  }
579  .foot form:focus-within {
580    border-color: var(--accent-ring);
581    box-shadow: 0 0 0 3px var(--accent-tint);
582  }
583  .foot textarea {
584    flex: 1; min-width: 0; resize: none; border: 0; outline: 0;
585    background: transparent; color: var(--ink);
586    font: inherit; font-size: 14.5px; line-height: 1.5;
587    padding: 7px 4px 7px 6px; max-height: 108px;
588  }
589  .foot textarea::placeholder { color: var(--ink-faint); }
590  .send {
591    flex: none; width: 36px; height: 36px; border: 0; border-radius: 10px;
592    cursor: pointer; color: #fff;
593    background: linear-gradient(145deg, var(--accent-lift), var(--accent-deep));
594    display: flex; align-items: center; justify-content: center;
595    transition: transform .15s ease, opacity .15s ease;
596  }
597  .send svg { width: 16px; height: 16px; fill: currentColor; }
598  .send:hover:not(:disabled) { transform: scale(1.06); }
599  .send:active:not(:disabled) { transform: scale(.94); }
600  .send:disabled { opacity: .38; cursor: default; }
601  .send:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
602
603  .note {
604    margin-top: 8px; font-size: 11px; color: var(--ink-faint);
605    text-align: center; line-height: 1.45;
606  }
607  .note a { color: var(--ink-soft); font-weight: 600; text-decoration: none; }
608  .note a:hover { color: var(--accent-text); text-decoration: underline; }
609  .note a:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 1px; border-radius: 3px; }
610
611  [hidden] { display: none !important; }
612
613  /* ─────────────  Утас  ───────────── */
614  /* ─────────  Гар утас: нээхэд бүтэн дэлгэц, зөвхөн чатбот  ───────── */
615  @media (max-width: 540px) {
616    .root { bottom: 14px; }
617    .root.right { right: 14px; }
618    .root.left  { left: 14px; }
619    .panel {
620      position: fixed; left: 0; right: 0; top: var(--vvt, 0px);
621      width: 100%;
622      height: 100vh;                       /* хуучин хөтөч */
623      height: var(--vvh, 100dvh);          /* гар гарвал JS багасгана (fitViewport) */
624      max-height: none;
625      border: 0; border-radius: 0; box-shadow: none;
626      animation: sheet .26s cubic-bezier(.22,1,.36,1);
627    }
628    /* Утасны дээд/доод хүрээ (notch, home indicator) */
629    .head { padding-top: calc(15px + env(safe-area-inset-top, 0px)); }
630    .foot { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
631    /* 16px-ээс жижиг бол iPhone фокуслахад дэлгэцийг томруулдаг */
632    .foot textarea, .std-q { font-size: 16px; }
633  }
634  @keyframes sheet {
635    from { opacity: .6; transform: translateY(24px); }
636    to   { opacity: 1;  transform: translateY(0); }
637  }
638
639  @media (prefers-reduced-motion: reduce) {
640    .launcher, .launcher svg, .panel, .msg, .typing i, .caret,
641    .srcs a, .chips button, .send, .tip, .hello,
642    .bot .float, .bot .eyes, .bot .ant, .bot .spark, .speak.on svg { animation: none !important; transition: none !important; }
643    .launcher::after { display: none; }
644  }`;
645
646  var ICON = {
647    // AI чатбот: чатын бөмбөлөг хэлбэрийн робот толгой — анивчдаг нүд,
648    // лугшдаг антен, гялалздаг алтан оч (MASM-ын алт). Хөдөлгөөн нь CSS-д (.bot).
649    chat: '<svg class="bot" viewBox="0 0 32 32" aria-hidden="true">' +
650      '<g class="float">' +
651        '<line class="stem" x1="14" y1="5" x2="14" y2="8.5"/>' +
652        '<circle class="ant" cx="14" cy="3.8" r="1.8"/>' +
653        '<path class="face" d="M7 8.5h14a5 5 0 0 1 5 5v6.5a5 5 0 0 1-5 5h-6.5L9.5 29v-4H7a5 5 0 0 1-5-5v-6.5a5 5 0 0 1 5-5z"/>' +
654        '<g class="eyes"><rect x="8.4" y="13.6" width="3.2" height="4.4" rx="1.6"/>' +
655          '<rect x="16.4" y="13.6" width="3.2" height="4.4" rx="1.6"/></g>' +
656        '<path class="mouth" d="M11.3 20.6q2.7 1.9 5.4 0"/>' +
657      '</g>' +
658      '<path class="spark" d="M27 1.5l.95 2.3 2.3.95-2.3.95L27 8l-.95-2.3-2.3-.95 2.3-.95z"/>' +
659      '</svg>',
660    close: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.3 5.71 12 12l6.3 6.29-1.42 1.42L10.59 13.4 4.3 19.71 2.88 18.3 9.17 12 2.88 5.71 4.3 4.29l6.29 6.3 6.29-6.3z"/></svg>',
661    shield: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 4 5v6.1c0 4.7 3.4 9.1 8 10.9 4.6-1.8 8-6.2 8-10.9V5l-8-3zm-1.1 13.4-3.2-3.2 1.4-1.4 1.8 1.8 4.6-4.6 1.4 1.4-6 6z"/></svg>',
662    refresh: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>',
663    soundOn: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05A4.47 4.47 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06A9 9 0 0 0 14 3.23z"/></svg>',
664    stop: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
665    soundOff: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.8 8.8 0 0 0 21 12a9 9 0 0 0-7-8.77v2.06A7 7 0 0 1 19 12zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a9 9 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z"/></svg>',
666    send: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.4 20.4 21 12 3.4 3.6 3.4 10l12.6 2-12.6 2z"/></svg>',
667    link: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7zM5 5h5V3H3v18h18v-7h-2v5H5V5z"/></svg>'
668  };
669
670  // --- DOM барих ---
671  var host = document.createElement("div");
672  document.body.appendChild(host);
673  var sr = host.attachShadow({ mode: "open" });
674
675  var style = document.createElement("style");
676  style.textContent = STYLE;
677  sr.appendChild(style);
678
679  var root = document.createElement("div");
680  root.className = "root " + (CFG.position === "left" ? "left" : "right");
681
682  // accent-аас бүрэн палитр
683  root.style.setProperty("--accent", CFG.accent);
684  root.style.setProperty("--accent-lift", shade(CFG.accent, 0.12));
685  root.style.setProperty("--accent-deep", shade(CFG.accent, -0.22));
686  // Цайвар (-l) ба хар (-d) горимын хувилбар — аль нь идэвхтэйг CSS сонгоно.
687  var onDark = shade(CFG.accent, 0.62);   // хар саарал дэвсгэр дээр уншигдах цайвар
688  root.style.setProperty("--a-text-l", shade(CFG.accent, -0.05));
689  root.style.setProperty("--a-text-d", onDark);
690  root.style.setProperty("--a-ring-l", rgba(CFG.accent, 0.32));
691  root.style.setProperty("--a-ring-d", rgba(onDark, 0.45));
692  root.style.setProperty("--a-tint-l", rgba(CFG.accent, 0.07));
693  root.style.setProperty("--a-tint-d", rgba(onDark, 0.10));
694  root.style.setProperty("--a-dot-l", shade(CFG.accent, 0.12));
695  root.style.setProperty("--a-dot-d", onDark);
696  root.style.setProperty("--accent-glow", rgba(CFG.accent, 0.45));
697  root.style.setProperty("--err-bg", "rgba(203,49,49,.07)");
698  root.style.setProperty("--err-line", "rgba(203,49,49,.28)");
699  root.style.setProperty("--err-ink", "#b02727");
700
701  root.innerHTML = `
702    <button class="launcher" aria-label="${esc(CFG.title)} — чат нээх"
703            aria-expanded="false" aria-controls="masm-chat-panel">${ICON.chat}<span
704            class="ver" aria-hidden="true">v${esc(VERSION)}</span></button>
705    <span class="tip" aria-hidden="true">${CFG.tooltip.map(function (line) {
706      return "<span>" + esc(line) + "</span>";
707    }).join("")}</span>
708    ${CFG.hello.length ? `<div class="hello" role="note">
709      <button class="x" type="button" aria-label="Мэндчилгээг хаах">&times;</button>
710      <div class="t"><i aria-hidden="true">&#128075;</i>${esc(CFG.hello[0])}</div>
711      ${CFG.hello.slice(1).map(function (line) {
712        return "<p>" + esc(line) + "</p>";
713      }).join("")}
714    </div>` : ""}
715    <div class="panel" id="masm-chat-panel" role="dialog"
716         aria-label="${esc(CFG.title)}" hidden>
717      <div class="head">
718        <div class="avatar${CFG.logo ? " logo" : ""}">${CFG.logo
719          ? '<img src="' + esc(CFG.logo) + '" alt="" width="30" height="30">'
720          : ICON.shield}</div>
721        <div class="meta">
722          <div class="t">${esc(CFG.title)}</div>
723          <div class="sub"><span class="dot"></span>${esc(CFG.subtitle)}</div>
724        </div>
725        <div class="acts">
726          <button class="sound" aria-pressed="true"></button>
727          <button class="reset" title="Шинэ яриа эхлүүлэх"
728                  aria-label="Шинэ яриа эхлүүлэх">${ICON.refresh}</button>
729          <button class="close" title="Хаах" aria-label="Хаах">${ICON.close}</button>
730        </div>
731      </div>
732      <div class="tabs" role="tablist">
733        <button class="tab on" type="button" role="tab" data-view="chat"
734                aria-selected="true">Асуулт хариулт</button>
735        <button class="tab" type="button" role="tab" data-view="std"
736                aria-selected="false">Стандарт хайх</button>
737      </div>
738      <div class="log" role="log" aria-live="polite" aria-atomic="false"></div>
739      <div class="std" role="tabpanel" hidden>
740        <div class="std-box">
741          <input type="search" class="std-q" autocomplete="off"
742                 placeholder="Стандартын дугаар эсвэл нэр…"
743                 aria-label="Стандарт хайх">
744        </div>
745        <div class="std-info"></div>
746        <div class="std-list"></div>
747      </div>
748      <div class="foot">
749        <form>
750          <textarea rows="1" placeholder="Асуултаа бичнэ үү…"
751                    aria-label="Асуулт" autocomplete="off"></textarea>
752          <button type="submit" class="send" aria-label="Илгээх">${ICON.send}</button>
753        </form>
754        <div class="note">Хөгжүүлэгч: <a href="https://www.facebook.com/tselmegsoft"
755          target="_blank" rel="noopener noreferrer">Цэлмэг Софт ХХК</a></div>
756      </div>
757    </div>`;
758  sr.appendChild(root);
759
760  // Лого ачаалагдахгүй бол толгойд бамбайн дүрс харуулна.
761  sr.querySelectorAll(".avatar.logo img").forEach(function (img) {
762    img.addEventListener("error", function () {
763      var box = img.parentNode;
764      box.classList.remove("logo");
765      box.innerHTML = ICON.shield;
766    });
767  });
768
769  var elLauncher = sr.querySelector(".launcher");
770  var elHello = sr.querySelector(".hello");
771  var elPanel = sr.querySelector(".panel");
772  var elLog = sr.querySelector(".log");
773  var elForm = sr.querySelector("form");
774  var elInput = sr.querySelector("textarea");
775  var elSend = sr.querySelector(".send");
776  var elClose = sr.querySelector(".close");
777  var elReset = sr.querySelector(".reset");
778  var elSound = sr.querySelector(".sound");
779  var elFoot = sr.querySelector(".foot");
780  var elStd = sr.querySelector(".std");
781  var elStdQ = sr.querySelector(".std-q");
782  var elStdList = sr.querySelector(".std-list");
783  var elStdInfo = sr.querySelector(".std-info");
784
785  // --- Илгээх дуу: Web Audio-оор нийлэгжүүлнэ (аудио файл татахгүй) ---
786  // Хэрэглэгч товч дарсан үед л тоглогдох тул хөтчийн autoplay хязгаарт өртөхгүй.
787  // Унтраасан эсэхийг хөтөч санана; data-sound="off" бол анхнаасаа унтраалттай.
788  var SOUND_KEY = "masm-chat-sound";
789  var soundOn = attr("sound", "on") !== "off";
790  try {
791    var saved = localStorage.getItem(SOUND_KEY);
792    if (saved !== null) soundOn = saved === "on";
793  } catch (e) { /* хаалттай */ }
794  var audioCtx = null;
795
796  function renderSound() {
797    elSound.innerHTML = soundOn ? ICON.soundOn : ICON.soundOff;
798    elSound.setAttribute("aria-pressed", String(soundOn));
799    var label = soundOn ? "Дуу унтраах" : "Дуу асаах";
800    elSound.title = label;
801    elSound.setAttribute("aria-label", label);
802  }
803  renderSound();
804
805  // notes: [эхлэх давтамж, төгсөх давтамж, хоцрох сек, үргэлжлэх сек, чанга, долгион]
806  function playNotes(notes) {
807    if (!soundOn) return;
808    try {
809      var Ctx = window.AudioContext || window.webkitAudioContext;
810      if (!Ctx) return;
811      audioCtx = audioCtx || new Ctx();
812      if (audioCtx.state === "suspended") audioCtx.resume();
813      var t = audioCtx.currentTime;
814      notes.forEach(function (n) {
815        var start = t + n[2], dur = n[3];
816        var osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
817        osc.type = n[5] || "sine";
818        osc.frequency.setValueAtTime(n[0], start);
819        if (n[1] !== n[0]) osc.frequency.exponentialRampToValueAtTime(n[1], start + dur * 0.6);
820        // Хонх шиг: хурдан дуугарч, удаан намжина
821        gain.gain.setValueAtTime(0.0001, start);
822        gain.gain.exponentialRampToValueAtTime(n[4], start + 0.012);
823        gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
824        osc.connect(gain).connect(audioCtx.destination);
825        osc.start(start);
826        osc.stop(start + dur + 0.02);
827      });
828    } catch (e) { /* дуу заавал биш */ }
829  }
830
831  // Илгээх: хоёр өнгөтэй зөөлөн "блип" — дээшээ гулсах нь "илгээгдлээ" гэсэн мэдрэмж.
832  function playSend() {
833    playNotes([[587, 880, 0, 0.14, 0.09], [880, 1175, 0.07, 0.14, 0.09]]);
834  }
835
836  // Нээх: C–E–G–C дээшээ гарах хонхны аккорд + нэг октав дээрх гялбаа — "тавтай морил".
837  function playOpen() {
838    playNotes([
839      [523.25, 523.25, 0.00, 0.55, 0.075],
840      [659.25, 659.25, 0.07, 0.55, 0.070],
841      [783.99, 783.99, 0.14, 0.60, 0.065],
842      [1046.5, 1046.5, 0.21, 0.80, 0.060],
843      [2093.0, 2093.0, 0.21, 0.45, 0.012, "triangle"]   // хонхны өнгө
844    ]);
845  }
846
847  elSound.addEventListener("click", function () {
848    soundOn = !soundOn;
849    try { localStorage.setItem(SOUND_KEY, soundOn ? "on" : "off"); } catch (e) {}
850    renderSound();
851    if (!soundOn) stopSpeech();
852    playSend();   // асаахад жишээ болгож нэг удаа сонсгоно
853  });
854
855  // --- Хариултыг эмэгтэй хоолойгоор уншуулах (сервер: /tts, edge-tts) ---
856  // Сервер зөвхөн тухайн сессийн ботын хариултыг уншина: виджет текстийн
857  // SHA-256-ийн эхний 16 тэмдэгтийг илгээж, сервер хадгалсан хариултаас хайна.
858  // Автоматаар уншихгүй — хариулт бүрийн баруун дээд буланд байгаа яригчийн
859  // дүрсийг хэрэглэгч өөрөө дарж тоглуулна (дахин дарвал зогсоно).
860  var canSpeak = !!(window.crypto && window.crypto.subtle && window.Audio && window.TextEncoder);
861  var speech = null;   // { audio, btn }
862
863  async function textHash(text) {
864    var buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text.trim()));
865    return Array.prototype.map.call(new Uint8Array(buf), function (b) {
866      return ("0" + b.toString(16)).slice(-2);
867    }).join("").slice(0, 16);
868  }
869
870  function setSpeakBtn(btn, playing) {
871    btn.innerHTML = playing ? ICON.stop : ICON.soundOn;
872    var label = playing ? "Зогсоох" : "Харилцан яриаг тоглуулах";
873    btn.title = label;
874    btn.setAttribute("aria-label", label);
875    btn.classList.toggle("on", playing);
876    btn.setAttribute("aria-pressed", String(playing));
877  }
878
879  function stopSpeech() {
880    if (!speech) return;
881    var s = speech;
882    speech = null;
883    try { s.audio.pause(); s.audio.removeAttribute("src"); s.audio.load(); } catch (e) {}
884    setSpeakBtn(s.btn, false);
885  }
886
887  async function speak(text, btn) {
888    stopSpeech();
889    if (!canSpeak || !sessionId) return;
890    var audio = new Audio();
891    var mine = { audio: audio, btn: btn };
892    speech = mine;
893    setSpeakBtn(btn, true);
894    var finish = function () { if (speech === mine) stopSpeech(); };
895    audio.addEventListener("ended", finish);
896    audio.addEventListener("error", finish);
897    try {
898      var h = await textHash(text);
899      if (speech !== mine) return;   // хүлээх хооронд зогсоосон
900      audio.src = CFG.api + "/tts?s=" + encodeURIComponent(sessionId) + "&h=" + h;
901      await audio.play();
902    } catch (e) {
903      // Сүлжээ эсвэл хөтчийн алдаа — товчийг анхны төлөвт нь буцаана.
904      finish();
905    }
906  }
907
908  function addSpeakButton(bubble, text) {
909    if (!canSpeak || !text) return;
910    var btn = document.createElement("button");
911    btn.type = "button";
912    btn.className = "speak";
913    setSpeakBtn(btn, false);
914    btn.addEventListener("click", function () {
915      if (speech && speech.btn === btn) stopSpeech(); else speak(text, btn);
916    });
917    bubble.classList.add("has-speak");
918    bubble.appendChild(btn);
919  }
920
921  var open = false;
922  var busy = false;
923
924  function esc(s) {
925    return String(s).replace(/[&<>"']/g, function (c) {
926      return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
927    });
928  }
929
930  function scrollDown() { elLog.scrollTop = elLog.scrollHeight; }
931
932  // Хариулт дотор бичигдсэн холбоосыг (ж нь стандартын дэлгэрэнгүй хуудас)
933  // урт хаягаар бус "Дэлгэрэнгүй" товчоор харуулна. Өмнөх "Дэлгэрэнгүй:" гэсэн
934  // шошгыг товч давхарлахгүйн тулд залгина. innerHTML ашиглахгүй — зөвхөн DOM.
935  var URL_RE = /(дэлгэрэнгүй|холбоос|линк|дэлгэрэнгүйг)\s*:?\s*(https?:\/\/[^\s<>"')]+)|(https?:\/\/[^\s<>"')]+)/gi;
936  // Товч болгох цорын ганц холбоос — стандартын цахим сангийн дэлгэрэнгүй хуудас.
937  // Мэдлэгийн сангийн файл (/chatbot/kb/...) зэрэг бусад холбоос текст хэвээрээ
938  // үлдэнэ; тэдгээр нь хариултын доорх "Эх сурвалж" жагсаалтад гарна.
939  var DETAIL_RE = /^https?:\/\/(?:www\.)?estandard\.gov\.mn\/website\/masm\/standards_details\.aspx\?/i;
940
941  function linkButton(url, label) {
942    var a = document.createElement("a");
943    a.href = url;
944    a.target = "_blank";
945    a.rel = "noopener noreferrer";
946    a.className = "link-btn";
947    a.title = url;
948    var icon = document.createElement("span");
949    icon.className = "ico";
950    icon.innerHTML = ICON.link;
951    var name = document.createElement("span");
952    name.textContent = label;
953    a.appendChild(icon);
954    a.appendChild(name);
955    return a;
956  }
957
958  function linkify(host, text) {
959    host.textContent = "";
960    var last = 0, match;
961    URL_RE.lastIndex = 0;
962    while ((match = URL_RE.exec(text)) !== null) {
963      // Өгүүлбэрийн төгсгөлийн цэг, таслалыг холбоосоос гаргана
964      var raw = match[2] || match[3];
965      var url = raw.replace(/[.,;:!?]+$/, "");
966      if (match.index > last) {
967        host.appendChild(document.createTextNode(text.slice(last, match.index)));
968      }
969      // Хаягаас таслаад авсан цэг, таслалыг текст хэвээр нь үлдээнэ
970      var end = match.index + match[0].length - (raw.length - url.length);
971      if (DETAIL_RE.test(url)) {
972        host.appendChild(linkButton(url, "Дэлгэрэнгүй"));
973      } else {
974        host.appendChild(document.createTextNode(text.slice(match.index, end)));
975      }
976      last = end;
977      URL_RE.lastIndex = last;
978    }
979    if (last < text.length) host.appendChild(document.createTextNode(text.slice(last)));
980  }
981
982  function addMessage(kind, text) {
983    var row = document.createElement("div");
984    row.className = "msg " + kind;
985    var bubble = document.createElement("div");
986    bubble.className = "bubble";
987    if (text) {
988      if (kind === "bot") linkify(bubble, text);
989      else bubble.textContent = text;
990    }
991    row.appendChild(bubble);
992    elLog.appendChild(row);
993    scrollDown();
994    return bubble;
995  }
996
997  // Цаг үеийн мэдээ: сервер `news` event (эсвэл сэргээсэн яриаг `kind: "news"`
998  // эх сурвалжаар) илгээнэ — 2 баганатай зурагтай карт, дарахад мэдээ шинэ табд.
999  function renderNews(bubble, items) {
1000    items = (items || []).filter(function (n) { return n && n.kind === "
1000news" && n.url; });
1001    if (!items.length) return;
1002    var grid = document.createElement("div");
1003    grid.className = "news";
1004    items.forEach(function (n) {
1005      var a = document.createElement("a");
1006      a.href = n.url;
1007      a.target = "_blank";
1008      a.rel = "noopener noreferrer";
1009      a.title = n.title || "";
1010      if (n.image) {
1011        var img = document.createElement("img");
1012        img.src = n.image;
1013        img.alt = "";
1014        img.loading = "lazy";
1015        img.addEventListener("error", function () { img.remove(); });
1016        img.addEventListener("load", scrollDown);
1017        a.appendChild(img);
1018      }
1019      var body = document.createElement("div");
1020      body.className = "nw-b";
1021      if (n.date) {
1022        var d = document.createElement("div");
1023        d.className = "nw-d";
1024        d.textContent = n.date;
1025        body.appendChild(d);
1026      }
1027      var t = document.createElement("div");
1028      t.className = "nw-t";
1029      t.textContent = n.title || n.url;
1030      body.appendChild(t);
1031      a.appendChild(body);
1032      grid.appendChild(a);
1033    });
1034    bubble.classList.add("has-news");
1035    bubble.appendChild(grid);
1036  }
1037
1038  function renderSources(bubble, sources) {
1039    sources = (sources || []).filter(function (s) { return s.kind !== "news"; });
1040    if (!CFG.sources || !sources.length) return;
1041    // Нэг URL олон удаа давтагдвал нэг л удаа харуулна.
1042    var seen = {}, unique = [];
1043    sources.forEach(function (s) {
1044      if (!seen[s.url]) { seen[s.url] = 1; unique.push(s); }
1045    });
1046
1047    var box = document.createElement("div");
1048    box.className = "srcs";
1049    var label = document.createElement("div");
1050    label.className = "lbl";
1051    label.textContent = "Эх сурвалж";
1052    box.appendChild(label);
1053
1054    unique.slice(0, 4).forEach(function (s) {
1055      var a = document.createElement("a");
1056      a.href = s.url;
1057      a.target = "_blank";
1058      a.rel = "noopener noreferrer";
1059      var icon = document.createElement("span");
1060      icon.className = "ico";
1061      icon.innerHTML = ICON.link;
1062
1063      var name = document.createElement("span");
1064      name.className = "nm";
1065      name.textContent = s.title || s.url;
1066
1067      a.appendChild(icon);
1068      a.appendChild(name);
1069      box.appendChild(a);
1070    });
1071    bubble.appendChild(box);
1072    scrollDown();
1073  }
1074
1075  function setBusy(state) {
1076    busy = state;
1077    elSend.disabled = state;
1078    elInput.disabled = state;
1079  }
1080
1081  function autoGrow() {
1082    elInput.style.height = "auto";
1083    elInput.style.height = Math.min(elInput.scrollHeight, 108) + "px";
1084  }
1085
1086  function greet() {
1087    elLog.innerHTML = "";
1088    addMessage("bot", CFG.greeting);
1089    if (!CFG.suggestions.length) return;
1090
1091    var chips = document.createElement("div");
1092    chips.className = "chips";
1093    CFG.suggestions.forEach(function (question) {
1094      var b = document.createElement("button");
1095      b.type = "button";
1096      b.textContent = question;
1097      b.addEventListener("click", function () {
1098        if (busy) return;
1099        chips.remove();
1100        send(question);
1101      });
1102      chips.appendChild(b);
1103    });
1104    elLog.appendChild(chips);
1105    scrollDown();
1106  }
1107
1108  // --- SSE уншиж, хариуг урсгах ---
1109  async function send(question) {
1110    var chips = sr.querySelector(".chips");
1111    if (chips) chips.remove();
1112
1113    stopSpeech();
1114    playSend();
1115    addMessage("user", question);
1116    setBusy(true);
1117
1118    var bubble = addMessage("bot", "");
1119    var prog = document.createElement("div");
1120    prog.className = "prog";
1121    prog.innerHTML = '<span class="typing"><i></i><i></i><i></i></span>' +
1122                     '<span class="txt">Бодож байна…</span>';
1123    bubble.appendChild(prog);
1124
1125    var textNode = document.createElement("span");
1126    var caret = document.createElement("span");
1127    caret.className = "caret";
1128    caret.hidden = true;
1129    bubble.appendChild(textNode);
1130    bubble.appendChild(caret);
1131    scrollDown();
1132
1133    var answer = "";
1134
1135    try {
1136      var payload = { question: question, top_k: 5 };
1137      if (sessionId) payload.session_id = sessionId;
1138
1139      var res = await fetch(CFG.api + "/chat/stream", {
1140        method: "POST",
1141        headers: { "Content-Type": "application/json" },
1142        body: JSON.stringify(payload)
1143      });
1144
1145      if (!res.ok) {
1146        prog.remove();
1147        bubble.parentElement.className = "msg err";
1148        textNode.textContent = "Уучлаарай, алдаа гарлаа (" + res.status + ").";
1149        return;
1150      }
1151
1152      var reader = res.body.getReader();
1153      var decoder = new TextDecoder();
1154      var buffer = "";
1155
1156      while (true) {
1157        var step = await reader.read();
1158        if (step.done) break;
1159        buffer += decoder.decode(step.value, { stream: true });
1160
1161        var parts = buffer.split("\n\n");
1162        buffer = parts.pop();
1163
1164        for (var i = 0; i < parts.length; i++) {
1165          var evName = "message", raw = "";
1166          var lines = parts[i].split("\n");
1167          for (var j = 0; j < lines.length; j++) {
1168            if (lines[j].indexOf("event:") === 0) evName = lines[j].slice(6).trim();
1169            else if (lines[j].indexOf("data:") === 0) raw += lines[j].slice(5).trim();
1170          }
1171          if (!raw) continue;
1172          var data = JSON.parse(raw);
1173
1174          if (evName === "session") {
1175            sessionId = data.session_id;
1176            saveSession(sessionId);
1177          } else if (evName === "status") {
1178            var t = prog.querySelector(".txt");
1179            if (t) t.textContent = data.text;
1180          } else if (evName === "sources") {
1181            bubble.__sources = data.sources;
1182          } else if (evName === "news") {
1183            bubble.__news = data.items;
1184          } else if (evName === "delta") {
1185            if (!answer) { prog.remove();
1185 caret.hidden = false; }
1186            answer += data.text;
1187            textNode.textContent = answer;
1188            scrollDown();
1189          } else if (evName === "error") {
1190            prog.remove();
1191            caret.hidden = true;
1192            bubble.parentElement.className = "msg err";
1193            textNode.textContent = data.detail;
1194          } else if (evName === "done") {
1195            caret.hidden = true;
1196            if (answer) {
1197              linkify(textNode, answer);   // урсгаж дуусмагц холбоосыг идэвхжүүлнэ
1198              renderNews(bubble, bubble.__news);
1199              renderSources(bubble, bubble.__sources);
1200              addSpeakButton(bubble, answer.trim());
1201            }
1202          }
1203        }
1204      }
1205    } catch (e) {
1206      if (prog.isConnected) prog.remove();
1207      caret.hidden = true;
1208      bubble.parentElement.className = "msg err";
1209      textNode.textContent = "Сервертэй холбогдож чадсангүй. Дараа дахин оролдоно уу.";
1210    } finally {
1211      caret.hidden = true;
1212      setBusy(false);
1213      if (open) elInput.focus();
1214    }
1215  }
1216
1217  // --- Үйлдлүүд ---
1218  // --- Таб: "Асуулт хариулт" (чат) ба "Стандарт хайх" ---
1219  function view(name) {
1220    var isChat = name !== "std";
1221    sr.querySelectorAll(".tab").forEach(function (t) {
1222      var on = (t.getAttribute("data-view") === "chat") === isChat;
1223      t.classList.toggle("on", on);
1224      t.setAttribute("aria-selected", String(on));
1225    });
1226    elLog.hidden = !isChat;
1227    elFoot.hidden = !isChat;
1228    elStd.hidden = isChat;
1229    if (isChat) {
1230      elInput.focus();
1231    } else {
1232      elStdQ.focus();
1233      if (!elStdList.children.length && !elStdQ.value) {
1234        elStdInfo.textContent = "Стандартын дугаар (ж нь 6452, MNS ISO 9001) эсвэл "
1235          + "нэрнийх нь үгээр (ж нь цемент, тоолуур) хайна.";
1236      }
1237    }
1238  }
1239
1240  sr.querySelectorAll(".tab").forEach(function (t) {
1241    t.addEventListener("click", function () { view(t.getAttribute("data-view")); });
1242  });
1243
1244  // --- Стандартын хайлт ---
1245  var stdTimer = null, stdSeq = 0;
1246
1247  function money(value) {
1248    var n = parseInt(value, 10);
1249    return isNaN(n) ? "" : String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "₮";
1250  }
1251
1252  function stdRow(item) {
1253    var row = document.createElement("div");
1254    row.className = "std-item";
1255
1256    var num = document.createElement("span");
1257    num.className = "num";
1258    num.textContent = item.n;
1259    row.appendChild(num);
1260
1261    var ttl = document.createElement("div");
1262    ttl.className = "ttl";
1263    ttl.textContent = item.t;
1264    row.appendChild(ttl);
1265
1266    var bits = [];
1267    if (item.c) bits.push(item.c);
1268    if (item.s) bits.push(item.s);
1269    if (bits.length) {
1270      var meta = document.createElement("div");
1271      meta.className = "meta2";
1272      meta.textContent = bits.join(" · ");
1273      row.appendChild(meta);
1274    }
1275
1276    var price = document.createElement("div");
1277    price.className = "meta2";
1278    if (item.f) {
1279      price.innerHTML = '<span class="free">Үндэсний стандарт — нэвтэрч үнэгүй татна</span>';
1280    } else if (item.p) {
1281      price.textContent = "Үнэ: " + money(item.p);
1282    }
1283    if (price.textContent || price.innerHTML) row.appendChild(price);
1284
1285    var go = document.createElement("a");
1286    go.className = "go";
1287    go.href = item.u;
1288    go.target = "_blank";
1289    go.rel = "noopener noreferrer";
1290    go.innerHTML = ICON.link + "<span>Дэлгэрэнгүй харах</span>";
1291    row.appendChild(go);
1292    return row;
1293  }
1294
1295  async function stdSearch(query) {
1296    var seq = ++stdSeq;
1297    elStdInfo.textContent = "Хайж байна…";
1298    try {
1299      var res = await fetch(CFG.api + "/standards?q=" + encodeURIComponent(query) + "&limit=30");
1300      var data = await res.json();
1301      if (seq !== stdSeq) return;              // хуучирсан хариу — орхино
1302      if (!res.ok) throw new Error(data.detail || "Алдаа");
1303      elStdList.innerHTML = "";
1304      if (!data.items.length) {
1305        elStdInfo.textContent = "«" + query + "» гэсэн стандарт олдсонгүй. "
1306          + "Дугаараар (6452) эсвэл өөр үгээр хайж үзнэ үү.";
1307        return;
1308      }
1309      elStdInfo.textContent = data.items.length >= 30
1310        ? "Хамгийн тохирох 30 стандарт (нийт сан: " + data.total + ")"
1311        : data.items.length + " стандарт олдлоо";
1312      data.items.forEach(function (item) { elStdList.appendChild(stdRow(item)); });
1313      elStdList.scrollTop = 0;
1314    } catch (e) {
1315      if (seq !== stdSeq) return;
1316      elStdList.innerHTML = "";
1317      elStdInfo.textContent = "Хайлт түр ажиллахгүй байна. Дараа оролдоно уу.";
1318    }
1319  }
1320
1321  elStdQ.addEventListener("input", function () {
1322    clearTimeout(stdTimer);
1323    var value = this.value.trim();
1324    if (value.length < 2) {
1325      stdSeq++;
1326      elStdList.innerHTML = "";
1327      elStdInfo.textContent = "Дор хаяж 2 тэмдэгт бичнэ үү.";
1328      return;
1329    }
1330    stdTimer = setTimeout(function () { stdSearch(value); }, 280);
1331  });
1332
1333  // --- Мэндчилгээний бөмбөлөг ---
1334  // Чат хаалттай байх бүх хугацаанд харагдана. "×" дарвал зөвхөн тухайн
1335  // үзэлтэд нуугдана — хуудас сэргээх бүрд дахин мэндчилнэ (хөтчид юу ч
1336  // хадгалахгүй).
1337  var helloOff = false;
1338
1339  function showHello(on) {
1340    if (!elHello) return;
1341    var visible = on && !helloOff;
1342    elHello.classList.toggle("on", visible);
1343    root.classList.toggle("greeting", visible);
1344  }
1345
1346  if (elHello) {
1347    elHello.querySelector(".x").addEventListener("click", function (ev) {
1348      ev.stopPropagation();
1349      helloOff = true;
1350      showHello(false);
1351      elLauncher.focus();
1352    });
1353    elHello.addEventListener("click", function () { if (!open) toggle(true); });
1354    // Хуудас ачаалмагц биш, бага зэрэг хүлээгээд гаргана — анхаарал татна
1355    setTimeout(function () { showHello(!open); }, 900);
1356  }
1357
1358  // --- Гар утас: бүтэн дэлгэц ---
1359  // CSS нь цонхыг бүтэн дэлгэц болгоно; энд (1) ард байгаа сайтын гүйлгээг
1360  // түгжинэ, (2) гар (keyboard) гарахад цонхыг харагдах хэсэгт багтаана —
1361  // эс бөгөөс бичих талбар гарын ард нуугдана (visualViewport).
1362  var mobileQuery = window.matchMedia("(max-width: 540px)");
1363  var savedOverflow = null;
1364
1365  function fitViewport() {
1366    var vv = window.visualViewport;
1367    if (open && mobileQuery.matches && vv) {
1368      root.style.setProperty("--vvh", vv.height + "px");
1369      root.style.setProperty("--vvt", vv.offsetTop + "px");
1370    } else {
1371      root.style.removeProperty("--vvh");
1372      root.style.removeProperty("--vvt");
1373    }
1374  }
1375
1376  function lockPage(lock) {
1377    var el = document.documentElement;
1378    if (lock && savedOverflow === null) {
1379      savedOverflow = el.style.overflow;
1380      el.style.overflow = "hidden";
1381    } else if (!lock && savedOverflow !== null) {
1382      el.style.overflow = savedOverflow;
1383      savedOverflow = null;
1384    }
1385  }
1386
1387  function mobileMode() {
1388    lockPage(open && mobileQuery.matches);
1389    fitViewport();
1390  }
1391
1392  if (window.visualViewport) {
1393    window.visualViewport.addEventListener("resize", fitViewport);
1394    window.visualViewport.addEventListener("scroll", fitViewport);
1395  }
1396  // Утсаа эргүүлэх, цонхны хэмжээ өөрчлөгдөхөд (утас <-> том дэлгэц)
1397  if (mobileQuery.addEventListener) mobileQuery.addEventListener("change", mobileMode);
1398  else if (mobileQuery.addListener) mobileQuery.addListener(mobileMode);
1399
1400  function toggle(show) {
1401    open = show === undefined ? !open : show;
1402    elPanel.hidden = !open;
1403    mobileMode();
1404    showHello(!open);
1405    if (!open) stopSpeech();
1406    // Цонх нээлттэй үед нээх товчийг нуана — эс бөгөөс цонхны дээр давхарлана.
1407    // Хаах товч толгой хэсэгт байгаа, мөн Esc ажиллана.
1408    elLauncher.hidden = open;
1409    elLauncher.classList.add("seen");
1410    elLauncher.setAttribute("aria-expanded", String(open));
1411    if (open) {
1412      if (!elLog.children.length) {
1413        if (sessionId) restore(); else greet();
1414      }
1415      elInput.focus();
1416      scrollDown();
1417    }
1418  }
1419
1420  // --- Өмнөх яриаг серверээс сэргээх ---
1421  async function restore() {
1422    greet();  // сүлжээ удаан бол хоосон цонх харагдахгүйн тулд эхлээд мэндчилнэ
1423    var id = sessionId;
1424    try {
1425      var res = await fetch(CFG.api + "/session/" + encodeURIComponent(id));
1426      if (res.status === 404) { if (sessionId === id) { sessionId = null; saveSession(null); } return; }
1427      if (!res.ok) return;
1428      var data = await res.json();
1429      // Хүлээх хооронд хэрэглэгч асуулт илгээсэн эсвэл шинэ яриа эхлүүлсэн бол бүү дар.
1430      if (!data.messages || !data.messages.length || busy || sessionId !== id) return;
1431      if (elLog.querySelector(".msg.user")) return;
1432
1433      elLog.innerHTML = "";
1434      addMessage("bot", CFG.greeting);
1435      data.messages.forEach(function (m) {
1436        var bubble = addMessage(m.role === "user" ? "user" : "bot", m.content);
1437        if (m.role !== "user") {
1438          renderNews(bubble, m.sources);
1439          renderSources(bubble, m.sources);
1440          addSpeakButton(bubble, m.content);
1441        }
1442      });
1443      scrollDown();
1444    } catch (e) { /* сэргээж чадахгүй бол мэндчилгээ хэвээр үлдэнэ */ }
1445  }
1446
1447  elLauncher.addEventListener("click", function () {
1448    if (!open) playOpen();   // зөвхөн нээхэд (хэрэглэгчийн дарсан үйлдэл тул autoplay хязгаарт өртөхгүй)
1449    toggle();
1450  });
1451  elClose.addEventListener("click", function () { toggle(false); elLauncher.focus(); });
1452
1453  elReset.addEventListener("click", async function () {
1454    if (busy) return;
1455    stopSpeech();
1456    var old = sessionId;
1457    sessionId = null;
1458    saveSession(null);
1459    greet();
1460    elInput.focus();
1461    if (old) {
1462      try {
1463        await fetch(CFG.api + "/session/" + encodeURIComponent(old), { method: "DELETE" });
1464      } catch (e) { /* сервер талын цэвэрлэгээ заавал биш */ }
1465    }
1466  });
1467
1468  function submit() {
1469    var question = elInput.value.trim();
1470    if (!question || busy) return;
1471    elInput.value = "";
1472    autoGrow();
1473    send(question);
1474  }
1475
1476  elForm.addEventListener("submit", function (event) {
1477    event.preventDefault();
1478    submit();
1479  });
1480
1481  elInput.addEventListener("input", autoGrow);
1482  elInput.addEventListener("keydown", function (event) {
1483    // Enter — илгээх, Shift+Enter — шинэ мөр
1484    if (event.key === "Enter" && !event.shiftKey) {
1485      event.preventDefault();
1486      submit();
1487    }
1488  });
1489
1490  document.addEventListener("keydown", function (event) {
1491    if (event.key === "Escape" && open) { toggle(false); elLauncher.focus(); }
1492  });
1493
1494  // Гаднаас удирдах бага зэргийн API
1495  window.MasmChat = {
1496    version: VERSION,
1497    open: function () { toggle(true); },
1498    close: function () { toggle(false); },
1499    // "Стандарт хайх" таб дээр байсан ч асуултыг чат таб руу шилжүүлж асууна
1500    ask: function (question) { toggle(true); view("chat"); send(question); },
1501    // "Стандарт хайх" табыг нээж, өгсөн үгээр шууд хайна (ж нь "цемент", "6452")
1502    standards: function (query) {
1503      toggle(true);
1504      view("std");
1505      if (query) {
1506        elStdQ.value = String(query);
1507        elStdQ.dispatchEvent(new Event("input"));
1508      }
1509    }
1510  };
1511})();

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.