Spaces:
Runtime error
Runtime error
Upload 34 files
Browse files- .gitattributes +0 -33
- .gitignore +3 -0
- .idea/.gitignore +3 -0
- .idea/emotion_analysis_system.iml +10 -0
- .idea/inspectionProfiles/Project_Default.xml +6 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +10 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +6 -0
- .idea/workspace.xml +91 -0
- READme.txt +16 -0
- __pycache__/auth.cpython-311.pyc +0 -0
- __pycache__/extensions.cpython-311.pyc +0 -0
- __pycache__/forms.cpython-311.pyc +0 -0
- __pycache__/models.cpython-311.pyc +0 -0
- __pycache__/profile.cpython-311.pyc +0 -0
- app.py +475 -0
- auth.py +55 -0
- config.py +0 -0
- extensions.py +5 -0
- forms.py +22 -0
- instance/chats.db +0 -0
- models.py +45 -0
- profile.py +57 -0
- requirements.txt +14 -0
- static/script.js +329 -0
- static/styles.css +836 -0
- templates/auth/login.html +40 -0
- templates/auth/register.html +54 -0
- templates/base.html +71 -0
- templates/errors/401.html +10 -0
- templates/errors/404.html +10 -0
- templates/index.html +76 -0
- templates/profile.html +88 -0
.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 |
+
"associatedIndex": 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 %}
|