PageSourceSearch

https://aliviy.io/wp-content/themes/reboot-child/js/studio-engine-1.js

js aliviy.io collected 2026-10-02 00:11:37 UTC 25,915 bytes, 508 lines download raw bytes

1/**
2 * Конструктор запроса на странице-инструменте (studio/engine.php).
3 *
4 * Конфиг лежит в <script type="application/json" class="al-studio__cfg">:
5 *   fields — поля (chips, select, lang, switch, range, text, textarea);
6 *   tpl    — шаблон запроса: строки с подстановками {ключ} и условные части
7 *            {"t": "…", "if": "ключ", "eq"/"in"/"not": …}.
8 * Часть шаблона с пустым необязательным полем выпадает целиком; пустое
9 * обязательное поле показывается в превью как [подпись], а запрос не уходит.
10 * Отправка — через кнопку .aliviy-prompt[data-prompt] (chat-prompts.php).
11 */
12(function () {
13	"use strict";
14	var root = document.querySelector(".al-studio[data-slug]");
15	if (!root) { return; }
16	var cfgEl = root.querySelector(".al-studio__cfg");
17	var CFG;
18	try { CFG = JSON.parse(cfgEl.textContent); } catch (e) { return; }
19
20	var SLUG = root.getAttribute("data-slug");
21	var STORE = "alStudio:" + SLUG + ":v1";
22	function $(s, el) { return (el || root).querySelector(s); }
23	function $$(s, el) { return Array.prototype.slice.call((el || root).querySelectorAll(s)); }
24	function goal(name) {
25		if (window.ym) { try { window.ym(97565126, "reachGoal", name, { page: SLUG }); } catch (x) {} }
26	}
27
28	/* ---------- поля ---------- */
29	var F = {};                 // ключ → описание поля
30	CFG.fields.forEach(function (f) { F[f.k] = f; });
31	function def(f) {
32		if (f.type === "switch") { return !!f.def; }
33		if (f.type === "range") { return f.def != null ? f.def : f.min; }
34		if (f.type === "lang") { return "Русский"; }
35		if (f.type === "chips" && f.multi) { return [].concat(f.def || []).map(String); }
36		if (f.type === "select" && f.def == null && f.options && f.options.length) { return String(f.options[0].v); }
37		if (f.type === "chips" || f.type === "select") { return f.def != null ? String(f.def) : ""; }
38		return "";
39	}
40	var DEF = {}, state = {};
41	Object.keys(F).forEach(function (k) { DEF[k] = def(F[k]); });
42	function reset() { state = JSON.parse(JSON.stringify(DEF)); }
43	reset();
44
45	// сохранённый выбор: только варианты, числа и переключатели (тексты — нет, там личное)
46	try {
47		var saved = JSON.parse(localStorage.getItem(STORE) || "{}");
48		Object.keys(saved).forEach(function (k) {
49			var f = F[k], v = saved[k];
50			if (!f || f.type === "text" || f.type === "textarea") { return; }
51			if (f.type === "switch") { state[k] = !!v; }
52			else if (f.type === "range") { v = parseFloat(v); if (v >= f.min && v <= f.max) { state[k] = v; } }
53			else if (f.type === "lang") { if ($('select[data-k="' + k + '"] option[value="' + String(v).replace(/"/g, "") + '"]')) { state[k] = v; } }
54			else if (f.multi) { if (Array.isArray(v)) { state[k] = v.map(String).filter(function (x) { return opt(f, x); }); } }
55			else if ((f.options || []).some(function (o) { return String(o.v) === String(v); })) { state[k] = String(v); }
56		});
57	} catch (x) {}
58	function save() {
59		try {
60			var o = {};
61			Object.keys(F).forEach(function (k) { if (F[k].type !== "text" && F[k].type !== "textarea") { o[k] = state[k]; } });
62			localStorage.setItem(STORE, JSON.stringify(o));
63		} catch (x) {}
64	}
65
66	function opt(f, v) {
67		return (f.options || []).filter(function (o) { return String(o.v) === String(v); })[0];
68	}
69	function visible(k) {
70		var row = $('.al-studio__row[data-k="' + k + '"]');
71		return !row || !row.hidden;
72	}
73	function clean(v) { return String(v == null ? "" : v).replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim(); }
74	/** Значение для подстановки в запрос ("" — поле пустое или скрыто). */
75	function phrase(k) {
76		var f = F[k];
77		if (!f || !visible(k)) { return ""; }
78		var v = state[k];
79		switch (f.type) {
80			case "chips": case "select":
81				if (f.multi) {
82					var ps = (f.options || []).filter(function (o) { return v.indexOf(String(o.v)) >= 0; }).map(function (o) { return o.p != null ? o.p : o.t; });
83					return ps.length > 1 ? ps.slice(0, -1).join(", ") + " и " + ps[ps.length - 1] : ps.join("");
84				}
85				var o = opt(f, v);
86				return o ? (o.p != null ? o.p : o.t) : "";
87			case "lang":
88				var el = $('select[data-k="' + k + '"]'), op = el && el.options[el.selectedIndex];
89				return op ? op.getAttribute("data-p") : "";
90			case "switch": return v ? (f.p != null ? f.p : f.on) : "";
91			case "range": return Number(v).toLocaleString("ru-RU");
92			default:
93				if (f.type === "textarea") {
94					// код и тексты: отступы и последний знак («;» в коде) сохраняем, убираем только пустые строки по краям
95					return String(v == null ? "" : v).replace(/^\s*\n/, "").replace(/\s+$/, "");
96				}
97				v = clean(v);
98				// внешние кавычки снимаем, только если весь текст — одна цитата: «слово «маяк»» не трогаем
99				if (/^[«"“„]/.test(v) && /[»"”]$/.test(v) && (v.match(/[«»"“”„]/g) || []).length === 2) { v = v.slice(1, -1).trim(); }
100				// текстовое поле без точки в конце: шаблон сам ставит знаки
101				return v.replace(/[.;,\s]+$/, "");
102		}
103	}
104	function truthy(k) {
105		var f = F[k];
106		if (!f || !visible(k)) { return false; }
107		if (f.type === "switch") { return !!state[k]; }
108		return phrase(k) !== "";
109	}
110	function passes(c) {
111		if (c.all && !c.all.every(passes)) { return false; }
112		if (c.any && !c.any.some(passes)) { return false; }
113		if (c.if && !truthy(c.if)) { return false; }
114		if (c.not && truthy(c.not)) { return false; }
115		var key = c.key || c.if;
116		// скрытое поле для условий — пустое, иначе его старое значение протекает в чужой режим
117		var sv = key && !visible(key) ? (F[key] && F[key].multi ? [] : "") : state[key], has = function (x) { return Array.isArray(sv) ? sv.indexOf(String(x)) >= 0 : String(sv) === String(x); };
118		if (c.eq != null && !has(c.eq)) { return false; }
119		if (c.in && !c.in.some(has)) { return false; }
120		if (c.nin && c.nin.some(has)) { return false; }
121		if (c.ne != null && has(c.ne)) { return false; }
122		var nv = Array.isArray(sv) ? sv.length : parseFloat(sv);
123		if (c.gt != null && !(nv > c.gt)) { return false; }
124		if (c.lt != null && !(nv < c.lt)) { return false; }
125		return true;
126	}
127
128	/* ---------- сборка запроса ---------- */
129	function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
130	function build() {
131		var text = [], html = [], missing = [];
132		CFG.tpl.forEach(function (part) {
133			var t = typeof part === "string" ? part : part.t;
134			if (typeof part !== "string" && !passes(part)) { return; }
135			t = t.replace(/([^\n ]) {2,}/g, "$1 ");
136			var drop = false, miss = [];
137			var tt = t.replace(/\{([a-z0-9_]+)(?:\|(ug|u|len|words)(?:\*([\d.]+))?)?\}/g, function (m, k, mod, mul) {
138				// {today} / {year} — дата из браузера: бот сам текущую дату не знает
139				if (k === "today" && !F[k]) { return new Date().toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }).replace(/\s*г\.?$/, ""); }
140				if (k === "year" && !F[k]) { return String(new Date().getFullYear()); }
141				var v = phrase(k);
142				// {k|u} — число с единицей в нужном падеже: «3 года», «5 вариантов»
143				if (mod === "u" && v !== "" && F[k] && F[k].units) { v += " " + plural(state[k], F[k].units[0], F[k].units[1], F[k].units[2]); }
144				// {k|ug} — родительный падеж по unitsG: «для 1 игрока», «для 5 игроков»
145				if (mod === "ug" && v !== "" && F[k] && F[k].unitsG) { v += " " + plural(state[k], F[k].unitsG[0], F[k].unitsG[1], F[k].unitsG[2]); }
146				// {k|len*0.33} / {k|words*0.5} — длина вставленного текста в знаках/словах × доля: боту даём готовое число
147				if ((mod === "len" || mod === "words") && v !== "") {
148					var n = mod === "len" ? v.length : (v.match(/[A-Za-zА-Яа-яЁё0-9]+(?:[-'’][A-Za-zА-Яа-яЁё0-9]+)*/g) || []).length;
149					v = Math.max(1, Math.round(n * (mul ? parseFloat(mul) : 1))).toLocaleString("ru-RU");
150				}
151				if (v === "") {
152					if (F[k] && F[k].req && visible(k)) { miss.push(k); return "\u0000" + k + "\u0000"; }
153					drop = true;
154				}
155				return v;
156			});
157			if (drop) { return; }
158			miss.forEach(function (k) { if (missing.indexOf(k) < 0) { missing.push(k); } });
159			text.push(tt.replace(/\u0000([a-z0-9_]+)\u0000/g, function (m, k) { return "[" + curLabel(k).toLowerCase() + "]"; }));
160			html.push(esc(tt).replace(/\u0000([a-z0-9_]+)\u0000/g, function (m, k) { return '<span class="is-empty">[' + esc(curLabel(k).toLowerCase()) + "]</span>"; }));
161		});
162		// многострочные вставки (код, текст для перевода) — отдельным абзацем
163		// склейка частей: перед частью с переноса строки пробел не ставим
164		function glue(list) { return list.reduce(function (acc, x) { return !acc ? x : acc + (/^\n/.test(x) ? "" : " ") + x; }, ""); }
165		var s = glue(text).replace(/([.!?…])([»"])\./g, "$1$2").trim();
166		var h = glue(html).replace(/([.!?…])([»"])\./g, "$1$2").trim();
167		return { text: s, html: h, missing: missing };
168	}
169
170	var out = $(".al-studio__prompt"), send = $(".al-studio__send"), err = $(".al-studio__err"), lenEl = $(".al-studio__len");
171	var tipEl = $(".al-studio__tip"), copyBtn = $(".al-studio__copy"), resetBtn = $(".al-studio__reset");
172	function render() {
173		applyWhen();
174		var r = build();
175		out.innerHTML = r.html || '<span class="is-empty">Выберите варианты слева — запрос соберётся здесь.</span>';
176		lenEl.textContent = r.text ? r.text.length + " зн." : "";
177		send.setAttribute("data-prompt", r.missing.length ? "" : r.text);
178		copyBtn.disabled = !r.text;
179		paintMore();
180		// помощникам: значения конструктора изменились (root.addEventListener("al-studio:change", …))
181		try { root.dispatchEvent(new CustomEvent("al-studio:change")); } catch (x) {}
182	}
183
184	/* ---------- видимость полей: when = {"ключ": ["значение", …]} ---------- */
185	function applyWhen() {
186		$$(".al-studio__row[data-when]").forEach(function (row) {
187			var w; try { w = JSON.parse(row.getAttribute("data-when")); } catch (x) { return; }
188			var ok = Object.keys(w).every(function (k) {
189				var need = [].concat(w[k]).map(String), v = state[k];
190				var filled = Array.isArray(v) ? v.length > 0 : typeof v === "boolean" ? v : clean(v) !== "";
191				if (need[0] === "*") { return filled; }
192				if (need[0] === "!") { return !filled; }
193				return [].concat(v).some(function (x) { return need.indexOf(String(x)) >= 0; });
194			});
195			row.hidden = !ok;
196		});
197		// пара, у которой спрятаны обе половины, не должна оставлять пустую строку
198		$$(".al-studio__pair").forEach(function (p) {
199			p.hidden = $$(".al-studio__row", p).every(function (r) { return r.hidden; });
200		});
201		applyBy();
202	}
203
204	/* ---------- подпись, подсказка и плейсхолдер по выбору: by = {"k": "task", "v": {"code": {"label", "ph", "tip"}}} ---------- */
205	function byOf(k) {
206		var f = F[k];
207		if (!f || !f.by || !f.by.v) { return null; }
208		var v = state[f.by.k];
209		return f.by.v[String(Array.isArray(v) ? v[0] : v)] || null;
210	}
211	function curLabel(k) { var b = byOf(k); return b && b.label ? b.label : F[k].label; }
212	function applyBy() {
213		Object.keys(F).forEach(function (k) {
214			var f = F[k];
215			if (!f.by) { return; }
216			var b = byOf(k) || {}, row = $('.al-studio__row[data-k="' + k + '"]');
217			if (!row) { return; }
218			var lab = row.querySelector(".al-studio__label"), q = row.querySelector(".al-q"), inp = row.querySelector(".al-studio__input");
219			var label = b.label || f.label, tip = b.tip != null ? b.tip : (f.tip || ""), ph = b.ph != null ? b.ph : (f.ph || "");
220			if (lab && lab.firstChild && lab.firstChild.nodeType === 3 && lab.firstChild.nodeValue !== label) { lab.firstChild.nodeValue = label; }
221			if (q && q.getAttribute("data-tip") !== tip) { q.setAttribute("data-tip", tip); q.setAttribute("aria-label", "Подсказка: " + tip); q.hidden = !tip; }
222			if (inp && inp.getAttribute("placeholder") !== ph) { inp.setAttribute("placeholder", ph); }
223		});
224	}
225
226	/* ---------- подсказка под выбором ---------- */
227	function showTip(t) { tipEl.textContent = t || ""; tipEl.hidden = !t; }
228	function hintFor(k) {
229		var f = F[k];
230		if (!f) { return ""; }
231		if (f.type === "chips" || f.type === "select") { var o = opt(f, f.multi ? state[k][state[k].length - 1] : state[k]); return o && o.hint ? o.hint : ""; }
232		if (f.type === "switch") { return state[k] ? (f.hintOn || "") : (f.hintOff || ""); }
233		return "";
234	}
235
236	/* ---------- элементы ---------- */
237	function paintChips(k) {
238		$$('.al-chip[data-k="' + k + '"]').forEach(function (c) {
239			var v = c.getAttribute("data-v");
240			c.setAttribute("aria-pressed", (Array.isArray(state[k]) ? state[k].indexOf(v) >= 0 : v === String(state[k])) ? "true" : "false");
241		});
242	}
243	function paintSwitch(k) {
244		var b = $('.al-switch[data-k="' + k + '"]');
245		if (!b) { return; }
246		b.setAttribute("aria-checked", state[k] ? "true" : "false");
247		b.querySelector(".al-switch__txt").textContent = state[k] ? F[k].on : F[k].off;
248	}
249	function zone(f, v) {
250		var z = (f.zones || []).filter(function (x) { return v <= x.max; })[0];
251		return z ? z.t : "";
252	}
253	function plural(n, a, b, c) {
254		var d = n % 10, e = n % 100;
255		return d === 1 && e !== 11 ? a : d >= 2 && d <= 4 && (e < 12 || e > 14) ? b : c;
256	}
257	function paintRange(k) {
258		var f = F[k], rng = $('input[type=range][data-k="' + k + '"]'), num = $('[data-num="' + k + '"]');
259		if (!rng) { return; }
260		rng.value = state[k];
261		if (document.activeElement !== num) { num.value = state[k]; }
262		var suf = num.parentNode.querySelector(".al-range__suf");
263		if (suf && f.units) { suf.textContent = plural(state[k], f.units[0], f.units[1], f.units[2]); }
264		var p = (rng.value - rng.min) / (rng.max - rng.min) * 100;
265		rng.style.setProperty("--p", Math.max(0, Math.min(100, p)) + "%");
266		var z = rng.closest(".al-range").querySelector(".al-range__zone");
267		if (z) { z.textContent = zone(f, state[k]); }
268	}
269	function paintAll() {
270		Object.keys(F).forEach(function (k) {
271			var f = F[k];
272			if (f.type === "chips") { paintChips(k); }
273			else if (f.type === "switch") { paintSwitch(k); }
274			else if (f.type === "range") { paintRange(k); }
275			else if (f.type === "select" || f.type === "lang") { var s = $('select[data-k="' + k + '"]'); if (s) { s.value = state[k]; } }
276			else { var i = $('[data-k="' + k + '"].al-studio__input'); if (i && i.value !== state[k]) { i.value = state[k]; } }
277		});
278	}
279
280	$$(".al-chip[data-k]").forEach(function (c) {
281		c.addEventListener("click", function () {
282			var k = c.getAttribute("data-k");
283			var v = c.getAttribute("data-v");
284			if (F[k].multi) {
285				var i = state[k].indexOf(v);
286				if (i >= 0) { state[k].splice(i, 1); } else { state[k].push(v); }
287			} else { state[k] = v; }
288			paintChips(k); showTip(hintFor(k)); hideErr(); save(); render();
289		});
290	});
291	$$("select[data-k]").forEach(function (s) {
292		s.addEventListener("change", function () {
293			var k = s.getAttribute("data-k");
294			state[k] = s.value;
295			showTip(F[k].type === "lang" ? (s.value === "Русский" ? "" : "Проверьте текст у носителя языка перед публикацией — особенно термины.") : hintFor(k));
296			save(); render();
297		});
298	});
299	$$(".al-switch[data-k]").forEach(function (b) {
300		b.addEventListener("click", function () {
301			var k = b.getAttribute("data-k");
302			state[k] = !state[k];
303			paintSwitch(k); showTip(hintFor(k)); save(); render();
304		});
305	});
306	$$('input[type=range][data-k]').forEach(function (rng) {
307		var k = rng.getAttribute("data-k"), f = F[k], num = $('[data-num="' + k + '"]');
308		rng.addEventListener("input", function () { state[k] = parseFloat(rng.value); paintRange(k); render(); });
309		rng.addEventListener("change", save);
310		num.addEventListener("input", function () {
311			var v = parseFloat(String(num.value).replace(",", "."));
312			if (isNaN(v) || v < f.min || v > f.max) { num.classList.add("is-out"); return; }
313			num.classList.remove("is-out");
314			state[k] = v; paintRange(k); render();
315		});
316		function commit() {
317			var v = parseFloat(String(num.value).replace(",", "."));
318			if (isNaN(v)) { v = state[k]; }
319			v = Math.max(f.min, Math.min(f.max, v));
320			num.classList.remove("is-out");
321			state[k] = v; num.value = v; paintRange(k); render(); save();
322		}
323		num.addEventListener("change", commit);
324		num.addEventListener("blur", commit);
325		num.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); num.blur(); } });
326		num.addEventListener("wheel", function () { if (document.activeElement === num) { num.blur(); } }, { passive: true });
327	});
328	$$(".al-studio__input[data-k]:not(select)").forEach(function (i) {
329		var k = i.getAttribute("data-k");
330		i.addEventListener("input", function () { state[k] = i.value; if (i.classList.contains("is-err") && clean(i.value)) { hideErr(); } render(); });
331		if (i.tagName === "INPUT") {
332			i.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); send.click(); } });
333		}
334	});
335
336	/* ---------- «Ещё настройки»: сколько заполнено ---------- */
337	function paintMore() {
338		var box = $(".al-more");
339		if (!box) { return; }
340		var n = $$(".al-studio__row", box).filter(function (r) {
341			var k = r.getAttribute("data-k"), f = F[k];
342			if (!f || r.hidden) { return false; }
343			if (f.type === "text" || f.type === "textarea") { return clean(state[k]) !== ""; }
344			return JSON.stringify(state[k]) !== JSON.stringify(DEF[k]);
345		}).length;
346		var c = $(".al-more__count");
347		c.textContent = n; c.hidden = !n;
348	}
349
350	/* ---------- отправка, копирование, сброс ---------- */
351	function showErr(msg, el) {
352		err.textContent = msg; err.hidden = false;
353		if (el) { el.classList.add("is-err"); el.focus(); }
354	}
355	function hideErr() {
356		err.hidden = true;
357		$$(".is-err").forEach(function (e) { e.classList.remove("is-err"); });
358	}
359	// пустой запрос не уходит: гасим событие до общего обработчика в chat-prompts.php
360	send.addEventListener("click", function (e) {
361		if (!send.getAttribute("data-prompt")) {
362			e.preventDefault(); e.stopPropagation();
363			var k = build().missing[0], el = k && $('[data-k="' + k + '"].al-studio__input');
364			showErr(k ? "Заполните поле «" + curLabel(k) + "» — без него запрос не собрать." : "Выберите варианты — запрос пока пустой.", el);
365			return;
366		}
367		goal("studio_send");
368	});
369	copyBtn.addEventListener("click", function () {
370		var t = build().text;
371		if (!t) { return; }
372		function done() { copyBtn.textContent = "Скопировано ✓"; setTimeout(function () { copyBtn.textContent = "Скопировать запрос"; }, 1800); }
373		if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(t).then(done, function () {}); }
374		else {
375			var ta = document.createElement("textarea");
376			ta.value = t; ta.style.position = "fixed"; ta.style.opacity = "0";
377			document.body.appendChild(ta); ta.select();
378			try { document.execCommand("copy"); done(); } catch (x) {}
379			document.body.removeChild(ta);
380		}
381		goal("studio_copy");
382	});
383	resetBtn.addEventListener("click", function () {
384		reset();
385		try { localStorage.removeItem(STORE); } catch (x) {}
386		paintAll(); hideErr(); render();
387		showTip("Настройки сброшены.");
388	});
389
390	/* ---------- вкладки ---------- */
391	var helperDone = false;
392	function openTab(key) {
393		$$(".al-studio__tab").forEach(function (t) {
394			var on = t.getAttribute("data-tab") === key;
395			t.setAttribute("aria-selected", on ? "true" : "false");
396			t.setAttribute("tabindex", on ? "0" : "-1");
397			$("#" + t.getAttribute("aria-controls")).hidden = !on;
398		});
399		if (key === "help" && helperDone) { $("#al-sp-help").dispatchEvent(new CustomEvent("al-studio:open")); }
400		if (key === "help" && !helperDone) {
401			var p = $("#al-sp-help"), h;
402			try { h = JSON.parse(p.getAttribute("data-helper")); } catch (x) { h = null; }
403			var fn = h && window.AliviyStudioHelpers && window.AliviyStudioHelpers[h.type];
404			if (fn) { fn(p, h, api); helperDone = true; }
405			goal("studio_helper");
406		}
407		if (key === "ex") { goal("studio_examples"); }
408	}
409	$$(".al-studio__tab").forEach(function (t) {
410		t.addEventListener("click", function () { openTab(t.getAttribute("data-tab")); });
411	});
412	var tabsEl = $(".al-studio__tabs");
413	if (tabsEl) {
414		tabsEl.addEventListener("keydown", function (e) {
415			var tabs = $$(".al-studio__tab"), i = tabs.indexOf(document.activeElement), n = -1;
416			if (i < 0) { return; }
417			if (e.key === "ArrowRight") { n = (i + 1) % tabs.length; }
418			else if (e.key === "ArrowLeft") { n = (i - 1 + tabs.length) % tabs.length; }
419			else if (e.key === "Home") { n = 0; }
420			else if (e.key === "End") { n = tabs.length - 1; }
421			if (n < 0) { return; }
422			e.preventDefault();
423			openTab(tabs[n].getAttribute("data-tab"));
424			tabs[n].focus();
425		});
426	}
427
428	/* ---------- примеры ---------- */
429	$$("[data-ex]").forEach(function (c) {
430		c.addEventListener("click", function () {
431			var k = c.getAttribute("data-ex");
432			$$("[data-ex]").forEach(function (x) { x.setAttribute("aria-pressed", x === c ? "true" : "false"); });
433			$$("[data-ex-panel]").forEach(function (p) { p.hidden = p.getAttribute("data-ex-panel") !== k; });
434		});
435	});
436	$$("[data-use]").forEach(function (b) {
437		var set = CFG.examples[+b.getAttribute("data-use")];
438		if (!set) { b.hidden = true; return; }
439		b.addEventListener("click", function () {
440			reset();
441			Object.keys(set).forEach(function (k) { if (F[k]) { state[k] = set[k]; } });
442			paintAll(); render(); save();
443			openTab("build");
444			root.scrollIntoView({ behavior: "smooth", block: "start" });
445			showTip("Настройки примера подставлены — поменяйте под свою задачу.");
446		});
447	});
448
449	/* ---------- всплывающие подсказки «?» ---------- */
450	var bubble = null, bubbleFor = null, bubbleY = 0;
451	function closeBubble() { if (bubble) { bubble.hidden = true; } if (bubbleFor) { bubbleFor.setAttribute("aria-expanded", "false"); } bubbleFor = null; }
452	function openBubble(b) {
453		if (!bubble) {
454			bubble = document.createElement("div");
455			bubble.className = "al-bubble"; bubble.setAttribute("role", "tooltip"); bubble.id = "al-bubble";
456			document.body.appendChild(bubble);
457		}
458		if (bubbleFor === b) { closeBubble(); return; }
459		closeBubble();
460		bubble.textContent = b.getAttribute("data-tip");
461		bubble.hidden = false; bubbleFor = b; bubbleY = window.scrollY;
462		b.setAttribute("aria-expanded", "true"); b.setAttribute("aria-describedby", "al-bubble");
463		var r = b.getBoundingClientRect(), w = Math.min(300, window.innerWidth - 24);
464		bubble.style.width = w + "px";
465		var left = Math.max(12, Math.min(r.left + r.width / 2 - w / 2, window.innerWidth - w - 12)), top = r.bottom + 8;
466		if (top + bubble.offsetHeight > window.innerHeight - 8) { top = r.top - bubble.offsetHeight - 8; }
467		bubble.style.left = Math.round(left) + "px"; bubble.style.top = Math.round(top) + "px";
468	}
469	$$(".al-q").forEach(function (b) {
470		b.setAttribute("aria-expanded", "false");
471		b.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); openBubble(b); });
472	});
473	document.addEventListener("click", function (e) { if (bubble && !bubble.hidden && !e.target.closest(".al-bubble")) { closeBubble(); } });
474	document.addEventListener("keydown", function (e) { if (e.key === "Escape") { closeBubble(); } });
475	// закрываем только при заметной прокрутке: на телефоне страница «вздрагивает» от адресной строки
476	window.addEventListener("scroll", function () { if (bubbleFor && Math.abs(window.scrollY - bubbleY) > 60) { closeBubble(); } }, { passive: true });
477	window.addEventListener("resize", closeBubble);
478
479	document.addEventListener("aliviy:greetlink", function () { goal("studio_from_greeting"); });
480
481	/* ---------- API для помощников ---------- */
482	var api = {
483		root: root,
484		goal: goal,
485		/** Подставить текст в конструктор и/или отправить в чат через общий обработчик. */
486		toChat: function (text) {
487			var b = document.createElement("button");
488			b.type = "button"; b.className = "aliviy-prompt"; b.hidden = true;
489			b.setAttribute("data-prompt", text);
490			root.appendChild(b); b.click(); root.removeChild(b);
491			goal("studio_helper_send");
492		},
493		/** Текущее значение поля конструктора. */
494		get: function (k) { return state[k]; },
495		set: function (k, v) { if (F[k]) { state[k] = v; paintAll(); render(); save(); } },
496		/** Открыть вкладку: "build" | "ex" | "help". */
497		openTab: function (key) { openTab(key); }
498	};
499	window.AliviyStudio = api;
500
501	paintAll();
502	render();
503	// helper.first: вкладка помощника открыта с загрузки — рисуем его сразу
504	var hp0 = $("#al-sp-help");
505	if (hp0 && !hp0.hidden) { openTab("help"); }
506	var first = Object.keys(F)[0];
507	showTip(hintFor(first));
508})();

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.