| 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"); |
|
|
| |
| 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 = '<div class="empty-chat">Нет активного чата</div>'; |
| } |
|
|
| 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 = ` |
| <div class="chat-item-main"> |
| <i class="fas fa-comment chat-icon"></i> |
| <div class="chat-item-content"> |
| <span class="chat-title">${chat.title}</span> |
| <span class="chat-date">${formatDate(chat.created_at)}</span> |
| </div> |
| </div> |
| <button class="delete-chat-btn" title="Удалить чат"> |
| <i class="fas fa-trash"></i> |
| </button> |
| `; |
| 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 = '<div class="empty-chat">Чат удалён</div>'; |
| 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 = '<div class="message bot-message">Привет! Отправьте текст или голосовое сообщение для анализа эмоций.</div>'; |
| 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); |
| } |
| } |
|
|
| |
| 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'; |
| } |
|
|
| |
| |
| 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 = |
| '<div class="empty-state"><i class="fas fa-comment-slash"></i><p>Нет данных для отображения</p></div>'; |
| 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: |
| '<b>%{x|%d %b %Y}</b><br>' + |
| 'Эмоция: %{fullData.name}<br>' + |
| 'Сообщений: %{y}<br>' + |
| 'Средняя уверенность: %{customdata.avgConfidence}<extra></extra>' |
| }; |
| }); |
|
|
| 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 = ` |
| <ul style="color: white;"> |
| <li>💡 Преобладает: ${dominant[0]} (${((dominant[1]/totalMessages)*100).toFixed(1)}%)</li> |
| <li>📉 Пик грусти: ${sadPeak[0]} (${sadPeak[1]} сообщений)</li> |
| </ul> |
| `; |
|
|
| |
| 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 } |
| }); |
|
|
| |
| const statsHTML = pieLabels.slice(0, 6).map((emotion, i) => { |
| const percentage = ((pieValues[i] / totalMessages) * 100).toFixed(1); |
| return ` |
| <div class="emotion-stat"> |
| <div class="emotion-label" style="color: ${getEmotionColor(emotion)}"> |
| <span>${emotion.replace(/[\u{1F600}-\u{1F64F}]/gu, '')}</span> |
| </div> |
| <div class="confidence-bar"> |
| <div class="confidence-fill" |
| style="width: ${percentage}%; |
| background: ${getEmotionColor(emotion)};"></div> |
| </div> |
| <div class="confidence-value">${percentage}%</div> |
| </div>`; |
| }).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 = |
| `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>Ошибка загрузки данных: ${error.message}</p></div>`; |
| } |
| } |
|
|
| |
| 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 = '<option value="all">Все участники</option>'; |
|
|
| 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 () { |
| |
| document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active')); |
| |
| this.classList.add('active'); |
|
|
| |
| const range = this.getAttribute('data-range'); |
|
|
| |
| updateTelegramAnalytics(range); |
| }); |
| }); |
|
|
| |
| 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(); |
| } |
| }); |