Arghet6 commited on
Commit
ba486f5
·
verified ·
1 Parent(s): f7c760b

Upload 9 files

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ static/avatars/38073be902884c20986765aa948ccb53_123.jpg filter=lfs diff=lfs merge=lfs -text
2
+ static/avatars/be2c7651449642e4ae32b9eeeb073a06_123.jpg filter=lfs diff=lfs merge=lfs -text
3
+ static/avatars/e98b2fefcf1f47eb91a49384f2127307_123.jpg filter=lfs diff=lfs merge=lfs -text
static/admin.css ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .admin-wrapper {
2
+ display: flex;
3
+ min-height: 100vh;
4
+ }
5
+
6
+ .admin-sidebar {
7
+ width: 250px;
8
+ background: #2c3e50;
9
+ color: white;
10
+ display: flex;
11
+ flex-direction: column;
12
+ }
13
+
14
+ .admin-brand {
15
+ padding: 20px;
16
+ font-size: 1.2rem;
17
+ font-weight: bold;
18
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
19
+ }
20
+
21
+ .admin-nav {
22
+ flex: 1;
23
+ padding: 10px 0;
24
+ }
25
+
26
+ .nav-item {
27
+ display: flex;
28
+ align-items: center;
29
+ padding: 10px 20px;
30
+ color: rgba(255, 255, 255, 0.8);
31
+ text-decoration: none;
32
+ transition: all 0.3s;
33
+ }
34
+
35
+ .nav-item:hover {
36
+ background: #34495e;
37
+ color: white;
38
+ }
39
+
40
+ .nav-item i {
41
+ margin-right: 10px;
42
+ width: 20px;
43
+ text-align: center;
44
+ }
45
+
46
+ .nav-divider {
47
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
48
+ margin: 10px 0;
49
+ }
50
+
51
+ .admin-user {
52
+ padding: 15px 20px;
53
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
54
+ display: flex;
55
+ align-items: center;
56
+ }
57
+
58
+ .admin-user i {
59
+ margin-right: 10px;
60
+ font-size: 1.2rem;
61
+ }
62
+
63
+ .admin-main {
64
+ flex: 1;
65
+ background-color: #f8f9fa;
66
+ padding: 20px;
67
+ overflow-y: auto;
68
+ }
69
+
70
+ .admin-container {
71
+ background: white;
72
+ border-radius: 5px;
73
+ padding: 20px;
74
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
75
+ }
76
+
77
+ .admin-header {
78
+ display: flex;
79
+ justify-content: space-between;
80
+ align-items: center;
81
+ margin-bottom: 20px;
82
+ }
83
+
84
+ .admin-header h1 {
85
+ margin: 0;
86
+ font-size: 1.5rem;
87
+ display: flex;
88
+ align-items: center;
89
+ }
90
+
91
+ .admin-header h1 i {
92
+ margin-right: 10px;
93
+ }
94
+
95
+ .admin-actions {
96
+ display: flex;
97
+ gap: 10px;
98
+ }
99
+
100
+ .search-form {
101
+ width: 300px;
102
+ }
103
+
104
+ .filter-form {
105
+ width: 200px;
106
+ }
107
+
108
+ .stat-card {
109
+ display: flex;
110
+ align-items: center;
111
+ padding: 15px;
112
+ border-radius: 5px;
113
+ color: white;
114
+ margin-bottom: 20px;
115
+ }
116
+
117
+ .stat-icon {
118
+ font-size: 2rem;
119
+ margin-right: 15px;
120
+ opacity: 0.8;
121
+ }
122
+
123
+ .stat-info h3 {
124
+ margin: 0;
125
+ font-size: 1.8rem;
126
+ font-weight: bold;
127
+ }
128
+
129
+ .stat-info p {
130
+ margin: 0;
131
+ opacity: 0.8;
132
+ }
133
+
134
+ .text-truncate {
135
+ max-width: 200px;
136
+ white-space: nowrap;
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+ }
140
+
141
+ /* Адаптивность */
142
+ @media (max-width: 768px) {
143
+ .admin-wrapper {
144
+ flex-direction: column;
145
+ }
146
+
147
+ .admin-sidebar {
148
+ width: 100%;
149
+ }
150
+
151
+ .admin-header {
152
+ flex-direction: column;
153
+ align-items: flex-start;
154
+ gap: 10px;
155
+ }
156
+
157
+ .search-form, .filter-form {
158
+ width: 100%;
159
+ }
160
+ }
static/admin.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Подтверждение удаления
3
+ document.querySelectorAll('.delete-btn').forEach(btn => {
4
+ btn.addEventListener('click', function(e) {
5
+ if (!confirm('Вы уверены, что хотите выполнить это действие?')) {
6
+ e.preventDefault();
7
+ }
8
+ });
9
+ });
10
+
11
+ // Инициализация tooltips
12
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
13
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
14
+ return new bootstrap.Tooltip(tooltipTriggerEl);
15
+ });
16
+ });
static/avatars/38073be902884c20986765aa948ccb53_123.jpg ADDED

Git LFS Details

  • SHA256: 25a15fa50f1198fbed372f9760c49b22cafea05291c5f38358f957b1fcf43dce
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
static/avatars/3b0e6f8f70194180b625c8f23c2b386c_0073d417-eaf6-498a-90d3-ef4055a289ab.jpeg ADDED
static/avatars/96f8d763e08941cd9c61571c2c554a5e_1e73be6cbb4992bfffc35436a370c2a8.jpg ADDED
static/avatars/be2c7651449642e4ae32b9eeeb073a06_123.jpg ADDED

Git LFS Details

  • SHA256: 25a15fa50f1198fbed372f9760c49b22cafea05291c5f38358f957b1fcf43dce
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
static/avatars/e98b2fefcf1f47eb91a49384f2127307_123.jpg ADDED

Git LFS Details

  • SHA256: 25a15fa50f1198fbed372f9760c49b22cafea05291c5f38358f957b1fcf43dce
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
static/script.js CHANGED
@@ -1,5 +1,6 @@
1
  document.addEventListener("DOMContentLoaded", () => {
2
  let mediaRecorder, audioChunks = [], audioStream, currentChatId = null;
 
3
  const recordBtn = document.getElementById("record-btn");
4
  const stopBtn = document.getElementById("stop-btn");
5
  const sendBtn = document.getElementById("send-btn");
@@ -13,37 +14,44 @@ document.addEventListener("DOMContentLoaded", () => {
13
  const fileName = document.getElementById("file-name");
14
  const clearFileBtn = document.getElementById("clear-file");
15
 
 
 
 
 
 
 
 
 
 
16
  // Инициализация при загрузке
17
  initializeChats();
18
 
19
- function initializeChats() {
20
- const savedChatId = localStorage.getItem('currentChatId');
21
-
22
- fetch("/get_chats")
23
- .then(response => response.json())
24
- .then(chats => {
25
- renderChatList(chats);
26
-
27
-
28
- if (savedChatId && chats.some(c => c.chat_id === savedChatId)) {
29
- loadChat(savedChatId);
30
- }
31
-
32
- else if (chats.length > 0) {
33
- loadChat(chats[0].chat_id);
34
- }
35
-
36
- else {
37
  showEmptyChatUI();
38
- }
39
- })
40
- .catch(error => {
41
- console.error("Ошибка загрузки чатов:", error);
42
- showEmptyChatUI();
43
- });
44
- }
45
 
46
  function renderChatList(chats) {
 
47
  chatList.innerHTML = '';
48
  chats.forEach(chat => {
49
  const chatItem = document.createElement("div");
@@ -61,14 +69,11 @@ function initializeChats() {
61
  <i class="fas fa-trash"></i>
62
  </button>
63
  `;
64
-
65
- // Обработчик клика по чату
66
  chatItem.querySelector('.chat-item-main').addEventListener('click', () => {
67
  loadChat(chat.chat_id);
68
  localStorage.setItem('currentChatId', chat.chat_id);
69
  });
70
 
71
- // Обработчик удаления чата
72
  chatItem.querySelector('.delete-chat-btn').addEventListener('click', (e) => {
73
  e.stopPropagation();
74
  deleteChat(chat.chat_id);
@@ -85,10 +90,7 @@ function initializeChats() {
85
  }
86
 
87
  async function deleteChat(chatId) {
88
- if (!confirm('Вы точно хотите удалить этот чат? Это действие нельзя отменить.')) {
89
- return;
90
- }
91
-
92
  try {
93
  const response = await fetch(`/delete_chat/${chatId}`, {
94
  method: 'DELETE',
@@ -97,14 +99,13 @@ function initializeChats() {
97
  'X-CSRFToken': getCSRFToken()
98
  }
99
  });
100
-
101
  const result = await response.json();
102
-
103
  if (result.success) {
104
- if (currentChatId === chatId) {
105
- startNewChat();
106
- }
107
- initializeChats();
 
108
  } else {
109
  throw new Error(result.error || 'Ошибка при удалении чата');
110
  }
@@ -114,7 +115,12 @@ function initializeChats() {
114
  }
115
  }
116
 
117
- newChatBtn.addEventListener("click", startNewChat);
 
 
 
 
 
118
 
119
  function startNewChat() {
120
  fetch("/start_chat", {
@@ -124,7 +130,9 @@ function initializeChats() {
124
  .then(response => response.json())
125
  .then(data => {
126
  currentChatId = data.chat_id;
127
- currentChatTitle.textContent = data.title;
 
 
128
  chatBox.innerHTML = '<div class="message bot-message">Привет! Отправьте текст или голосовое сообщение для анализа эмоций.</div>';
129
  initializeChats();
130
  localStorage.setItem('currentChatId', data.chat_id);
@@ -134,25 +142,22 @@ function initializeChats() {
134
 
135
  function loadChat(chatId) {
136
  fetch(`/load_chat/${chatId}`)
137
- .then(response => response.json())
138
- .then(data => {
139
- if (data.error) throw new Error(data.error);
140
-
141
- currentChatId = chatId;
142
- currentChatTitle.textContent = data.title;
143
- updateActiveChat(chatId);
144
-
145
- chatBox.innerHTML = "";
146
- data.messages.forEach(msg => {
147
- appendMessage(msg.sender, msg.content);
 
 
 
 
148
  });
149
-
150
- localStorage.setItem('currentChatId', chatId);
151
- })
152
- .catch(error => {
153
- console.error("Ошибка загрузки чата:", error);
154
- appendMessage("bot", `❌ Ошибка: ${error.message}`);
155
- });
156
  }
157
 
158
  function updateActiveChat(chatId) {
@@ -161,19 +166,16 @@ function initializeChats() {
161
  });
162
  }
163
 
164
- // Обработчики отправки сообщений
165
- sendBtn.addEventListener("click", sendMessage);
166
- userInput.addEventListener("keypress", (e) => {
167
  if (e.key === "Enter") sendMessage();
168
  });
169
 
170
  async function sendMessage() {
171
- const text = userInput.value.trim();
172
  if (!text || !currentChatId) return;
173
-
174
  appendAndSaveMessage("user", text);
175
  userInput.value = "";
176
-
177
  try {
178
  const response = await fetch("/analyze", {
179
  method: "POST",
@@ -189,11 +191,15 @@ function initializeChats() {
189
  }
190
 
191
  // Обработчики аудио
192
- audioFileInput.addEventListener("change", handleAudioUpload);
193
- clearFileBtn.addEventListener("click", clearAudioFile);
 
 
 
 
194
 
195
  function handleAudioUpload() {
196
- const file = audioFileInput.files[0];
197
  if (file) {
198
  fileName.textContent = file.name;
199
  fileInfo.style.display = 'flex';
@@ -208,20 +214,16 @@ function initializeChats() {
208
 
209
  async function sendAudioFile(file) {
210
  if (!currentChatId) return;
211
-
212
  appendAndSaveMessage("user", "Загружен аудиофайл...");
213
-
214
  try {
215
  const formData = new FormData();
216
  formData.append("audio", file);
217
  formData.append("chat_id", currentChatId);
218
-
219
  const response = await fetch("/analyze_audio", {
220
  method: "POST",
221
  body: formData
222
  });
223
  const data = await response.json();
224
-
225
  if (data.transcribed_text) {
226
  appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`);
227
  }
@@ -233,25 +235,23 @@ function initializeChats() {
233
  }
234
  }
235
 
236
- // Обработчики записи голоса
237
- recordBtn.addEventListener("click", startRecording);
238
- stopBtn.addEventListener("click", stopRecording);
239
 
240
  async function startRecording() {
241
  try {
242
  audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
243
  mediaRecorder = new MediaRecorder(audioStream);
244
  audioChunks = [];
245
-
246
  mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
247
  mediaRecorder.onstop = async () => {
248
  const audioBlob = new Blob(audioChunks, { type: "audio/wav" });
249
  sendAudioBlob(audioBlob);
250
  };
251
-
252
  mediaRecorder.start();
253
- recordBtn.disabled = true;
254
- stopBtn.disabled = false;
255
  appendMessage("user", "Запись начата...");
256
  } catch (error) {
257
  console.error("Ошибка записи:", error);
@@ -262,28 +262,24 @@ function initializeChats() {
262
  function stopRecording() {
263
  if (mediaRecorder?.state === "recording") {
264
  mediaRecorder.stop();
265
- recordBtn.disabled = false;
266
- stopBtn.disabled = true;
267
- audioStream.getTracks().forEach(track => track.stop());
268
  }
269
  }
270
 
271
  async function sendAudioBlob(audioBlob) {
272
  if (!currentChatId) return;
273
-
274
  appendAndSaveMessage("user", "Отправлено голосовое сообщение...");
275
-
276
  try {
277
  const formData = new FormData();
278
  formData.append("audio", audioBlob, "recording.wav");
279
  formData.append("chat_id", currentChatId);
280
-
281
  const response = await fetch("/analyze_audio", {
282
  method: "POST",
283
  body: formData
284
  });
285
  const data = await response.json();
286
-
287
  if (data.transcribed_text) {
288
  appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`);
289
  }
@@ -294,18 +290,18 @@ function initializeChats() {
294
  }
295
  }
296
 
297
- // Вспомогательные функции
298
  function appendMessage(sender, text) {
299
  const message = document.createElement("div");
300
  message.className = `message ${sender}-message`;
301
  message.innerHTML = text;
302
- chatBox.appendChild(message);
303
- chatBox.scrollTop = chatBox.scrollHeight;
 
 
304
  }
305
 
306
  function appendAndSaveMessage(sender, text) {
307
  appendMessage(sender, text);
308
-
309
  if (currentChatId) {
310
  fetch("/save_message", {
311
  method: "POST",
@@ -322,8 +318,402 @@ function initializeChats() {
322
  }
323
  }
324
 
325
- function getCSRFToken() {
326
- const meta = document.querySelector('meta[name="csrf-token"]');
327
- return meta ? meta.content : '';
 
 
 
 
 
 
328
  }
329
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  document.addEventListener("DOMContentLoaded", () => {
2
  let mediaRecorder, audioChunks = [], audioStream, currentChatId = null;
3
+
4
  const recordBtn = document.getElementById("record-btn");
5
  const stopBtn = document.getElementById("stop-btn");
6
  const sendBtn = document.getElementById("send-btn");
 
14
  const fileName = document.getElementById("file-name");
15
  const clearFileBtn = document.getElementById("clear-file");
16
 
17
+ // Emotion Map (скопирован из profile.html)
18
+ const emotionMap = {
19
+ 'joy': '😊 Радость',
20
+ 'neutral': '😐 Нейтрально',
21
+ 'anger': '😠 Злость',
22
+ 'sadness': '😢 Грусть',
23
+ 'surprise': '😲 Удивление'
24
+ };
25
+
26
  // Инициализация при загрузке
27
  initializeChats();
28
 
29
+ function initializeChats() {
30
+ const savedChatId = localStorage.getItem('currentChatId');
31
+ fetch("/get_chats")
32
+ .then(response => response.json())
33
+ .then(chats => {
34
+ renderChatList(chats);
35
+ if (savedChatId && chats.some(c => c.chat_id === savedChatId)) {
36
+ loadChat(savedChatId);
37
+ } else if (chats.length > 0) {
38
+ loadChat(chats[0].chat_id);
39
+ } else {
40
+ showEmptyChatUI();
41
+ }
42
+ })
43
+ .catch(error => {
44
+ console.error("Ошибка загрузки чатов:", error);
 
 
45
  showEmptyChatUI();
46
+ });
47
+ }
48
+
49
+ function showEmptyChatUI() {
50
+ if (chatBox) chatBox.innerHTML = '<div class="empty-chat">Нет активного чата</div>';
51
+ }
 
52
 
53
  function renderChatList(chats) {
54
+ if (!chatList) return;
55
  chatList.innerHTML = '';
56
  chats.forEach(chat => {
57
  const chatItem = document.createElement("div");
 
69
  <i class="fas fa-trash"></i>
70
  </button>
71
  `;
 
 
72
  chatItem.querySelector('.chat-item-main').addEventListener('click', () => {
73
  loadChat(chat.chat_id);
74
  localStorage.setItem('currentChatId', chat.chat_id);
75
  });
76
 
 
77
  chatItem.querySelector('.delete-chat-btn').addEventListener('click', (e) => {
78
  e.stopPropagation();
79
  deleteChat(chat.chat_id);
 
90
  }
91
 
92
  async function deleteChat(chatId) {
93
+ if (!confirm('Вы точно хотите удалить этот чат? Это действие нельзя отменить.')) return;
 
 
 
94
  try {
95
  const response = await fetch(`/delete_chat/${chatId}`, {
96
  method: 'DELETE',
 
99
  'X-CSRFToken': getCSRFToken()
100
  }
101
  });
 
102
  const result = await response.json();
 
103
  if (result.success) {
104
+ if (currentChatId === chatId) {
105
+ chatBox.innerHTML = '<div class="empty-chat">Чат удалён</div>';
106
+ currentChatId = null;
107
+ }
108
+ initializeChats(); // Перезагружаем список чатов
109
  } else {
110
  throw new Error(result.error || 'Ошибка при удалении чата');
111
  }
 
115
  }
116
  }
117
 
118
+ function getCSRFToken() {
119
+ const meta = document.querySelector('meta[name="csrf-token"]');
120
+ return meta ? meta.content : '';
121
+ }
122
+
123
+ newChatBtn?.addEventListener("click", startNewChat);
124
 
125
  function startNewChat() {
126
  fetch("/start_chat", {
 
130
  .then(response => response.json())
131
  .then(data => {
132
  currentChatId = data.chat_id;
133
+ if (currentChatTitle) {
134
+ currentChatTitle.textContent = data.title;
135
+ }
136
  chatBox.innerHTML = '<div class="message bot-message">Привет! Отправьте текст или голосовое сообщение для анализа эмоций.</div>';
137
  initializeChats();
138
  localStorage.setItem('currentChatId', data.chat_id);
 
142
 
143
  function loadChat(chatId) {
144
  fetch(`/load_chat/${chatId}`)
145
+ .then(response => response.json())
146
+ .then(data => {
147
+ if (data.error) throw new Error(data.error);
148
+ currentChatId = chatId;
149
+ currentChatTitle.textContent = data.title;
150
+ updateActiveChat(chatId);
151
+ chatBox.innerHTML = "";
152
+ data.messages.forEach(msg => {
153
+ appendMessage(msg.sender, msg.content);
154
+ });
155
+ localStorage.setItem('currentChatId', chatId);
156
+ })
157
+ .catch(error => {
158
+ console.error("Ошибка загрузки чата:", error);
159
+ appendMessage("bot", `❌ Ошибка: ${error.message}`);
160
  });
 
 
 
 
 
 
 
161
  }
162
 
163
  function updateActiveChat(chatId) {
 
166
  });
167
  }
168
 
169
+ sendBtn?.addEventListener("click", sendMessage);
170
+ userInput?.addEventListener("keypress", (e) => {
 
171
  if (e.key === "Enter") sendMessage();
172
  });
173
 
174
  async function sendMessage() {
175
+ const text = userInput?.value.trim();
176
  if (!text || !currentChatId) return;
 
177
  appendAndSaveMessage("user", text);
178
  userInput.value = "";
 
179
  try {
180
  const response = await fetch("/analyze", {
181
  method: "POST",
 
191
  }
192
 
193
  // Обработчики аудио
194
+ if (audioFileInput) {
195
+ audioFileInput.addEventListener("change", handleAudioUpload);
196
+ }
197
+ if (clearFileBtn) {
198
+ clearFileBtn.addEventListener("click", clearAudioFile);
199
+ }
200
 
201
  function handleAudioUpload() {
202
+ const file = audioFileInput?.files[0];
203
  if (file) {
204
  fileName.textContent = file.name;
205
  fileInfo.style.display = 'flex';
 
214
 
215
  async function sendAudioFile(file) {
216
  if (!currentChatId) return;
 
217
  appendAndSaveMessage("user", "Загружен аудиофайл...");
 
218
  try {
219
  const formData = new FormData();
220
  formData.append("audio", file);
221
  formData.append("chat_id", currentChatId);
 
222
  const response = await fetch("/analyze_audio", {
223
  method: "POST",
224
  body: formData
225
  });
226
  const data = await response.json();
 
227
  if (data.transcribed_text) {
228
  appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`);
229
  }
 
235
  }
236
  }
237
 
238
+ // Запись голоса
239
+ if (recordBtn) recordBtn.addEventListener("click", startRecording);
240
+ if (stopBtn) stopBtn.addEventListener("click", stopRecording);
241
 
242
  async function startRecording() {
243
  try {
244
  audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
245
  mediaRecorder = new MediaRecorder(audioStream);
246
  audioChunks = [];
 
247
  mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
248
  mediaRecorder.onstop = async () => {
249
  const audioBlob = new Blob(audioChunks, { type: "audio/wav" });
250
  sendAudioBlob(audioBlob);
251
  };
 
252
  mediaRecorder.start();
253
+ if (recordBtn) recordBtn.disabled = true;
254
+ if (stopBtn) stopBtn.disabled = false;
255
  appendMessage("user", "Запись начата...");
256
  } catch (error) {
257
  console.error("Ошибка записи:", error);
 
262
  function stopRecording() {
263
  if (mediaRecorder?.state === "recording") {
264
  mediaRecorder.stop();
265
+ if (recordBtn) recordBtn.disabled = false;
266
+ if (stopBtn) stopBtn.disabled = true;
267
+ if (audioStream) audioStream.getTracks().forEach(track => track.stop());
268
  }
269
  }
270
 
271
  async function sendAudioBlob(audioBlob) {
272
  if (!currentChatId) return;
 
273
  appendAndSaveMessage("user", "Отправлено голосовое сообщение...");
 
274
  try {
275
  const formData = new FormData();
276
  formData.append("audio", audioBlob, "recording.wav");
277
  formData.append("chat_id", currentChatId);
 
278
  const response = await fetch("/analyze_audio", {
279
  method: "POST",
280
  body: formData
281
  });
282
  const data = await response.json();
 
283
  if (data.transcribed_text) {
284
  appendAndSaveMessage("user", `Распознанный текст: ${data.transcribed_text}`);
285
  }
 
290
  }
291
  }
292
 
 
293
  function appendMessage(sender, text) {
294
  const message = document.createElement("div");
295
  message.className = `message ${sender}-message`;
296
  message.innerHTML = text;
297
+ if (chatBox) {
298
+ chatBox.appendChild(message);
299
+ chatBox.scrollTop = chatBox.scrollHeight;
300
+ }
301
  }
302
 
303
  function appendAndSaveMessage(sender, text) {
304
  appendMessage(sender, text);
 
305
  if (currentChatId) {
306
  fetch("/save_message", {
307
  method: "POST",
 
318
  }
319
  }
320
 
321
+ // Telegram анализ
322
+ document.getElementById('telegram-upload-form')?.addEventListener('submit', async function(e) {
323
+ e.preventDefault();
324
+ const fileInput = document.getElementById('telegram-file');
325
+ const file = fileInput.files[0];
326
+
327
+ if (!file) {
328
+ alert('Пожалуйста, выберите файл');
329
+ return;
330
  }
331
+
332
+ try {
333
+ const formData = new FormData();
334
+ formData.append('file', file);
335
+
336
+ const response = await fetch('/analyze_telegram_chat', {
337
+ method: 'POST',
338
+ body: formData
339
+ });
340
+
341
+ const result = await response.json();
342
+
343
+ if (result.error) {
344
+ throw new Error(result.error);
345
+ }
346
+
347
+ alert('Анализ завершен успешно!');
348
+ updateTelegramAnalytics(); // Обновляем графики
349
+ } catch (error) {
350
+ console.error('Ошибка загрузки файла:', error);
351
+ alert(`Ошибка: ${error.message}`);
352
+ }
353
+ });
354
+
355
+
356
+
357
+ // Функция скользящего среднего
358
+ function movingAverage(data, windowSize) {
359
+ const result = [];
360
+ for (let i = 0; i < data.length; i++) {
361
+ const start = Math.max(0, i - windowSize + 1);
362
+ const slice = data.slice(start, i + 1);
363
+ const avg = slice.reduce((sum, val) => sum + val, 0) / slice.length;
364
+ result.push(avg);
365
+ }
366
+ return result;
367
+ }
368
+
369
+ // Цвета эмоций
370
+ function getEmotionColor(emotion) {
371
+ const colors = {
372
+ '😊 Радость': '#00b894',
373
+ '😢 Грусть': '#0984e3',
374
+ '😠 Злость': '#d63031',
375
+ '😲 Удивление': '#fdcb6e',
376
+ '😨 Страх': '#a29bfe',
377
+ '😐 Нейтрально': '#636e72'
378
+ };
379
+ return colors[emotion] || '#4a4ae8';
380
+ }
381
+
382
+ // Иконки эмоций
383
+ function getEmotionIcon(emotion) {
384
+ const icons = {
385
+ '😊 Радость': 'fa-smile',
386
+ '😢 Грусть': 'fa-sad-tear',
387
+ '😠 Злость': 'fa-angry',
388
+ '😲 Удивление': 'fa-surprise',
389
+ '😨 Страх': 'fa-flushed',
390
+ '😐 Нейтрально': 'fa-meh'
391
+ };
392
+ return icons[emotion] || 'fa-comment';
393
+ }
394
+
395
+ // Основная функция анализа Telegram
396
+ // Основная функция анализа Telegram
397
+ async function updateTelegramAnalytics(range = 'month') {
398
+ try {
399
+ const response = await fetch('/get_telegram_analysis');
400
+ const analyses = await response.json();
401
+ if (!analyses || analyses.length === 0) {
402
+ document.getElementById('emotion-timeline').innerHTML =
403
+ '<div class="empty-state"><i class="fas fa-comment-slash"></i><p>Нет данных для отображения</p></div>';
404
+ document.getElementById('emotion-distribution').innerHTML = '';
405
+ return;
406
+ }
407
+
408
+ const allData = analyses.flatMap(a => JSON.parse(a.data));
409
+ const emotionMap = {
410
+ 'joy': '😊 Радость',
411
+ 'sadness': '😢 Грусть',
412
+ 'anger': '😠 Злость',
413
+ 'surprise': '😲 Удивление',
414
+ 'fear': '😨 Страх',
415
+ 'no_emotion': '😐 Нейтрально'
416
+ };
417
+
418
+ // Фильтрация по пользователю
419
+ const userSelect = document.getElementById('user-select');
420
+ const selectedUser = userSelect?.value;
421
+ let filteredData = allData;
422
+ if (selectedUser && selectedUser !== 'all') {
423
+ filteredData = allData.filter(d => d.from === selectedUser);
424
+ }
425
+
426
+ const processedData = filteredData.map(d => ({
427
+ emotion: emotionMap[d.emotion] || d.emotion,
428
+ from: d.from,
429
+ text: d.text,
430
+ date: new Date(d.timestamp),
431
+ confidence: d.confidence
432
+ }));
433
+
434
+ // --- Блок подготовки графиков ---
435
+ const groupByTime = (date, range) => {
436
+ const d = new Date(date);
437
+ if (range === 'week') {
438
+ d.setHours(0, 0, 0, 0);
439
+ d.setDate(d.getDate() - d.getDay());
440
+ return d;
441
+ } else if (range === 'month') {
442
+ return new Date(d.getFullYear(), d.getMonth(), 1);
443
+ } else if (range === 'year') {
444
+ return new Date(d.getFullYear(), 0, 1);
445
+ }
446
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
447
+ };
448
+
449
+ const groupedData = {};
450
+ processedData.forEach(d => {
451
+ const timeKey = groupByTime(d.date, range).getTime();
452
+ if (!groupedData[timeKey]) {
453
+ groupedData[timeKey] = {
454
+ date: new Date(timeKey),
455
+ emotions: {}
456
+ };
457
+ }
458
+ if (!groupedData[timeKey].emotions[d.emotion]) {
459
+ groupedData[timeKey].emotions[d.emotion] = {
460
+ count: 0,
461
+ totalConfidence: 0
462
+ };
463
+ }
464
+ groupedData[timeKey].emotions[d.emotion].count++;
465
+ groupedData[timeKey].emotions[d.emotion].totalConfidence += d.confidence;
466
+ });
467
+
468
+ const timeKeys = Object.keys(groupedData).sort();
469
+ const emotions = [...new Set(processedData.map(d => d.emotion))];
470
+ const traces = emotions.map(emotion => {
471
+ const x = [];
472
+ const y = [];
473
+ const customdata = [];
474
+ timeKeys.forEach(key => {
475
+ const dataPoint = groupedData[key];
476
+ if (dataPoint.emotions[emotion]) {
477
+ x.push(dataPoint.date);
478
+ y.push(dataPoint.emotions[emotion].count);
479
+ customdata.push({
480
+ emotion: emotion,
481
+ avgConfidence: (dataPoint.emotions[emotion].totalConfidence /
482
+ dataPoint.emotions[emotion].count).toFixed(2)
483
+ });
484
+ } else {
485
+ x.push(dataPoint.date);
486
+ y.push(0);
487
+ customdata.push(null);
488
+ }
489
+ });
490
+
491
+ return {
492
+ x: x,
493
+ y: y,
494
+ name: emotion,
495
+ type: 'scatter',
496
+ mode: 'lines+markers',
497
+ line: { shape: 'spline' },
498
+ marker: { color: getEmotionColor(emotion), size: 6 },
499
+ fill: 'tonexty',
500
+ fillcolor: `${getEmotionColor(emotion)}7F`,
501
+ customdata: customdata,
502
+ hovertemplate:
503
+ '<b>%{x|%d %b %Y}</b><br>' +
504
+ 'Эмоция: %{fullData.name}<br>' +
505
+ 'Сообщений: %{y}<br>' +
506
+ 'Средняя уверенность: %{customdata.avgConfidence}<extra></extra>'
507
+ };
508
+ });
509
+
510
+ Plotly.newPlot('emotion-timeline', traces, {
511
+ title: false,
512
+ plot_bgcolor: 'rgba(0,0,0,0)',
513
+ paper_bgcolor: 'rgba(0,0,0,0)',
514
+ font: { color: 'white' },
515
+ xaxis: {
516
+ title: 'Дата',
517
+ tickformat: range === 'year' ? '%Y' : range === 'month' ? '%b %Y' : '%d %b',
518
+ gridcolor: 'rgba(255,255,255,0.1)'
519
+ },
520
+ yaxis: {
521
+ title: 'Количество сообщений',
522
+ gridcolor: 'rgba(255,255,255,0.1)'
523
+ },
524
+ hovermode: 'closest',
525
+ legend: {
526
+ orientation: 'h',
527
+ y: -0.2
528
+ },
529
+ margin: { t: 0, b: 80 }
530
+ });
531
+
532
+ // Тепловая карта
533
+ const dates = [...new Set(processedData.map(d => d.date.toDateString()))];
534
+ const emotionLabels = Object.values(emotionMap);
535
+ const z = emotionLabels.map(e => dates.map(d =>
536
+ processedData.filter(msg => msg.date.toDateString() === d && msg.emotion === e).length
537
+ ));
538
+
539
+ Plotly.newPlot('calendar-heatmap', [{
540
+ type: 'heatmap',
541
+ z: z,
542
+ x: dates,
543
+ y: emotionLabels,
544
+ colorscale: [
545
+ [0, '#2d3436'], // Темный фон
546
+ [0.5, '#6c5ce7'],
547
+ [1, '#00b894']
548
+ ],
549
+ showscale: true,
550
+ colorbar: {
551
+ title: 'Частота',
552
+ titleside: 'top',
553
+ tickmode: 'array',
554
+ tickvals: [0, Math.max(...z.flat())],
555
+ ticktext: ['Мало', 'Много'],
556
+ ticks: 'outside'
557
+ }
558
+ }], {
559
+ title: 'Тепловая карта эмоций по дням',
560
+ xaxis: {
561
+ title: 'Дата',
562
+ tickangle: -45
563
+ },
564
+ yaxis: {
565
+ title: 'Эмоции',
566
+ automargin: true
567
+ },
568
+ margin: { t: 30, r: 30, l: 80, b: 80 }
569
+ });
570
+
571
+ // Автоматический анализ
572
+ const totalMessages = processedData.length;
573
+ const emotionCounts = {};
574
+ processedData.forEach(d => {
575
+ emotionCounts[d.emotion] = (emotionCounts[d.emotion] || 0) + 1;
576
+ });
577
+ const sorted = Object.entries(emotionCounts).sort((a, b) => b[1] - a[1]);
578
+ const dominant = sorted[0];
579
+
580
+ const sadnessPeaks = processedData
581
+ .filter(d => d.emotion === '😢 Грусть')
582
+ .reduce((acc, d) => {
583
+ const key = d.date.toDateString();
584
+ acc[key] = (acc[key] || 0) + 1;
585
+ return acc;
586
+ }, {});
587
+ const sadPeak = Object.entries(sadnessPeaks).sort((a, b) => b[1] - a[1])[0];
588
+
589
+ document.getElementById('summary-content').innerHTML = `
590
+ <ul style="color: white;">
591
+ <li>💡 Преобладает: ${dominant[0]} (${((dominant[1]/totalMessages)*100).toFixed(1)}%)</li>
592
+ <li>📉 Пик грусти: ${sadPeak[0]} (${sadPeak[1]} сообщений)</li>
593
+ </ul>
594
+ `;
595
+
596
+ // Круговая диаграмма и статистика
597
+ const pieLabels = Object.keys(emotionCounts);
598
+ const pieValues = Object.values(emotionCounts);
599
+
600
+ Plotly.newPlot('emotion-distribution-pie', [{
601
+ labels: pieLabels,
602
+ values: pieValues,
603
+ type: 'pie',
604
+ textinfo: 'label+percent',
605
+ hoverinfo: 'label+value+percent',
606
+ marker: {
607
+ colors: pieLabels.map(e => getEmotionColor(e))
608
+ },
609
+ textfont: {
610
+ color: 'white'
611
+ },
612
+ hole: 0.4,
613
+ rotation: 45
614
+ }], {
615
+ title: false,
616
+ plot_bgcolor: 'rgba(0,0,0,0)',
617
+ paper_bgcolor: 'rgba(0,0,0,0)',
618
+ font: { color: 'white' },
619
+ showlegend: false,
620
+ margin: { t: 0, b: 0, l: 0, r: 0 }
621
+ });
622
+
623
+ // Статистика по эмоциям — 2 строки по 3 эмоции
624
+ const statsHTML = pieLabels.slice(0, 6).map((emotion, i) => {
625
+ const percentage = ((pieValues[i] / totalMessages) * 100).toFixed(1);
626
+ return `
627
+ <div class="emotion-stat">
628
+ <div class="emotion-label" style="color: ${getEmotionColor(emotion)}">
629
+ <span>${emotion.replace(/[\u{1F600}-\u{1F64F}]/gu, '')}</span>
630
+ </div>
631
+ <div class="confidence-bar">
632
+ <div class="confidence-fill"
633
+ style="width: ${percentage}%;
634
+ background: ${getEmotionColor(emotion)};"></div>
635
+ </div>
636
+ <div class="confidence-value">${percentage}%</div>
637
+ </div>`;
638
+ }).join('');
639
+
640
+ document.getElementById('emotion-distribution').innerHTML = statsHTML;
641
+
642
+ // Адаптация сетки на фронтенде
643
+ const statsContainer = document.getElementById('emotion-distribution');
644
+ if (statsContainer) {
645
+ statsContainer.style.display = 'grid';
646
+ statsContainer.style.gridTemplateColumns = 'repeat(3, 1fr)';
647
+ statsContainer.style.gap = '15px';
648
+ }
649
+
650
+ // --- Выбор пользователя ---
651
+ populateUserSelect(processedData);
652
+
653
+ } catch (error) {
654
+ console.error('Ошибка обновления аналитики:', error);
655
+ document.getElementById('emotion-timeline').innerHTML =
656
+ `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>Ошибка загрузки данных: ${error.message}</p></div>`;
657
+ }
658
+ }
659
+
660
+ // Глобальная переменная для хранения списка пользователей
661
+ let telegramUsers = [];
662
+
663
+ function populateUserSelect(processedData) {
664
+ const userSelect = document.getElementById('user-select');
665
+ if (!userSelect) return;
666
+
667
+ const users = [...new Set(processedData.map(d => d.from))];
668
+
669
+ // Если пользователи не изменились — ничего не делаем
670
+ if (JSON.stringify(users.sort()) === JSON.stringify(telegramUsers.sort())) return;
671
+
672
+ telegramUsers = users;
673
+ userSelect.innerHTML = '<option value="all">Все участники</option>';
674
+
675
+ users.forEach(user => {
676
+ const option = document.createElement('option');
677
+ option.value = user;
678
+ option.textContent = user;
679
+ userSelect.appendChild(option);
680
+ });
681
+
682
+ // Добавляем обработчик только один раз
683
+ if (!userSelect.dataset.listenerAdded) {
684
+ userSelect.addEventListener('change', () => {
685
+ updateTelegramAnalytics(document.querySelector('.time-btn.active')?.dataset.range || 'month');
686
+ });
687
+ userSelect.dataset.listenerAdded = 'true';
688
+ }
689
+ }
690
+ document.querySelectorAll('.time-btn').forEach(button => {
691
+ button.addEventListener('click', function () {
692
+ // Удаляем класс 'active' у всех кнопок
693
+ document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
694
+ // Добавляем класс 'active' текущей кнопке
695
+ this.classList.add('active');
696
+
697
+ // Получаем диапазон времени из атрибута data-range
698
+ const range = this.getAttribute('data-range');
699
+
700
+ // Вызываем функцию обновления графиков с новым диапазоном
701
+ updateTelegramAnalytics(range);
702
+ });
703
+ });
704
+
705
+ // Проверяем, есть ли сохранённый активный временной интервал в localStorage или просто ставим 'month'
706
+ window.addEventListener('load', () => {
707
+ const activeTimeBtn = document.querySelector('.time-btn.active');
708
+ if (activeTimeBtn) {
709
+ const range = activeTimeBtn.dataset.range || 'month';
710
+ updateTelegramAnalytics(range);
711
+ } else {
712
+ updateTelegramAnalytics('month');
713
+ }
714
+ });
715
+ // Инициализация при загрузке страницы
716
+ if (window.location.pathname.includes('/profile')) {
717
+ updateTelegramAnalytics();
718
+ }
719
+ });
static/styles.css CHANGED
@@ -28,11 +28,71 @@ body {
28
  color: var(--light-text);
29
  line-height: 1.6;
30
  }
31
-
 
 
32
  /* Контейнер приложения */
33
  .app-container {
34
  display: flex;
35
  min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
  /* Боковая панель */
@@ -43,6 +103,14 @@ body {
43
  flex-direction: column;
44
  padding: 20px 0;
45
  border-right: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
 
 
 
 
 
46
  }
47
 
48
  .sidebar-header {
@@ -50,6 +118,16 @@ body {
50
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
51
  }
52
 
 
 
 
 
 
 
 
 
 
 
53
  .sidebar-header h2 {
54
  display: flex;
55
  align-items: center;
@@ -150,10 +228,156 @@ body {
150
  }
151
 
152
  /* Основное содержимое */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  .app-main {
154
  flex: 1;
155
  padding: 30px;
156
  background: linear-gradient(135deg, #1e1e2f, #2a2a40);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
  .flash-messages {
@@ -209,6 +433,17 @@ body {
209
  }
210
 
211
  /* Стили для чата */
 
 
 
 
 
 
 
 
 
 
 
212
  .chat-container {
213
  display: flex;
214
  height: calc(100vh - 60px);
@@ -345,6 +580,7 @@ body {
345
  }
346
 
347
  .welcome-message h4 {
 
348
  margin-bottom: 10px;
349
  font-size: 1.5rem;
350
  }
@@ -479,6 +715,17 @@ input[type="file"] {
479
  cursor: pointer;
480
  }
481
 
 
 
 
 
 
 
 
 
 
 
 
482
  .record-controls {
483
  display: flex;
484
  gap: 10px;
@@ -493,7 +740,7 @@ input[type="file"] {
493
  justify-content: center;
494
  border: none;
495
  cursor: pointer;
496
- transition: all 0.3s ease;
497
  }
498
 
499
  .record-btn {
@@ -833,4 +1080,740 @@ input[type="file"] {
833
 
834
  .delete-chat-btn:hover {
835
  color: #ff4444;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  }
 
28
  color: var(--light-text);
29
  line-height: 1.6;
30
  }
31
+ h2, label{
32
+ color: #fff;
33
+ }
34
  /* Контейнер приложения */
35
  .app-container {
36
  display: flex;
37
  min-height: 100vh;
38
+ position: relative;
39
+ }
40
+
41
+ /* Предупреждения */
42
+ .alert-text {
43
+ color: #fff;
44
+ }
45
+
46
+ body::-webkit-scrollbar {
47
+ display: none;
48
+ width: 0;
49
+ background: transparent;
50
+ }
51
+
52
+ .enhanced-alert {
53
+ padding: 20px 25px !important;
54
+ font-size: 1.1rem !important;
55
+ border: 2px solid rgba(255,255,255,0.3) !important;
56
+ backdrop-filter: blur(5px);
57
+ display: flex !important;
58
+ align-items: center;
59
+ gap: 15px;
60
+ }
61
+
62
+ .flash-info.enhanced-alert {
63
+ background: rgba(74, 74, 232, 0.9) !important;
64
+ color: #fff !important;
65
+ border-color: var(--primary-color) !important;
66
+ }
67
+
68
+ .alert-icon {
69
+ font-size: 1.5rem;
70
+ min-width: 30px;
71
+ }
72
+
73
+ .alert-icon .fa-info-circle {
74
+ color: #ff0000;
75
+ }
76
+
77
+ .flash-close {
78
+ background: none !important;
79
+ color: rgba(255,255,255,0.8) !important;
80
+ padding: 0 !important;
81
+ }
82
+
83
+ .flash-close:hover {
84
+ color: #ff0000 !important;
85
+ }
86
+
87
+ /* Анимация появления */
88
+ @keyframes pulse-alert {
89
+ 0% { transform: translateY(20px); opacity: 0; }
90
+ 80% { transform: translateY(-5px); }
91
+ 100% { transform: translateY(0); opacity: 1; }
92
+ }
93
+
94
+ .enhanced-alert {
95
+ animation: pulse-alert 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
96
  }
97
 
98
  /* Боковая панель */
 
103
  flex-direction: column;
104
  padding: 20px 0;
105
  border-right: 1px solid rgba(255, 255, 255, 0.1);
106
+ position: fixed;
107
+ top: 0;
108
+ left: 0;
109
+ height: 100vh;
110
+ width: 280px;
111
+ z-index: 1000;
112
+ overflow-y: auto;
113
+ transition: transform 0.3s ease;
114
  }
115
 
116
  .sidebar-header {
 
118
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
119
  }
120
 
121
+ .logo-link {
122
+ text-decoration: none !important;
123
+ border-bottom: none !important;
124
+ }
125
+
126
+ .logo-link:hover,
127
+ .logo-link:focus {
128
+ box-shadow: none !important;
129
+ }
130
+
131
  .sidebar-header h2 {
132
  display: flex;
133
  align-items: center;
 
228
  }
229
 
230
  /* Основное содержимое */
231
+ /* Welcome Page Styles */
232
+ .welcome-container {
233
+ display: flex;
234
+ justify-content: center;
235
+ align-items: center;
236
+ min-height: 100vh;
237
+ padding: 2rem;
238
+ }
239
+
240
+ .welcome-content {
241
+ max-width: 1200px;
242
+ text-align: center;
243
+ }
244
+
245
+ .welcome-header h1 {
246
+ font-size: 3.5rem;
247
+ margin-bottom: 1rem;
248
+ color: var(--light-text);
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ gap: 1rem;
253
+ }
254
+
255
+ .tagline {
256
+ font-size: 1.5rem;
257
+ color: rgba(255, 255, 255, 0.8);
258
+ margin-bottom: 3rem;
259
+ }
260
+
261
+ .welcome-cards {
262
+ display: grid;
263
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
264
+ gap: 2rem;
265
+ margin-bottom: 4rem;
266
+ }
267
+
268
+ .feature-card {
269
+ background: rgba(255, 255, 255, 0.05);
270
+ padding: 2rem;
271
+ border-radius: var(--border-radius);
272
+ transition: all 0.3s ease;
273
+ }
274
+
275
+ .feature-card:hover {
276
+ transform: translateY(-10px);
277
+ box-shadow: var(--box-shadow);
278
+ }
279
+
280
+ .feature-icon {
281
+ width: 80px;
282
+ height: 80px;
283
+ margin: 0 auto 1.5rem;
284
+ background: rgba(74, 74, 232, 0.2);
285
+ border-radius: 50%;
286
+ display: flex;
287
+ align-items: center;
288
+ justify-content: center;
289
+ font-size: 2rem;
290
+ }
291
+
292
+ .feature-card h3 {
293
+ font-size: 1.5rem;
294
+ margin-bottom: 1rem;
295
+ color: var(--light-text);
296
+ }
297
+
298
+ .feature-card p {
299
+ color: rgba(255, 255, 255, 0.7);
300
+ line-height: 1.6;
301
+ }
302
+
303
+ .auth-buttons {
304
+ display: flex;
305
+ gap: 1.5rem;
306
+ justify-content: center;
307
+ }
308
+
309
+ .btn-secondary {
310
+ display: inline-flex;
311
+ align-items: center;
312
+ gap: 0.8rem;
313
+ padding: 1rem 2rem;
314
+ background: rgba(255, 255, 255, 0.1);
315
+ color: white;
316
+ border: none;
317
+ border-radius: var(--border-radius);
318
+ text-decoration: none;
319
+ transition: all 0.3s ease;
320
+ }
321
+
322
+ .btn-secondary:hover {
323
+ background: rgba(255, 255, 255, 0.2);
324
+ transform: translateY(-2px);
325
+ }
326
+
327
+ @media (max-width: 768px) {
328
+ .welcome-header h1 {
329
+ font-size: 2.5rem;
330
+ }
331
+
332
+ .tagline {
333
+ font-size: 1.2rem;
334
+ }
335
+
336
+ .auth-buttons {
337
+ flex-direction: column;
338
+ }
339
+ }
340
+
341
  .app-main {
342
  flex: 1;
343
  padding: 30px;
344
  background: linear-gradient(135deg, #1e1e2f, #2a2a40);
345
+ margin-left: 280px;
346
+ min-height: 100vh;
347
+ }
348
+
349
+ @media (max-width: 992px) {
350
+ .app-sidebar {
351
+ transform: translateX(-100%);
352
+ }
353
+
354
+ .app-main {
355
+ margin-left: 0;
356
+ }
357
+
358
+ .app-container.sidebar-open .app-sidebar {
359
+ transform: translateX(0);
360
+ }
361
+ }
362
+
363
+ .mobile-menu-toggle {
364
+ display: none;
365
+ position: fixed;
366
+ top: 20px;
367
+ left: 20px;
368
+ z-index: 1100;
369
+ padding: 10px 15px;
370
+ background: rgba(74, 74, 232, 0.9);
371
+ color: white;
372
+ border: none;
373
+ border-radius: 8px;
374
+ font-size: 1.1rem;
375
+ }
376
+
377
+ @media (max-width: 992px) {
378
+ .mobile-menu-toggle {
379
+ display: block;
380
+ }
381
  }
382
 
383
  .flash-messages {
 
433
  }
434
 
435
  /* Стили для чата */
436
+ @keyframes pulse {
437
+ 0% { transform: scale(1); }
438
+ 50% { transform: scale(1.1); }
439
+ 100% { transform: scale(1); }
440
+ }
441
+
442
+ .recording {
443
+ animation: pulse 1.2s infinite ease-in-out !important;
444
+ box-shadow: 0 0 20px rgba(214, 48, 49, 0.6) !important;
445
+ }
446
+
447
  .chat-container {
448
  display: flex;
449
  height: calc(100vh - 60px);
 
580
  }
581
 
582
  .welcome-message h4 {
583
+ color: rgba(255, 255, 255, 0.7);
584
  margin-bottom: 10px;
585
  font-size: 1.5rem;
586
  }
 
715
  cursor: pointer;
716
  }
717
 
718
+ @keyframes pulse {
719
+ 0% { transform: scale(1); }
720
+ 50% { transform: scale(1.1); }
721
+ 100% { transform: scale(1); }
722
+ }
723
+
724
+ .record-btn.recording {
725
+ animation: pulse 1.2s infinite;
726
+ box-shadow: 0 0 20px rgba(214, 48, 49, 0.5);
727
+ }
728
+
729
  .record-controls {
730
  display: flex;
731
  gap: 10px;
 
740
  justify-content: center;
741
  border: none;
742
  cursor: pointer;
743
+ transition: all 0.3s ease, animation 0.3s ease;
744
  }
745
 
746
  .record-btn {
 
1080
 
1081
  .delete-chat-btn:hover {
1082
  color: #ff4444;
1083
+ }
1084
+
1085
+ .chat-import-section {
1086
+ background: rgba(255, 255, 255, 0.05);
1087
+ border-radius: var(--border-radius);
1088
+ padding: 20px;
1089
+ margin-bottom: 30px;
1090
+ border: 1px dashed rgba(255, 255, 255, 0.1);
1091
+ transition: all 0.3s ease;
1092
+ }
1093
+
1094
+ .chat-import-section:hover {
1095
+ border-color: var(--primary-color);
1096
+ background: rgba(74, 74, 232, 0.05);
1097
+ }
1098
+
1099
+ .import-box {
1100
+ display: flex;
1101
+ flex-direction: column;
1102
+ gap: 15px;
1103
+ margin-top: 15px;
1104
+ }
1105
+
1106
+ .import-chat-btn {
1107
+ display: flex;
1108
+ align-items: center;
1109
+ justify-content: center;
1110
+ gap: 10px;
1111
+ padding: 12px;
1112
+ background: linear-gradient(135deg, var(--secondary-color), #5d4ae8);
1113
+ color: white;
1114
+ border: none;
1115
+ border-radius: var(--border-radius);
1116
+ cursor: pointer;
1117
+ transition: all 0.3s ease;
1118
+ }
1119
+
1120
+ .import-chat-btn:hover {
1121
+ transform: translateY(-2px);
1122
+ box-shadow: 0 4px 12px rgba(108, 92, 231, 0.3);
1123
+ }
1124
+
1125
+ .hint {
1126
+ font-size: 0.85rem;
1127
+ color: rgba(255, 255, 255, 0.6);
1128
+ text-align: center;
1129
+ margin-top: 10px;
1130
+ }
1131
+
1132
+ /* Стили для графиков аналитики */
1133
+ .analytics-section {
1134
+ background: rgba(255, 255, 255, 0.05);
1135
+ border-radius: var(--border-radius);
1136
+ padding: 25px;
1137
+ margin-bottom: 30px;
1138
+ }
1139
+
1140
+ .chart-container {
1141
+ position: relative;
1142
+ height: 300px;
1143
+ margin: 20px 0;
1144
+ }
1145
+
1146
+ .emotion-stats {
1147
+ display: grid;
1148
+ grid-template-columns: repeat(3, 1fr);
1149
+ grid-auto-rows: minmax(min-content, max-content);
1150
+ gap: 15px;
1151
+ margin-top: 25px;
1152
+ }
1153
+
1154
+ .emotion-stat {
1155
+ background: rgba(255, 255, 255, 0.05);
1156
+ border-radius: var(--border-radius);
1157
+ padding: 15px;
1158
+ transition: all 0.3s ease;
1159
+ }
1160
+
1161
+ .emotion-stat:hover {
1162
+ transform: translateY(-3px);
1163
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
1164
+ }
1165
+
1166
+ .emotion-label {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ gap: 8px;
1170
+ font-weight: 500;
1171
+ margin-bottom: 10px;
1172
+ font-size: 0.95rem;
1173
+ }
1174
+
1175
+ .emotion-count {
1176
+ font-size: 0.85rem;
1177
+ color: rgba(255, 255, 255, 0.7);
1178
+ margin-bottom: 8px;
1179
+ }
1180
+
1181
+ .confidence-bar {
1182
+ height: 6px;
1183
+ background: rgba(255, 255, 255, 0.1);
1184
+ border-radius: 3px;
1185
+ margin: 10px 0;
1186
+ overflow: hidden;
1187
+ }
1188
+
1189
+ .confidence-fill {
1190
+ height: 100%;
1191
+ border-radius: 3px;
1192
+ }
1193
+
1194
+ .confidence-value {
1195
+ font-size: 0.8rem;
1196
+ text-align: right;
1197
+ color: rgba(255, 255, 255, 0.7);
1198
+ }
1199
+
1200
+ /* Цвета для разных эмоций */
1201
+ .emotion-stat[data-emotion="joy"] .confidence-fill,
1202
+ .emotion-stat[data-emotion="happy"] .confidence-fill {
1203
+ background: linear-gradient(90deg, #00b894, #00cec9);
1204
+ }
1205
+
1206
+ .emotion-stat[data-emotion="sadness"] .confidence-fill,
1207
+ .emotion-stat[data-emotion="sad"] .confidence-fill {
1208
+ background: linear-gradient(90deg, #0984e3, #6c5ce7);
1209
+ }
1210
+
1211
+ .emotion-stat[data-emotion="anger"] .confidence-fill,
1212
+ .emotion-stat[data-emotion="angry"] .confidence-fill {
1213
+ background: linear-gradient(90deg, #d63031, #e17055);
1214
+ }
1215
+
1216
+ .emotion-stat[data-emotion="neutral"] .confidence-fill {
1217
+ background: linear-gradient(90deg, #636e72, #b2bec3);
1218
+ }
1219
+
1220
+ .emotion-stat[data-emotion="surprise"] .confidence-fill {
1221
+ background: linear-gradient(90deg, #fdcb6e, #e17055);
1222
+ }
1223
+
1224
+ /* Стили для боковой панели чатов */
1225
+ .import-chat-sidebar {
1226
+ padding: 15px;
1227
+ margin: 10px 0;
1228
+ background: rgba(74, 74, 232, 0.1);
1229
+ border-radius: var(--border-radius);
1230
+ text-align: center;
1231
+ }
1232
+
1233
+ .import-chat-sidebar label {
1234
+ display: block;
1235
+ padding: 10px;
1236
+ background: rgba(255, 255, 255, 0.1);
1237
+ border-radius: var(--border-radius);
1238
+ cursor: pointer;
1239
+ transition: all 0.3s ease;
1240
+ font-size: 0.9rem;
1241
+ }
1242
+
1243
+ .import-chat-sidebar label:hover {
1244
+ background: rgba(74, 74, 232, 0.2);
1245
+ }
1246
+
1247
+ /* Адаптивные стили */
1248
+ @media (max-width: 768px) {
1249
+ .emotion-stats {
1250
+ grid-template-columns: 1fr;
1251
+ }
1252
+
1253
+ .chart-container {
1254
+ height: 250px;
1255
+ }
1256
+ }
1257
+
1258
+ @media (max-width: 576px) {
1259
+ .analytics-section,
1260
+ .chat-import-section {
1261
+ padding: 15px;
1262
+ }
1263
+
1264
+ .import-chat-btn {
1265
+ padding: 10px;
1266
+ font-size: 0.9rem;
1267
+ }
1268
+ }
1269
+
1270
+ /* Telegram Analysis Styles */
1271
+ .telegram-analysis-section .section-title,
1272
+ .telegram-analysis-section h4 {
1273
+ color: var(--light-text) !important;
1274
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1275
+ padding-bottom: 10px;
1276
+ margin-bottom: 20px;
1277
+ }
1278
+
1279
+ .telegram-analysis-section {
1280
+ margin-top: 40px;
1281
+ padding: 25px;
1282
+ background: rgba(255, 255, 255, 0.03);
1283
+ border-radius: var(--border-radius);
1284
+ border: 1px solid rgba(255, 255, 255, 0.1);
1285
+ }
1286
+
1287
+ #emotion-distribution {
1288
+ background: rgba(255,255,255,0.05);
1289
+ border-radius: var(--border-radius);
1290
+ padding: 15px;
1291
+ margin-top: 15px;
1292
+ max-height: 400px; /* Фиксированная высота */
1293
+ overflow-y: auto; /* Скролл при переполнении */
1294
+ }
1295
+
1296
+ .emotion-stat {
1297
+ background: rgba(255,255,255,0.03);
1298
+ border-radius: 8px;
1299
+ padding: 12px;
1300
+ margin: 8px 0;
1301
+ border-left: 4px solid transparent;
1302
+ transition: all 0.3s ease;
1303
+ display: flex;
1304
+ flex-direction: column;
1305
+ gap: 8px;
1306
+ }
1307
+
1308
+ .emotion-label {
1309
+ display: flex;
1310
+ align-items: center;
1311
+ gap: 8px;
1312
+ font-size: 0.95rem;
1313
+ color: var(--light-text);
1314
+ }
1315
+
1316
+ .emotion-label i {
1317
+ font-size: 1.2rem !important; /* Увеличиваем иконки */
1318
+ width: 25px; /* Фиксированная ширина */
1319
+ color: inherit !important; /* Цвет из JS */
1320
+ }
1321
+
1322
+ .confidence-bar {
1323
+ height: 6px;
1324
+ background: rgba(255,255,255,0.1);
1325
+ border-radius: 3px;
1326
+ }
1327
+
1328
+ .confidence-value {
1329
+ text-align: right;
1330
+ font-size: 0.85rem;
1331
+ color: rgba(255,255,255,0.8);
1332
+ }
1333
+
1334
+ .chart-header {
1335
+ display: flex;
1336
+ justify-content: space-between;
1337
+ align-items: center;
1338
+ margin-bottom: 20px;
1339
+ flex-wrap: wrap;
1340
+ gap: 15px;
1341
+ }
1342
+
1343
+ .time-filter {
1344
+ display: flex;
1345
+ gap: 8px;
1346
+ flex-wrap: wrap;
1347
+ }
1348
+
1349
+ .time-btn {
1350
+ padding: 6px 12px;
1351
+ background: rgba(255, 255, 255, 0.05);
1352
+ border: 1px solid rgba(255, 255, 255, 0.1);
1353
+ border-radius: 20px;
1354
+ color: rgba(255, 255, 255, 0.7);
1355
+ cursor: pointer;
1356
+ font-size: 0.85rem;
1357
+ transition: all 0.3s ease;
1358
+ }
1359
+
1360
+ .time-btn:hover {
1361
+ background: rgba(255, 255, 255, 0.1);
1362
+ }
1363
+
1364
+ .time-btn.active {
1365
+ background: rgba(74, 74, 232, 0.3);
1366
+ border-color: var(--primary-color);
1367
+ color: white;
1368
+ }
1369
+
1370
+ .chart-row {
1371
+ gap: 20px;
1372
+ margin-top: 25px;
1373
+ }
1374
+
1375
+ .chart-col {
1376
+ background: rgba(255, 255, 255, 0.03);
1377
+ padding: 20px;
1378
+ border-radius: var(--border-radius);
1379
+ border: 1px solid rgba(255, 255, 255, 0.1);
1380
+ }
1381
+
1382
+ .chart-col h4 {
1383
+ margin-bottom: 15px;
1384
+ display: flex;
1385
+ align-items: center;
1386
+ gap: 10px;
1387
+ }
1388
+
1389
+ @media (max-width: 768px) {
1390
+ .chart-row {
1391
+ flex-direction: column;
1392
+ }
1393
+
1394
+ .chart-col {
1395
+ width: 100%;
1396
+ }
1397
+ }
1398
+
1399
+ .clear-file-btn {
1400
+ background: none;
1401
+ border: none;
1402
+ color: rgba(255, 255, 255, 0.6);
1403
+ cursor: pointer;
1404
+ padding: 0 5px;
1405
+ }
1406
+
1407
+ .clear-file-btn:hover {
1408
+ color: var(--danger-color);
1409
+ }
1410
+
1411
+ #emotion-timeline {
1412
+ height: 400px;
1413
+ margin: 20px 0;
1414
+ background: rgba(0, 0, 0, 0.2);
1415
+ border-radius: var(--border-radius);
1416
+ }
1417
+
1418
+ .emotion-stat {
1419
+ background: rgba(255, 255, 255, 0.05);
1420
+ padding: 15px;
1421
+ margin: 10px 0;
1422
+ border-radius: var(--border-radius);
1423
+ transition: transform 0.3s ease;
1424
+ }
1425
+
1426
+ .emotion-stat:hover {
1427
+ transform: translateY(-3px);
1428
+ }
1429
+
1430
+ .emotion-label {
1431
+ display: flex;
1432
+ align-items: center;
1433
+ gap: 10px;
1434
+ font-size: 0.95rem;
1435
+ margin-bottom: 8px;
1436
+ }
1437
+
1438
+ .confidence-bar {
1439
+ height: 6px;
1440
+ background: rgba(255, 255, 255, 0.1);
1441
+ border-radius: 3px;
1442
+ margin: 10px 0;
1443
+ }
1444
+
1445
+ .confidence-fill {
1446
+ height: 100%;
1447
+ border-radius: 3px;
1448
+ transition: width 0.5s ease;
1449
+ }
1450
+
1451
+ .file-info {
1452
+ display: flex;
1453
+ align-items: center;
1454
+ gap: 8px;
1455
+ padding: 8px;
1456
+ background: rgba(255, 255, 255, 0.05);
1457
+ border-radius: var(--border-radius);
1458
+ }
1459
+
1460
+ #selected-file-name {
1461
+ font-size: 0.9rem;
1462
+ max-width: 250px;
1463
+ overflow: hidden;
1464
+ text-overflow: ellipsis;
1465
+ white-space: nowrap;
1466
+ }
1467
+
1468
+ .message-preview-panel {
1469
+ position: fixed;
1470
+ right: 20px;
1471
+ top: 100px;
1472
+ width: 300px;
1473
+ background: #1e1e2f;
1474
+ padding: 15px;
1475
+ border-radius: 8px;
1476
+ color: white;
1477
+ z-index: 100;
1478
+ box-shadow: 0 0 10px rgba(0,0,0,0.5);
1479
+ }
1480
+
1481
+ .analysis-summary {
1482
+ margin-top: 20px;
1483
+ padding: 15px;
1484
+ background: #2c2c3e;
1485
+ border-radius: 8px;
1486
+ }
1487
+
1488
+ .user-filter {
1489
+ display: flex;
1490
+ align-items: center;
1491
+ gap: 10px;
1492
+ margin-bottom: 10px;
1493
+ }
1494
+
1495
+ /* Auth Styles */
1496
+ .auth-container {
1497
+ display: flex;
1498
+ justify-content: center;
1499
+ align-items: center;
1500
+ min-height: 100vh;
1501
+ background: linear-gradient(135deg, var(--darker-bg), var(--dark-bg));
1502
+ padding: 20px;
1503
+ }
1504
+
1505
+ .auth-card {
1506
+ width: 100%;
1507
+ max-width: 450px;
1508
+ background: rgba(255, 255, 255, 0.05);
1509
+ border-radius: var(--border-radius);
1510
+ overflow: hidden;
1511
+ box-shadow: var(--box-shadow);
1512
+ backdrop-filter: blur(10px);
1513
+ border: 1px solid rgba(255, 255, 255, 0.1);
1514
+ }
1515
+
1516
+ .auth-header {
1517
+ padding: 25px;
1518
+ background: rgba(74, 74, 232, 0.1);
1519
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1520
+ text-align: center;
1521
+ }
1522
+
1523
+ .auth-header h3 {
1524
+ color: var(--light-text);
1525
+ font-size: 1.5rem;
1526
+ display: flex;
1527
+ align-items: center;
1528
+ justify-content: center;
1529
+ gap: 10px;
1530
+ }
1531
+
1532
+ .auth-body {
1533
+ padding: 25px;
1534
+ }
1535
+
1536
+ .form-group {
1537
+ margin-bottom: 20px;
1538
+ }
1539
+
1540
+ .form-label {
1541
+ display: block;
1542
+ margin-bottom: 8px;
1543
+ color: rgba(255, 255, 255, 0.8);
1544
+ font-size: 0.95rem;
1545
+ display: flex;
1546
+ align-items: center;
1547
+ gap: 8px;
1548
+ }
1549
+
1550
+ .form-input {
1551
+ width: 100%;
1552
+ padding: 12px 15px;
1553
+ background: rgba(255, 255, 255, 0.1);
1554
+ border: 1px solid rgba(255, 255, 255, 0.1);
1555
+ border-radius: var(--border-radius);
1556
+ color: var(--light-text);
1557
+ font-size: 0.95rem;
1558
+ transition: all 0.3s ease;
1559
+ }
1560
+
1561
+ .form-input:focus {
1562
+ outline: none;
1563
+ border-color: var(--primary-color);
1564
+ background: rgba(255, 255, 255, 0.15);
1565
+ box-shadow: 0 0 0 3px rgba(74, 74, 232, 0.2);
1566
+ }
1567
+
1568
+ .form-error {
1569
+ color: var(--danger-color);
1570
+ font-size: 0.85rem;
1571
+ margin-top: 5px;
1572
+ }
1573
+
1574
+ .btn-auth {
1575
+ width: 100%;
1576
+ padding: 14px;
1577
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
1578
+ color: white;
1579
+ border: none;
1580
+ border-radius: var(--border-radius);
1581
+ font-size: 1rem;
1582
+ font-weight: 500;
1583
+ cursor: pointer;
1584
+ transition: all 0.3s ease;
1585
+ display: flex;
1586
+ align-items: center;
1587
+ justify-content: center;
1588
+ gap: 10px;
1589
+ }
1590
+
1591
+ .btn-auth:hover {
1592
+ transform: translateY(-2px);
1593
+ box-shadow: 0 4px 15px rgba(74, 74, 232, 0.4);
1594
+ }
1595
+
1596
+ .auth-footer {
1597
+ text-align: center;
1598
+ margin-top: 20px;
1599
+ color: rgba(255, 255, 255, 0.7);
1600
+ font-size: 0.95rem;
1601
+ }
1602
+
1603
+ .auth-link {
1604
+ color: var(--primary-color);
1605
+ text-decoration: none;
1606
+ transition: all 0.3s ease;
1607
+ }
1608
+
1609
+ .auth-link:hover {
1610
+ color: var(--secondary-color);
1611
+ text-decoration: underline;
1612
+ }
1613
+
1614
+ /* Responsive */
1615
+ @media (max-width: 576px) {
1616
+ .auth-card {
1617
+ border-radius: 0;
1618
+ border: none;
1619
+ }
1620
+
1621
+ .auth-container {
1622
+ padding: 0;
1623
+ align-items: flex-start;
1624
+ }
1625
+ }
1626
+
1627
+ .analysis-result-card {
1628
+ background: rgba(255, 255, 255, 0.05);
1629
+ border-radius: var(--border-radius);
1630
+ padding: 15px;
1631
+ margin-top: 10px;
1632
+ border-left: 4px solid transparent;
1633
+ transition: transform 0.3s ease;
1634
+ }
1635
+
1636
+ .analysis-result-card:hover {
1637
+ transform: translateY(-5px);
1638
+ }
1639
+
1640
+ .analysis-result-card.joy {
1641
+ border-color: var(--success-color);
1642
+ }
1643
+
1644
+ .analysis-result-card.anger {
1645
+ border-color: var(--danger-color);
1646
+ }
1647
+
1648
+ .analysis-result-card.sadness {
1649
+ border-color: var(--info-color);
1650
+ }
1651
+
1652
+ .analysis-result-card.neutral {
1653
+ border-color: var(--warning-color);
1654
+ }
1655
+
1656
+ .analysis-result-card.fear {
1657
+ border-color: #a29bfe;
1658
+ }
1659
+
1660
+ .analysis-header {
1661
+ display: flex;
1662
+ justify-content: space-between;
1663
+ align-items: center;
1664
+ margin-bottom: 10px;
1665
+ }
1666
+
1667
+ .emotion-label {
1668
+ font-size: 1.1rem;
1669
+ font-weight: 600;
1670
+ }
1671
+
1672
+ .confidence-bar-container {
1673
+ height: 8px;
1674
+ background: rgba(255, 255, 255, 0.1);
1675
+ border-radius: 4px;
1676
+ overflow: hidden;
1677
+ }
1678
+
1679
+ .confidence-bar {
1680
+ height: 100%;
1681
+ background: rgba(255, 255, 255, 0.1);
1682
+ border-radius: 4px;
1683
+ overflow: hidden;
1684
+ }
1685
+
1686
+ .confidence-fill {
1687
+ height: 100%;
1688
+ transition: width 0.5s ease;
1689
+ }
1690
+
1691
+ .confidence-fill[joy] {
1692
+ background: linear-gradient(90deg, #00b894, #00cec9);
1693
+ }
1694
+
1695
+ .confidence-fill[anger] {
1696
+ background: linear-gradient(90deg, #d63031, #e17055);
1697
+ }
1698
+
1699
+ .confidence-fill[sadness] {
1700
+ background: linear-gradient(90deg, #0984e3, #6c5ce7);
1701
+ }
1702
+
1703
+ .confidence-fill[neutral] {
1704
+ background: linear-gradient(90deg, #fdcb6e, #ffeaa7);
1705
+ }
1706
+
1707
+ .confidence-fill[fear] {
1708
+ background: linear-gradient(90deg, #a29bfe, #6c5ce7);
1709
+ }
1710
+
1711
+ .confidence-percent {
1712
+ margin-top: 8px;
1713
+ font-size: 0.85rem;
1714
+ color: rgba(255, 255, 255, 0.7);
1715
+ }
1716
+
1717
+ /* Admin Styles */
1718
+ .admin-container {
1719
+ max-width: 1200px;
1720
+ margin: 0 auto;
1721
+ padding: 2rem;
1722
+ }
1723
+
1724
+ .admin-stats {
1725
+ display: grid;
1726
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1727
+ gap: 1.5rem;
1728
+ margin: 2rem 0;
1729
+ }
1730
+
1731
+ .stat-card {
1732
+ background: rgba(255, 255, 255, 0.05);
1733
+ border-radius: var(--border-radius);
1734
+ padding: 1.5rem;
1735
+ display: flex;
1736
+ align-items: center;
1737
+ gap: 1rem;
1738
+ transition: transform 0.3s ease;
1739
+ }
1740
+
1741
+ .stat-card:hover {
1742
+ transform: translateY(-5px);
1743
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
1744
+ }
1745
+
1746
+ .stat-icon {
1747
+ width: 50px;
1748
+ height: 50px;
1749
+ background: rgba(74, 74, 232, 0.2);
1750
+ border-radius: 50%;
1751
+ display: flex;
1752
+ align-items: center;
1753
+ justify-content: center;
1754
+ font-size: 1.5rem;
1755
+ color: var(--primary-color);
1756
+ }
1757
+
1758
+ .stat-content h3 {
1759
+ font-size: 1rem;
1760
+ color: rgba(255, 255, 255, 0.7);
1761
+ margin-bottom: 0.5rem;
1762
+ }
1763
+
1764
+ .stat-value {
1765
+ font-size: 1.8rem;
1766
+ font-weight: 600;
1767
+ color: white;
1768
+ }
1769
+
1770
+ .stat-change {
1771
+ font-size: 0.8rem;
1772
+ color: var(--success-color);
1773
+ }
1774
+
1775
+ .btn-admin {
1776
+ display: inline-flex;
1777
+ align-items: center;
1778
+ gap: 0.5rem;
1779
+ padding: 1rem 1.5rem;
1780
+ background: rgba(74, 74, 232, 0.2);
1781
+ border: 1px solid var(--primary-color);
1782
+ border-radius: var(--border-radius);
1783
+ color: white;
1784
+ text-decoration: none;
1785
+ margin-right: 1rem;
1786
+ margin-bottom: 1rem;
1787
+ transition: all 0.3s ease;
1788
+ }
1789
+
1790
+ .btn-admin:hover {
1791
+ background: rgba(74, 74, 232, 0.4);
1792
+ transform: translateY(-2px);
1793
+ }
1794
+
1795
+ /* Таблицы в админке */
1796
+ .admin-table {
1797
+ width: 100%;
1798
+ border-collapse: collapse;
1799
+ margin: 1.5rem 0;
1800
+ }
1801
+
1802
+ .admin-table th, .admin-table td {
1803
+ padding: 1rem;
1804
+ text-align: left;
1805
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1806
+ }
1807
+
1808
+ .admin-table th {
1809
+ background: rgba(74, 74, 232, 0.2);
1810
+ color: var(--primary-color);
1811
+ }
1812
+
1813
+ .admin-table tr:hover {
1814
+ background: rgba(255, 255, 255, 0.03);
1815
+ }
1816
+
1817
+ .admin-actions {
1818
+ margin: 2rem 0;
1819
  }