/
pmff
/
ect-callcenter-demo
Обзор
Документация
Войти
/
pmff
/
ect-callcenter-demo
Код
Запросы
0
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
script.js
238 строк
9 KB
PMFF
Собрать демо контроля звонков для ЕЦТ
18 июл 2026, 15:20
18 июл 2026, 15:20
98be6e9
Код
Авторство
О чём код?
(() => { "use strict"; const TOTAL_SECONDS = 58; const STAGE_STARTS = [0, 14.8, 21.8, 29.2, 39.2, 51.2]; const root = document.documentElement; const consoleElement = document.querySelector(".console"); const startButton = document.querySelector("#start-button"); const playButton = document.querySelector("#play-button"); const resetButton = document.querySelector("#reset-button"); const scenarioStatus = document.querySelector("#scenario-status"); const timer = document.querySelector("#timer"); const callTime = document.querySelector("#call-time"); const callBadge = document.querySelector("#call-badge"); const qualityCounter = document.querySelector("#quality-counter"); const qualityScore = document.querySelector("#quality-score"); const confidence = document.querySelector("#confidence"); const eventCount = document.querySelector("#event-count"); const timelineProgress = document.querySelector("#timeline-progress"); const transcript = document.querySelector("#transcript"); const transcriptEmpty = document.querySelector("#transcript-empty"); const eventPlaceholder = document.querySelector("#event-placeholder"); const routeCard = document.querySelector("#route-card"); const completionCard = document.querySelector("#completion-card"); const stageNumber = document.querySelector("#stage-number"); const stages = [...document.querySelectorAll(".stage")]; const transcriptLines = [...document.querySelectorAll(".transcript-line")]; const events = [...document.querySelectorAll(".event")]; const valueFields = [...document.querySelectorAll("[data-value-at]")]; const qualityChecks = [...document.querySelectorAll("[data-check-at]")]; let elapsed = 0; let running = false; let completed = false; let lastFrame = null; let animationFrame = null; function formatTime(seconds) { const safeSeconds = Math.max(0, Math.floor(seconds)); const minutes = Math.floor(safeSeconds / 60); const remainder = safeSeconds % 60; return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`; } function currentStage(seconds) { let active = 0; STAGE_STARTS.forEach((start, index) => { if (seconds >= start) active = index; }); return active; } function setButtonState(button, isPlaying) { const icon = button.querySelector(".button-icon"); const label = button.querySelector(".button-label"); if (icon) icon.textContent = isPlaying ? "Ⅱ" : "▶"; if (label) label.textContent = isPlaying ? "Пауза" : completed ? "Повторить" : "Запустить"; button.setAttribute("aria-label", isPlaying ? "Приостановить демонстрацию" : "Запустить демонстрацию"); } function revealByTime(elements, attribute, seconds) { let count = 0; elements.forEach((element) => { const threshold = Number(element.dataset[attribute]); const visible = seconds >= threshold; element.classList.toggle("is-visible", visible); if (visible) count += 1; }); return count; } function render(seconds) { const stageIndex = currentStage(seconds); const progress = Math.min(100, (seconds / TOTAL_SECONDS) * 100); const displayedCallTime = Math.min(seconds, 14.2); timer.textContent = formatTime(seconds); callTime.textContent = formatTime(displayedCallTime); timelineProgress.style.width = `${progress}%`; root.style.setProperty("--demo-progress", `${progress}%`); stageNumber.textContent = String(stageIndex + 1).padStart(2, "0"); stages.forEach((stage, index) => { stage.classList.toggle("is-active", index === stageIndex && !completed); stage.classList.toggle("is-complete", index < stageIndex || completed); }); const transcriptCount = revealByTime(transcriptLines, "revealAt", seconds); transcriptEmpty.hidden = transcriptCount > 0; const visibleEvents = revealByTime(events, "revealAt", seconds); eventPlaceholder.hidden = visibleEvents > 0; eventCount.textContent = `${visibleEvents} ${visibleEvents === 1 ? "событие" : "событий"}`; valueFields.forEach((field) => { const visible = seconds >= Number(field.dataset.valueAt); field.classList.toggle("is-filled", visible); field.textContent = visible ? field.dataset.value : field.dataset.placeholder; }); const checkedCount = qualityChecks.reduce((count, check) => { const checked = seconds >= Number(check.dataset.checkAt); check.classList.toggle("is-checked", checked); return count + Number(checked); }, 0); qualityCounter.textContent = `${checkedCount} / 6 критериев`; qualityScore.textContent = `${Math.round((checkedCount / 6) * 100)}%`; const confidenceValue = seconds < 15.5 ? null : Math.min(97, Math.round(70 + (seconds - 15.5) * 2.2)); confidence.textContent = confidenceValue ? `${confidenceValue}%` : "—%"; routeCard.classList.toggle("is-ready", seconds >= STAGE_STARTS[4]); completionCard.classList.toggle("is-visible", completed); if (seconds < STAGE_STARTS[1]) { scenarioStatus.textContent = running ? "Идёт разговор" : "Ожидает запуска"; callBadge.textContent = running ? "разговор идёт" : "готов к запуску"; } else if (seconds < STAGE_STARTS[2]) { scenarioStatus.textContent = "Извлечение данных"; callBadge.textContent = "звонок завершён"; } else if (seconds < STAGE_STARTS[3]) { scenarioStatus.textContent = "Проверка скрипта"; callBadge.textContent = "запись сохранена"; } else if (seconds < STAGE_STARTS[4]) { scenarioStatus.textContent = "Проверка дублей"; callBadge.textContent = "запись сохранена"; } else if (seconds < STAGE_STARTS[5]) { scenarioStatus.textContent = "Маршрутизация"; callBadge.textContent = "запись сохранена"; } else { scenarioStatus.textContent = completed ? "Лид передан" : "Передача в CRM"; callBadge.textContent = "запись сохранена"; } consoleElement.classList.toggle("is-running", running && seconds < 14.5); consoleElement.classList.toggle("is-complete", completed); if (running && transcriptCount > 0) { const visibleLine = transcriptLines[transcriptCount - 1]; visibleLine.scrollIntoView({ block: "nearest", behavior: "smooth" }); } if (visibleEvents > 0) { events[visibleEvents - 1].scrollIntoView({ block: "nearest", behavior: "smooth" }); } setButtonState(startButton, running); setButtonState(playButton, running); } function tick(timestamp) { if (!running) return; if (lastFrame === null) lastFrame = timestamp; const delta = (timestamp - lastFrame) / 1000; lastFrame = timestamp; elapsed = Math.min(TOTAL_SECONDS, elapsed + delta); if (elapsed >= TOTAL_SECONDS) { running = false; completed = true; lastFrame = null; render(elapsed); return; } render(elapsed); animationFrame = requestAnimationFrame(tick); } function play() { if (completed) reset(false); if (running) return; running = true; lastFrame = null; render(elapsed); animationFrame = requestAnimationFrame(tick); } function pause() { running = false; lastFrame = null; if (animationFrame) cancelAnimationFrame(animationFrame); render(elapsed); } function toggle() { if (running) pause(); else play(); } function reset(shouldRender = true) { running = false; completed = false; elapsed = 0; lastFrame = null; if (animationFrame) cancelAnimationFrame(animationFrame); transcript.scrollTop = 0; document.querySelector("#event-stream").scrollTop = 0; if (shouldRender) render(elapsed); } valueFields.forEach((field) => { field.dataset.value = field.textContent.trim(); }); startButton.addEventListener("click", toggle); playButton.addEventListener("click", toggle); resetButton.addEventListener("click", () => reset()); document.addEventListener("keydown", (event) => { if (event.code === "Space" && !event.target.matches("button, a, input, textarea")) { event.preventDefault(); toggle(); } if (event.key.toLowerCase() === "r") { reset(); } }); document.addEventListener("visibilitychange", () => { if (document.hidden && running) pause(); }); render(0); const params = new URLSearchParams(window.location.search); if (params.get("autoplay") === "1") { window.setTimeout(play, 700); } })();