diplomus / static /script.js
Arghet6's picture
Upload 44 files
6bcf797 verified
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 = '<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);
}
}
// 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 =
'<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 }
});
// Статистика по эмоциям — 2 строки по 3 эмоции
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 () {
// Удаляем класс '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();
}
});