Spaces:
Runtime error
Runtime error
Upload 25 files
Browse files- admin.py +1 -1
- app.py +91 -28
- forms.py +58 -10
- models.py +6 -1
- profile.py +16 -12
- requirements.txt +0 -1
- templates/admin/base.html +64 -0
- templates/admin/dashboard.html +122 -0
- templates/admin/reports.html +91 -0
- templates/admin/users.html +163 -0
- templates/auth/login.html +34 -30
- templates/auth/register.html +50 -44
- templates/base.html +37 -14
- templates/base_public.html +118 -0
- templates/edit_profile.html +39 -0
- templates/profile.html +263 -27
- templates/welcome.html +97 -0
admin.py
CHANGED
@@ -83,7 +83,7 @@ def view_reports():
|
|
83 |
if emotion_filter:
|
84 |
query = query.filter(AnalysisReport.emotion == emotion_filter)
|
85 |
|
86 |
-
reports = query.paginate(page=page, per_page=
|
87 |
|
88 |
# Получаем список всех эмоций для фильтра
|
89 |
emotions = db.session.query(
|
|
|
83 |
if emotion_filter:
|
84 |
query = query.filter(AnalysisReport.emotion == emotion_filter)
|
85 |
|
86 |
+
reports = query.paginate(page=page, per_page=20, error_out=False)
|
87 |
|
88 |
# Получаем список всех эмоций для фильтра
|
89 |
emotions = db.session.query(
|
app.py
CHANGED
@@ -1,7 +1,8 @@
|
|
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
|
@@ -16,6 +17,8 @@ import json
|
|
16 |
from admin import admin_bp
|
17 |
from flask_migrate import Migrate
|
18 |
from models import User
|
|
|
|
|
19 |
|
20 |
instance_path = Path(__file__).parent / 'instance'
|
21 |
instance_path.mkdir(exist_ok=True, mode=0o755)
|
@@ -29,8 +32,10 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
29 |
# Инициализация Flask-Login
|
30 |
db.init_app(app)
|
31 |
login_manager.init_app(app)
|
32 |
-
login_manager.login_view = '
|
33 |
migrate = Migrate(app, db)
|
|
|
|
|
34 |
# Инициализация моделей
|
35 |
def init_models():
|
36 |
try:
|
@@ -62,6 +67,7 @@ def init_models():
|
|
62 |
print(f"Ошибка загрузки моделей: {e}")
|
63 |
return None
|
64 |
|
|
|
65 |
models = init_models()
|
66 |
if not models:
|
67 |
raise RuntimeError("Не удалось загрузить модели")
|
@@ -73,6 +79,7 @@ def datetimeformat(value, format='%d.%m.%Y %H:%M'):
|
|
73 |
return ""
|
74 |
return value.strftime(format)
|
75 |
|
|
|
76 |
@app.context_processor
|
77 |
def utility_processor():
|
78 |
return {
|
@@ -91,6 +98,8 @@ def utility_processor():
|
|
91 |
'surprise': '#fdcb6e'
|
92 |
}.get(emotion, '#4a4ae8')
|
93 |
}
|
|
|
|
|
94 |
# Импорт Blueprint
|
95 |
from auth import auth_bp
|
96 |
from profile import profile_bp
|
@@ -105,6 +114,7 @@ speech_to_text_model = models['speech_to_text_model']
|
|
105 |
text_classifier = models['text_classifier']
|
106 |
audio_classifier = models['audio_classifier']
|
107 |
|
|
|
108 |
@app.cli.command('create-admin')
|
109 |
def create_admin():
|
110 |
"""Создание администратора"""
|
@@ -121,6 +131,7 @@ def create_admin():
|
|
121 |
db.session.commit()
|
122 |
print(f"Администратор {email} создан")
|
123 |
|
|
|
124 |
def transcribe_audio(audio_path):
|
125 |
"""Преобразование аудио в текст с помощью Whisper"""
|
126 |
if not speech_to_text_model:
|
@@ -135,9 +146,9 @@ def transcribe_audio(audio_path):
|
|
135 |
|
136 |
# Инициализация Flask-Login
|
137 |
login_manager = LoginManager(app)
|
138 |
-
login_manager.login_view = 'login'
|
139 |
-
|
140 |
-
|
141 |
|
142 |
|
143 |
@login_manager.user_loader
|
@@ -224,9 +235,10 @@ def login():
|
|
224 |
user_obj = User(id=user['id'], username=user['username'],
|
225 |
email=user['email'], password_hash=user['password_hash'])
|
226 |
login_user(user_obj)
|
|
|
227 |
return redirect(url_for('index'))
|
228 |
-
|
229 |
-
|
230 |
|
231 |
return render_template('auth/login.html')
|
232 |
|
@@ -261,26 +273,71 @@ def register():
|
|
261 |
return render_template('auth/register.html')
|
262 |
|
263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
@app.route('/logout')
|
265 |
@login_required
|
266 |
def logout():
|
|
|
267 |
logout_user()
|
268 |
-
return redirect(url_for('
|
269 |
|
270 |
|
271 |
-
# Основные маршруты
|
272 |
@app.route("/")
|
273 |
-
@login_required
|
274 |
def index():
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
|
|
|
|
284 |
|
285 |
|
286 |
@app.route('/profile')
|
@@ -323,6 +380,7 @@ def profile():
|
|
323 |
finally:
|
324 |
conn.close()
|
325 |
|
|
|
326 |
@app.route("/analyze", methods=["POST"])
|
327 |
@login_required
|
328 |
def analyze_text():
|
@@ -423,9 +481,6 @@ def analyze_audio():
|
|
423 |
return jsonify({'error': str(e)}), 500
|
424 |
|
425 |
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
@app.route('/analyze_telegram_chat', methods=['POST'])
|
430 |
@login_required
|
431 |
def analyze_telegram_chat():
|
@@ -536,8 +591,19 @@ def delete_chat(chat_id):
|
|
536 |
try:
|
537 |
# Удаляем связанные сообщения
|
538 |
conn.execute("DELETE FROM messages WHERE chat_id = ?", (chat_id,))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
539 |
# Удаляем сам чат
|
540 |
-
conn.execute("DELETE FROM chats WHERE chat_id = ? AND user_id = ?",
|
|
|
541 |
conn.commit()
|
542 |
return jsonify({"success": True})
|
543 |
except Exception as e:
|
@@ -561,6 +627,7 @@ def get_telegram_analysis():
|
|
561 |
finally:
|
562 |
conn.close()
|
563 |
|
|
|
564 |
@app.route('/load_chat/<chat_id>')
|
565 |
@login_required
|
566 |
def load_chat(chat_id):
|
@@ -590,7 +657,6 @@ def load_chat(chat_id):
|
|
590 |
conn.close()
|
591 |
|
592 |
|
593 |
-
|
594 |
@app.route('/save_message', methods=['POST'])
|
595 |
@login_required
|
596 |
def save_message():
|
@@ -646,7 +712,4 @@ def save_message():
|
|
646 |
|
647 |
|
648 |
if __name__ == "__main__":
|
649 |
-
|
650 |
-
serve(app, host="0.0.0.0", port=8080)
|
651 |
-
|
652 |
-
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template, flash, redirect, url_for, current_app
|
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 |
+
from flask import session
|
6 |
import torch
|
7 |
from pydub import AudioSegment
|
8 |
import os
|
|
|
17 |
from admin import admin_bp
|
18 |
from flask_migrate import Migrate
|
19 |
from models import User
|
20 |
+
from werkzeug.utils import secure_filename
|
21 |
+
from forms import EditProfileForm
|
22 |
|
23 |
instance_path = Path(__file__).parent / 'instance'
|
24 |
instance_path.mkdir(exist_ok=True, mode=0o755)
|
|
|
32 |
# Инициализация Flask-Login
|
33 |
db.init_app(app)
|
34 |
login_manager.init_app(app)
|
35 |
+
login_manager.login_view = 'welcome'
|
36 |
migrate = Migrate(app, db)
|
37 |
+
|
38 |
+
|
39 |
# Инициализация моделей
|
40 |
def init_models():
|
41 |
try:
|
|
|
67 |
print(f"Ошибка загрузки моделей: {e}")
|
68 |
return None
|
69 |
|
70 |
+
|
71 |
models = init_models()
|
72 |
if not models:
|
73 |
raise RuntimeError("Не удалось загрузить модели")
|
|
|
79 |
return ""
|
80 |
return value.strftime(format)
|
81 |
|
82 |
+
|
83 |
@app.context_processor
|
84 |
def utility_processor():
|
85 |
return {
|
|
|
98 |
'surprise': '#fdcb6e'
|
99 |
}.get(emotion, '#4a4ae8')
|
100 |
}
|
101 |
+
|
102 |
+
|
103 |
# Импорт Blueprint
|
104 |
from auth import auth_bp
|
105 |
from profile import profile_bp
|
|
|
114 |
text_classifier = models['text_classifier']
|
115 |
audio_classifier = models['audio_classifier']
|
116 |
|
117 |
+
|
118 |
@app.cli.command('create-admin')
|
119 |
def create_admin():
|
120 |
"""Создание администратора"""
|
|
|
131 |
db.session.commit()
|
132 |
print(f"Администратор {email} создан")
|
133 |
|
134 |
+
|
135 |
def transcribe_audio(audio_path):
|
136 |
"""Преобразование аудио в текст с помощью Whisper"""
|
137 |
if not speech_to_text_model:
|
|
|
146 |
|
147 |
# Инициализация Flask-Login
|
148 |
login_manager = LoginManager(app)
|
149 |
+
login_manager.login_view = 'auth_bp.login'
|
150 |
+
login_manager.login_message = "Для доступа к этой странице необходимо авторизоваться"
|
151 |
+
login_manager.login_message_category = "info"
|
152 |
|
153 |
|
154 |
@login_manager.user_loader
|
|
|
235 |
user_obj = User(id=user['id'], username=user['username'],
|
236 |
email=user['email'], password_hash=user['password_hash'])
|
237 |
login_user(user_obj)
|
238 |
+
session.pop('_flashes', None)
|
239 |
return redirect(url_for('index'))
|
240 |
+
else:
|
241 |
+
flash('Неверный email или пароль', 'danger') # <-- теперь здесь
|
242 |
|
243 |
return render_template('auth/login.html')
|
244 |
|
|
|
273 |
return render_template('auth/register.html')
|
274 |
|
275 |
|
276 |
+
from werkzeug.security import check_password_hash, generate_password_hash
|
277 |
+
|
278 |
+
@app.route('/edit_profile', methods=['GET', 'POST'])
|
279 |
+
@login_required
|
280 |
+
def edit_profile():
|
281 |
+
form = EditProfileForm(obj=current_user)
|
282 |
+
if form.validate_on_submit():
|
283 |
+
# Обновляем имя и почту
|
284 |
+
current_user.username = form.username.data
|
285 |
+
current_user.email = form.email.data
|
286 |
+
|
287 |
+
# Обновляем аватар
|
288 |
+
if form.avatar.data:
|
289 |
+
filename = secure_filename(form.avatar.data.filename)
|
290 |
+
unique_filename = f"{uuid.uuid4().hex}_{filename}"
|
291 |
+
avatar_path = os.path.join(current_app.root_path, 'static/avatars', unique_filename)
|
292 |
+
form.avatar.data.save(avatar_path)
|
293 |
+
current_user.avatar = unique_filename
|
294 |
+
|
295 |
+
# Обработка смены пароля
|
296 |
+
if form.current_password.data:
|
297 |
+
# Проверяем текущий пароль
|
298 |
+
if check_password_hash(current_user.password_hash, form.current_password.data):
|
299 |
+
# Меняем пароль на новый
|
300 |
+
current_user.password_hash = generate_password_hash(form.new_password.data)
|
301 |
+
flash('Пароль успешно изменён', 'success')
|
302 |
+
else:
|
303 |
+
flash('Текущий пароль неверный', 'danger')
|
304 |
+
return redirect(url_for('edit_profile'))
|
305 |
+
|
306 |
+
db.session.commit()
|
307 |
+
flash('Профиль обновлён', 'success')
|
308 |
+
return redirect(url_for('profile'))
|
309 |
+
|
310 |
+
return render_template('edit_profile.html', form=form)
|
311 |
+
|
312 |
+
|
313 |
+
|
314 |
+
# Основные маршруты
|
315 |
+
@app.route("/welcome")
|
316 |
+
def welcome():
|
317 |
+
return render_template("welcome.html")
|
318 |
+
|
319 |
+
|
320 |
@app.route('/logout')
|
321 |
@login_required
|
322 |
def logout():
|
323 |
+
session.clear()
|
324 |
logout_user()
|
325 |
+
return redirect(url_for('welcome'))
|
326 |
|
327 |
|
|
|
328 |
@app.route("/")
|
|
|
329 |
def index():
|
330 |
+
if current_user.is_authenticated:
|
331 |
+
conn = get_db_connection()
|
332 |
+
try:
|
333 |
+
chats = conn.execute(
|
334 |
+
"SELECT chat_id, title FROM chats WHERE user_id = ? ORDER BY created_at DESC",
|
335 |
+
(current_user.id,)
|
336 |
+
).fetchall()
|
337 |
+
return render_template("index.html", chats=chats)
|
338 |
+
finally:
|
339 |
+
conn.close()
|
340 |
+
return redirect(url_for('welcome'))
|
341 |
|
342 |
|
343 |
@app.route('/profile')
|
|
|
380 |
finally:
|
381 |
conn.close()
|
382 |
|
383 |
+
|
384 |
@app.route("/analyze", methods=["POST"])
|
385 |
@login_required
|
386 |
def analyze_text():
|
|
|
481 |
return jsonify({'error': str(e)}), 500
|
482 |
|
483 |
|
|
|
|
|
|
|
484 |
@app.route('/analyze_telegram_chat', methods=['POST'])
|
485 |
@login_required
|
486 |
def analyze_telegram_chat():
|
|
|
591 |
try:
|
592 |
# Удаляем связанные сообщения
|
593 |
conn.execute("DELETE FROM messages WHERE chat_id = ?", (chat_id,))
|
594 |
+
|
595 |
+
# Удаляем анализы эмоций, связанные с сообщениями этого чата
|
596 |
+
# (если у вас есть связь между analysis_reports и chat_id)
|
597 |
+
conn.execute("""
|
598 |
+
DELETE FROM analysis_reports
|
599 |
+
WHERE content IN (
|
600 |
+
SELECT content FROM messages WHERE chat_id = ?
|
601 |
+
) AND user_id = ?
|
602 |
+
""", (chat_id, current_user.id))
|
603 |
+
|
604 |
# Удаляем сам чат
|
605 |
+
conn.execute("DELETE FROM chats WHERE chat_id = ? AND user_id = ?",
|
606 |
+
(chat_id, current_user.id))
|
607 |
conn.commit()
|
608 |
return jsonify({"success": True})
|
609 |
except Exception as e:
|
|
|
627 |
finally:
|
628 |
conn.close()
|
629 |
|
630 |
+
|
631 |
@app.route('/load_chat/<chat_id>')
|
632 |
@login_required
|
633 |
def load_chat(chat_id):
|
|
|
657 |
conn.close()
|
658 |
|
659 |
|
|
|
660 |
@app.route('/save_message', methods=['POST'])
|
661 |
@login_required
|
662 |
def save_message():
|
|
|
712 |
|
713 |
|
714 |
if __name__ == "__main__":
|
715 |
+
app.run(debug=True)
|
|
|
|
|
|
forms.py
CHANGED
@@ -2,21 +2,69 @@ 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(
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
from wtforms import StringField, PasswordField, SubmitField
|
3 |
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
|
4 |
from models import User
|
5 |
+
from wtforms import FileField
|
6 |
+
from wtforms.validators import Email, Optional
|
7 |
+
from flask_wtf.file import FileAllowed
|
8 |
+
|
9 |
+
|
10 |
+
class EditProfileForm(FlaskForm):
|
11 |
+
username = StringField('Имя пользователя', validators=[Optional()])
|
12 |
+
email = StringField('Email', validators=[Optional(), Email()])
|
13 |
+
avatar = FileField('Аватарка', validators=[FileAllowed(['jpg', 'png', 'jpeg'], 'Только изображения')])
|
14 |
+
|
15 |
+
current_password = PasswordField('Текущий пароль', validators=[Optional()])
|
16 |
+
new_password = PasswordField('Новый пароль', validators=[Optional()])
|
17 |
+
confirm_password = PasswordField('Подтвердите новый пароль', validators=[
|
18 |
+
Optional(),
|
19 |
+
EqualTo('new_password', message='Пароли должны совпадать')
|
20 |
+
])
|
21 |
+
|
22 |
+
submit = SubmitField('Сохранить')
|
23 |
+
|
24 |
+
|
25 |
+
def validate_email(self, email):
|
26 |
+
user = User.query.filter_by(email=email.data).first()
|
27 |
+
if user:
|
28 |
+
raise ValidationError('Этот email уже используется')
|
29 |
+
|
30 |
|
31 |
class LoginForm(FlaskForm):
|
32 |
email = StringField('Email', validators=[DataRequired(), Email()])
|
33 |
password = PasswordField('Пароль', validators=[DataRequired()])
|
34 |
submit = SubmitField('Войти')
|
35 |
|
36 |
+
|
37 |
class RegistrationForm(FlaskForm):
|
38 |
+
username = StringField(
|
39 |
+
'Имя пользователя',
|
40 |
+
validators=[
|
41 |
+
DataRequired(message="Пожалуйста, введите имя пользователя."),
|
42 |
+
Length(min=4, max=25, message="Имя пользователя должно быть от 4 до 25 символов.")
|
43 |
+
]
|
44 |
+
)
|
45 |
+
|
46 |
+
email = StringField(
|
47 |
+
'Email',
|
48 |
+
validators=[
|
49 |
+
DataRequired(message="Пожалуйста, введите email."),
|
50 |
+
Email(message="Пожалуйста, введите корректный email.")
|
51 |
+
]
|
52 |
+
)
|
53 |
|
54 |
+
password = PasswordField(
|
55 |
+
'Пароль',
|
56 |
+
validators=[
|
57 |
+
DataRequired(message="Пожалуйста, введите пароль."),
|
58 |
+
Length(min=6, message="Пароль должен содержать не менее 6 символов.")
|
59 |
+
]
|
60 |
+
)
|
61 |
+
|
62 |
+
confirm_password = PasswordField(
|
63 |
+
'Подтвердите пароль',
|
64 |
+
validators=[
|
65 |
+
DataRequired(message="Пожалуйста, подтвердите пароль."),
|
66 |
+
EqualTo('password', message="Пароли не совпадают.")
|
67 |
+
]
|
68 |
+
)
|
69 |
+
|
70 |
+
submit = SubmitField('Зарегистрироваться')
|
models.py
CHANGED
@@ -3,6 +3,7 @@ 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)
|
@@ -13,6 +14,7 @@ class User(UserMixin, db.Model):
|
|
13 |
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
|
14 |
chats = db.relationship('Chat', backref='user', lazy=True)
|
15 |
reports = db.relationship('AnalysisReport', backref='user', lazy=True)
|
|
|
16 |
|
17 |
def set_password(self, password):
|
18 |
self.password_hash = generate_password_hash(password)
|
@@ -20,6 +22,7 @@ class User(UserMixin, db.Model):
|
|
20 |
def check_password(self, password):
|
21 |
return check_password_hash(self.password_hash, password)
|
22 |
|
|
|
23 |
class Chat(db.Model):
|
24 |
__tablename__ = 'chats'
|
25 |
chat_id = db.Column(db.String(36), primary_key=True)
|
@@ -28,6 +31,7 @@ class Chat(db.Model):
|
|
28 |
title = db.Column(db.String(100))
|
29 |
messages = db.relationship('Message', backref='chat', lazy=True)
|
30 |
|
|
|
31 |
class Message(db.Model):
|
32 |
__tablename__ = 'messages'
|
33 |
id = db.Column(db.Integer, primary_key=True)
|
@@ -36,6 +40,7 @@ class Message(db.Model):
|
|
36 |
content = db.Column(db.Text)
|
37 |
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
|
38 |
|
|
|
39 |
class AnalysisReport(db.Model):
|
40 |
__tablename__ = 'analysis_reports'
|
41 |
id = db.Column(db.Integer, primary_key=True)
|
@@ -43,4 +48,4 @@ class AnalysisReport(db.Model):
|
|
43 |
content = db.Column(db.Text)
|
44 |
emotion = db.Column(db.String(50))
|
45 |
confidence = db.Column(db.Float)
|
46 |
-
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
|
3 |
from extensions import db
|
4 |
from datetime import datetime
|
5 |
|
6 |
+
|
7 |
class User(UserMixin, db.Model):
|
8 |
__tablename__ = 'users'
|
9 |
id = db.Column(db.Integer, primary_key=True)
|
|
|
14 |
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
|
15 |
chats = db.relationship('Chat', backref='user', lazy=True)
|
16 |
reports = db.relationship('AnalysisReport', backref='user', lazy=True)
|
17 |
+
avatar = db.Column(db.String(128), default='default.png')
|
18 |
|
19 |
def set_password(self, password):
|
20 |
self.password_hash = generate_password_hash(password)
|
|
|
22 |
def check_password(self, password):
|
23 |
return check_password_hash(self.password_hash, password)
|
24 |
|
25 |
+
|
26 |
class Chat(db.Model):
|
27 |
__tablename__ = 'chats'
|
28 |
chat_id = db.Column(db.String(36), primary_key=True)
|
|
|
31 |
title = db.Column(db.String(100))
|
32 |
messages = db.relationship('Message', backref='chat', lazy=True)
|
33 |
|
34 |
+
|
35 |
class Message(db.Model):
|
36 |
__tablename__ = 'messages'
|
37 |
id = db.Column(db.Integer, primary_key=True)
|
|
|
40 |
content = db.Column(db.Text)
|
41 |
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
|
42 |
|
43 |
+
|
44 |
class AnalysisReport(db.Model):
|
45 |
__tablename__ = 'analysis_reports'
|
46 |
id = db.Column(db.Integer, primary_key=True)
|
|
|
48 |
content = db.Column(db.Text)
|
49 |
emotion = db.Column(db.String(50))
|
50 |
confidence = db.Column(db.Float)
|
51 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
profile.py
CHANGED
@@ -9,13 +9,14 @@ 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 |
-
|
18 |
-
|
19 |
|
20 |
# Преобразуем в список словарей с правильными датами
|
21 |
formatted_reports = []
|
@@ -37,21 +38,24 @@ def profile():
|
|
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 |
-
#
|
41 |
emotion_map = {
|
42 |
'joy': '😊 Радость',
|
|
|
43 |
'neutral': '😐 Нейтрально',
|
|
|
44 |
'anger': '😠 Злость',
|
|
|
45 |
'sadness': '😢 Грусть',
|
46 |
-
'surprise': '😲 Удивление',
|
47 |
-
'happy': '😊 Радость',
|
48 |
'sad': '😢 Грусть',
|
49 |
-
'
|
|
|
|
|
50 |
}
|
51 |
|
52 |
return render_template('profile.html',
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
9 |
|
10 |
profile_bp = Blueprint('profile', __name__)
|
11 |
|
12 |
+
|
13 |
@profile_bp.route('/profile')
|
14 |
@login_required
|
15 |
def profile():
|
16 |
# Получаем отчеты
|
17 |
+
reports = AnalysisReport.query.filter_by(user_id=current_user.id) \
|
18 |
+
.order_by(AnalysisReport.created_at.desc()) \
|
19 |
+
.all()
|
20 |
|
21 |
# Преобразуем в список словарей с правильными датами
|
22 |
formatted_reports = []
|
|
|
38 |
most_common_emotion = max(emotion_stats, key=lambda x: x.count).emotion if emotion_stats else None
|
39 |
total_reports = len(formatted_reports)
|
40 |
|
41 |
+
# Полный словарь соответствий эмоций
|
42 |
emotion_map = {
|
43 |
'joy': '😊 Радость',
|
44 |
+
'happy': '😊 Радость',
|
45 |
'neutral': '😐 Нейтрально',
|
46 |
+
'no_emotion': '😐 Нейтрально',
|
47 |
'anger': '😠 Злость',
|
48 |
+
'angry': '😠 Злость',
|
49 |
'sadness': '😢 Грусть',
|
|
|
|
|
50 |
'sad': '😢 Грусть',
|
51 |
+
'surprise': '😲 Удивление',
|
52 |
+
'fear': '😨 Страх',
|
53 |
+
'disgust': '🤢 Отвращение'
|
54 |
}
|
55 |
|
56 |
return render_template('profile.html',
|
57 |
+
reports=formatted_reports,
|
58 |
+
most_common_emotion=most_common_emotion,
|
59 |
+
total_reports=total_reports,
|
60 |
+
emotion_map=emotion_map,
|
61 |
+
datetime=datetime)
|
requirements.txt
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
flask==2.3.2
|
2 |
-
waitress>=2.1.2
|
3 |
werkzeug==2.3.7
|
4 |
flask-login==0.6.2
|
5 |
flask-sqlalchemy==2.5.1
|
|
|
1 |
flask==2.3.2
|
|
|
2 |
werkzeug==2.3.7
|
3 |
flask-login==0.6.2
|
4 |
flask-sqlalchemy==2.5.1
|
templates/admin/base.html
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 %}Админ-панель{% endblock %}</title>
|
7 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
8 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='admin.css') }}">
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="admin-wrapper">
|
13 |
+
<!-- Боковая панель -->
|
14 |
+
<div class="admin-sidebar">
|
15 |
+
<div class="admin-brand">
|
16 |
+
<i class="fas fa-lock"></i> Админ-панель
|
17 |
+
</div>
|
18 |
+
<nav class="admin-nav">
|
19 |
+
<a href="{{ url_for('admin_bp.dashboard') }}" class="nav-item">
|
20 |
+
<i class="fas fa-tachometer-alt"></i> Дашборд
|
21 |
+
</a>
|
22 |
+
<a href="{{ url_for('admin_bp.manage_users') }}" class="nav-item">
|
23 |
+
<i class="fas fa-users"></i> Пользователи
|
24 |
+
</a>
|
25 |
+
<a href="{{ url_for('admin_bp.view_reports') }}" class="nav-item">
|
26 |
+
<i class="fas fa-chart-bar"></i> Отчеты
|
27 |
+
</a>
|
28 |
+
<div class="nav-divider"></div>
|
29 |
+
<a href="{{ url_for('index') }}" class="nav-item">
|
30 |
+
<i class="fas fa-arrow-left"></i> На сайт
|
31 |
+
</a>
|
32 |
+
</nav>
|
33 |
+
<div class="admin-user">
|
34 |
+
<i class="fas fa-user-circle"></i>
|
35 |
+
<span>{{ current_user.username }}</span>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
|
39 |
+
<!-- Основное содержимое -->
|
40 |
+
<main class="admin-main">
|
41 |
+
<div class="admin-container">
|
42 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
43 |
+
{% if messages %}
|
44 |
+
<div class="flashes">
|
45 |
+
{% for category, message in messages %}
|
46 |
+
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
47 |
+
{{ message }}
|
48 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
49 |
+
</div>
|
50 |
+
{% endfor %}
|
51 |
+
</div>
|
52 |
+
{% endif %}
|
53 |
+
{% endwith %}
|
54 |
+
|
55 |
+
{% block content %}{% endblock %}
|
56 |
+
</div>
|
57 |
+
</main>
|
58 |
+
</div>
|
59 |
+
|
60 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
61 |
+
<script src="{{ url_for('static', filename='admin.js') }}"></script>
|
62 |
+
{% block scripts %}{% endblock %}
|
63 |
+
</body>
|
64 |
+
</html>
|
templates/admin/dashboard.html
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "admin/base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Дашборд{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="admin-header">
|
7 |
+
<h1><i class="fas fa-tachometer-alt"></i> Дашборд</h1>
|
8 |
+
</div>
|
9 |
+
|
10 |
+
<div class="row mb-4">
|
11 |
+
<div class="col-md-3">
|
12 |
+
<div class="stat-card bg-primary">
|
13 |
+
<div class="stat-icon">
|
14 |
+
<i class="fas fa-users"></i>
|
15 |
+
</div>
|
16 |
+
<div class="stat-info">
|
17 |
+
<h3>{{ users_count }}</h3>
|
18 |
+
<p>Пользователей</p>
|
19 |
+
</div>
|
20 |
+
</div>
|
21 |
+
</div>
|
22 |
+
<div class="col-md-3">
|
23 |
+
<div class="stat-card bg-success">
|
24 |
+
<div class="stat-icon">
|
25 |
+
<i class="fas fa-user-plus"></i>
|
26 |
+
</div>
|
27 |
+
<div class="stat-info">
|
28 |
+
<h3>{{ new_users }}</h3>
|
29 |
+
<p>Новых за 30 дней</p>
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
<div class="col-md-3">
|
34 |
+
<div class="stat-card bg-info">
|
35 |
+
<div class="stat-icon">
|
36 |
+
<i class="fas fa-chart-bar"></i>
|
37 |
+
</div>
|
38 |
+
<div class="stat-info">
|
39 |
+
<h3>{{ reports_count }}</h3>
|
40 |
+
<p>Анализов</p>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
<div class="col-md-3">
|
45 |
+
<div class="stat-card bg-warning">
|
46 |
+
<div class="stat-icon">
|
47 |
+
<i class="fas fa-comments"></i>
|
48 |
+
</div>
|
49 |
+
<div class="stat-info">
|
50 |
+
<h3>{{ active_users }}</h3>
|
51 |
+
<p>Активных</p>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
|
57 |
+
<div class="row">
|
58 |
+
<div class="col-md-6">
|
59 |
+
<div class="card mb-4">
|
60 |
+
<div class="card-header">
|
61 |
+
<h5><i class="fas fa-chart-pie"></i> Распределение эмоций</h5>
|
62 |
+
</div>
|
63 |
+
<div class="card-body">
|
64 |
+
<div class="table-responsive">
|
65 |
+
<table class="table">
|
66 |
+
<thead>
|
67 |
+
<tr>
|
68 |
+
<th>Эмоция</th>
|
69 |
+
<th>Количество</th>
|
70 |
+
<th>Процент</th>
|
71 |
+
</tr>
|
72 |
+
</thead>
|
73 |
+
<tbody>
|
74 |
+
{% for emotion, count in emotion_stats %}
|
75 |
+
<tr>
|
76 |
+
<td>{{ emotion_map.get(emotion, emotion) }}</td>
|
77 |
+
<td>{{ count }}</td>
|
78 |
+
<td>
|
79 |
+
<div class="progress">
|
80 |
+
<div class="progress-bar"
|
81 |
+
style="width: {{ (count / reports_count * 100) if reports_count > 0 else 0 }}%">
|
82 |
+
{{ (count / reports_count * 100)|round(1) if reports_count > 0 else 0 }}%
|
83 |
+
</div>
|
84 |
+
</div>
|
85 |
+
</td>
|
86 |
+
</tr>
|
87 |
+
{% endfor %}
|
88 |
+
</tbody>
|
89 |
+
</table>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
</div>
|
94 |
+
<div class="col-md-6">
|
95 |
+
<div class="card">
|
96 |
+
<div class="card-header">
|
97 |
+
<h5><i class="fas fa-user-chart"></i> Топ пользователей</h5>
|
98 |
+
</div>
|
99 |
+
<div class="card-body">
|
100 |
+
<div class="table-responsive">
|
101 |
+
<table class="table">
|
102 |
+
<thead>
|
103 |
+
<tr>
|
104 |
+
<th>Пользователь</th>
|
105 |
+
<th>Анализов</th>
|
106 |
+
</tr>
|
107 |
+
</thead>
|
108 |
+
<tbody>
|
109 |
+
{% for user, count in user_activity %}
|
110 |
+
<tr>
|
111 |
+
<td>{{ user }}</td>
|
112 |
+
<td>{{ count }}</td>
|
113 |
+
</tr>
|
114 |
+
{% endfor %}
|
115 |
+
</tbody>
|
116 |
+
</table>
|
117 |
+
</div>
|
118 |
+
</div>
|
119 |
+
</div>
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
{% endblock %}
|
templates/admin/reports.html
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "admin/base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Отчеты анализа{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="admin-header">
|
7 |
+
<h1><i class="fas fa-chart-bar"></i> Отчеты анализа</h1>
|
8 |
+
<div class="admin-actions">
|
9 |
+
<form class="filter-form" method="get" action="{{ url_for('admin_bp.view_reports') }}">
|
10 |
+
<select class="form-select" name="emotion" onchange="this.form.submit()">
|
11 |
+
<option value="">Все эмоции</option>
|
12 |
+
{% for emotion in emotions %}
|
13 |
+
<option value="{{ emotion.emotion }}" {% if emotion.emotion == current_emotion %}selected{% endif %}>
|
14 |
+
{{ emotion_map.get(emotion.emotion, emotion.emotion) }}
|
15 |
+
</option>
|
16 |
+
{% endfor %}
|
17 |
+
</select>
|
18 |
+
</form>
|
19 |
+
</div>
|
20 |
+
</div>
|
21 |
+
|
22 |
+
<div class="card">
|
23 |
+
<div class="card-body">
|
24 |
+
<div class="table-responsive">
|
25 |
+
<table class="table table-hover">
|
26 |
+
<thead>
|
27 |
+
<tr>
|
28 |
+
<th>ID</th>
|
29 |
+
<th>Пользователь</th>
|
30 |
+
<th>Текст</th>
|
31 |
+
<th>Эмоция</th>
|
32 |
+
<th>Уверенность</th>
|
33 |
+
<th>Дата</th>
|
34 |
+
</tr>
|
35 |
+
</thead>
|
36 |
+
<tbody>
|
37 |
+
{% for report in reports.items %}
|
38 |
+
<tr>
|
39 |
+
<td>{{ report.id }}</td>
|
40 |
+
<td>{{ report.user.username }}</td>
|
41 |
+
<td class="text-truncate" style="max-width: 200px;" title="{{ report.content }}">
|
42 |
+
{{ report.content }}
|
43 |
+
</td>
|
44 |
+
<td>
|
45 |
+
<span class="badge" style="background: {{ get_emotion_color(report.emotion) }}">
|
46 |
+
{{ emotion_map.get(report.emotion, report.emotion) }}
|
47 |
+
</span>
|
48 |
+
</td>
|
49 |
+
<td>{{ (report.confidence * 100)|round(1) }}%</td>
|
50 |
+
<td>{{ report.created_at|datetimeformat }}</td>
|
51 |
+
</tr>
|
52 |
+
{% endfor %}
|
53 |
+
</tbody>
|
54 |
+
</table>
|
55 |
+
</div>
|
56 |
+
|
57 |
+
<!-- Пагинация -->
|
58 |
+
<nav aria-label="Page navigation">
|
59 |
+
<ul class="pagination justify-content-center">
|
60 |
+
{% if reports.has_prev %}
|
61 |
+
<li class="page-item">
|
62 |
+
<a class="page-link" href="{{ url_for('admin_bp.view_reports', page=reports.prev_num, emotion=current_emotion) }}">
|
63 |
+
«
|
64 |
+
</a>
|
65 |
+
</li>
|
66 |
+
{% endif %}
|
67 |
+
|
68 |
+
{% for page_num in reports.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=3) %}
|
69 |
+
{% if page_num %}
|
70 |
+
<li class="page-item {% if page_num == reports.page %}active{% endif %}">
|
71 |
+
<a class="page-link" href="{{ url_for('admin_bp.view_reports', page=page_num, emotion=current_emotion) }}">
|
72 |
+
{{ page_num }}
|
73 |
+
</a>
|
74 |
+
</li>
|
75 |
+
{% else %}
|
76 |
+
<li class="page-item disabled"><span class="page-link">...</span></li>
|
77 |
+
{% endif %}
|
78 |
+
{% endfor %}
|
79 |
+
|
80 |
+
{% if reports.has_next %}
|
81 |
+
<li class="page-item">
|
82 |
+
<a class="page-link" href="{{ url_for('admin_bp.view_reports', page=reports.next_num, emotion=current_emotion) }}">
|
83 |
+
»
|
84 |
+
</a>
|
85 |
+
</li>
|
86 |
+
{% endif %}
|
87 |
+
</ul>
|
88 |
+
</nav>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
{% endblock %}
|
templates/admin/users.html
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "admin/base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Управление пользователями{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="admin-header">
|
7 |
+
<h1><i class="fas fa-users"></i> Управление пользователями</h1>
|
8 |
+
<div class="admin-actions">
|
9 |
+
<form class="search-form" method="get" action="{{ url_for('admin_bp.manage_users') }}">
|
10 |
+
<div class="input-group">
|
11 |
+
<input type="text" class="form-control" name="search" placeholder="Поиск..."
|
12 |
+
value="{{ search_query }}">
|
13 |
+
<button class="btn btn-outline-secondary" type="submit">
|
14 |
+
<i class="fas fa-search"></i>
|
15 |
+
</button>
|
16 |
+
</div>
|
17 |
+
</form>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
|
21 |
+
<div class="card">
|
22 |
+
<div class="card-body">
|
23 |
+
<div class="table-responsive">
|
24 |
+
<table class="table table-hover">
|
25 |
+
<thead>
|
26 |
+
<tr>
|
27 |
+
<th>ID</th>
|
28 |
+
<th>Имя</th>
|
29 |
+
<th>Email</th>
|
30 |
+
<th>Дата регистрации</th>
|
31 |
+
<th>Статус</th>
|
32 |
+
<th>Действия</th>
|
33 |
+
</tr>
|
34 |
+
</thead>
|
35 |
+
<tbody>
|
36 |
+
{% for user in users.items %}
|
37 |
+
<tr>
|
38 |
+
<td>{{ user.id }}</td>
|
39 |
+
<td>{{ user.username }}</td>
|
40 |
+
<td>{{ user.email }}</td>
|
41 |
+
<td>{{ user.created_at|datetimeformat }}</td>
|
42 |
+
<td>
|
43 |
+
{% if user.is_admin %}
|
44 |
+
<span class="badge bg-danger">Админ</span>
|
45 |
+
{% else %}
|
46 |
+
<span class="badge bg-secondary">Пользователь</span>
|
47 |
+
{% endif %}
|
48 |
+
</td>
|
49 |
+
<td>
|
50 |
+
<div class="btn-group">
|
51 |
+
<button class="btn btn-sm btn-{{ 'danger' if user.is_admin else 'success' }} toggle-admin"
|
52 |
+
data-user-id="{{ user.id }}">
|
53 |
+
{{ 'Убрать админа' if user.is_admin else 'Сделать админом' }}
|
54 |
+
</button>
|
55 |
+
{% if user.id != current_user.id %}
|
56 |
+
<button class="btn btn-sm btn-outline-danger delete-user"
|
57 |
+
data-user-id="{{ user.id }}">
|
58 |
+
<i class="fas fa-trash"></i>
|
59 |
+
</button>
|
60 |
+
{% endif %}
|
61 |
+
</div>
|
62 |
+
</td>
|
63 |
+
</tr>
|
64 |
+
{% endfor %}
|
65 |
+
</tbody>
|
66 |
+
</table>
|
67 |
+
</div>
|
68 |
+
|
69 |
+
<!-- Пагинация -->
|
70 |
+
<nav aria-label="Page navigation">
|
71 |
+
<ul class="pagination justify-content-center">
|
72 |
+
{% if users.has_prev %}
|
73 |
+
<li class="page-item">
|
74 |
+
<a class="page-link" href="{{ url_for('admin_bp.manage_users', page=users.prev_num, search=search_query) }}">
|
75 |
+
«
|
76 |
+
</a>
|
77 |
+
</li>
|
78 |
+
{% endif %}
|
79 |
+
|
80 |
+
{% for page_num in users.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=3) %}
|
81 |
+
{% if page_num %}
|
82 |
+
<li class="page-item {% if page_num == users.page %}active{% endif %}">
|
83 |
+
<a class="page-link" href="{{ url_for('admin_bp.manage_users', page=page_num, search=search_query) }}">
|
84 |
+
{{ page_num }}
|
85 |
+
</a>
|
86 |
+
</li>
|
87 |
+
{% else %}
|
88 |
+
<li class="page-item disabled"><span class="page-link">...</span></li>
|
89 |
+
{% endif %}
|
90 |
+
{% endfor %}
|
91 |
+
|
92 |
+
{% if users.has_next %}
|
93 |
+
<li class="page-item">
|
94 |
+
<a class="page-link" href="{{ url_for('admin_bp.manage_users', page=users.next_num, search=search_query) }}">
|
95 |
+
»
|
96 |
+
</a>
|
97 |
+
</li>
|
98 |
+
{% endif %}
|
99 |
+
</ul>
|
100 |
+
</nav>
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
{% endblock %}
|
104 |
+
|
105 |
+
{% block scripts %}
|
106 |
+
{{ super() }}
|
107 |
+
<script>
|
108 |
+
document.addEventListener('DOMContentLoaded', function() {
|
109 |
+
// Функция для получения CSRF-токена из cookies
|
110 |
+
function getCookie(name) {
|
111 |
+
const cookieValue = document.cookie
|
112 |
+
.split('; ')
|
113 |
+
.find(row => row.startsWith(name + '='))
|
114 |
+
?.split('=')[1];
|
115 |
+
return cookieValue ? decodeURIComponent(cookieValue) : null;
|
116 |
+
}
|
117 |
+
|
118 |
+
// Обработка переключения админа
|
119 |
+
document.querySelectorAll('.toggle-admin').forEach(btn => {
|
120 |
+
btn.addEventListener('click', function() {
|
121 |
+
const userId = this.dataset.userId;
|
122 |
+
fetch(`/admin/toggle_admin/${userId}`, {
|
123 |
+
method: 'POST',
|
124 |
+
headers: {
|
125 |
+
'Content-Type': 'application/json',
|
126 |
+
'X-CSRFToken': getCookie('csrf_token')
|
127 |
+
}
|
128 |
+
})
|
129 |
+
.then(response => response.json())
|
130 |
+
.then(data => {
|
131 |
+
if (data.status === 'success') {
|
132 |
+
location.reload();
|
133 |
+
}
|
134 |
+
});
|
135 |
+
});
|
136 |
+
});
|
137 |
+
|
138 |
+
// Обработка удаления пользователя
|
139 |
+
document.querySelectorAll('.delete-user').forEach(btn => {
|
140 |
+
btn.addEventListener('click', function() {
|
141 |
+
if (!confirm('Вы уверены, что хотите удалить этого пользователя?')) return;
|
142 |
+
|
143 |
+
const userId = this.dataset.userId;
|
144 |
+
fetch(`/admin/delete_user/${userId}`, {
|
145 |
+
method: 'POST',
|
146 |
+
headers: {
|
147 |
+
'Content-Type': 'application/json',
|
148 |
+
'X-CSRFToken': getCookie('csrf_token')
|
149 |
+
}
|
150 |
+
})
|
151 |
+
.then(response => response.json())
|
152 |
+
.then(data => {
|
153 |
+
if (data.status === 'success') {
|
154 |
+
location.reload();
|
155 |
+
} else {
|
156 |
+
alert(data.message || 'Ошибка при удалении');
|
157 |
+
}
|
158 |
+
});
|
159 |
+
});
|
160 |
+
});
|
161 |
+
});
|
162 |
+
</script>
|
163 |
+
{% endblock %}
|
templates/auth/login.html
CHANGED
@@ -1,40 +1,44 @@
|
|
1 |
-
{% extends "
|
2 |
|
3 |
{% block title %}Вход{% endblock %}
|
4 |
|
5 |
{% block content %}
|
6 |
-
<div class="
|
7 |
-
<div class="
|
8 |
-
<div class="
|
9 |
-
<
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
</div>
|
22 |
-
|
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 %}
|
|
|
1 |
+
{% extends "base_public.html" %}
|
2 |
|
3 |
{% block title %}Вход{% endblock %}
|
4 |
|
5 |
{% block content %}
|
6 |
+
<div class="auth-container">
|
7 |
+
<div class="auth-card">
|
8 |
+
<div class="auth-header">
|
9 |
+
<h3><i class="fas fa-sign-in-alt"></i> Вход в систему</h3>
|
10 |
+
</div>
|
11 |
+
<div class="auth-body">
|
12 |
+
|
13 |
+
<form method="POST" action="{{ url_for('auth_bp.login') }}">
|
14 |
+
{{ form.hidden_tag() }}
|
15 |
+
<div class="form-group">
|
16 |
+
<label for="email" class="form-label">
|
17 |
+
<i class="fas fa-envelope"></i> Email
|
18 |
+
</label>
|
19 |
+
{{ form.email(class="form-input", placeholder="Введите ваш email") }}
|
20 |
+
{% for error in form.email.errors %}
|
21 |
+
<div class="form-error">{{ error }}</div>
|
22 |
+
{% endfor %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
</div>
|
24 |
+
<div class="form-group">
|
25 |
+
<label for="password" class="form-label">
|
26 |
+
<i class="fas fa-lock"></i> Пароль
|
27 |
+
</label>
|
28 |
+
{{ form.password(class="form-input", placeholder="Введите пароль") }}
|
29 |
+
{% for error in form.password.errors %}
|
30 |
+
<div class="form-error">{{ error }}</div>
|
31 |
+
{% endfor %}
|
32 |
+
</div>
|
33 |
+
<button type="submit" class="btn-auth">
|
34 |
+
<i class="fas fa-sign-in-alt"></i> Войти
|
35 |
+
</button>
|
36 |
+
</form>
|
37 |
+
<div class="auth-footer">
|
38 |
+
Нет аккаунта? <a href="{{ url_for('auth_bp.register') }}" class="auth-link">Зарегистрируйтесь</a>
|
39 |
</div>
|
40 |
</div>
|
41 |
+
|
42 |
</div>
|
43 |
</div>
|
44 |
{% endblock %}
|
templates/auth/register.html
CHANGED
@@ -1,52 +1,58 @@
|
|
1 |
-
{% extends "
|
2 |
|
3 |
{% block title %}Регистрация{% endblock %}
|
4 |
|
5 |
{% block content %}
|
6 |
-
<div class="
|
7 |
-
<div class="
|
8 |
-
<div class="
|
9 |
-
<
|
10 |
-
|
11 |
-
|
12 |
-
<
|
13 |
-
|
14 |
-
|
15 |
-
<
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
{
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
</
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
</div>
|
51 |
</div>
|
52 |
</div>
|
|
|
1 |
+
{% extends "base_public.html" %}
|
2 |
|
3 |
{% block title %}Регистрация{% endblock %}
|
4 |
|
5 |
{% block content %}
|
6 |
+
<div class="auth-container">
|
7 |
+
<div class="auth-card">
|
8 |
+
<div class="auth-header">
|
9 |
+
<h3><i class="fas fa-user-plus"></i> Регистрация</h3>
|
10 |
+
</div>
|
11 |
+
<div class="auth-body">
|
12 |
+
<form method="POST" action="{{ url_for('.register') }}">
|
13 |
+
{{ form.hidden_tag() }}
|
14 |
+
<div class="form-group">
|
15 |
+
<label for="username" class="form-label">
|
16 |
+
<i class="fas fa-user"></i> Имя пользователя
|
17 |
+
</label>
|
18 |
+
{{ form.username(class="form-input", placeholder="Придумайте имя пользователя") }}
|
19 |
+
{% for error in form.username.errors %}
|
20 |
+
<div class="form-error">{{ error }}</div>
|
21 |
+
{% endfor %}
|
22 |
+
</div>
|
23 |
+
<div class="form-group">
|
24 |
+
<label for="email" class="form-label">
|
25 |
+
<i class="fas fa-envelope"></i> Email
|
26 |
+
</label>
|
27 |
+
{{ form.email(class="form-input", placeholder="Введите ваш email") }}
|
28 |
+
{% for error in form.email.errors %}
|
29 |
+
<div class="form-error">{{ error }}</div>
|
30 |
+
{% endfor %}
|
31 |
+
</div>
|
32 |
+
<div class="form-group">
|
33 |
+
<label for="password" class="form-label">
|
34 |
+
<i class="fas fa-lock"></i> Пароль
|
35 |
+
</label>
|
36 |
+
{{ form.password(class="form-input", placeholder="Придумайте пароль") }}
|
37 |
+
{% for error in form.password.errors %}
|
38 |
+
<div class="form-error">{{ error }}</div>
|
39 |
+
{% endfor %}
|
40 |
+
</div>
|
41 |
+
<div class="form-group">
|
42 |
+
<label for="confirm_password" class="form-label">
|
43 |
+
<i class="fas fa-lock"></i> Подтверждение пароля
|
44 |
+
</label>
|
45 |
+
{{ form.confirm_password(class="form-input", placeholder="Повторите пароль") }}
|
46 |
+
{% for error in form.confirm_password.errors %}
|
47 |
+
<div class="form-error">{{ error }}</div>
|
48 |
+
{% endfor %}
|
49 |
</div>
|
50 |
+
<button type="submit" class="btn-auth">
|
51 |
+
<i class="fas fa-user-plus"></i> Зарегистрироваться
|
52 |
+
</button>
|
53 |
+
</form>
|
54 |
+
<div class="auth-footer">
|
55 |
+
Уже есть аккаунт? <a href="{{ url_for('auth_bp.login') }}" class="auth-link">Войдите</a>
|
56 |
</div>
|
57 |
</div>
|
58 |
</div>
|
templates/base.html
CHANGED
@@ -7,14 +7,18 @@
|
|
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 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
18 |
|
19 |
<div class="sidebar-nav">
|
20 |
<a href="{{ url_for('index') }}" class="nav-item">
|
@@ -24,6 +28,11 @@
|
|
24 |
<a href="{{ url_for('profile.profile') }}" class="nav-item">
|
25 |
<i class="fas fa-user"></i> Профиль
|
26 |
</a>
|
|
|
|
|
|
|
|
|
|
|
27 |
{% endif %}
|
28 |
</div>
|
29 |
|
@@ -47,18 +56,27 @@
|
|
47 |
|
48 |
<!-- Основное содержимое -->
|
49 |
<div class="app-main">
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
{%
|
|
|
|
|
|
|
|
|
59 |
</div>
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
62 |
|
63 |
{% block content %}{% endblock %}
|
64 |
</div>
|
@@ -66,6 +84,11 @@
|
|
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>
|
|
|
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 |
+
<!-- Bootstrap JS (для работы закрытия) -->
|
11 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
12 |
</head>
|
13 |
<body>
|
14 |
<div class="app-container">
|
15 |
<!-- Боковая панель -->
|
16 |
<div class="app-sidebar">
|
17 |
+
<div class="sidebar-header">
|
18 |
+
<a href="{{ url_for('welcome') }}" class="logo-link">
|
19 |
+
<h2><i class="fas fa-brain"></i> EmotionAnalyzer</h2>
|
20 |
+
</a>
|
21 |
+
</div>
|
22 |
|
23 |
<div class="sidebar-nav">
|
24 |
<a href="{{ url_for('index') }}" class="nav-item">
|
|
|
28 |
<a href="{{ url_for('profile.profile') }}" class="nav-item">
|
29 |
<i class="fas fa-user"></i> Профиль
|
30 |
</a>
|
31 |
+
{% if current_user.is_authenticated and current_user.is_admin %}
|
32 |
+
<a href="{{ url_for('admin_bp.dashboard') }}" class="nav-item">
|
33 |
+
<i class="fas fa-lock"></i> Админ-панель
|
34 |
+
</a>
|
35 |
+
{% endif %}
|
36 |
{% endif %}
|
37 |
</div>
|
38 |
|
|
|
56 |
|
57 |
<!-- Основное содержимое -->
|
58 |
<div class="app-main">
|
59 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
60 |
+
{% if messages %}
|
61 |
+
<div class="flash-messages">
|
62 |
+
{% for category, message in messages %}
|
63 |
+
<div class="flash-message flash-{{ category }} enhanced-alert">
|
64 |
+
<div class="alert-icon">
|
65 |
+
{% if category == 'error' %}
|
66 |
+
<i class="fas fa-exclamation-circle"></i>
|
67 |
+
{% elif category == 'success' %}
|
68 |
+
<i class="fas fa-check-circle"></i>
|
69 |
+
{% else %}
|
70 |
+
<i class="fas fa-info-circle"></i>
|
71 |
+
{% endif %}
|
72 |
</div>
|
73 |
+
<div class="alert-text">{{ message }}</div>
|
74 |
+
<button class="flash-close " onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
|
75 |
+
</div>
|
76 |
+
{% endfor %}
|
77 |
+
</div>
|
78 |
+
{% endif %}
|
79 |
+
{% endwith %}
|
80 |
|
81 |
{% block content %}{% endblock %}
|
82 |
</div>
|
|
|
84 |
|
85 |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
86 |
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
87 |
+
<script>
|
88 |
+
document.querySelector('.mobile-menu-toggle').addEventListener('click', () => {
|
89 |
+
document.querySelector('.app-container').classList.toggle('sidebar-open');
|
90 |
+
});
|
91 |
+
</script>
|
92 |
{% block scripts %}{% endblock %}
|
93 |
</body>
|
94 |
</html>
|
templates/base_public.html
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
</head>
|
10 |
+
|
11 |
+
<body>
|
12 |
+
<!-- Логотип -->
|
13 |
+
<div class="logo-container">
|
14 |
+
<a href="{{ url_for('welcome') }}" class="logo-link">
|
15 |
+
<h2><i class="fas fa-brain"></i> EmotionAnalyzer</h2>
|
16 |
+
</a>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<!-- Основной контент -->
|
20 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
21 |
+
{% if messages %}
|
22 |
+
<div class="flash-container">
|
23 |
+
{% for category, message in messages %}
|
24 |
+
<div class="flash flash-{{ category }}">{{ message }}</div>
|
25 |
+
{% endfor %}
|
26 |
+
</div>
|
27 |
+
{% endif %}
|
28 |
+
{% endwith %}
|
29 |
+
|
30 |
+
<div class="public-container">
|
31 |
+
{% block content %}{% endblock %}
|
32 |
+
</div>
|
33 |
+
|
34 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
35 |
+
<script>
|
36 |
+
setTimeout(() => {
|
37 |
+
document.querySelectorAll('.flash').forEach(flash => flash.remove());
|
38 |
+
}, 4000);
|
39 |
+
</script>
|
40 |
+
{% block scripts %}{% endblock %}
|
41 |
+
</body>
|
42 |
+
</html>
|
43 |
+
|
44 |
+
<style>
|
45 |
+
/*Cтили для окна ошибок*/
|
46 |
+
.flash-container {
|
47 |
+
position: fixed;
|
48 |
+
top: 20px;
|
49 |
+
right: 20px;
|
50 |
+
z-index: 9999;
|
51 |
+
display: flex;
|
52 |
+
flex-direction: column;
|
53 |
+
gap: 10px;
|
54 |
+
}
|
55 |
+
|
56 |
+
.flash {
|
57 |
+
padding: 12px 18px;
|
58 |
+
border-radius: 6px;
|
59 |
+
color: white;
|
60 |
+
font-weight: 500;
|
61 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
62 |
+
opacity: 0.95;
|
63 |
+
animation: slide-in 0.4s ease;
|
64 |
+
}
|
65 |
+
|
66 |
+
.flash-success {
|
67 |
+
background-color: #2ecc71;
|
68 |
+
}
|
69 |
+
|
70 |
+
.flash-danger {
|
71 |
+
background-color: #e74c3c;
|
72 |
+
}
|
73 |
+
|
74 |
+
@keyframes slide-in {
|
75 |
+
from { transform: translateX(100%); opacity: 0; }
|
76 |
+
to { transform: translateX(0); opacity: 1; }
|
77 |
+
}
|
78 |
+
|
79 |
+
|
80 |
+
/* Стили для логотипа */
|
81 |
+
.logo-container {
|
82 |
+
padding-top: 20px;
|
83 |
+
padding-left: 20px;
|
84 |
+
background: linear-gradient(135deg, var(--darker-bg), var(--dark-bg));
|
85 |
+
}
|
86 |
+
|
87 |
+
.logo-link {
|
88 |
+
text-decoration: none !important;
|
89 |
+
}
|
90 |
+
|
91 |
+
.logo-link h2 {
|
92 |
+
color: #fff; /* Темно-синий цвет */
|
93 |
+
font-size: 2rem;
|
94 |
+
margin: 0;
|
95 |
+
transition: color 0.3s ease;
|
96 |
+
}
|
97 |
+
|
98 |
+
.logo-link:hover h2 {
|
99 |
+
color: #4a4ae8; /* Цвет при наведении */
|
100 |
+
}
|
101 |
+
|
102 |
+
.fa-brain {
|
103 |
+
color: inherit; /* Наследует цвет от родителя */
|
104 |
+
margin-right: 10px;
|
105 |
+
}
|
106 |
+
|
107 |
+
|
108 |
+
|
109 |
+
@media (max-width: 768px) {
|
110 |
+
.logo-link h2 {
|
111 |
+
font-size: 1.5rem;
|
112 |
+
}
|
113 |
+
|
114 |
+
.public-container {
|
115 |
+
padding: 20px;
|
116 |
+
}
|
117 |
+
}
|
118 |
+
</style>
|
templates/edit_profile.html
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
{% block content %}
|
3 |
+
<div class="edit-profile">
|
4 |
+
<h2><i class="fas fa-user-cog"></i> Редактирование профиля</h2>
|
5 |
+
<form method="POST" enctype="multipart/form-data">
|
6 |
+
{{ form.hidden_tag() }}
|
7 |
+
|
8 |
+
<div class="form-group">
|
9 |
+
{{ form.username.label }} {{ form.username(class="form-control") }}
|
10 |
+
</div>
|
11 |
+
|
12 |
+
<div class="form-group">
|
13 |
+
{{ form.email.label }} {{ form.email(class="form-control") }}
|
14 |
+
</div>
|
15 |
+
|
16 |
+
<div class="form-group">
|
17 |
+
{{ form.avatar.label }} {{ form.avatar() }}
|
18 |
+
</div>
|
19 |
+
|
20 |
+
<hr>
|
21 |
+
|
22 |
+
<h2>Изменить пароль</h2>
|
23 |
+
|
24 |
+
<div class="form-group">
|
25 |
+
{{ form.current_password.label }} {{ form.current_password(class="form-control") }}
|
26 |
+
</div>
|
27 |
+
|
28 |
+
<div class="form-group">
|
29 |
+
{{ form.new_password.label }} {{ form.new_password(class="form-control") }}
|
30 |
+
</div>
|
31 |
+
|
32 |
+
<div class="form-group">
|
33 |
+
{{ form.confirm_password.label }} {{ form.confirm_password(class="form-control") }}
|
34 |
+
</div>
|
35 |
+
|
36 |
+
{{ form.submit(class="btn btn-primary") }}
|
37 |
+
</form>
|
38 |
+
</div>
|
39 |
+
{% endblock %}
|
templates/profile.html
CHANGED
@@ -1,35 +1,33 @@
|
|
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 |
-
<
|
|
|
11 |
</div>
|
12 |
<div class="user-details">
|
13 |
-
<h2
|
14 |
-
<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>
|
@@ -38,26 +36,26 @@
|
|
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 |
-
{%
|
|
|
|
|
|
|
|
|
|
|
49 |
<div class="report-card">
|
50 |
<div class="report-header">
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
{% else %}
|
55 |
-
Дата не указана
|
56 |
-
{% endif %}
|
57 |
-
</span>
|
58 |
<span class="report-emotion {{ report['emotion'] }}">
|
59 |
-
|
60 |
-
|
61 |
</div>
|
62 |
<div class="report-content">
|
63 |
<p>{{ report['content'][:200] }}{% if report['content']|length > 200 %}...{% endif %}</p>
|
@@ -72,7 +70,25 @@
|
|
72 |
{% endfor %}
|
73 |
</div>
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
|
|
|
|
|
|
|
|
|
|
76 |
{% else %}
|
77 |
<div class="empty-state">
|
78 |
<i class="fas fa-comment-slash"></i>
|
@@ -84,5 +100,225 @@
|
|
84 |
</div>
|
85 |
{% endif %}
|
86 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
{% endblock %}
|
|
|
1 |
{% extends "base.html" %}
|
|
|
2 |
{% block title %}Личный кабинет{% endblock %}
|
|
|
3 |
{% block content %}
|
4 |
<div class="profile-container">
|
5 |
+
|
6 |
+
<!-- User Info -->
|
7 |
<div class="profile-header">
|
8 |
<div class="user-info">
|
9 |
<div class="user-avatar">
|
10 |
+
<img src="{{ url_for('static', filename='avatars/' ~ current_user.avatar) }}" alt="Avatar"
|
11 |
+
class="avatar-img">
|
12 |
</div>
|
13 |
<div class="user-details">
|
14 |
+
<h2>Логин: {{ current_user.username }}</h2>
|
15 |
+
<p>Почта: {{ current_user.email }}</p>
|
16 |
+
<a href="{{ url_for('edit_profile') }}" class="btn btn-primary edit-profile-btn">
|
17 |
+
<i class="fas fa-user-edit"></i> Редактировать профиль
|
18 |
+
</a>
|
19 |
</div>
|
20 |
</div>
|
|
|
21 |
<div class="stats-cards">
|
22 |
<div class="stat-card">
|
23 |
+
<div class="stat-icon"><i class="fas fa-chart-bar"></i></div>
|
|
|
|
|
24 |
<div class="stat-content">
|
25 |
<h3>Всего анализов</h3>
|
26 |
<p class="stat-value">{{ total_reports }}</p>
|
27 |
</div>
|
28 |
</div>
|
|
|
29 |
<div class="stat-card">
|
30 |
+
<div class="stat-icon"><i class="fas fa-smile"></i></div>
|
|
|
|
|
31 |
<div class="stat-content">
|
32 |
<h3>Преобладающая эмоция</h3>
|
33 |
<p class="stat-value">{{ emotion_map.get(most_common_emotion, most_common_emotion) }}</p>
|
|
|
36 |
</div>
|
37 |
</div>
|
38 |
|
39 |
+
<!-- Reports History -->
|
40 |
<div class="reports-section">
|
41 |
+
<h3 class="section-title"><i class="fas fa-history"></i> История анализов</h3>
|
|
|
|
|
42 |
|
43 |
{% if reports %}
|
44 |
+
<div class="reports-list" id="reports-list">
|
45 |
+
{% set page_size = 6 %}
|
46 |
+
{% set current_page = request.args.get('page', default='1') | int %}
|
47 |
+
{% set start_index = (current_page - 1) * page_size %}
|
48 |
+
{% set end_index = [start_index + page_size, reports|length] | min %}
|
49 |
+
|
50 |
+
{% for report in reports[start_index:end_index] %}
|
51 |
<div class="report-card">
|
52 |
<div class="report-header">
|
53 |
+
<span class="report-date">
|
54 |
+
{{ report['created_at'] if report['created_at'] is string else report['created_at'].strftime('%d.%m.%Y %H:%M') }}
|
55 |
+
</span>
|
|
|
|
|
|
|
|
|
56 |
<span class="report-emotion {{ report['emotion'] }}">
|
57 |
+
{{ emotion_map.get(report['emotion'], report['emotion']) }}
|
58 |
+
</span>
|
59 |
</div>
|
60 |
<div class="report-content">
|
61 |
<p>{{ report['content'][:200] }}{% if report['content']|length > 200 %}...{% endif %}</p>
|
|
|
70 |
{% endfor %}
|
71 |
</div>
|
72 |
|
73 |
+
<!-- Пагинация -->
|
74 |
+
{% if reports|length > page_size %}
|
75 |
+
<div class="pagination">
|
76 |
+
{% set total_pages = (reports|length / page_size)|round(0, 'ceil')|int %}
|
77 |
+
|
78 |
+
<!-- Кнопка "Назад" -->
|
79 |
+
<a href="?page={{ current_page - 1 }}" class="pagination-btn {% if current_page == 1 %}disabled{% endif %}">«
|
80 |
+
Назад</a>
|
81 |
+
|
82 |
+
<!-- Номера страниц -->
|
83 |
+
{% for i in range(1, total_pages + 1) %}
|
84 |
+
<a href="?page={{ i }}" class="pagination-btn {% if current_page == i %}active{% endif %}">{{ i }}</a>
|
85 |
+
{% endfor %}
|
86 |
|
87 |
+
<!-- Кнопка "Вперед" -->
|
88 |
+
<a href="?page={{ current_page + 1 }}"
|
89 |
+
class="pagination-btn {% if current_page >= total_pages %}disabled{% endif %}">Вперед »</a>
|
90 |
+
</div>
|
91 |
+
{% endif %}
|
92 |
{% else %}
|
93 |
<div class="empty-state">
|
94 |
<i class="fas fa-comment-slash"></i>
|
|
|
100 |
</div>
|
101 |
{% endif %}
|
102 |
</div>
|
103 |
+
|
104 |
+
<!-- Telegram Analysis Section -->
|
105 |
+
<div class="telegram-analysis-section">
|
106 |
+
<h3 class="section-title"><i class="fab fa-telegram"></i> Анализ Telegram чатов</h3>
|
107 |
+
<div class="chat-import-section">
|
108 |
+
<form id="telegram-upload-form" enctype="multipart/form-data">
|
109 |
+
<div class="file-upload">
|
110 |
+
<label for="telegram-file" class="file-upload-btn">
|
111 |
+
<i class="fas fa-file-import"></i> Выбрать файл чата
|
112 |
+
</label>
|
113 |
+
<input type="file" id="telegram-file" accept=".json" required/>
|
114 |
+
<div class="file-info" style="display: none;">
|
115 |
+
<span id="selected-file-name">Файл не выбран</span>
|
116 |
+
<button type="button" id="clear-file" class="clear-file-btn">
|
117 |
+
<i class="fas fa-times"></i>
|
118 |
+
</button>
|
119 |
+
</div>
|
120 |
+
<button type="submit" class="btn-primary mt-3">
|
121 |
+
<i class="fas fa-chart-bar"></i> Анализировать
|
122 |
+
</button>
|
123 |
+
</div>
|
124 |
+
<p class="hint">Экспортируйте чат через Telegram Desktop (JSON format)</p>
|
125 |
+
</form>
|
126 |
+
|
127 |
+
</div>
|
128 |
+
|
129 |
+
<!-- Фильтры -->
|
130 |
+
<div class="chart-header">
|
131 |
+
<h4><i class="fas fa-chart-line"></i> Изменение эмоционального фона</h4>
|
132 |
+
<div class="time-filter">
|
133 |
+
<button class="time-btn active" data-range="week">Неделя</button>
|
134 |
+
<button class="time-btn" data-range="month">Месяц</button>
|
135 |
+
<button class="time-btn" data-range="year">Год</button>
|
136 |
+
<button class="time-btn" data-range="all">Все время</button>
|
137 |
+
</div>
|
138 |
+
<div class="user-filter">
|
139 |
+
<label for="user-select" style="color: white;"><i class="fas fa-user-friends"></i> Участник:</label>
|
140 |
+
<select id="user-select" class="form-control">
|
141 |
+
<option value="all">Все участники</option>
|
142 |
+
</select>
|
143 |
+
</div>
|
144 |
+
</div>
|
145 |
+
|
146 |
+
<!-- Блок автоматического анализа -->
|
147 |
+
<div id="analysis-summary" class="analysis-summary">
|
148 |
+
<h4><i class="fas fa-lightbulb"></i> Автоматический анализ</h4>
|
149 |
+
<ul id="summary-content"></ul>
|
150 |
+
</div>
|
151 |
+
|
152 |
+
<!-- Основной график -->
|
153 |
+
<div class="chart-container" id="emotion-timeline"></div>
|
154 |
+
|
155 |
+
<!-- Тепловая карта -->
|
156 |
+
<div class="chart-container" id="calendar-heatmap"></div>
|
157 |
+
|
158 |
+
<!-- Дополнительная статистика -->
|
159 |
+
<div class="chart-row">
|
160 |
+
<div class="chart-col">
|
161 |
+
<h4><i class="fas fa-chart-pie"></i> Распределение эмоций</h4>
|
162 |
+
<div class="chart-container" id="emotion-distribution-pie"></div>
|
163 |
+
</div>
|
164 |
+
<div class="chart-col">
|
165 |
+
<h4><i class="fas fa-percentage"></i> Статистика по эмоциям</h4>
|
166 |
+
<div class="emotion-stats" id="emotion-distribution"></div>
|
167 |
+
</div>
|
168 |
+
</div>
|
169 |
+
</div>
|
170 |
</div>
|
171 |
+
|
172 |
+
|
173 |
+
<!-- Plotly -->
|
174 |
+
<script src="https://cdn.plot.ly/plotly-2.24.1.min.js "></script>
|
175 |
+
|
176 |
+
<!-- Main Script -->
|
177 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
178 |
+
|
179 |
+
<!-- File upload handler -->
|
180 |
+
<script>
|
181 |
+
const telegramFileInput = document.getElementById('telegram-file');
|
182 |
+
if (telegramFileInput) {
|
183 |
+
telegramFileInput.addEventListener('change', function(e) {
|
184 |
+
const fileName = e.target.files[0]?.name || 'Файл не выбран';
|
185 |
+
const selectedFileName = document.getElementById('selected-file-name');
|
186 |
+
const fileInfo = document.querySelector('.file-info');
|
187 |
+
|
188 |
+
if (selectedFileName) {
|
189 |
+
selectedFileName.textContent = fileName;
|
190 |
+
}
|
191 |
+
if (fileInfo) {
|
192 |
+
fileInfo.style.display = 'flex';
|
193 |
+
}
|
194 |
+
});
|
195 |
+
}
|
196 |
+
</script>
|
197 |
+
|
198 |
+
<style>
|
199 |
+
.edit-profile-btn {
|
200 |
+
display: inline-flex;
|
201 |
+
align-items: center;
|
202 |
+
font-size: 0.85rem;
|
203 |
+
padding: 5px 10px;
|
204 |
+
border-radius: 5px;
|
205 |
+
}
|
206 |
+
|
207 |
+
.edit-profile-btn i {
|
208 |
+
font-size: 0.9rem;
|
209 |
+
margin-right: 6px;
|
210 |
+
}
|
211 |
+
|
212 |
+
.avatar-img {
|
213 |
+
width: 100px;
|
214 |
+
height: 100px;
|
215 |
+
border-radius: 10%;
|
216 |
+
object-fit: cover;
|
217 |
+
}
|
218 |
+
.detailed-analysis {
|
219 |
+
margin-top: 10px;
|
220 |
+
padding: 10px;
|
221 |
+
background: rgba(255,255,255,0.05);
|
222 |
+
border-radius: 8px;
|
223 |
+
}
|
224 |
+
|
225 |
+
.detailed-analysis-item {
|
226 |
+
display: flex;
|
227 |
+
justify-content: space-between;
|
228 |
+
margin-bottom: 5px;
|
229 |
+
}
|
230 |
+
|
231 |
+
.emotion-bar {
|
232 |
+
height: 8px;
|
233 |
+
background: rgba(255,255,255,0.1);
|
234 |
+
border-radius: 4px;
|
235 |
+
margin-top: 3px;
|
236 |
+
}
|
237 |
+
|
238 |
+
.emotion-fill {
|
239 |
+
height: 100%;
|
240 |
+
border-radius: 4px;
|
241 |
+
}
|
242 |
+
|
243 |
+
.advice-box {
|
244 |
+
margin-top: 10px;
|
245 |
+
padding: 10px;
|
246 |
+
background: rgba(74, 74, 232, 0.1);
|
247 |
+
border-left: 3px solid var(--primary-color);
|
248 |
+
font-style: italic;
|
249 |
+
}
|
250 |
+
|
251 |
+
.pagination {
|
252 |
+
display: flex;
|
253 |
+
justify-content: center;
|
254 |
+
gap: 8px;
|
255 |
+
margin-top: 20px;
|
256 |
+
flex-wrap: wrap;
|
257 |
+
}
|
258 |
+
.pagination-btn {
|
259 |
+
padding: 8px 12px;
|
260 |
+
border-radius: 4px;
|
261 |
+
background: #3c3c5a;
|
262 |
+
color: white;
|
263 |
+
text-decoration: none;
|
264 |
+
transition: 0.2s;
|
265 |
+
}
|
266 |
+
.pagination-btn:hover:not(.disabled):not(.active) {
|
267 |
+
background: #5a5a7e;
|
268 |
+
}
|
269 |
+
.pagination-btn.active {
|
270 |
+
background: #6c63ff;
|
271 |
+
font-weight: bold;
|
272 |
+
color: #fff;
|
273 |
+
}
|
274 |
+
.pagination-btn.disabled {
|
275 |
+
opacity: 0.5;
|
276 |
+
pointer-events: none;
|
277 |
+
}
|
278 |
+
|
279 |
+
/* В секции style в profile.html */
|
280 |
+
.chat-controls {
|
281 |
+
margin: 15px 0;
|
282 |
+
display: flex;
|
283 |
+
gap: 10px;
|
284 |
+
}
|
285 |
+
|
286 |
+
.btn-danger {
|
287 |
+
background-color: #d63031;
|
288 |
+
color: white;
|
289 |
+
border: none;
|
290 |
+
padding: 8px 15px;
|
291 |
+
border-radius: 4px;
|
292 |
+
cursor: pointer;
|
293 |
+
transition: background-color 0.2s;
|
294 |
+
}
|
295 |
+
|
296 |
+
.btn-danger:hover {
|
297 |
+
background-color: #ff4757;
|
298 |
+
}
|
299 |
+
|
300 |
+
.report-emotion.anger {
|
301 |
+
color: #d63031; /* Красный для злости */
|
302 |
+
}
|
303 |
+
.report-emotion.joy {
|
304 |
+
color: #00b894; /* Зеленый для радости */
|
305 |
+
}
|
306 |
+
.report-emotion.sadness {
|
307 |
+
color: #0984e3; /* Синий для грусти */
|
308 |
+
}
|
309 |
+
.report-emotion.surprise {
|
310 |
+
color: #fdcb6e; /* Желтый для удивления */
|
311 |
+
}
|
312 |
+
.report-emotion.fear {
|
313 |
+
color: #a29bfe; /* Фиолетовый для страха */
|
314 |
+
}
|
315 |
+
.report-emotion.neutral {
|
316 |
+
color: #636e72; /* Серый для нейтрального */
|
317 |
+
}
|
318 |
+
|
319 |
+
.report-emotion.no_emotion {
|
320 |
+
color: #636e72; /* Серый цвет для нейтрального состояния */
|
321 |
+
}
|
322 |
+
</style>
|
323 |
+
|
324 |
{% endblock %}
|
templates/welcome.html
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base_public.html" %}
|
2 |
+
|
3 |
+
{% block title %}EmotionAnalyzer — Анализ эмоций в тексте и голосе{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="main-container">
|
7 |
+
|
8 |
+
<!-- Hero-секция -->
|
9 |
+
<section class="hero">
|
10 |
+
<div class="hero-content">
|
11 |
+
<h1 class="hero-title">EmotionAnalyzer</h1>
|
12 |
+
<p class="hero-subtitle">
|
13 |
+
ИИ-платформа для глубокого анализа эмоционального состояния по тексту и голосу. Всё просто — введите сообщение или запишите аудио!
|
14 |
+
</p>
|
15 |
+
<div class="cta-buttons">
|
16 |
+
{% if not current_user.is_authenticated %}
|
17 |
+
<a href="{{ url_for('auth_bp.register') }}" class="btn-primary">Начать бесплатно</a>
|
18 |
+
<a href="{{ url_for('auth_bp.login') }}" class="btn-secondary">Войти</a>
|
19 |
+
{% else %}
|
20 |
+
<a href="{{ url_for('index') }}" class="btn-primary">Перейти к анализу</a>
|
21 |
+
{% endif %}
|
22 |
+
</div>
|
23 |
+
</div>
|
24 |
+
</section>
|
25 |
+
|
26 |
+
<!-- Особенности -->
|
27 |
+
<section class="features">
|
28 |
+
<h2 class="section-title">Что умеет EmotionAnalyzer</h2>
|
29 |
+
<div class="features-grid">
|
30 |
+
|
31 |
+
<!-- Текстовый анализ -->
|
32 |
+
<div class="feature-card">
|
33 |
+
<i class="fas fa-comment-dots feature-icon text"></i>
|
34 |
+
<h3>Эмоции в тексте</h3>
|
35 |
+
<p>Анализируйте эмоции в сообщениях, письмах, соцсетях. Распознаём: радость, грусть, страх, злобу, удивление, отвращение.</p>
|
36 |
+
</div>
|
37 |
+
|
38 |
+
<!-- Голосовой анализ -->
|
39 |
+
<div class="feature-card">
|
40 |
+
<i class="fas fa-microphone feature-icon voice"></i>
|
41 |
+
<h3>Голос и интонация</h3>
|
42 |
+
<p>Анализ эмоций по аудиозаписям: выявление интонационных паттернов и эмоционального фона по голосу.</p>
|
43 |
+
</div>
|
44 |
+
|
45 |
+
<!-- Визуализация -->
|
46 |
+
<div class="feature-card">
|
47 |
+
<i class="fas fa-chart-line feature-icon chart"></i>
|
48 |
+
<h3>Графики и отчёты</h3>
|
49 |
+
<p>Просматривайте результаты в виде диаграмм, сравнивайте эмоции в динамике, экспортируйте PDF-отчёты.</p>
|
50 |
+
</div>
|
51 |
+
|
52 |
+
<!-- Пользовательский опыт -->
|
53 |
+
<div class="feature-card">
|
54 |
+
<i class="fas fa-brain feature-icon ai"></i>
|
55 |
+
<h3>ИИ с обучением</h3>
|
56 |
+
<p>Нейросеть адаптируется под ваши данные и стиль общения для ещё более точного анализа эмоций.</p>
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
</section>
|
60 |
+
|
61 |
+
<!-- Ценности -->
|
62 |
+
<section class="values">
|
63 |
+
<h2 class="section-title">Почему выбирают нас</h2>
|
64 |
+
<div class="advantages-grid">
|
65 |
+
<div class="advantage-card">
|
66 |
+
<i class="fas fa-lock icon-primary"></i>
|
67 |
+
<h4>Конфиденциальность</h4>
|
68 |
+
<p>Все данные защищены. Ничего не хранится без вашего разрешения.</p>
|
69 |
+
</div>
|
70 |
+
<div class="advantage-card">
|
71 |
+
<i class="fas fa-rocket icon-success"></i>
|
72 |
+
<h4>Быстрота</h4>
|
73 |
+
<p>Результаты анализа — уже через 2 секунды после отправки.</p>
|
74 |
+
</div>
|
75 |
+
<div class="advantage-card">
|
76 |
+
<i class="fas fa-globe icon-info"></i>
|
77 |
+
<h4>Доступ отовсюду</h4>
|
78 |
+
<p>Работает на телефоне, планшете, ноутбуке. Без установки.</p>
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
</section>
|
82 |
+
|
83 |
+
<!-- CTA -->
|
84 |
+
<section class="cta-final">
|
85 |
+
<div class="cta-final-content">
|
86 |
+
<h2>Попробуйте EmotionAnalyzer прямо сейчас</h2>
|
87 |
+
<p>Быстрый, точный и удобный анализ эмоций в одном клике</p>
|
88 |
+
{% if not current_user.is_authenticated %}
|
89 |
+
<a href="{{ url_for('auth_bp.register') }}" class="btn-primary btn-lg">Создать аккаунт</a>
|
90 |
+
{% else %}
|
91 |
+
<a href="{{ url_for('index') }}" class="btn-primary btn-lg">Начать анализ</a>
|
92 |
+
{% endif %}
|
93 |
+
</div>
|
94 |
+
</section>
|
95 |
+
|
96 |
+
</div>
|
97 |
+
{% endblock %}
|