mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:44:47 +00:00
fix(voice): continuous=false + auto-restart — eliminates word duplication
Replaced continuous=true with phrase-by-phrase mode in all 3 voice inputs (setupVoiceMicForField, setupVoiceInput, setupVoiceMic). Chrome mobile resets ev.results mid-session with continuous=true causing repeated words. Now each phrase is an independent SR() session; baseText grows cleanly. Shared _buildVoiceEngine factory in clients.js. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61f23c9bca
commit
dd8691671a
@ -205,77 +205,103 @@ const Clients = (function () {
|
|||||||
return { ok: true, value: "+" + normalized };
|
return { ok: true, value: "+" + normalized };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVoiceMicForField(micBtn, textarea, statusEl) {
|
// Единая фабрика голосового ввода.
|
||||||
if (!micBtn || !textarea) return;
|
// continuous=false + авто-рестарт по фразам — исключает дубли, стабильно на Android/iOS.
|
||||||
|
function _buildVoiceEngine(micBtn, textarea, opts) {
|
||||||
|
// opts: { statusEl, statusClass, onChange }
|
||||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
if (!SR) {
|
if (!SR) {
|
||||||
micBtn.disabled = true;
|
micBtn.disabled = true;
|
||||||
micBtn.title = "Браузер не поддерживает голос";
|
micBtn.title = "Браузер не поддерживает голос";
|
||||||
micBtn.style.opacity = "0.5";
|
micBtn.style.opacity = "0.5";
|
||||||
if (statusEl) statusEl.textContent = "недоступно";
|
if (opts.statusEl) opts.statusEl.textContent = "недоступно";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rec = null, recording = false;
|
|
||||||
let baseText = ""; // текст до начала записи
|
|
||||||
let confirmedFinal = ""; // финальные части накопленные в этой сессии записи
|
|
||||||
|
|
||||||
micBtn.addEventListener("click", () => {
|
let active = false; // пользователь включил микрофон
|
||||||
if (recording) { rec?.stop(); return; }
|
let baseText = ""; // подтверждённый текст (растёт по фразам)
|
||||||
|
let curRec = null;
|
||||||
|
|
||||||
|
function _setStatus(txt, cls) {
|
||||||
|
if (!opts.statusEl) return;
|
||||||
|
opts.statusEl.textContent = txt;
|
||||||
|
if (opts.statusClass && cls) opts.statusEl.className = opts.statusClass + (cls !== "ok" ? " " + cls : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPhrase() {
|
||||||
|
let rec;
|
||||||
try {
|
try {
|
||||||
rec = new SR();
|
rec = new SR();
|
||||||
rec.lang = "ru-RU"; rec.continuous = true; rec.interimResults = true;
|
rec.lang = "ru-RU";
|
||||||
|
rec.continuous = false; // одна фраза — один сеанс, нет накопленных results
|
||||||
|
rec.interimResults = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (statusEl) statusEl.textContent = "Микрофон недоступен";
|
_setStatus("Микрофон недоступен", "err");
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
baseText = (textarea.value || "").trim();
|
curRec = rec;
|
||||||
confirmedFinal = "";
|
|
||||||
|
|
||||||
rec.onstart = () => {
|
|
||||||
recording = true;
|
|
||||||
micBtn.classList.add("rec");
|
|
||||||
micBtn.textContent = "⏹ Стоп";
|
|
||||||
if (statusEl) statusEl.textContent = "Слушаю...";
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
|
||||||
rec.onresult = (ev) => {
|
rec.onresult = (ev) => {
|
||||||
// Пересчитываем ВСЕ финальные и interim с нуля каждый раз — гарантия от дублей
|
// Только результаты ЭТОЙ фразы — ev.results всегда свежий (continuous=false)
|
||||||
let finalAll = "";
|
let fin = "", itr = "";
|
||||||
let interim = "";
|
|
||||||
for (let i = 0; i < ev.results.length; i++) {
|
for (let i = 0; i < ev.results.length; i++) {
|
||||||
const t = ev.results[i][0].transcript;
|
const t = ev.results[i][0].transcript.trim();
|
||||||
if (ev.results[i].isFinal) finalAll += t;
|
if (!t) continue;
|
||||||
else interim += t;
|
if (ev.results[i].isFinal) fin += (fin ? " " : "") + t;
|
||||||
|
else itr += (itr ? " " : "") + t;
|
||||||
}
|
}
|
||||||
confirmedFinal = finalAll.trim();
|
const shown = fin || itr;
|
||||||
const finalPart = confirmedFinal ? (baseText ? " " : "") + confirmedFinal : "";
|
textarea.value = baseText + (baseText && shown ? " " : "") + shown;
|
||||||
const interimPart = interim.trim() ? ((baseText || confirmedFinal) ? " " : "") + interim.trim() : "";
|
|
||||||
textarea.value = baseText + finalPart + interimPart;
|
|
||||||
};
|
|
||||||
rec.onerror = (ev) => {
|
|
||||||
if (statusEl) statusEl.textContent = "Ошибка: " + (ev.error || "");
|
|
||||||
recording = false;
|
|
||||||
micBtn.classList.remove("rec");
|
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rec.onend = () => {
|
rec.onend = () => {
|
||||||
recording = false;
|
// Зафиксировать текущий текст как base и запустить следующую фразу (если active)
|
||||||
micBtn.classList.remove("rec");
|
baseText = textarea.value.trim();
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
if (active) {
|
||||||
// Фиксируем итоговый текст: baseText + final
|
startPhrase();
|
||||||
if (confirmedFinal) {
|
} else {
|
||||||
baseText = (baseText + (baseText ? " " : "") + confirmedFinal).trim();
|
micBtn.classList.remove("rec");
|
||||||
textarea.value = baseText;
|
micBtn.textContent = "🎤 Диктовать";
|
||||||
|
_setStatus("", "ok");
|
||||||
|
if (opts.onChange) opts.onChange(textarea.value || "");
|
||||||
|
haptic && haptic("impact");
|
||||||
}
|
}
|
||||||
if (statusEl && statusEl.textContent === "Слушаю...") statusEl.textContent = "";
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
};
|
||||||
try { rec.start(); } catch (e) {
|
|
||||||
if (statusEl) statusEl.textContent = "Не запустить: " + e.message;
|
rec.onerror = (ev) => {
|
||||||
|
if (ev.error === "no-speech") return; // тишина — onend сработает, авто-перезапуск
|
||||||
|
_setStatus("Ошибка: " + (ev.error || ""), "err");
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
|
};
|
||||||
|
|
||||||
|
try { rec.start(); }
|
||||||
|
catch (e) {
|
||||||
|
_setStatus("Не запустить: " + e.message, "err");
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
micBtn.addEventListener("click", () => {
|
||||||
|
if (active) {
|
||||||
|
active = false;
|
||||||
|
curRec?.stop(); // onend → видит active=false → сбросит кнопку
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active = true;
|
||||||
|
baseText = (textarea.value || "").trim();
|
||||||
|
micBtn.classList.add("rec");
|
||||||
|
micBtn.textContent = "⏹ Стоп";
|
||||||
|
_setStatus("Слушаю...", "ok");
|
||||||
|
haptic && haptic("impact");
|
||||||
|
startPhrase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupVoiceMicForField(micBtn, textarea, statusEl) {
|
||||||
|
_buildVoiceEngine(micBtn, textarea, { statusEl });
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== Список клиентов ===================== */
|
/* ===================== Список клиентов ===================== */
|
||||||
|
|
||||||
async function renderList() {
|
async function renderList() {
|
||||||
@ -1263,81 +1289,9 @@ const Clients = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupVoiceInput(micBtn, textarea, status) {
|
function setupVoiceInput(micBtn, textarea, status) {
|
||||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
_buildVoiceEngine(micBtn, textarea, {
|
||||||
if (!SR) {
|
statusEl: status,
|
||||||
micBtn.disabled = true;
|
statusClass: "note-status",
|
||||||
micBtn.title = "Браузер не поддерживает голосовой ввод";
|
|
||||||
micBtn.style.opacity = "0.5";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let rec = null;
|
|
||||||
let recording = false;
|
|
||||||
let baseText = ""; // что было в textarea ДО старта записи
|
|
||||||
let confirmedFinal = ""; // финальная фраза текущей сессии записи
|
|
||||||
|
|
||||||
micBtn.addEventListener("click", () => {
|
|
||||||
if (recording) { rec?.stop(); return; }
|
|
||||||
try {
|
|
||||||
rec = new SR();
|
|
||||||
rec.lang = "ru-RU";
|
|
||||||
rec.continuous = true;
|
|
||||||
rec.interimResults = true;
|
|
||||||
} catch (e) {
|
|
||||||
status.textContent = "Микрофон недоступен: " + e.message;
|
|
||||||
status.className = "note-status err";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
baseText = (textarea.value || "").trim();
|
|
||||||
confirmedFinal = "";
|
|
||||||
|
|
||||||
rec.onstart = () => {
|
|
||||||
recording = true;
|
|
||||||
micBtn.classList.add("rec");
|
|
||||||
micBtn.textContent = "⏹ Стоп";
|
|
||||||
status.textContent = "Слушаю...";
|
|
||||||
status.className = "note-status";
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
|
||||||
// Защита от дублей: пересчитываем ВСЕ финальные и interim с нуля
|
|
||||||
// на каждом событии. Не полагаемся на ev.resultIndex (в Chrome он
|
|
||||||
// ведёт себя нестабильно при паузах — отсюда дублирование слов).
|
|
||||||
rec.onresult = (ev) => {
|
|
||||||
let finalAll = "", interim = "";
|
|
||||||
for (let i = 0; i < ev.results.length; i++) {
|
|
||||||
const t = ev.results[i][0].transcript;
|
|
||||||
if (ev.results[i].isFinal) finalAll += t;
|
|
||||||
else interim += t;
|
|
||||||
}
|
|
||||||
confirmedFinal = finalAll.trim();
|
|
||||||
const sep = baseText ? " " : "";
|
|
||||||
const fp = confirmedFinal ? sep + confirmedFinal : "";
|
|
||||||
const ip = interim.trim() ? ((baseText || confirmedFinal) ? " " : "") + interim.trim() : "";
|
|
||||||
textarea.value = baseText + fp + ip;
|
|
||||||
};
|
|
||||||
rec.onerror = (ev) => {
|
|
||||||
status.textContent = "Ошибка распознавания: " + (ev.error || "неизвестно");
|
|
||||||
status.className = "note-status err";
|
|
||||||
recording = false;
|
|
||||||
micBtn.classList.remove("rec");
|
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
|
||||||
};
|
|
||||||
rec.onend = () => {
|
|
||||||
recording = false;
|
|
||||||
micBtn.classList.remove("rec");
|
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
|
||||||
if (status.textContent === "Слушаю...") status.textContent = "";
|
|
||||||
// Фиксируем подтверждённый текст в baseText на случай повторного запуска
|
|
||||||
if (confirmedFinal) {
|
|
||||||
baseText = (baseText + (baseText ? " " : "") + confirmedFinal).trim();
|
|
||||||
textarea.value = baseText;
|
|
||||||
}
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
|
||||||
try { rec.start(); }
|
|
||||||
catch (e) {
|
|
||||||
status.textContent = "Не запустить: " + e.message;
|
|
||||||
status.className = "note-status err";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -243,7 +243,8 @@ const Measurements = (function () {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== Голосовой ввод заметок (без дублей) ===================== */
|
/* ===================== Голосовой ввод заметок ===================== */
|
||||||
|
// continuous=false + авто-рестарт по фразам — исключает дубли на Android/iOS Chrome
|
||||||
function setupVoiceMic(micBtn, textarea, statusEl, onChange) {
|
function setupVoiceMic(micBtn, textarea, statusEl, onChange) {
|
||||||
if (!micBtn || !textarea) return;
|
if (!micBtn || !textarea) return;
|
||||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
@ -254,66 +255,75 @@ const Measurements = (function () {
|
|||||||
if (statusEl) statusEl.textContent = "недоступно в этом браузере";
|
if (statusEl) statusEl.textContent = "недоступно в этом браузере";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rec = null;
|
|
||||||
let recording = false;
|
|
||||||
let baseText = "";
|
|
||||||
let confirmedFinal = "";
|
|
||||||
|
|
||||||
micBtn.addEventListener("click", () => {
|
let active = false;
|
||||||
if (recording) { rec?.stop(); return; }
|
let baseText = "";
|
||||||
|
let curRec = null;
|
||||||
|
|
||||||
|
function startPhrase() {
|
||||||
|
let rec;
|
||||||
try {
|
try {
|
||||||
rec = new SR();
|
rec = new SR();
|
||||||
rec.lang = "ru-RU";
|
rec.lang = "ru-RU";
|
||||||
rec.continuous = true;
|
rec.continuous = false;
|
||||||
rec.interimResults = true;
|
rec.interimResults = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (statusEl) statusEl.textContent = "Микрофон недоступен: " + e.message;
|
if (statusEl) statusEl.textContent = "Микрофон недоступен: " + e.message;
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
baseText = (textarea.value || "").trim();
|
curRec = rec;
|
||||||
confirmedFinal = "";
|
|
||||||
|
|
||||||
rec.onstart = () => {
|
|
||||||
recording = true;
|
|
||||||
micBtn.classList.add("rec");
|
|
||||||
micBtn.textContent = "⏹ Стоп";
|
|
||||||
if (statusEl) statusEl.textContent = "Слушаю...";
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
|
||||||
rec.onresult = (ev) => {
|
rec.onresult = (ev) => {
|
||||||
// Пересчёт с нуля каждый раз — гарантия от дублей
|
let fin = "", itr = "";
|
||||||
let finalAll = "", interim = "";
|
|
||||||
for (let i = 0; i < ev.results.length; i++) {
|
for (let i = 0; i < ev.results.length; i++) {
|
||||||
const t = ev.results[i][0].transcript;
|
const t = ev.results[i][0].transcript.trim();
|
||||||
if (ev.results[i].isFinal) finalAll += t;
|
if (!t) continue;
|
||||||
else interim += t;
|
if (ev.results[i].isFinal) fin += (fin ? " " : "") + t;
|
||||||
|
else itr += (itr ? " " : "") + t;
|
||||||
}
|
}
|
||||||
confirmedFinal = finalAll.trim();
|
const shown = fin || itr;
|
||||||
const fp = confirmedFinal ? (baseText ? " " : "") + confirmedFinal : "";
|
textarea.value = baseText + (baseText && shown ? " " : "") + shown;
|
||||||
const ip = interim.trim() ? ((baseText || confirmedFinal) ? " " : "") + interim.trim() : "";
|
|
||||||
textarea.value = baseText + fp + ip;
|
|
||||||
};
|
|
||||||
rec.onerror = (ev) => {
|
|
||||||
if (statusEl) statusEl.textContent = "Ошибка: " + (ev.error || "неизвестно");
|
|
||||||
recording = false;
|
|
||||||
micBtn.classList.remove("rec");
|
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rec.onend = () => {
|
rec.onend = () => {
|
||||||
recording = false;
|
baseText = textarea.value.trim();
|
||||||
micBtn.classList.remove("rec");
|
if (active) {
|
||||||
micBtn.textContent = "🎤 Диктовать";
|
startPhrase();
|
||||||
if (confirmedFinal) {
|
} else {
|
||||||
baseText = (baseText + (baseText ? " " : "") + confirmedFinal).trim();
|
micBtn.classList.remove("rec");
|
||||||
textarea.value = baseText;
|
micBtn.textContent = "🎤 Диктовать";
|
||||||
|
if (statusEl && statusEl.textContent === "Слушаю...") statusEl.textContent = "";
|
||||||
|
if (onChange) onChange(textarea.value || "");
|
||||||
|
haptic && haptic("impact");
|
||||||
}
|
}
|
||||||
if (statusEl && statusEl.textContent === "Слушаю...") statusEl.textContent = "";
|
|
||||||
if (onChange) onChange(textarea.value || "");
|
|
||||||
haptic && haptic("impact");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rec.onerror = (ev) => {
|
||||||
|
if (ev.error === "no-speech") return;
|
||||||
|
if (statusEl) statusEl.textContent = "Ошибка: " + (ev.error || "неизвестно");
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
|
};
|
||||||
|
|
||||||
try { rec.start(); } catch (e) {
|
try { rec.start(); } catch (e) {
|
||||||
if (statusEl) statusEl.textContent = "Не запустить: " + e.message;
|
if (statusEl) statusEl.textContent = "Не запустить: " + e.message;
|
||||||
|
active = false; micBtn.classList.remove("rec"); micBtn.textContent = "🎤 Диктовать";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
micBtn.addEventListener("click", () => {
|
||||||
|
if (active) {
|
||||||
|
active = false;
|
||||||
|
curRec?.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active = true;
|
||||||
|
baseText = (textarea.value || "").trim();
|
||||||
|
micBtn.classList.add("rec");
|
||||||
|
micBtn.textContent = "⏹ Стоп";
|
||||||
|
if (statusEl) statusEl.textContent = "Слушаю...";
|
||||||
|
haptic && haptic("impact");
|
||||||
|
startPhrase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,14 +12,14 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap">
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
<link rel="stylesheet" href="assets/styles.css?v=20260514f">
|
<link rel="stylesheet" href="assets/styles.css?v=20260514g">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260514f">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260514g">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
|
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
|
||||||
<div class="loader splash" id="splash">
|
<div class="loader splash" id="splash">
|
||||||
<div class="brand-logo-wrap">
|
<div class="brand-logo-wrap">
|
||||||
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514f" alt="@wasrusgen1">
|
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514g" alt="@wasrusgen1">
|
||||||
<div class="splash-dust" aria-hidden="true">
|
<div class="splash-dust" aria-hidden="true">
|
||||||
<span class="dust d1"></span> <span class="dust d2"></span>
|
<span class="dust d1"></span> <span class="dust d2"></span>
|
||||||
<span class="dust d3"></span> <span class="dust d4"></span>
|
<span class="dust d3"></span> <span class="dust d4"></span>
|
||||||
@ -35,15 +35,15 @@
|
|||||||
<div class="brand-tagline-gold">CRM</div>
|
<div class="brand-tagline-gold">CRM</div>
|
||||||
</div>
|
</div>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
<script src="assets/icons.js?v=20260514f"></script>
|
<script src="assets/icons.js?v=20260514g"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260514f"></script>
|
<script src="assets/podbor.config.js?v=20260514g"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260514f"></script>
|
<script src="assets/podbor.picts.js?v=20260514g"></script>
|
||||||
<script src="assets/podbor.js?v=20260514f"></script>
|
<script src="assets/podbor.js?v=20260514g"></script>
|
||||||
<script src="assets/clients.js?v=20260514f"></script>
|
<script src="assets/clients.js?v=20260514g"></script>
|
||||||
<script src="assets/zamer-picts.js?v=20260514f"></script>
|
<script src="assets/zamer-picts.js?v=20260514g"></script>
|
||||||
<script src="assets/measurements.js?v=20260514f"></script>
|
<script src="assets/measurements.js?v=20260514g"></script>
|
||||||
<script src="assets/request.js?v=20260514f"></script>
|
<script src="assets/request.js?v=20260514g"></script>
|
||||||
<script src="assets/assembly.js?v=20260514f"></script>
|
<script src="assets/assembly.js?v=20260514g"></script>
|
||||||
<script src="assets/app.js?v=20260514f"></script>
|
<script src="assets/app.js?v=20260514g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user