Arghet6 commited on
Commit
2210ef6
·
verified ·
1 Parent(s): 0779763

Upload 34 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  *.pth filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ model_cache/
2
+ models_cache/
3
+ instance/
.idea/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
.idea/emotion_analysis_system.iml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="XmlDuplicatedId" enabled="false" level="ERROR" enabled_by_default="false" />
5
+ </profile>
6
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/misc.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.11 (emotion_analysis_system)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
7
+ <component name="PyCharmProfessionalAdvertiser">
8
+ <option name="shown" value="true" />
9
+ </component>
10
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/emotion_analysis_system.iml" filepath="$PROJECT_DIR$/.idea/emotion_analysis_system.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
.idea/workspace.xml ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="80cad30e-20ea-4a3e-a71a-c380b9fb453b" name="Changes" comment="добавил gunicorn">
8
+ <change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
9
+ </list>
10
+ <option name="SHOW_DIALOG" value="false" />
11
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
12
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
13
+ <option name="LAST_RESOLUTION" value="IGNORE" />
14
+ </component>
15
+ <component name="Git.Settings">
16
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
17
+ </component>
18
+ <component name="ProjectColorInfo">{
19
+ &quot;associatedIndex&quot;: 7
20
+ }</component>
21
+ <component name="ProjectId" id="2wCKFqS6j3fHR0o6mVerabcct5P" />
22
+ <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
23
+ <component name="ProjectViewState">
24
+ <option name="hideEmptyMiddlePackages" value="true" />
25
+ <option name="showLibraryContents" value="true" />
26
+ </component>
27
+ <component name="PropertiesComponent"><![CDATA[{
28
+ "keyToString": {
29
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
30
+ "RunOnceActivity.ShowReadmeOnStart": "true",
31
+ "git-widget-placeholder": "master",
32
+ "last_opened_file_path": "C:/Users/Айрат/Desktop/autoservice",
33
+ "settings.editor.selected.configurable": "org.jetbrains.plugins.github.ui.GithubSettingsConfigurable"
34
+ }
35
+ }]]></component>
36
+ <component name="SharedIndexes">
37
+ <attachedChunks>
38
+ <set>
39
+ <option value="bundled-python-sdk-09665e90c3a7-d3b881c8e49f-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-233.15026.15" />
40
+ </set>
41
+ </attachedChunks>
42
+ </component>
43
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
44
+ <component name="TaskManager">
45
+ <task active="true" id="Default" summary="Default task">
46
+ <changelist id="80cad30e-20ea-4a3e-a71a-c380b9fb453b" name="Changes" comment="" />
47
+ <created>1745540015058</created>
48
+ <option name="number" value="Default" />
49
+ <option name="presentableId" value="Default" />
50
+ <updated>1745540015058</updated>
51
+ </task>
52
+ <task id="LOCAL-00001" summary="добавил gunicorn">
53
+ <option name="closed" value="true" />
54
+ <created>1746432339386</created>
55
+ <option name="number" value="00001" />
56
+ <option name="presentableId" value="LOCAL-00001" />
57
+ <option name="project" value="LOCAL" />
58
+ <updated>1746432339386</updated>
59
+ </task>
60
+ <task id="LOCAL-00002" summary="добавил gunicorn">
61
+ <option name="closed" value="true" />
62
+ <created>1746433287813</created>
63
+ <option name="number" value="00002" />
64
+ <option name="presentableId" value="LOCAL-00002" />
65
+ <option name="project" value="LOCAL" />
66
+ <updated>1746433287813</updated>
67
+ </task>
68
+ <task id="LOCAL-00003" summary="добавил gunicorn">
69
+ <option name="closed" value="true" />
70
+ <created>1746441739553</created>
71
+ <option name="number" value="00003" />
72
+ <option name="presentableId" value="LOCAL-00003" />
73
+ <option name="project" value="LOCAL" />
74
+ <updated>1746441739553</updated>
75
+ </task>
76
+ <task id="LOCAL-00004" summary="добавил gunicorn">
77
+ <option name="closed" value="true" />
78
+ <created>1746442047419</created>
79
+ <option name="number" value="00004" />
80
+ <option name="presentableId" value="LOCAL-00004" />
81
+ <option name="project" value="LOCAL" />
82
+ <updated>1746442047419</updated>
83
+ </task>
84
+ <option name="localTasksCounter" value="5" />
85
+ <servers />
86
+ </component>
87
+ <component name="VcsManagerConfiguration">
88
+ <MESSAGE value="добавил gunicorn" />
89
+ <option name="LAST_COMMIT_MESSAGE" value="добавил gunicorn" />
90
+ </component>
91
+ </project>
READme.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Проблема с NumPy
2
+ Переустановите NumPy и PyTorch:
3
+
4
+ pip uninstall numpy torch -y
5
+ pip install numpy torch --upgrade
6
+
7
+ Конфликт версий PyTorch и NumPy
8
+ Установите совместимые версии (например, для CPU):
9
+
10
+ pip install numpy==1.23.5 torch==2.0.1 --upgrade
11
+
12
+ Проблема с путями или кэшем
13
+ Очистите кэш pip и переустановите зависимости:
14
+
15
+ pip cache purge
16
+ pip install -r requirements.txt --force-reinstall
__pycache__/auth.cpython-311.pyc ADDED
Binary file (3.73 kB). View file
 
__pycache__/extensions.cpython-311.pyc ADDED
Binary file (391 Bytes). View file
 
__pycache__/forms.cpython-311.pyc ADDED
Binary file (2.55 kB). View file
 
__pycache__/models.cpython-311.pyc ADDED
Binary file (4.58 kB). View file
 
__pycache__/profile.cpython-311.pyc ADDED
Binary file (2.93 kB). View file
 
app.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, render_template, flash, redirect, url_for
2
+ from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
3
+ from werkzeug.security import generate_password_hash, check_password_hash
4
+ from transformers import pipeline
5
+ import torch
6
+ from pydub import AudioSegment
7
+ import os
8
+ import io
9
+ import uuid
10
+ from datetime import datetime
11
+ import sqlite3
12
+ from pathlib import Path
13
+ import whisper
14
+ from extensions import db, login_manager
15
+
16
+
17
+ instance_path = Path(__file__).parent / 'instance'
18
+ instance_path.mkdir(exist_ok=True, mode=0o755)
19
+
20
+ app = Flask(__name__)
21
+ app.secret_key = 'очень_сложный_секретный_ключ_здесь'
22
+ db_path = instance_path / 'chats.db'
23
+ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
24
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
25
+
26
+ # Инициализация Flask-Login
27
+ db.init_app(app)
28
+ login_manager.init_app(app)
29
+ login_manager.login_view = 'auth_bp.login'
30
+
31
+ # Инициализация моделей
32
+ def init_models():
33
+ try:
34
+ emotion_map = {
35
+ 'joy': '😊 Радость',
36
+ 'neutral': '😐 Нейтрально',
37
+ 'anger': '😠 Злость',
38
+ 'sadness': '😢 Грусть',
39
+ 'surprise': '😲 Удивление'
40
+ }
41
+
42
+ speech_to_text_model = whisper.load_model("base")
43
+ text_classifier = pipeline(
44
+ "text-classification",
45
+ model="cointegrated/rubert-tiny2-cedr-emotion-detection"
46
+ )
47
+ audio_classifier = pipeline(
48
+ "audio-classification",
49
+ model="superb/hubert-large-superb-er"
50
+ )
51
+
52
+ return {
53
+ 'emotion_map': emotion_map,
54
+ 'speech_to_text_model': speech_to_text_model,
55
+ 'text_classifier': text_classifier,
56
+ 'audio_classifier': audio_classifier
57
+ }
58
+ except Exception as e:
59
+ print(f"Ошибка загрузки моделей: {e}")
60
+ return None
61
+
62
+ models = init_models()
63
+ if not models:
64
+ raise RuntimeError("Не удалось загрузить модели")
65
+
66
+ # Импорт Blueprint
67
+ from auth import auth_bp
68
+ from profile import profile_bp
69
+
70
+ app.register_blueprint(auth_bp)
71
+ app.register_blueprint(profile_bp)
72
+
73
+ # Делаем переменные доступными
74
+ emotion_map = models['emotion_map']
75
+ speech_to_text_model = models['speech_to_text_model']
76
+ text_classifier = models['text_classifier']
77
+ audio_classifier = models['audio_classifier']
78
+
79
+
80
+ def transcribe_audio(audio_path):
81
+ """Преобразование аудио в текст с помощью Whisper"""
82
+ if not speech_to_text_model:
83
+ return None
84
+ try:
85
+ result = speech_to_text_model.transcribe(audio_path, language="ru")
86
+ return result["text"]
87
+ except Exception as e:
88
+ print(f"Ошибка преобразования аудио в текст: {e}")
89
+ return None
90
+
91
+
92
+ # Инициализация Flask-Login
93
+ login_manager = LoginManager(app)
94
+ login_manager.login_view = 'login'
95
+
96
+
97
+ # Модель пользователя для Flask-Login
98
+ class User(UserMixin):
99
+ def __init__(self, id, username, email, password_hash):
100
+ self.id = id
101
+ self.username = username
102
+ self.email = email
103
+ self.password_hash = password_hash
104
+
105
+ def check_password(self, password):
106
+ return check_password_hash(self.password_hash, password)
107
+
108
+
109
+ @login_manager.user_loader
110
+ def load_user(user_id):
111
+ conn = get_db_connection()
112
+ user = conn.execute(
113
+ "SELECT id, username, email, password_hash FROM users WHERE id = ?",
114
+ (user_id,)
115
+ ).fetchone()
116
+ conn.close()
117
+ if user:
118
+ return User(id=user['id'], username=user['username'], email=user['email'], password_hash=user['password_hash'])
119
+ return None
120
+
121
+
122
+ # Инициализация БД
123
+ def get_db_connection():
124
+ instance_path = Path('instance')
125
+ instance_path.mkdir(exist_ok=True)
126
+ db_path = instance_path / 'chats.db'
127
+ conn = sqlite3.connect(str(db_path))
128
+ conn.row_factory = sqlite3.Row
129
+ return conn
130
+
131
+
132
+ def init_db():
133
+ conn = get_db_connection()
134
+ try:
135
+ conn.execute('''
136
+ CREATE TABLE IF NOT EXISTS users (
137
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
138
+ username TEXT UNIQUE NOT NULL,
139
+ email TEXT UNIQUE NOT NULL,
140
+ password_hash TEXT NOT NULL,
141
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
142
+ )
143
+ ''')
144
+ conn.execute('''
145
+ CREATE TABLE IF NOT EXISTS chats (
146
+ chat_id TEXT PRIMARY KEY,
147
+ user_id INTEGER,
148
+ created_at TEXT,
149
+ title TEXT,
150
+ FOREIGN KEY(user_id) REFERENCES users(id)
151
+ )
152
+ ''')
153
+ conn.execute('''
154
+ CREATE TABLE IF NOT EXISTS messages (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ chat_id TEXT,
157
+ sender TEXT,
158
+ content TEXT,
159
+ timestamp TEXT,
160
+ FOREIGN KEY(chat_id) REFERENCES chats(chat_id)
161
+ )
162
+ ''')
163
+ conn.execute('''
164
+ CREATE TABLE IF NOT EXISTS analysis_reports (
165
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ user_id INTEGER,
167
+ content TEXT,
168
+ emotion TEXT,
169
+ confidence REAL,
170
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
171
+ FOREIGN KEY(user_id) REFERENCES users(id)
172
+ )
173
+ ''')
174
+ conn.commit()
175
+ finally:
176
+ conn.close()
177
+
178
+
179
+ init_db()
180
+
181
+
182
+ # Маршруты аутентификации
183
+ @app.route('/login', methods=['GET', 'POST'])
184
+ def login():
185
+ if request.method == 'POST':
186
+ email = request.form.get('email')
187
+ password = request.form.get('password')
188
+
189
+ conn = get_db_connection()
190
+ user = conn.execute(
191
+ "SELECT id, username, email, password_hash FROM users WHERE email = ?",
192
+ (email,)
193
+ ).fetchone()
194
+ conn.close()
195
+
196
+ if user and check_password_hash(user['password_hash'], password):
197
+ user_obj = User(id=user['id'], username=user['username'],
198
+ email=user['email'], password_hash=user['password_hash'])
199
+ login_user(user_obj)
200
+ return redirect(url_for('index'))
201
+
202
+ flash('Неверный email или пароль', 'danger')
203
+
204
+ return render_template('auth/login.html')
205
+
206
+
207
+ @app.route('/register', methods=['GET', 'POST'])
208
+ def register():
209
+ if request.method == 'POST':
210
+ username = request.form.get('username')
211
+ email = request.form.get('email')
212
+ password = request.form.get('password')
213
+ confirm_password = request.form.get('confirm_password')
214
+
215
+ if password != confirm_password:
216
+ flash('Пароли не совпадают', 'danger')
217
+ return redirect(url_for('register'))
218
+
219
+ conn = get_db_connection()
220
+ try:
221
+ password_hash = generate_password_hash(password)
222
+ conn.execute(
223
+ "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
224
+ (username, email, password_hash)
225
+ )
226
+ conn.commit()
227
+ flash('Регистрация прошла успешно! Теперь вы можете войти.', 'success')
228
+ return redirect(url_for('login'))
229
+ except sqlite3.IntegrityError:
230
+ flash('Пользователь с таким email или именем уже существует', 'danger')
231
+ finally:
232
+ conn.close()
233
+
234
+ return render_template('auth/register.html')
235
+
236
+
237
+ @app.route('/logout')
238
+ @login_required
239
+ def logout():
240
+ logout_user()
241
+ return redirect(url_for('login'))
242
+
243
+
244
+ # Основные маршруты
245
+ @app.route("/")
246
+ @login_required
247
+ def index():
248
+ conn = get_db_connection()
249
+ try:
250
+ chats = conn.execute(
251
+ "SELECT chat_id, title FROM chats WHERE user_id = ? ORDER BY created_at DESC",
252
+ (current_user.id,)
253
+ ).fetchall()
254
+ return render_template("index.html", chats=chats)
255
+ finally:
256
+ conn.close()
257
+
258
+
259
+ @app.route("/analyze", methods=["POST"])
260
+ @login_required
261
+ def analyze_text():
262
+ if not text_classifier:
263
+ return jsonify({"error": "Model not loaded"}), 500
264
+
265
+ try:
266
+ data = request.get_json()
267
+ text = data.get("text", "").strip()
268
+
269
+ if not text:
270
+ return jsonify({"error": "Empty text"}), 400
271
+
272
+ # Получаем предсказания модели
273
+ result = text_classifier(text)
274
+
275
+ # Проверяем структуру ответа
276
+ if not result or not isinstance(result, list):
277
+ return jsonify({"error": "Invalid model response"}), 500
278
+
279
+ # Берем первый результат (самый вероятный)
280
+ prediction = result[0] if result else {}
281
+
282
+ # Проверяем наличие нужных полей
283
+ if not all(key in prediction for key in ['label', 'score']):
284
+ return jsonify({"error": "Invalid prediction format"}), 500
285
+
286
+ # Сохраняем в базу данных
287
+ conn = get_db_connection()
288
+ conn.execute(
289
+ "INSERT INTO analysis_reports (user_id, content, emotion, confidence) VALUES (?, ?, ?, ?)",
290
+ (current_user.id, text, prediction['label'], prediction['score'])
291
+ )
292
+ conn.commit()
293
+ conn.close()
294
+
295
+ return jsonify({
296
+ "emotion": emotion_map.get(prediction['label'], "❓ Неизвестно"),
297
+ "confidence": float(prediction['score'])
298
+ })
299
+
300
+ except Exception as e:
301
+ return jsonify({"error": str(e)}), 500
302
+
303
+
304
+ @app.route('/analyze_audio', methods=['POST'])
305
+ @login_required
306
+ def analyze_audio():
307
+ if not audio_classifier or not speech_to_text_model:
308
+ return jsonify({"error": "Model not loaded"}), 500
309
+
310
+ if 'audio' not in request.files:
311
+ return jsonify({'error': 'No audio file'}), 400
312
+
313
+ try:
314
+ audio_file = request.files['audio']
315
+ temp_path = "temp_audio.wav"
316
+
317
+ audio = AudioSegment.from_file(io.BytesIO(audio_file.read()))
318
+ audio = audio.set_frame_rate(16000).set_channels(1)
319
+ audio.export(temp_path, format="wav", codec="pcm_s16le")
320
+
321
+ transcribed_text = transcribe_audio(temp_path)
322
+ result = audio_classifier(temp_path)
323
+ os.remove(temp_path)
324
+
325
+ emotion_mapping = {
326
+ 'hap': 'happy',
327
+ 'sad': 'sad',
328
+ 'neu': 'neutral',
329
+ 'ang': 'angry'
330
+ }
331
+ emotions = {emotion_mapping.get(item['label'].lower(), 'neutral'): item['score']
332
+ for item in result if item['label'].lower() in emotion_mapping}
333
+
334
+ dominant_emotion = max(emotions.items(), key=lambda x: x[1])
335
+ response_map = {
336
+ 'happy': '😊 Радость',
337
+ 'sad': '😢 Грусть',
338
+ 'angry': '😠 Злость',
339
+ 'neutral': '😐 Нейтрально'
340
+ }
341
+
342
+ conn = get_db_connection()
343
+ conn.execute(
344
+ "INSERT INTO analysis_reports (user_id, content, emotion, confidence) VALUES (?, ?, ?, ?)",
345
+ (current_user.id, transcribed_text, dominant_emotion[0], dominant_emotion[1])
346
+ )
347
+ conn.commit()
348
+ conn.close()
349
+
350
+ return jsonify({
351
+ 'emotion': response_map.get(dominant_emotion[0], 'неизвестно'),
352
+ 'confidence': round(dominant_emotion[1], 2),
353
+ 'transcribed_text': transcribed_text if transcribed_text else "Не удалось распознать текст"
354
+ })
355
+ except Exception as e:
356
+ return jsonify({'error': str(e)}), 500
357
+
358
+
359
+ @app.route('/get_chats')
360
+ @login_required
361
+ def get_chats():
362
+ conn = get_db_connection()
363
+ try:
364
+ chats = conn.execute(
365
+ "SELECT chat_id, title FROM chats WHERE user_id = ? ORDER BY created_at DESC",
366
+ (current_user.id,)
367
+ ).fetchall()
368
+ return jsonify([dict(chat) for chat in chats])
369
+ finally:
370
+ conn.close()
371
+
372
+
373
+ @app.route('/start_chat', methods=['POST'])
374
+ @login_required
375
+ def start_chat():
376
+ conn = get_db_connection()
377
+ try:
378
+ chat_id = str(uuid.uuid4())
379
+ conn.execute(
380
+ "INSERT INTO chats (chat_id, user_id, title, created_at) VALUES (?, ?, ?, ?)",
381
+ (chat_id, current_user.id, f"Новый чат {datetime.now().strftime('%d.%m')}", datetime.now())
382
+ )
383
+ conn.commit()
384
+ return jsonify({"chat_id": chat_id, "title": f"Новый чат {datetime.now().strftime('%d.%m')}"})
385
+ except Exception as e:
386
+ return jsonify({"error": str(e)}), 500
387
+ finally:
388
+ conn.close()
389
+
390
+
391
+ @app.route('/load_chat/<chat_id>')
392
+ @login_required
393
+ def load_chat(chat_id):
394
+ conn = get_db_connection()
395
+ try:
396
+ # Получаем информацию о чате
397
+ chat = conn.execute(
398
+ "SELECT chat_id, title FROM chats WHERE chat_id = ? AND user_id = ?",
399
+ (chat_id, current_user.id)
400
+ ).fetchone()
401
+
402
+ if not chat:
403
+ return jsonify({"error": "Чат не найден"}), 404
404
+
405
+ # Получаем сообщения чата
406
+ messages = conn.execute(
407
+ "SELECT sender, content FROM messages WHERE chat_id = ? ORDER BY timestamp ASC",
408
+ (chat_id,)
409
+ ).fetchall()
410
+
411
+ return jsonify({
412
+ "chat_id": chat["chat_id"],
413
+ "title": chat["title"],
414
+ "messages": [dict(msg) for msg in messages]
415
+ })
416
+ finally:
417
+ conn.close()
418
+
419
+
420
+
421
+ @app.route('/save_message', methods=['POST'])
422
+ @login_required
423
+ def save_message():
424
+ data = request.get_json()
425
+ if not data or 'chat_id' not in data or 'content' not in data or 'sender' not in data:
426
+ return jsonify({"error": "Неверные данные"}), 400
427
+
428
+ conn = get_db_connection()
429
+ try:
430
+ # Проверяем, что чат принадлежит текущему пользователю
431
+ chat = conn.execute(
432
+ "SELECT chat_id FROM chats WHERE chat_id = ? AND user_id = ?",
433
+ (data['chat_id'], current_user.id)
434
+ ).fetchone()
435
+
436
+ if not chat:
437
+ return jsonify({"error": "Чат не найден"}), 404
438
+
439
+ # Анализируем эмоцию в тексте
440
+ emotion = "neutral"
441
+ confidence = 0.0
442
+ if text_classifier and data['content'].strip():
443
+ try:
444
+ predictions = text_classifier(data['content'])[0]
445
+ top_prediction = max(predictions, key=lambda x: x["score"])
446
+ emotion = top_prediction["label"]
447
+ confidence = top_prediction["score"]
448
+
449
+ # Сохраняем анализ в базу
450
+ conn.execute(
451
+ "INSERT INTO analysis_reports (user_id, content, emotion, confidence) VALUES (?, ?, ?, ?)",
452
+ (current_user.id, data['content'], emotion, confidence)
453
+ )
454
+ except Exception as e:
455
+ print(f"Ошибка анализа эмоции: {e}")
456
+
457
+ # Сохраняем сообщение
458
+ conn.execute(
459
+ "INSERT INTO messages (chat_id, sender, content, timestamp) VALUES (?, ?, ?, ?)",
460
+ (data['chat_id'], data['sender'], data['content'], datetime.now())
461
+ )
462
+ conn.commit()
463
+
464
+ return jsonify({
465
+ "status": "success",
466
+ "emotion": emotion_map.get(emotion, "❓ Неизвестно"),
467
+ "confidence": round(confidence, 2)
468
+ })
469
+ except Exception as e:
470
+ return jsonify({"error": str(e)}), 500
471
+ finally:
472
+ conn.close()
473
+
474
+ if __name__ == "__main__":
475
+ app.run(debug=True)
auth.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, redirect, url_for, flash
2
+ from flask_login import login_user, logout_user, login_required
3
+ from werkzeug.security import generate_password_hash, check_password_hash
4
+ from models import User, db # Используем SQLAlchemy модели
5
+ from forms import LoginForm, RegistrationForm
6
+ from datetime import datetime
7
+
8
+ auth_bp = Blueprint('auth_bp', __name__)
9
+
10
+
11
+ @auth_bp.route('/login', methods=['GET', 'POST'])
12
+ def login():
13
+ form = LoginForm()
14
+ if form.validate_on_submit():
15
+ user = User.query.filter_by(email=form.email.data).first() # Запрос через SQLAlchemy
16
+
17
+ if user and user.check_password(form.password.data):
18
+ login_user(user) # Используем модель User из Flask-SQLAlchemy
19
+ return redirect(url_for('index'))
20
+
21
+ flash('Неверный email или пароль', 'danger')
22
+
23
+ return render_template('auth/login.html', form=form)
24
+
25
+
26
+ @auth_bp.route('/register', methods=['GET', 'POST'])
27
+ def register():
28
+ form = RegistrationForm()
29
+ if form.validate_on_submit():
30
+ try:
31
+ # Создаем нового пользователя через SQLAlchemy
32
+ user = User(
33
+ username=form.username.data,
34
+ email=form.email.data,
35
+ password_hash=generate_password_hash(form.password.data)
36
+ )
37
+
38
+ db.session.add(user)
39
+ db.session.commit()
40
+
41
+ flash('Регистрация прошла успешно! Теперь вы можете войти.', 'success')
42
+ return redirect(url_for('auth_bp.login'))
43
+
44
+ except Exception as e:
45
+ db.session.rollback()
46
+ flash('Пользователь с таким email или именем уже существует', 'danger')
47
+
48
+ return render_template('auth/register.html', form=form)
49
+
50
+
51
+ @auth_bp.route('/logout')
52
+ @login_required
53
+ def logout():
54
+ logout_user()
55
+ return redirect(url_for('auth_bp.login'))
config.py ADDED
File without changes
extensions.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from flask_login import LoginManager
3
+
4
+ db = SQLAlchemy()
5
+ login_manager = LoginManager()
forms.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_wtf import FlaskForm
2
+ from wtforms import StringField, PasswordField, SubmitField
3
+ from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
4
+ from models import User
5
+
6
+ class LoginForm(FlaskForm):
7
+ email = StringField('Email', validators=[DataRequired(), Email()])
8
+ password = PasswordField('Пароль', validators=[DataRequired()])
9
+ submit = SubmitField('Войти')
10
+
11
+ class RegistrationForm(FlaskForm):
12
+ username = StringField('Имя пользователя', validators=[DataRequired(), Length(min=4, max=25)])
13
+ email = StringField('Email', validators=[DataRequired(), Email()])
14
+ password = PasswordField('Пароль', validators=[DataRequired(), Length(min=6)])
15
+ confirm_password = PasswordField('Подтвердите пароль',
16
+ validators=[DataRequired(), EqualTo('password')])
17
+ submit = SubmitField('Зарегистрироваться')
18
+
19
+ def validate_email(self, email):
20
+ user = User.query.filter_by(email=email.data).first()
21
+ if user:
22
+ raise ValidationError('Этот email уже используется')
instance/chats.db ADDED
Binary file (45.1 kB). View file
 
models.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_login import UserMixin
2
+ from werkzeug.security import generate_password_hash, check_password_hash
3
+ from extensions import db
4
+ from datetime import datetime
5
+
6
+ class User(UserMixin, db.Model):
7
+ __tablename__ = 'users'
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ username = db.Column(db.String(50), unique=True, nullable=False)
10
+ email = db.Column(db.String(100), unique=True, nullable=False)
11
+ password_hash = db.Column(db.String(200), nullable=False)
12
+ created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
13
+ chats = db.relationship('Chat', backref='user', lazy=True)
14
+ reports = db.relationship('AnalysisReport', backref='user', lazy=True)
15
+
16
+ def set_password(self, password):
17
+ self.password_hash = generate_password_hash(password)
18
+
19
+ def check_password(self, password):
20
+ return check_password_hash(self.password_hash, password)
21
+
22
+ class Chat(db.Model):
23
+ __tablename__ = 'chats'
24
+ chat_id = db.Column(db.String(36), primary_key=True)
25
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
26
+ created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
27
+ title = db.Column(db.String(100))
28
+ messages = db.relationship('Message', backref='chat', lazy=True)
29
+
30
+ class Message(db.Model):
31
+ __tablename__ = 'messages'
32
+ id = db.Column(db.Integer, primary_key=True)
33
+ chat_id = db.Column(db.String(36), db.ForeignKey('chats.chat_id'))
34
+ sender = db.Column(db.String(10))
35
+ content = db.Column(db.Text)
36
+ timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
37
+
38
+ class AnalysisReport(db.Model):
39
+ __tablename__ = 'analysis_reports'
40
+ id = db.Column(db.Integer, primary_key=True)
41
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
42
+ content = db.Column(db.Text)
43
+ emotion = db.Column(db.String(50))
44
+ confidence = db.Column(db.Float)
45
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
profile.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from sqlalchemy import func
3
+ from flask import Blueprint, render_template
4
+ from flask_login import login_required, current_user
5
+ from datetime import datetime
6
+
7
+ from extensions import db
8
+ from models import AnalysisReport
9
+
10
+ profile_bp = Blueprint('profile', __name__)
11
+
12
+ @profile_bp.route('/profile')
13
+ @login_required
14
+ def profile():
15
+ # Получаем отчеты
16
+ reports = AnalysisReport.query.filter_by(user_id=current_user.id)\
17
+ .order_by(AnalysisReport.created_at.desc())\
18
+ .all()
19
+
20
+ # Преобразуем в список словарей с правильными датами
21
+ formatted_reports = []
22
+ for report in reports:
23
+ report_dict = {
24
+ 'content': report.content,
25
+ 'emotion': report.emotion,
26
+ 'confidence': report.confidence,
27
+ 'created_at': report.created_at.strftime('%Y-%m-%d %H:%M:%S') if report.created_at else None
28
+ }
29
+ formatted_reports.append(report_dict)
30
+
31
+ # Получаем статистику по эмоциям
32
+ emotion_stats = db.session.query(
33
+ AnalysisReport.emotion,
34
+ func.count(AnalysisReport.id).label('count')
35
+ ).filter_by(user_id=current_user.id).group_by(AnalysisReport.emotion).all()
36
+
37
+ most_common_emotion = max(emotion_stats, key=lambda x: x.count).emotion if emotion_stats else None
38
+ total_reports = len(formatted_reports)
39
+
40
+ # Определяем emotion_map
41
+ emotion_map = {
42
+ 'joy': '😊 Радость',
43
+ 'neutral': '😐 Нейтрально',
44
+ 'anger': '😠 Злость',
45
+ 'sadness': '😢 Грусть',
46
+ 'surprise': '😲 Удивление',
47
+ 'happy': '😊 Радость',
48
+ 'sad': '😢 Грусть',
49
+ 'angry': '😠 Злость'
50
+ }
51
+
52
+ return render_template('profile.html',
53
+ reports=formatted_reports,
54
+ most_common_emotion=most_common_emotion,
55
+ total_reports=total_reports,
56
+ emotion_map=emotion_map,
57
+ datetime=datetime)
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=2.0.0
2
+ flask-login>=0.5.0
3
+ flask-sqlalchemy>=2.5.1
4
+ flask-wtf>=1.0.0
5
+ transformers>=4.15.0
6
+ torch>=1.9.0
7
+ torchaudio>=0.9.0
8
+ pydub>=0.25.1
9
+ librosa>=0.8.0
10
+ numpy>=1.21.0
11
+ gunicorn
12
+ openai-whisper
13
+ python-dotenv>=0.19.0
14
+ email-validator>=1.1.3
static/script.js ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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");
6
+ const userInput = document.getElementById("user-input");
7
+ const chatBox = document.getElementById("chat-box");
8
+ const audioFileInput = document.getElementById("audio-file");
9
+ const newChatBtn = document.getElementById("new-chat-btn");
10
+ const chatList = document.getElementById("chat-list");
11
+ const currentChatTitle = document.getElementById("current-chat-title");
12
+ const fileInfo = document.getElementById("file-info");
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");
50
+ chatItem.className = "chat-item";
51
+ chatItem.dataset.chatId = chat.chat_id;
52
+ chatItem.innerHTML = `
53
+ <div class="chat-item-main">
54
+ <i class="fas fa-comment chat-icon"></i>
55
+ <div class="chat-item-content">
56
+ <span class="chat-title">${chat.title}</span>
57
+ <span class="chat-date">${formatDate(chat.created_at)}</span>
58
+ </div>
59
+ </div>
60
+ <button class="delete-chat-btn" title="Удалить чат">
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);
75
+ });
76
+
77
+ chatList.appendChild(chatItem);
78
+ });
79
+ }
80
+
81
+ function formatDate(dateString) {
82
+ if (!dateString) return '';
83
+ const date = new Date(dateString);
84
+ return date.toLocaleDateString('ru-RU');
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',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
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
+ }
111
+ } catch (error) {
112
+ console.error('Delete chat error:', error);
113
+ appendMessage('bot', `❌ Ошибка при удалении: ${error.message}`);
114
+ }
115
+ }
116
+
117
+ newChatBtn.addEventListener("click", startNewChat);
118
+
119
+ function startNewChat() {
120
+ fetch("/start_chat", {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ })
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);
131
+ })
132
+ .catch(console.error);
133
+ }
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) {
159
+ document.querySelectorAll(".chat-item").forEach(item => {
160
+ item.classList.toggle("active", item.dataset.chatId === chatId);
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",
180
+ headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify({ text, chat_id: currentChatId })
182
+ });
183
+ const data = await response.json();
184
+ appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`);
185
+ } catch (error) {
186
+ console.error("Ошибка:", error);
187
+ appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`);
188
+ }
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';
200
+ sendAudioFile(file);
201
+ }
202
+ }
203
+
204
+ function clearAudioFile() {
205
+ audioFileInput.value = '';
206
+ fileInfo.style.display = 'none';
207
+ }
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
+ }
228
+ appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`);
229
+ clearAudioFile();
230
+ } catch (error) {
231
+ console.error("Ошибка:", error);
232
+ appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`);
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);
258
+ appendMessage("bot", "❌ Не удалось получить доступ к микрофону");
259
+ }
260
+ }
261
+
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
+ }
290
+ appendAndSaveMessage("bot", `Эмоция: ${data.emotion} (${(data.confidence * 100).toFixed(1)}%)`);
291
+ } catch (error) {
292
+ console.error("Ошибка:", error);
293
+ appendAndSaveMessage("bot", `❌ Ошибка: ${error.message}`);
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",
312
+ headers: {
313
+ "Content-Type": "application/json",
314
+ "X-CSRFToken": getCSRFToken()
315
+ },
316
+ body: JSON.stringify({
317
+ chat_id: currentChatId,
318
+ sender: sender,
319
+ content: text
320
+ })
321
+ }).catch(console.error);
322
+ }
323
+ }
324
+
325
+ function getCSRFToken() {
326
+ const meta = document.querySelector('meta[name="csrf-token"]');
327
+ return meta ? meta.content : '';
328
+ }
329
+ });
static/styles.css ADDED
@@ -0,0 +1,836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Основные переменные */
2
+ :root {
3
+ --primary-color: #4a4ae8;
4
+ --primary-dark: #3b3b98;
5
+ --secondary-color: #6c5ce7;
6
+ --dark-bg: #1a1a2e;
7
+ --darker-bg: #16213e;
8
+ --dark-text: #e2e2e2;
9
+ --light-text: #ffffff;
10
+ --success-color: #00b894;
11
+ --danger-color: #d63031;
12
+ --warning-color: #fdcb6e;
13
+ --info-color: #0984e3;
14
+ --border-radius: 12px;
15
+ --box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
16
+ }
17
+
18
+ /* Базовые стили */
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
27
+ background-color: var(--darker-bg);
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
+ /* Боковая панель */
39
+ .app-sidebar {
40
+ width: 280px;
41
+ background: var(--dark-bg);
42
+ display: flex;
43
+ flex-direction: column;
44
+ padding: 20px 0;
45
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
46
+ }
47
+
48
+ .sidebar-header {
49
+ padding: 0 20px 20px;
50
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
51
+ }
52
+
53
+ .sidebar-header h2 {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 10px;
57
+ font-size: 1.3rem;
58
+ color: var(--light-text);
59
+ }
60
+
61
+ .sidebar-nav {
62
+ flex: 1;
63
+ padding: 20px 0;
64
+ }
65
+
66
+ .nav-item {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 12px;
70
+ padding: 12px 20px;
71
+ color: var(--light-text);
72
+ text-decoration: none;
73
+ transition: all 0.3s ease;
74
+ border-left: 3px solid transparent;
75
+ }
76
+
77
+ .nav-item:hover, .nav-item.active {
78
+ background: rgba(74, 74, 232, 0.1);
79
+ border-left: 3px solid var(--primary-color);
80
+ }
81
+
82
+ .nav-item i {
83
+ width: 20px;
84
+ text-align: center;
85
+ }
86
+
87
+ .sidebar-footer {
88
+ padding: 20px;
89
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
90
+ }
91
+
92
+ .user-info span{
93
+ color: white
94
+ }
95
+
96
+ .user-info {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 10px;
100
+ margin-bottom: 15px;
101
+ }
102
+
103
+ .user-info i {
104
+ font-size: 1.8rem;
105
+ color: var(--primary-color);
106
+ }
107
+
108
+ .logout-btn {
109
+ display: block;
110
+ width: 100%;
111
+ padding: 10px;
112
+ background: rgba(214, 48, 49, 0.1);
113
+ color: var(--danger-color);
114
+ border: none;
115
+ border-radius: var(--border-radius);
116
+ text-align: center;
117
+ cursor: pointer;
118
+ transition: all 0.3s ease;
119
+ }
120
+
121
+ .logout-btn:hover {
122
+ background: rgba(214, 48, 49, 0.2);
123
+ }
124
+
125
+ .auth-links {
126
+ display: flex;
127
+ gap: 10px;
128
+ }
129
+
130
+ .auth-link {
131
+ flex: 1;
132
+ padding: 10px;
133
+ text-align: center;
134
+ color: var(--light-text);
135
+ text-decoration: none;
136
+ border-radius: var(--border-radius);
137
+ transition: all 0.3s ease;
138
+ }
139
+
140
+ .auth-link:first-child {
141
+ background: rgba(74, 74, 232, 0.1);
142
+ }
143
+
144
+ .auth-link:last-child {
145
+ background: rgba(0, 184, 148, 0.1);
146
+ }
147
+
148
+ .auth-link:hover {
149
+ background: rgba(255, 255, 255, 0.1);
150
+ }
151
+
152
+ /* Основное содержимое */
153
+ .app-main {
154
+ flex: 1;
155
+ padding: 30px;
156
+ background: linear-gradient(135deg, #1e1e2f, #2a2a40);
157
+ }
158
+
159
+ .flash-messages {
160
+ position: fixed;
161
+ top: 20px;
162
+ right: 20px;
163
+ z-index: 1000;
164
+ max-width: 400px;
165
+ }
166
+
167
+ .flash-message {
168
+ padding: 15px 20px;
169
+ margin-bottom: 10px;
170
+ border-radius: var(--border-radius);
171
+ display: flex;
172
+ justify-content: space-between;
173
+ align-items: center;
174
+ animation: slideIn 0.3s ease-out;
175
+ box-shadow: var(--box-shadow);
176
+ }
177
+
178
+ .flash-success {
179
+ background: var(--success-color);
180
+ color: white;
181
+ }
182
+
183
+ .flash-error {
184
+ background: var(--danger-color);
185
+ color: white;
186
+ }
187
+
188
+ .flash-info {
189
+ background: var(--info-color);
190
+ color: white;
191
+ }
192
+
193
+ .flash-warning {
194
+ background: var(--warning-color);
195
+ color: #333;
196
+ }
197
+
198
+ .flash-close {
199
+ background: none;
200
+ border: none;
201
+ color: inherit;
202
+ cursor: pointer;
203
+ margin-left: 10px;
204
+ }
205
+
206
+ @keyframes slideIn {
207
+ from { transform: translateX(100%); opacity: 0; }
208
+ to { transform: translateX(0); opacity: 1; }
209
+ }
210
+
211
+ /* Стили для чата */
212
+ .chat-container {
213
+ display: flex;
214
+ height: calc(100vh - 60px);
215
+ background: rgba(255, 255, 255, 0.05);
216
+ border-radius: var(--border-radius);
217
+ overflow: hidden;
218
+ box-shadow: var(--box-shadow);
219
+ }
220
+
221
+ .chat-sidebar {
222
+ width: 300px;
223
+ background: var(--dark-bg);
224
+ padding: 20px;
225
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
226
+ display: flex;
227
+ flex-direction: column;
228
+ }
229
+
230
+ .new-chat-btn {
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ gap: 8px;
235
+ width: 100%;
236
+ padding: 12px;
237
+ margin-bottom: 20px;
238
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
239
+ color: white;
240
+ border: none;
241
+ border-radius: var(--border-radius);
242
+ cursor: pointer;
243
+ font-size: 0.95rem;
244
+ transition: all 0.3s ease;
245
+ }
246
+
247
+ .new-chat-btn:hover {
248
+ transform: translateY(-2px);
249
+ box-shadow: 0 4px 12px rgba(74, 74, 232, 0.3);
250
+ }
251
+
252
+ .chat-list {
253
+ flex: 1;
254
+ overflow-y: auto;
255
+ padding-right: 5px;
256
+ }
257
+
258
+ .chat-item {
259
+ padding: 12px;
260
+ margin-bottom: 8px;
261
+ background: rgba(255, 255, 255, 0.05);
262
+ border-radius: var(--border-radius);
263
+ cursor: pointer;
264
+ transition: all 0.3s ease;
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 12px;
268
+ }
269
+
270
+ .chat-item:hover {
271
+ background: rgba(255, 255, 255, 0.1);
272
+ }
273
+
274
+ .chat-item.active {
275
+ background: rgba(74, 74, 232, 0.2);
276
+ border-left: 3px solid var(--primary-color);
277
+ }
278
+
279
+ .chat-item i {
280
+ color: var(--primary-color);
281
+ font-size: 1.1rem;
282
+ }
283
+
284
+ .chat-item-content {
285
+ flex: 1;
286
+ overflow: hidden;
287
+ }
288
+
289
+ .chat-title {
290
+ display: block;
291
+ font-weight: 500;
292
+ white-space: nowrap;
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+ color: white;
296
+ }
297
+
298
+ .chat-date {
299
+ display: block;
300
+ font-size: 0.75rem;
301
+ color: rgba(255, 255, 255, 0.6);
302
+ }
303
+
304
+ .chat-main {
305
+ flex: 1;
306
+ display: flex;
307
+ flex-direction: column;
308
+ }
309
+
310
+ .chat-header {
311
+ padding: 20px;
312
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
313
+ }
314
+
315
+ .chat-header h3 {
316
+ font-size: 1.3rem;
317
+ color: var(--light-text);
318
+ }
319
+
320
+ .chat-box {
321
+ flex: 1;
322
+ padding: 20px;
323
+ overflow-y: auto;
324
+ background: rgba(0, 0, 0, 0.2);
325
+ }
326
+
327
+ .welcome-message {
328
+ text-align: center;
329
+ padding: 40px 20px;
330
+ max-width: 500px;
331
+ margin: 0 auto;
332
+ }
333
+
334
+ .welcome-icon {
335
+ width: 80px;
336
+ height: 80px;
337
+ margin: 0 auto 20px;
338
+ background: rgba(74, 74, 232, 0.2);
339
+ border-radius: 50%;
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ font-size: 2.5rem;
344
+ color: var(--primary-color);
345
+ }
346
+
347
+ .welcome-message h4 {
348
+ margin-bottom: 10px;
349
+ font-size: 1.5rem;
350
+ }
351
+
352
+ .welcome-message p {
353
+ color: rgba(255, 255, 255, 0.7);
354
+ margin-bottom: 20px;
355
+ }
356
+
357
+ .message {
358
+ max-width: 70%;
359
+ padding: 12px 16px;
360
+ margin-bottom: 12px;
361
+ border-radius: var(--border-radius);
362
+ position: relative;
363
+ animation: fadeIn 0.3s ease-out;
364
+ }
365
+
366
+ .user-message {
367
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
368
+ color: white;
369
+ margin-left: auto;
370
+ border-bottom-right-radius: 0;
371
+ }
372
+
373
+ .bot-message {
374
+ background: rgba(255, 255, 255, 0.1);
375
+ color: var(--light-text);
376
+ margin-right: auto;
377
+ border-bottom-left-radius: 0;
378
+ }
379
+
380
+ @keyframes fadeIn {
381
+ from { opacity: 0; transform: translateY(10px); }
382
+ to { opacity: 1; transform: translateY(0); }
383
+ }
384
+
385
+ .chat-controls {
386
+ padding: 20px;
387
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
388
+ }
389
+
390
+ .input-group {
391
+ display: flex;
392
+ gap: 10px;
393
+ margin-bottom: 15px;
394
+ }
395
+
396
+ .chat-input {
397
+ flex: 1;
398
+ padding: 12px 16px;
399
+ background: rgba(255, 255, 255, 0.1);
400
+ border: 1px solid rgba(255, 255, 255, 0.1);
401
+ border-radius: var(--border-radius);
402
+ color: var(--light-text);
403
+ font-size: 0.95rem;
404
+ transition: all 0.3s ease;
405
+ }
406
+
407
+ .chat-input:focus {
408
+ outline: none;
409
+ border-color: var(--primary-color);
410
+ background: rgba(255, 255, 255, 0.15);
411
+ }
412
+
413
+ .send-btn {
414
+ width: 50px;
415
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
416
+ color: white;
417
+ border: none;
418
+ border-radius: var(--border-radius);
419
+ cursor: pointer;
420
+ transition: all 0.3s ease;
421
+ }
422
+
423
+ .send-btn:hover {
424
+ transform: translateY(-2px);
425
+ box-shadow: 0 4px 12px rgba(74, 74, 232, 0.3);
426
+ }
427
+
428
+ .audio-controls {
429
+ display: flex;
430
+ gap: 15px;
431
+ }
432
+
433
+ .file-upload {
434
+ flex: 1;
435
+ position: relative;
436
+ }
437
+
438
+ .file-upload-btn {
439
+ display: block;
440
+ width: 100%;
441
+ padding: 12px;
442
+ background: rgba(255, 255, 255, 0.1);
443
+ color: var(--light-text);
444
+ border: 1px dashed rgba(255, 255, 255, 0.3);
445
+ border-radius: var(--border-radius);
446
+ text-align: center;
447
+ cursor: pointer;
448
+ transition: all 0.3s ease;
449
+ }
450
+
451
+ .file-upload-btn:hover {
452
+ background: rgba(255, 255, 255, 0.15);
453
+ border-color: var(--primary-color);
454
+ }
455
+
456
+ .file-upload-btn i {
457
+ margin-right: 8px;
458
+ }
459
+
460
+ input[type="file"] {
461
+ display: none;
462
+ }
463
+
464
+ .file-info {
465
+ display: none;
466
+ align-items: center;
467
+ justify-content: space-between;
468
+ padding: 10px 15px;
469
+ margin-top: 8px;
470
+ background: rgba(0, 184, 148, 0.1);
471
+ border-radius: var(--border-radius);
472
+ color: var(--success-color);
473
+ }
474
+
475
+ .clear-file-btn {
476
+ background: none;
477
+ border: none;
478
+ color: var(--success-color);
479
+ cursor: pointer;
480
+ }
481
+
482
+ .record-controls {
483
+ display: flex;
484
+ gap: 10px;
485
+ }
486
+
487
+ .record-btn, .stop-btn {
488
+ width: 50px;
489
+ height: 50px;
490
+ border-radius: 50%;
491
+ display: flex;
492
+ align-items: center;
493
+ justify-content: center;
494
+ border: none;
495
+ cursor: pointer;
496
+ transition: all 0.3s ease;
497
+ }
498
+
499
+ .record-btn {
500
+ background: linear-gradient(135deg, var(--danger-color), #c0392b);
501
+ color: white;
502
+ }
503
+
504
+ .stop-btn {
505
+ background: rgba(255, 255, 255, 0.1);
506
+ color: var(--light-text);
507
+ }
508
+
509
+ .record-btn:hover {
510
+ transform: scale(1.05);
511
+ box-shadow: 0 4px 12px rgba(214, 48, 49, 0.3);
512
+ }
513
+
514
+ /* Стили для профиля */
515
+ .profile-container {
516
+ max-width: 1200px;
517
+ margin: 0 auto;
518
+ padding: 30px;
519
+ }
520
+
521
+ .profile-header {
522
+ display: flex;
523
+ flex-direction: column;
524
+ gap: 30px;
525
+ margin-bottom: 40px;
526
+ }
527
+
528
+ .user-info {
529
+ display: flex;
530
+ align-items: center;
531
+ gap: 20px;
532
+ }
533
+
534
+ .user-avatar {
535
+ width: 80px;
536
+ height: 80px;
537
+ background: rgba(74, 74, 232, 0.2);
538
+ border-radius: 50%;
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ font-size: 2.5rem;
543
+ color: var(--primary-color);
544
+ }
545
+
546
+ .user-details h2 {
547
+ font-size: 1.8rem;
548
+ margin-bottom: 5px;
549
+ color: white;
550
+ }
551
+
552
+ .user-details p {
553
+ color: rgba(255, 255, 255, 0.7);
554
+ font-size: 0.95rem;
555
+ }
556
+
557
+ .stats-cards {
558
+ display: grid;
559
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
560
+ gap: 20px;
561
+ }
562
+
563
+ .stat-card {
564
+ background: rgba(255, 255, 255, 0.05);
565
+ border-radius: var(--border-radius);
566
+ padding: 20px;
567
+ display: flex;
568
+ align-items: center;
569
+ gap: 15px;
570
+ transition: all 0.3s ease;
571
+ }
572
+
573
+ .stat-card:hover {
574
+ transform: translateY(-5px);
575
+ box-shadow: var(--box-shadow);
576
+ }
577
+
578
+ .stat-icon {
579
+ width: 50px;
580
+ height: 50px;
581
+ background: rgba(74, 74, 232, 0.2);
582
+ border-radius: 50%;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ font-size: 1.5rem;
587
+ color: var(--primary-color);
588
+ }
589
+
590
+ .stat-content h3 {
591
+ font-size: 0.95rem;
592
+ color: rgba(255, 255, 255, 0.7);
593
+ margin-bottom: 5px;
594
+
595
+ }
596
+
597
+ .stat-content p {
598
+ color: white;
599
+ }
600
+
601
+ .stat-value {
602
+ font-size: 1.5rem;
603
+ font-weight: 600;
604
+ }
605
+
606
+ .section-title {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 10px;
610
+ margin-bottom: 20px;
611
+ font-size: 1.3rem;
612
+ color: var(--light-text);
613
+ }
614
+
615
+ .section-title i {
616
+ color: var(--primary-color);
617
+ }
618
+
619
+ .reports-list {
620
+ display: grid;
621
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
622
+ gap: 20px;
623
+ }
624
+
625
+ .report-card {
626
+ background: rgba(255, 255, 255, 0.05);
627
+ border-radius: var(--border-radius);
628
+ overflow: hidden;
629
+ transition: all 0.3s ease;
630
+ }
631
+
632
+ .report-card:hover {
633
+ transform: translateY(-5px);
634
+ box-shadow: var(--box-shadow);
635
+ }
636
+
637
+ .report-header {
638
+ padding: 15px;
639
+ display: flex;
640
+ justify-content: space-between;
641
+ align-items: center;
642
+ background: rgba(0, 0, 0, 0.2);
643
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
644
+ }
645
+
646
+ .report-date {
647
+ font-size: 0.8rem;
648
+ color: rgba(255, 255, 255, 0.6);
649
+ }
650
+
651
+ .report-emotion {
652
+ padding: 5px 10px;
653
+ border-radius: 20px;
654
+ font-size: 0.8rem;
655
+ font-weight: 500;
656
+ }
657
+
658
+ .report-emotion.joy, .report-emotion.happy {
659
+ background: rgba(0, 184, 148, 0.2);
660
+ color: var(--success-color);
661
+ }
662
+
663
+ .report-emotion.anger, .report-emotion.angry, .report-emotion.sadness, .report-emotion.sad {
664
+ background: rgba(214, 48, 49, 0.2);
665
+ color: var(--danger-color);
666
+ }
667
+
668
+ .report-emotion.neutral {
669
+ background: rgba(253, 203, 110, 0.2);
670
+ color: var(--warning-color);
671
+ }
672
+
673
+ .report-content {
674
+ padding: 15px;
675
+ }
676
+
677
+ .report-content p {
678
+ font-size: 0.95rem;
679
+ line-height: 1.6;
680
+ color: rgba(255, 255, 255, 0.8);
681
+ }
682
+
683
+ .report-footer {
684
+ padding: 15px;
685
+ display: flex;
686
+ align-items: center;
687
+ gap: 10px;
688
+ background: rgba(0, 0, 0, 0.1);
689
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
690
+ }
691
+
692
+ .confidence-meter {
693
+ flex: 1;
694
+ height: 6px;
695
+ background: rgba(255, 255, 255, 0.1);
696
+ border-radius: 3px;
697
+ overflow: hidden;
698
+ }
699
+
700
+ .confidence-fill {
701
+ height: 100%;
702
+ background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
703
+ border-radius: 3px;
704
+ }
705
+
706
+ .confidence-value {
707
+ font-size: 0.8rem;
708
+ font-weight: 500;
709
+ color: var(--primary-color);
710
+ }
711
+
712
+ .empty-state {
713
+ text-align: center;
714
+ padding: 60px 20px;
715
+ background: rgba(255, 255, 255, 0.05);
716
+ border-radius: var(--border-radius);
717
+ }
718
+
719
+ .empty-state i {
720
+ font-size: 3rem;
721
+ color: var(--primary-color);
722
+ margin-bottom: 20px;
723
+ }
724
+
725
+ .empty-state h4 {
726
+ font-size: 1.3rem;
727
+ margin-bottom: 10px;
728
+ }
729
+
730
+ .empty-state p {
731
+ color: rgba(255, 255, 255, 0.6);
732
+ margin-bottom: 20px;
733
+ }
734
+
735
+ .btn-primary {
736
+ display: inline-flex;
737
+ align-items: center;
738
+ gap: 8px;
739
+ padding: 12px 20px;
740
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
741
+ color: white;
742
+ border: none;
743
+ border-radius: var(--border-radius);
744
+ text-decoration: none;
745
+ font-weight: 500;
746
+ cursor: pointer;
747
+ transition: all 0.3s ease;
748
+ }
749
+
750
+ .btn-primary:hover {
751
+ transform: translateY(-2px);
752
+ box-shadow: 0 4px 12px rgba(74, 74, 232, 0.3);
753
+ }
754
+
755
+ /* Адаптивность */
756
+ @media (max-width: 992px) {
757
+ .app-sidebar {
758
+ width: 80px;
759
+ padding: 15px 0;
760
+ }
761
+
762
+ .sidebar-header h2 span,
763
+ .nav-item span,
764
+ .user-info span,
765
+ .auth-links {
766
+ display: none;
767
+ }
768
+
769
+ .sidebar-header h2 {
770
+ justify-content: center;
771
+ }
772
+
773
+ .nav-item {
774
+ justify-content: center;
775
+ padding: 15px 0;
776
+ }
777
+
778
+ .logout-btn {
779
+ padding: 15px 0;
780
+ border-radius: 0;
781
+ }
782
+
783
+ .chat-sidebar {
784
+ width: 250px;
785
+ }
786
+ }
787
+
788
+ @media (max-width: 768px) {
789
+ .chat-container {
790
+ flex-direction: column;
791
+ height: auto;
792
+ }
793
+
794
+ .chat-sidebar {
795
+ width: 100%;
796
+ border-right: none;
797
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
798
+ }
799
+
800
+ .stats-cards {
801
+ grid-template-columns: 1fr;
802
+ }
803
+ }
804
+
805
+ @media (max-width: 576px) {
806
+ .app-main {
807
+ padding: 15px;
808
+ }
809
+
810
+ .audio-controls {
811
+ flex-direction: column;
812
+ }
813
+
814
+ .record-controls {
815
+ justify-content: center;
816
+ }
817
+ }
818
+
819
+ .delete-chat-btn {
820
+ background: none;
821
+ border: none;
822
+ color: #999;
823
+ cursor: pointer;
824
+ padding: 5px;
825
+ margin-left: auto;
826
+ opacity: 0;
827
+ transition: opacity 0.2s;
828
+ }
829
+
830
+ .chat-item:hover .delete-chat-btn {
831
+ opacity: 1;
832
+ }
833
+
834
+ .delete-chat-btn:hover {
835
+ color: #ff4444;
836
+ }
templates/auth/login.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Вход{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row justify-content-center">
7
+ <div class="col-md-6">
8
+ <div class="card">
9
+ <div class="card-header">
10
+ <h4 class="mb-0">Вход в систему</h4>
11
+ </div>
12
+ <div class="card-body">
13
+ <form method="POST" action="{{ url_for('auth_bp.login') }}">
14
+ {{ form.hidden_tag() }}
15
+ <div class="mb-3">
16
+ {{ form.email.label(class="form-label") }}
17
+ {{ form.email(class="form-control") }}
18
+ {% for error in form.email.errors %}
19
+ <div class="text-danger">{{ error }}</div>
20
+ {% endfor %}
21
+ </div>
22
+ <div class="mb-3">
23
+ {{ form.password.label(class="form-label") }}
24
+ {{ form.password(class="form-control") }}
25
+ {% for error in form.password.errors %}
26
+ <div class="text-danger">{{ error }}</div>
27
+ {% endfor %}
28
+ </div>
29
+ <div class="d-grid gap-2">
30
+ {{ form.submit(class="btn btn-primary") }}
31
+ </div>
32
+ </form>
33
+ <div class="mt-3 text-center">
34
+ Нет аккаунта? <a href="{{ url_for('auth_bp.register') }}">Зарегистрируйтесь</a>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ {% endblock %}
templates/auth/register.html ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Регистрация{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row justify-content-center">
7
+ <div class="col-md-6">
8
+ <div class="card">
9
+ <div class="card-header">
10
+ <h4 class="mb-0">Регистрация</h4>
11
+ </div>
12
+ <div class="card-body">
13
+ <form method="POST" action="{{ url_for('.register') }}">
14
+ {{ form.hidden_tag() }}
15
+ <div class="mb-3">
16
+ {{ form.username.label(class="form-label") }}
17
+ {{ form.username(class="form-control") }}
18
+ {% for error in form.username.errors %}
19
+ <div class="text-danger">{{ error }}</div>
20
+ {% endfor %}
21
+ </div>
22
+ <div class="mb-3">
23
+ {{ form.email.label(class="form-label") }}
24
+ {{ form.email(class="form-control") }}
25
+ {% for error in form.email.errors %}
26
+ <div class="text-danger">{{ error }}</div>
27
+ {% endfor %}
28
+ </div>
29
+ <div class="mb-3">
30
+ {{ form.password.label(class="form-label") }}
31
+ {{ form.password(class="form-control") }}
32
+ {% for error in form.password.errors %}
33
+ <div class="text-danger">{{ error }}</div>
34
+ {% endfor %}
35
+ </div>
36
+ <div class="mb-3">
37
+ {{ form.confirm_password.label(class="form-label") }}
38
+ {{ form.confirm_password(class="form-control") }}
39
+ {% for error in form.confirm_password.errors %}
40
+ <div class="text-danger">{{ error }}</div>
41
+ {% endfor %}
42
+ </div>
43
+ <div class="d-grid gap-2">
44
+ {{ form.submit(class="btn btn-success") }}
45
+ </div>
46
+ </form>
47
+ <div class="mt-3 text-center">
48
+ Уже есть аккаунт? <a href="{{ url_for('auth_bp.login') }}">Войдите</a>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Emotion Analyzer{% endblock %}</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div class="app-container">
13
+ <!-- Боковая панель -->
14
+ <div class="app-sidebar">
15
+ <div class="sidebar-header">
16
+ <h2><i class="fas fa-brain"></i> EmotionAnalyzer</h2>
17
+ </div>
18
+
19
+ <div class="sidebar-nav">
20
+ <a href="{{ url_for('index') }}" class="nav-item">
21
+ <i class="fas fa-home"></i> Главная
22
+ </a>
23
+ {% if current_user.is_authenticated %}
24
+ <a href="{{ url_for('profile.profile') }}" class="nav-item">
25
+ <i class="fas fa-user"></i> Профиль
26
+ </a>
27
+ {% endif %}
28
+ </div>
29
+
30
+ <div class="sidebar-footer">
31
+ {% if current_user.is_authenticated %}
32
+ <div class="user-info">
33
+ <i class="fas fa-user-circle"></i>
34
+ <span>{{ current_user.username }}</span>
35
+ </div>
36
+ <a href="{{ url_for('auth_bp.logout') }}" class="logout-btn">
37
+ <i class="fas fa-sign-out-alt"></i> Выйти
38
+ </a>
39
+ {% else %}
40
+ <div class="auth-links">
41
+ <a href="{{ url_for('auth_bp.login') }}" class="auth-link">Войти</a>
42
+ <a href="{{ url_for('auth_bp.register') }}" class="auth-link">Регистрация</a>
43
+ </div>
44
+ {% endif %}
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Основное содержимое -->
49
+ <div class="app-main">
50
+ {% with messages = get_flashed_messages(with_categories=true) %}
51
+ {% if messages %}
52
+ <div class="flash-messages">
53
+ {% for category, message in messages %}
54
+ <div class="flash-message flash-{{ category }}">
55
+ {{ message }}
56
+ <button class="flash-close"><i class="fas fa-times"></i></button>
57
+ </div>
58
+ {% endfor %}
59
+ </div>
60
+ {% endif %}
61
+ {% endwith %}
62
+
63
+ {% block content %}{% endblock %}
64
+ </div>
65
+ </div>
66
+
67
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
68
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
69
+ {% block scripts %}{% endblock %}
70
+ </body>
71
+ </html>
templates/errors/401.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Title</title>
6
+ </head>
7
+ <body>
8
+
9
+ </body>
10
+ </html>
templates/errors/404.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Title</title>
6
+ </head>
7
+ <body>
8
+
9
+ </body>
10
+ </html>
templates/index.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="chat-container">
5
+ <div class="chat-sidebar">
6
+ <button id="new-chat-btn" class="new-chat-btn">
7
+ <i class="fas fa-plus"></i> Новый чат
8
+ </button>
9
+ <div class="chat-list" id="chat-list">
10
+ {% for chat in chats %}
11
+ <div class="chat-item" data-chat-id="{{ chat['chat_id'] }}">
12
+ <div class="chat-item-main">
13
+ <i class="fas fa-comment chat-icon"></i>
14
+ <div class="chat-item-content">
15
+ <span class="chat-title">{{ chat['title'] }}</span>
16
+ <span class="chat-date">{{ chat['created_at'].strftime('%d.%m.%Y') if chat['created_at'] else '' }}</span>
17
+ </div>
18
+ </div>
19
+ <button class="delete-chat-btn" title="Удалить чат">
20
+ <i class="fas fa-trash"></i>
21
+ </button>
22
+ </div>
23
+ {% endfor %}
24
+ </div>
25
+ </div>
26
+
27
+ <div class="chat-main">
28
+ <div class="chat-header">
29
+ <h3 id="current-chat-title">Анализатор эмоций</h3>
30
+ </div>
31
+
32
+ <div id="chat-box" class="chat-box">
33
+ <div class="welcome-message">
34
+ <div class="welcome-icon">
35
+ <i class="fas fa-robot"></i>
36
+ </div>
37
+ <h4>Привет! Я помогу проанализировать эмоции</h4>
38
+ <p>Отправьте текст или голосовое сообщение для анализа</p>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="chat-controls">
43
+ <div class="input-group">
44
+ <input type="text" id="user-input" class="chat-input" placeholder="Введите сообщение...">
45
+ <button id="send-btn" class="send-btn">
46
+ <i class="fas fa-paper-plane"></i>
47
+ </button>
48
+ </div>
49
+
50
+ <div class="audio-controls">
51
+ <div class="file-upload">
52
+ <label for="audio-file" id="upload-btn" class="file-upload-btn">
53
+ <i class="fas fa-file-audio"></i> Загрузить аудио
54
+ </label>
55
+ <input type="file" id="audio-file" accept="audio/*">
56
+ <div id="file-info" class="file-info">
57
+ <span id="file-name"></span>
58
+ <button id="clear-file" class="clear-file-btn">
59
+ <i class="fas fa-times"></i>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="record-controls">
65
+ <button id="record-btn" class="record-btn">
66
+ <i class="fas fa-microphone"></i>
67
+ </button>
68
+ <button id="stop-btn" class="stop-btn" disabled>
69
+ <i class="fas fa-stop"></i>
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ {% endblock %}
templates/profile.html ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Личный кабинет{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="profile-container">
7
+ <div class="profile-header">
8
+ <div class="user-info">
9
+ <div class="user-avatar">
10
+ <i class="fas fa-user-circle"></i>
11
+ </div>
12
+ <div class="user-details">
13
+ <h2>{{ current_user.username }}</h2>
14
+ <p>{{ current_user.email }}</p>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="stats-cards">
19
+ <div class="stat-card">
20
+ <div class="stat-icon">
21
+ <i class="fas fa-chart-bar"></i>
22
+ </div>
23
+ <div class="stat-content">
24
+ <h3>Всего анализов</h3>
25
+ <p class="stat-value">{{ total_reports }}</p>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="stat-card">
30
+ <div class="stat-icon">
31
+ <i class="fas fa-smile"></i>
32
+ </div>
33
+ <div class="stat-content">
34
+ <h3>Преобладающая эмоция</h3>
35
+ <p class="stat-value">{{ emotion_map.get(most_common_emotion, most_common_emotion) }}</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="reports-section">
42
+ <h3 class="section-title">
43
+ <i class="fas fa-history"></i> История анализов
44
+ </h3>
45
+
46
+ {% if reports %}
47
+ <div class="reports-list">
48
+ {% for report in reports %}
49
+ <div class="report-card">
50
+ <div class="report-header">
51
+ <span class="report-date">
52
+ {% if report['created_at'] %}
53
+ {{ datetime.strptime(report['created_at'], '%Y-%m-%d %H:%M:%S').strftime('%d.%m.%Y %H:%M') }}
54
+ {% else %}
55
+ Дата не указана
56
+ {% endif %}
57
+ </span>
58
+ <span class="report-emotion {{ report['emotion'] }}">
59
+ {{ emotion_map.get(report['emotion'], report['emotion']) }}
60
+ </span>
61
+ </div>
62
+ <div class="report-content">
63
+ <p>{{ report['content'][:200] }}{% if report['content']|length > 200 %}...{% endif %}</p>
64
+ </div>
65
+ <div class="report-footer">
66
+ <div class="confidence-meter">
67
+ <div class="confidence-fill" style="width: {{ (report['confidence'] * 100)|round(1) }}%"></div>
68
+ </div>
69
+ <span class="confidence-value">{{ (report['confidence'] * 100)|round(1) }}%</span>
70
+ </div>
71
+ </div>
72
+ {% endfor %}
73
+ </div>
74
+
75
+
76
+ {% else %}
77
+ <div class="empty-state">
78
+ <i class="fas fa-comment-slash"></i>
79
+ <h4>У вас пока нет анализов</h4>
80
+ <p>Начните использовать систему на главной странице</p>
81
+ <a href="{{ url_for('index') }}" class="btn-primary">
82
+ <i class="fas fa-arrow-right"></i> Перейти к анализу
83
+ </a>
84
+ </div>
85
+ {% endif %}
86
+ </div>
87
+ </div>
88
+ {% endblock %}