document.addEventListener("DOMContentLoaded", () => { let mediaRecorder, audioChunks = [], audioStream, currentChatId = null; const recordBtn = document.getElementById("record-btn"); const stopBtn = document.getElementById("stop-btn"); const sendBtn = document.getElementById("send-btn"); const userInput = document.getElementById("user-input"); const chatBox = document.getElementById("chat-box"); const audioFileInput = document.getElementById("audio-file"); const newChatBtn = document.getElementById("new-chat-btn"); const chatList = document.getElementById("chat-list"); const currentChatTitle = document.getElementById("current-chat-title"); const fileInfo = document.getElementById("file-info"); const fileName = document.getElementById("file-name"); const clearFileBtn = document.getElementById("clear-file"); // Emotion Map (скопирован из profile.html) const emotionMap = { 'joy': '😊 Радость', 'neutral': '😐 Нейтрально', 'anger': '😠 Злость', 'sadness': '😢 Грусть', 'surprise': '😲 Удивление' }; // Инициализация при загрузке initializeChats(); function initializeChats() { const savedChatId = localStorage.getItem('currentChatId'); fetch("/get_chats") .then(response => response.json()) .then(chats => { renderChatList(chats); if (savedChatId && chats.some(c => c.chat_id === savedChatId)) { loadChat(savedChatId); } else if (chats.length > 0) { loadChat(chats[0].chat_id); } else { showEmptyChatUI(); } }) .catch(error => { console.error("Ошибка загрузки чатов:", error); showEmptyChatUI(); }); } function showEmptyChatUI() { if (chatBox) chatBox.innerHTML = '
Нет активного чата
'; } function renderChatList(chats) { if (!chatList) return; chatList.innerHTML = ''; chats.forEach(chat => { const chatItem = document.createElement("div"); chatItem.className = "chat-item"; chatItem.dataset.chatId = chat.chat_id; chatItem.innerHTML = `
${chat.title} ${formatDate(chat.created_at)}
`; chatItem.querySelector('.chat-item-main').addEventListener('click', () => { loadChat(chat.chat_id); localStorage.setItem('currentChatId', chat.chat_id); }); chatItem.querySelector('.delete-chat-btn').addEventListener('click', (e) => { e.stopPropagation(); deleteChat(chat.chat_id); }); chatList.appendChild(chatItem); }); } function formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('ru-RU'); } async function deleteChat(chatId) { if (!confirm('Вы точно хотите удалить этот чат? Это действие нельзя отменить.')) return; try { const response = await fetch(`/delete_chat/${chatId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCSRFToken() } }); const result = await response.json(); if (result.success) { if (currentChatId === chatId) { chatBox.innerHTML = '
Чат удалён
'; currentChatId = null; } initializeChats(); // Перезагружаем список чатов } else { throw new Error(result.error || 'Ошибка при удалении чата'); } } catch (error) { console.error('Delete chat error:', error); appendMessage('bot', `❌ Ошибка при удалении: ${error.message}`); } } function getCSRFToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.content : ''; } newChatBtn?.addEventListener("click", startNewChat); function startNewChat() { fetch("/start_chat", { method: "POST", headers: { "Content-Type": "application/json" }, }) .then(response => response.json()) .then(data => { currentChatId = data.chat_id; if (currentChatTitle) { currentChatTitle.textContent = data.title; } chatBox.innerHTML = '
Привет! Отправьте текст или голосовое сообщение для анализа эмоций.
'; initializeChats(); localStorage.setItem('currentChatId', data.chat_id); }) .catch(console.error); } function loadChat(chatId) { fetch(`/load_chat/${chatId}`) .then(response => response.json()) .then(data => { if (data.error) throw new Error(data.error); currentChatId = chatId; currentChatTitle.textContent = data.title; updateActiveChat(chatId); chatBox.innerHTML = ""; data.messages.forEach(msg => { appendMessage(msg.sender, msg.content); }); localStorage.setItem('currentChatId', chatId); }) .catch(error => { console.error("Ошибка загрузки чата:", error); appendMessage("bot", `❌ Ошибка: ${error.message}`); }); } function updateActiveChat(chatId) { document.querySelectorAll(".chat-item").forEach(item => { item.classList.toggle("active", item.dataset.chatId === chatId); }); } sendBtn?.addEventListener("click", sendMessage); userInput?.addEventListener("keypress", (e) => { if (e.key === "Enter") sendMessage(); }); async function sendMessage() { const text = userInput?.value.trim(); if (!text || !currentChatId) return; appendAndSaveMessage("user", text); userInput.value = ""; try { const response = await fetch("/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, chat_id: currentChatId }) }); const data = await response.json(); appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`); } catch (error) { console.error("Ошибка:", error); appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`); } } // Обработчики аудио if (audioFileInput) { audioFileInput.addEventListener("change", handleAudioUpload); } if (clearFileBtn) { clearFileBtn.addEventListener("click", clearAudioFile); } function handleAudioUpload() { const file = audioFileInput?.files[0]; if (file) { fileName.textContent = file.name; fileInfo.style.display = 'flex'; sendAudioFile(file); } } function clearAudioFile() { audioFileInput.value = ''; fileInfo.style.display = 'none'; } async function sendAudioFile(file) { if (!currentChatId) return; appendAndSaveMessage("user", "Загружен аудиофайл..."); try { const formData = new FormData(); formData.append("audio", file); formData.append("chat_id", currentChatId); const response = await fetch("/analyze_audio", { method: "POST", body: formData }); const data = await response.json(); if (data.transcribed_text) { appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`); } appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`); clearAudioFile(); } catch (error) { console.error("Ошибка:", error); appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`); } } // Запись голоса if (recordBtn) recordBtn.addEventListener("click", startRecording); if (stopBtn) stopBtn.addEventListener("click", stopRecording); async function startRecording() { try { audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(audioStream); audioChunks = []; mediaRecorder.ondataavailable = e => audioChunks.push(e.data); mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunks, { type: "audio/wav" }); sendAudioBlob(audioBlob); }; mediaRecorder.start(); if (recordBtn) recordBtn.disabled = true; if (stopBtn) stopBtn.disabled = false; appendMessage("user", "Запись начата..."); } catch (error) { console.error("Ошибка записи:", error); appendMessage("bot", "❌ Не удалось получить доступ к микрофону"); } } function stopRecording() { if (mediaRecorder?.state === "recording") { mediaRecorder.stop(); if (recordBtn) recordBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; if (audioStream) audioStream.getTracks().forEach(track => track.stop()); } } async function sendAudioBlob(audioBlob) { if (!currentChatId) return; appendAndSaveMessage("user", "Отправлено голосовое сообщение..."); try { const formData = new FormData(); formData.append("audio", audioBlob, "recording.wav"); formData.append("chat_id", currentChatId); const response = await fetch("/analyze_audio", { method: "POST", body: formData }); const data = await response.json(); if (data.transcribed_text) { appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`); } appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`); } catch (error) { console.error("Ошибка:", error); appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`); } } function appendMessage(sender, text) { const message = document.createElement("div"); message.className = `message ${sender}-message`; message.innerHTML = text; if (chatBox) { chatBox.appendChild(message); chatBox.scrollTop = chatBox.scrollHeight; } } function appendAndSaveMessage(sender, text) { appendMessage(sender, text); if (currentChatId) { fetch("/save_message", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": getCSRFToken() }, body: JSON.stringify({ chat_id: currentChatId, sender: sender, content: text }) }).catch(console.error); } } // Telegram анализ document.getElementById('telegram-upload-form')?.addEventListener('submit', async function(e) { e.preventDefault(); const fileInput = document.getElementById('telegram-file'); const file = fileInput.files[0]; if (!file) { alert('Пожалуйста, выберите файл'); return; } try { const formData = new FormData(); formData.append('file', file); const response = await fetch('/analyze_telegram_chat', { method: 'POST', body: formData }); const result = await response.json(); if (result.error) { throw new Error(result.error); } alert('Анализ завершен успешно!'); updateTelegramAnalytics(); // Обновляем графики } catch (error) { console.error('Ошибка загрузки файла:', error); alert(`Ошибка: ${error.message}`); } }); // Функция скользящего среднего function movingAverage(data, windowSize) { const result = []; for (let i = 0; i < data.length; i++) { const start = Math.max(0, i - windowSize + 1); const slice = data.slice(start, i + 1); const avg = slice.reduce((sum, val) => sum + val, 0) / slice.length; result.push(avg); } return result; } // Цвета эмоций function getEmotionColor(emotion) { const colors = { '😊 Радость': '#00b894', '😢 Грусть': '#0984e3', '😠 Злость': '#d63031', '😲 Удивление': '#fdcb6e', '😨 Страх': '#a29bfe', '😐 Нейтрально': '#636e72' }; return colors[emotion] || '#4a4ae8'; } // Иконки эмоций function getEmotionIcon(emotion) { const icons = { '😊 Радость': 'fa-smile', '😢 Грусть': 'fa-sad-tear', '😠 Злость': 'fa-angry', '😲 Удивление': 'fa-surprise', '😨 Страх': 'fa-flushed', '😐 Нейтрально': 'fa-meh' }; return icons[emotion] || 'fa-comment'; } // Основная функция анализа Telegram // Основная функция анализа Telegram async function updateTelegramAnalytics(range = 'month') { try { const response = await fetch('/get_telegram_analysis'); const analyses = await response.json(); if (!analyses || analyses.length === 0) { document.getElementById('emotion-timeline').innerHTML = '

Нет данных для отображения

'; document.getElementById('emotion-distribution').innerHTML = ''; return; } const allData = analyses.flatMap(a => JSON.parse(a.data)); const emotionMap = { 'joy': '😊 Радость', 'sadness': '😢 Грусть', 'anger': '😠 Злость', 'surprise': '😲 Удивление', 'fear': '😨 Страх', 'no_emotion': '😐 Нейтрально' }; // Фильтрация по пользователю const userSelect = document.getElementById('user-select'); const selectedUser = userSelect?.value; let filteredData = allData; if (selectedUser && selectedUser !== 'all') { filteredData = allData.filter(d => d.from === selectedUser); } const processedData = filteredData.map(d => ({ emotion: emotionMap[d.emotion] || d.emotion, from: d.from, text: d.text, date: new Date(d.timestamp), confidence: d.confidence })); // --- Блок подготовки графиков --- const groupByTime = (date, range) => { const d = new Date(date); if (range === 'week') { d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - d.getDay()); return d; } else if (range === 'month') { return new Date(d.getFullYear(), d.getMonth(), 1); } else if (range === 'year') { return new Date(d.getFullYear(), 0, 1); } return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }; const groupedData = {}; processedData.forEach(d => { const timeKey = groupByTime(d.date, range).getTime(); if (!groupedData[timeKey]) { groupedData[timeKey] = { date: new Date(timeKey), emotions: {} }; } if (!groupedData[timeKey].emotions[d.emotion]) { groupedData[timeKey].emotions[d.emotion] = { count: 0, totalConfidence: 0 }; } groupedData[timeKey].emotions[d.emotion].count++; groupedData[timeKey].emotions[d.emotion].totalConfidence += d.confidence; }); const timeKeys = Object.keys(groupedData).sort(); const emotions = [...new Set(processedData.map(d => d.emotion))]; const traces = emotions.map(emotion => { const x = []; const y = []; const customdata = []; timeKeys.forEach(key => { const dataPoint = groupedData[key]; if (dataPoint.emotions[emotion]) { x.push(dataPoint.date); y.push(dataPoint.emotions[emotion].count); customdata.push({ emotion: emotion, avgConfidence: (dataPoint.emotions[emotion].totalConfidence / dataPoint.emotions[emotion].count).toFixed(2) }); } else { x.push(dataPoint.date); y.push(0); customdata.push(null); } }); return { x: x, y: y, name: emotion, type: 'scatter', mode: 'lines+markers', line: { shape: 'spline' }, marker: { color: getEmotionColor(emotion), size: 6 }, fill: 'tonexty', fillcolor: `${getEmotionColor(emotion)}7F`, customdata: customdata, hovertemplate: '%{x|%d %b %Y}
' + 'Эмоция: %{fullData.name}
' + 'Сообщений: %{y}
' + 'Средняя уверенность: %{customdata.avgConfidence}' }; }); Plotly.newPlot('emotion-timeline', traces, { title: false, plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', font: { color: 'white' }, xaxis: { title: 'Дата', tickformat: range === 'year' ? '%Y' : range === 'month' ? '%b %Y' : '%d %b', gridcolor: 'rgba(255,255,255,0.1)' }, yaxis: { title: 'Количество сообщений', gridcolor: 'rgba(255,255,255,0.1)' }, hovermode: 'closest', legend: { orientation: 'h', y: -0.2 }, margin: { t: 0, b: 80 } }); // Тепловая карта const dates = [...new Set(processedData.map(d => d.date.toDateString()))]; const emotionLabels = Object.values(emotionMap); const z = emotionLabels.map(e => dates.map(d => processedData.filter(msg => msg.date.toDateString() === d && msg.emotion === e).length )); Plotly.newPlot('calendar-heatmap', [{ type: 'heatmap', z: z, x: dates, y: emotionLabels, colorscale: [ [0, '#2d3436'], // Темный фон [0.5, '#6c5ce7'], [1, '#00b894'] ], showscale: true, colorbar: { title: 'Частота', titleside: 'top', tickmode: 'array', tickvals: [0, Math.max(...z.flat())], ticktext: ['Мало', 'Много'], ticks: 'outside' } }], { title: 'Тепловая карта эмоций по дням', xaxis: { title: 'Дата', tickangle: -45 }, yaxis: { title: 'Эмоции', automargin: true }, margin: { t: 30, r: 30, l: 80, b: 80 } }); // Автоматический анализ const totalMessages = processedData.length; const emotionCounts = {}; processedData.forEach(d => { emotionCounts[d.emotion] = (emotionCounts[d.emotion] || 0) + 1; }); const sorted = Object.entries(emotionCounts).sort((a, b) => b[1] - a[1]); const dominant = sorted[0]; const sadnessPeaks = processedData .filter(d => d.emotion === '😢 Грусть') .reduce((acc, d) => { const key = d.date.toDateString(); acc[key] = (acc[key] || 0) + 1; return acc; }, {}); const sadPeak = Object.entries(sadnessPeaks).sort((a, b) => b[1] - a[1])[0]; document.getElementById('summary-content').innerHTML = ` `; // Круговая диаграмма и статистика const pieLabels = Object.keys(emotionCounts); const pieValues = Object.values(emotionCounts); Plotly.newPlot('emotion-distribution-pie', [{ labels: pieLabels, values: pieValues, type: 'pie', textinfo: 'label+percent', hoverinfo: 'label+value+percent', marker: { colors: pieLabels.map(e => getEmotionColor(e)) }, textfont: { color: 'white' }, hole: 0.4, rotation: 45 }], { title: false, plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', font: { color: 'white' }, showlegend: false, margin: { t: 0, b: 0, l: 0, r: 0 } }); // Статистика по эмоциям — 2 строки по 3 эмоции const statsHTML = pieLabels.slice(0, 6).map((emotion, i) => { const percentage = ((pieValues[i] / totalMessages) * 100).toFixed(1); return `
${emotion.replace(/[\u{1F600}-\u{1F64F}]/gu, '')}
${percentage}%
`; }).join(''); document.getElementById('emotion-distribution').innerHTML = statsHTML; // Адаптация сетки на фронтенде const statsContainer = document.getElementById('emotion-distribution'); if (statsContainer) { statsContainer.style.display = 'grid'; statsContainer.style.gridTemplateColumns = 'repeat(3, 1fr)'; statsContainer.style.gap = '15px'; } // --- Выбор пользователя --- populateUserSelect(processedData); } catch (error) { console.error('Ошибка обновления аналитики:', error); document.getElementById('emotion-timeline').innerHTML = `

Ошибка загрузки данных: ${error.message}

`; } } // Глобальная переменная для хранения списка пользователей let telegramUsers = []; function populateUserSelect(processedData) { const userSelect = document.getElementById('user-select'); if (!userSelect) return; const users = [...new Set(processedData.map(d => d.from))]; // Если пользователи не изменились — ничего не делаем if (JSON.stringify(users.sort()) === JSON.stringify(telegramUsers.sort())) return; telegramUsers = users; userSelect.innerHTML = ''; users.forEach(user => { const option = document.createElement('option'); option.value = user; option.textContent = user; userSelect.appendChild(option); }); // Добавляем обработчик только один раз if (!userSelect.dataset.listenerAdded) { userSelect.addEventListener('change', () => { updateTelegramAnalytics(document.querySelector('.time-btn.active')?.dataset.range || 'month'); }); userSelect.dataset.listenerAdded = 'true'; } } document.querySelectorAll('.time-btn').forEach(button => { button.addEventListener('click', function () { // Удаляем класс 'active' у всех кнопок document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active')); // Добавляем класс 'active' текущей кнопке this.classList.add('active'); // Получаем диапазон времени из атрибута data-range const range = this.getAttribute('data-range'); // Вызываем функцию обновления графиков с новым диапазоном updateTelegramAnalytics(range); }); }); // Проверяем, есть ли сохранённый активный временной интервал в localStorage или просто ставим 'month' window.addEventListener('load', () => { const activeTimeBtn = document.querySelector('.time-btn.active'); if (activeTimeBtn) { const range = activeTimeBtn.dataset.range || 'month'; updateTelegramAnalytics(range); } else { updateTelegramAnalytics('month'); } }); // Инициализация при загрузке страницы if (window.location.pathname.includes('/profile')) { updateTelegramAnalytics(); } });