Files
sber-med-wordpress/assets/js/med-diag.js
T

355 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Med Diagnostics — логика сервиса (адаптирована для WordPress) */
(function () {
'use strict';
var API_URL = (window.MED_DIAG && MED_DIAG.apiUrl) || 'https://med.my-dpr.ru';
// Абсолютный URL: если ссылка относительная — добавляем домен API
function absUrl(url) {
if (!url) return url;
return url.indexOf('http') === 0 ? url : API_URL + url;
}
var uploadArea = document.getElementById('uploadArea');
var fileInput = document.getElementById('fileInput');
var analyzeBtn = document.getElementById('analyzeBtn');
var progressContainer = document.getElementById('progressContainer');
var progressFill = document.getElementById('progressFill');
var progressText = document.getElementById('progressText');
var resultCard = document.getElementById('resultCard');
var resultContent = document.getElementById('resultContent');
if (!uploadArea || !fileInput || !analyzeBtn) return; // шорткод не выведен
var selectedFile = null;
uploadArea.addEventListener('click', function () { fileInput.click(); });
uploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function () { uploadArea.classList.remove('dragover'); });
uploadArea.addEventListener('drop', function (e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', function (e) {
if (e.target.files.length > 0) handleFile(e.target.files[0]);
});
function handleFile(file) {
selectedFile = file;
uploadArea.querySelector('p strong').textContent = '📁 ' + file.name + ' (' + (file.size / 1024 / 1024).toFixed(1) + ' МБ)';
uploadArea.querySelector('.med-hint').textContent = 'Готово к анализу';
analyzeBtn.disabled = false;
}
analyzeBtn.addEventListener('click', function () {
if (!selectedFile) return;
sendVideo(selectedFile, selectedFile.name);
});
async function sendVideo(blob, filename) {
analyzeBtn.disabled = true;
progressContainer.style.display = 'block';
resultCard.style.display = 'none';
setProgress(5, 'Загрузка видео...');
var formData = new FormData();
formData.append('video', blob, filename);
// Автоповтор при обрыве соединения (нестабильная мобильная сеть)
var MAX_ATTEMPTS = 3;
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
var response = await fetch(API_URL + '/analyze', {
method: 'POST',
body: formData
});
if (!response.ok) {
var err = await response.json();
var e = new Error(err.error || 'Ошибка анализа');
e.videoUrl = err.video_url;
throw e;
}
setProgress(90, 'Формирование результата...');
var data = await response.json();
if (data.error) {
showError(data.error);
return;
}
setProgress(100, 'Готово!');
setTimeout(function () {
progressContainer.style.display = 'none';
renderResult(data);
}, 300);
return;
} catch (err) {
// TypeError "Load failed" = обрыв соединения — пробуем ещё раз
if (err instanceof TypeError && attempt < MAX_ATTEMPTS) {
setProgress(5, 'Соединение прервано, повторная попытка (' + attempt + '/' + (MAX_ATTEMPTS - 1) + ')...');
await new Promise(function (r) { setTimeout(r, 1500); });
continue;
}
if (err instanceof TypeError) {
showError('Соединение прервано при загрузке видео. Проверьте интернет и попробуйте ещё раз.');
} else {
showError(err.message, err.videoUrl);
}
return;
}
}
}
function setProgress(pct, text) {
progressFill.style.width = pct + '%';
progressText.textContent = text;
}
function showError(msg, videoUrl) {
progressContainer.style.display = 'none';
resultCard.style.display = 'block';
var html = '<div class="med-error-box"><strong>Ошибка:</strong> ' + msg + '</div>';
if (videoUrl) {
html += '<div style="text-align:center; margin-top:15px">' +
'<a class="med-btn-download" href="' + absUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
'</div>';
}
resultContent.innerHTML = html;
analyzeBtn.disabled = false;
}
function renderResult(data) {
resultCard.style.display = 'block';
var html = '';
// Кнопки скачивания (вверху блока результатов)
if (data.share_id) {
html += '<div class="med-download-buttons">' +
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/pdf">📄 Скачать PDF</a>' +
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/docx">📝 Скачать DOCX</a>';
if (data.video_url) {
html += '<a class="med-btn-download" href="' + absUrl(data.video_url) + '">🎬 Скачать видео</a>';
}
html += '<a class="med-btn-download" href="https://max.ru/miac_dnr_bot" target="_blank" rel="noopener">🏥 Запись на диспанцеризацию</a>' +
'</div>';
}
// Сводка
html += '<div class="med-summary-box">' + data.summary + '</div>';
// Предупреждения
if (data.warnings && data.warnings.length > 0) {
html += '<div class="med-warnings-box"><h3>⚠️ Требует внимания</h3><ul>';
data.warnings.forEach(function (w) { html += '<li>' + w + '</li>'; });
html += '</ul></div>';
}
// Таблица показателей
html += '<h3>Показатели</h3>';
html += '<table class="med-metrics-table"><thead><tr><th>Показатель</th><th>Значение</th><th>Оценка</th></tr></thead><tbody>';
data.metrics.forEach(function (m) {
var statusText = {
'ok': 'В норме',
'warning': 'Обратить внимание',
'danger': 'Отклонение',
'info': 'Информация'
}[m.status] || m.status;
html += '<tr>' +
'<td>' + m.name + '</td>' +
'<td><strong>' + m.value + '</strong>' + (m.comment ? '<br><small style="color:#888">' + m.comment + '</small>' : '') + '</td>' +
'<td><span class="med-status-badge med-status-' + m.status + '">' + statusText + '</span></td>' +
'</tr>';
});
html += '</tbody></table>';
// Рекомендации
if (data.advice && data.advice.length > 0) {
html += '<div class="med-advice-box"><h3>💡 Рекомендации</h3><ul>';
data.advice.forEach(function (a) { html += '<li>' + a + '</li>'; });
html += '</ul></div>';
}
html += '<div class="med-disclaimer">⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. Для точной оценки обратитесь к врачу.</div>';
// Кнопка "Поделиться"
if (data.share_url) {
html += '<div style="text-align:center; margin-top:20px">' +
'<button class="med-btn" onclick="window.MED_DIAG_SHARE && MED_DIAG_SHARE(\'' + data.share_url + '\', this)">🔗 Поделиться результатом</button>' +
'</div>';
}
resultContent.innerHTML = html;
resultCard.scrollIntoView({ behavior: 'smooth' });
}
// Поделиться (глобальная функция для inline-onclick)
window.MED_DIAG_SHARE = function (url, btn) {
navigator.clipboard.writeText(url).then(function () {
var oldText = btn.textContent;
btn.textContent = '✅ Ссылка скопирована!';
setTimeout(function () { btn.textContent = oldText; }, 2000);
});
};
// ─── Сканирование с камеры ───
var scanSection = document.getElementById('scanSection');
var scanBtn = document.getElementById('scanBtn');
var scanModal = document.getElementById('scanModal');
var scanVideo = document.getElementById('scanVideo');
var scanProgressText = document.getElementById('scanProgressText');
var scanProgressFill = document.getElementById('scanProgressFill');
var scanCancelBtn = document.getElementById('scanCancelBtn');
var SCAN_FRAMES = 310; // чуть больше 300, чтобы в файле гарантированно было >= 300 кадров
var SCAN_FPS = 30;
var FRAME_INTERVAL = 1000 / SCAN_FPS;
var mediaStream = null;
var mediaRecorder = null;
var scanCanvas = null;
var scanCtx = null;
var scanFrameCount = 0;
var scanRafId = null;
var scanLastFrameTime = 0;
var scanChunks = [];
var scanActive = false;
// Показываем кнопку сканирования только на мобильных устройствах
var isMobile = window.matchMedia('(max-width: 768px)').matches || ('ontouchstart' in window);
if (isMobile && scanSection) {
scanSection.style.display = 'block';
}
if (scanBtn) scanBtn.addEventListener('click', startScan);
if (scanCancelBtn) scanCancelBtn.addEventListener('click', cancelScan);
async function startScan() {
if (scanActive) return;
scanActive = true;
scanBtn.disabled = true;
// Включаем фронтальную камеру
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
});
} catch (err) {
scanActive = false;
scanBtn.disabled = false;
showError('Не удалось получить доступ к камере: ' + (err.message || 'разрешите доступ'));
return;
}
scanModal.classList.add('open');
scanVideo.srcObject = mediaStream;
try { await scanVideo.play(); } catch (e) {}
scanChunks = [];
scanFrameCount = 0;
scanLastFrameTime = 0;
updateScanProgress(0);
// Canvas 720×720 — квадрат, лицо целиком влезает в portrait и landscape.
// Серверное extract_frames масштабирует до 1280×720 для SDK.
scanCanvas = document.createElement('canvas');
scanCanvas.width = 720;
scanCanvas.height = 720;
scanCtx = scanCanvas.getContext('2d');
var captureStream;
try {
captureStream = scanCanvas.captureStream(SCAN_FPS);
} catch (e) {
// Fallback: записываем напрямую с камеры
captureStream = mediaStream;
}
var mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', 'video/mp4']
.find(function (t) { return MediaRecorder.isTypeSupported(t); }) || '';
mediaRecorder = new MediaRecorder(captureStream, {
mimeType: mimeType || undefined,
videoBitsPerSecond: 1500000 // 1.5 Мбит/с — маленький файл, качество достаточное
});
mediaRecorder.ondataavailable = function (e) { if (e.data.size > 0) scanChunks.push(e.data); };
mediaRecorder.onstop = function () {
var blob = new Blob(scanChunks, { type: mediaRecorder.mimeType || 'video/webm' });
var ext = blob.type.indexOf('mp4') !== -1 ? 'mp4' : 'webm';
scanModal.classList.remove('open');
stopCamera();
scanActive = false;
scanBtn.disabled = false;
sendVideo(blob, 'scan.' + ext);
};
mediaRecorder.start();
// Рисуем кадры с ограничением ~30 fps, считаем до 300
var drawFrame = function (ts) {
if (!scanActive) return;
if (scanFrameCount >= SCAN_FRAMES) {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
return;
}
if (ts - scanLastFrameTime >= FRAME_INTERVAL) {
// Квадратный center-crop: берём центральную квадратную область из видео.
var vw = scanVideo.videoWidth;
var vh = scanVideo.videoHeight;
if (vw && vh) {
var side = Math.min(vw, vh);
var sx = (vw - side) / 2;
var sy = (vh - side) / 2;
scanCtx.drawImage(scanVideo, sx, sy, side, side, 0, 0, 720, 720);
} else {
scanCtx.drawImage(scanVideo, 0, 0, 720, 720);
}
scanFrameCount++;
scanLastFrameTime = ts;
updateScanProgress(scanFrameCount);
}
scanRafId = requestAnimationFrame(drawFrame);
};
scanRafId = requestAnimationFrame(drawFrame);
}
function updateScanProgress(count) {
var pct = Math.min(100, Math.round(count / SCAN_FRAMES * 100));
scanProgressText.textContent = 'Кадров: ' + count + ' / ' + SCAN_FRAMES + ' (' + pct + '%)';
scanProgressFill.style.width = pct + '%';
}
function stopCamera() {
if (mediaStream) {
mediaStream.getTracks().forEach(function (t) { t.stop(); });
mediaStream = null;
}
if (scanRafId) cancelAnimationFrame(scanRafId);
scanRafId = null;
scanVideo.srcObject = null;
}
function cancelScan() {
scanActive = false;
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.onstop = null;
mediaRecorder.stop();
}
stopCamera();
scanModal.classList.remove('open');
scanBtn.disabled = false;
}
})();