sabarish commited on
Commit
e45ddff
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ .env
3
+ .pytest_cache/
4
+ uploads/
5
+ *.sqlite
6
+ *.sqlite3
7
+ instance/
8
+ test_out.pdf
9
+ *.log
README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI-Based Sentiment Analysis System
2
+
3
+ ## Prerequisites
4
+ - Python 3.8+
5
+ - MySQL Server
6
+
7
+ ## Setup Instructions
8
+
9
+ 1. **Clone or Extract the Repository**
10
+ 2. **Install Dependencies**
11
+ ```bash
12
+ pip install -r requirements.txt
13
+ ```
14
+ 3. **Download NLTK Data**
15
+ Run the following Python script to download necessary NLTK corpora:
16
+ ```python
17
+ import nltk
18
+ nltk.download('stopwords')
19
+ nltk.download('punkt')
20
+ ```
21
+ 4. **Database Setup**
22
+ - Ensure MySQL is running.
23
+ - Run the schema script or let SQLAlchemy create the tables automatically.
24
+ - You can run the script manually: `mysql -u root -p < sql/schema.sql` (Adjust credentials as needed).
25
+ - If using a different database name or credentials, update the `.env` file or `config.py`.
26
+ 5. **Run the Application**
27
+ ```bash
28
+ python app.py
29
+ ```
30
+ The application will start on `http://127.0.0.1:5000`.
31
+
32
+ ## Default Admin Account
33
+ - **Email:** admin@example.com
34
+ - **Password:** admin123
35
+ *(Note: If the DB is populated from `schema.sql`, this account is auto-created. Otherwise, you can register a new account from the UI and change the role in the database manually).*
app.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask
3
+ from markupsafe import Markup
4
+ from config import Config
5
+ from models import db, bcrypt, login_manager
6
+ from flask_mail import Mail
7
+
8
+ # Initialize extensions outside create_app so blueprints can import them if needed
9
+ # (Though best practice is usually to init in create_app, we'll keep the existing structure and add mail)
10
+ mail = Mail()
11
+
12
+ def create_app(config_class=Config):
13
+ app = Flask(__name__)
14
+ app.config.from_object(config_class)
15
+
16
+ # Mail configuration explicitly set to prevent crashing if config.py is sparse
17
+ app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
18
+ app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587))
19
+ app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
20
+ app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your_email@gmail.com')
21
+ app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your_app_password')
22
+
23
+ # Initialize extensions
24
+ db.init_app(app)
25
+ bcrypt.init_app(app)
26
+ login_manager.init_app(app)
27
+ mail.init_app(app)
28
+
29
+ login_manager.login_view = 'auth.login'
30
+ login_manager.login_message_category = 'info'
31
+
32
+ # Create upload directory if it doesn't exist
33
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
34
+
35
+ # Register blueprints (we will create these next)
36
+ from routes.auth import auth_bp
37
+ from routes.dashboard import dashboard_bp
38
+ from routes.feedback import feedback_bp
39
+ from routes.reports import reports_bp
40
+ from routes.upload import upload_bp
41
+ from routes.admin import admin_bp
42
+ from routes.profile import profile_bp
43
+ from routes.training import training_bp
44
+
45
+ app.register_blueprint(auth_bp, url_prefix='/auth')
46
+ app.register_blueprint(dashboard_bp, url_prefix='/dashboard')
47
+ app.register_blueprint(feedback_bp, url_prefix='/feedback')
48
+ app.register_blueprint(reports_bp, url_prefix='/reports')
49
+ app.register_blueprint(upload_bp, url_prefix='/upload')
50
+ app.register_blueprint(admin_bp, url_prefix='/admin')
51
+ app.register_blueprint(profile_bp, url_prefix='/profile')
52
+ app.register_blueprint(training_bp, url_prefix='/admin')
53
+
54
+ # Root route redirects to dashboard or login
55
+ @app.route('/')
56
+ def index():
57
+ from flask import redirect, url_for
58
+ from flask_login import current_user
59
+ if current_user.is_authenticated:
60
+ return redirect(url_for('dashboard.index'))
61
+ return redirect(url_for('auth.login'))
62
+
63
+ # Custom Jinja filters
64
+ @app.template_filter('sentiment_color')
65
+ def sentiment_color(sentiment):
66
+ return {
67
+ 'Positive': 'text-green-500',
68
+ 'Negative': 'text-red-500',
69
+ 'Neutral': 'text-gray-500'
70
+ }.get(sentiment, 'text-gray-500')
71
+
72
+ @app.template_filter('sentiment_badge')
73
+ def sentiment_badge(sentiment):
74
+ colors = {
75
+ 'Positive': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
76
+ 'Negative': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
77
+ 'Neutral': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
78
+ }
79
+ classes = colors.get(sentiment, colors['Neutral'])
80
+ return Markup(f'<span class="px-2 py-1 text-xs font-medium rounded-full {classes}">{sentiment}</span>')
81
+
82
+ return app
83
+
84
+ if __name__ == '__main__':
85
+ app = create_app()
86
+ with app.app_context():
87
+ # Create database tables if they don't exist
88
+ db.create_all()
89
+ app.run(debug=True, port=5000)
config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ basedir = os.path.abspath(os.path.dirname(__file__))
5
+ load_dotenv(os.path.join(basedir, '.env'))
6
+
7
+ class Config:
8
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess-super-secret'
9
+
10
+ # MySQL Connection String Example: mysql+pymysql://user:password@localhost/dbname
11
+ # Fallback to SQLite if MySQL is not configured for easy testing
12
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
13
+ 'mysql+pymysql://root:root@localhost/sentiment_db'
14
+
15
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
16
+
17
+ # File upload handling
18
+ UPLOAD_FOLDER = os.path.join(basedir, 'uploads')
19
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16 MB max limit
20
+ ALLOWED_EXTENSIONS = {'csv', 'xls', 'xlsx'}
create_admin.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import create_app
2
+ from models import db, User
3
+
4
+ app = create_app()
5
+
6
+ with app.app_context():
7
+ # Check if admin exists
8
+ admin_user = User.query.filter_by(role='Admin').first()
9
+
10
+ if not admin_user:
11
+ print("Creating default admin account...")
12
+ admin = User(name="System Administrator", email="admin@neurosent.com", role="Admin", department="IT")
13
+ admin.set_password("admin123")
14
+ db.session.add(admin)
15
+ db.session.commit()
16
+ print("Admin account created! Email: admin@neurosent.com | Pass: admin123")
17
+ else:
18
+ # Update existing admin password to be sure
19
+ print(f"Admin already exists: {admin_user.email}")
20
+ admin_user.set_password("admin123")
21
+ db.session.commit()
22
+ print("Admin password reset to: admin123")
data/sample_data.csv ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "id","review_text","department","date"
2
+ 1,"The new course material is absolutely fantastic and very easy to follow!","Computer Science","2023-11-01"
3
+ 2,"I am confused by the recent grading system, it doesn't make any sense.","Mathematics","2023-11-02"
4
+ 3,"The library facilities are okay, but the wifi is sometimes slow.","General","2023-11-03"
5
+ 4,"Excellent support from the teaching staff during project work.","Engineering","2023-11-04"
6
+ 5,"Terrible experience with the online portal, it crashed during my exam.","IT Support","2023-11-05"
7
+ 6,"I love the campus environment, very peaceful and great for studying.","General","2023-11-06"
8
+ 7,"The food in the cafeteria is too expensive and lacks variety.","Facilities","2023-11-07"
9
+ 8,"Professor Smith's lectures are incredibly inspiring. Highly recommend!","Physics","2023-11-08"
10
+ 9,"The assignment deadlines are unreasonable and stressful.","Management","2023-11-09"
11
+ 10,"Standard procedure followed without any issues.","Administration","2023-11-10"
models.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from flask_bcrypt import Bcrypt
3
+ from flask_login import UserMixin, LoginManager
4
+ from datetime import datetime
5
+
6
+ db = SQLAlchemy()
7
+ bcrypt = Bcrypt()
8
+ login_manager = LoginManager()
9
+
10
+ @login_manager.user_loader
11
+ def load_user(user_id):
12
+ return User.query.get(int(user_id))
13
+
14
+ class User(db.Model, UserMixin):
15
+ __tablename__ = 'users'
16
+ id = db.Column(db.Integer, primary_key=True)
17
+ name = db.Column(db.String(100), nullable=False)
18
+ email = db.Column(db.String(120), unique=True, nullable=False)
19
+ password_hash = db.Column(db.String(255), nullable=False)
20
+ role = db.Column(db.Enum('Admin', 'HOD', 'Staff', 'Student'), nullable=False, default='Student')
21
+ department = db.Column(db.String(100))
22
+ created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
23
+
24
+ feedbacks = db.relationship('Feedback', backref='author', lazy=True)
25
+ uploads = db.relationship('Upload', backref='uploader', lazy=True)
26
+
27
+ def set_password(self, password):
28
+ self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
29
+
30
+ def check_password(self, password):
31
+ return bcrypt.check_password_hash(self.password_hash, password)
32
+
33
+ class Upload(db.Model):
34
+ __tablename__ = 'uploads'
35
+ id = db.Column(db.Integer, primary_key=True)
36
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
37
+ filename = db.Column(db.String(255), nullable=False)
38
+ total_rows = db.Column(db.Integer, default=0)
39
+ processed_rows = db.Column(db.Integer, default=0)
40
+ status = db.Column(db.Enum('Pending', 'Processing', 'Completed', 'Failed'), default='Pending')
41
+ upload_date = db.Column(db.DateTime, default=db.func.current_timestamp())
42
+
43
+ feedbacks = db.relationship('Feedback', backref='upload_source', lazy=True, cascade="all, delete-orphan")
44
+
45
+ class Feedback(db.Model):
46
+ __tablename__ = 'feedback'
47
+ id = db.Column(db.Integer, primary_key=True)
48
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
49
+ upload_id = db.Column(db.Integer, db.ForeignKey('uploads.id'), nullable=True)
50
+ original_text = db.Column(db.Text, nullable=False)
51
+ cleaned_text = db.Column(db.Text)
52
+ sentiment = db.Column(db.Enum('Positive', 'Negative', 'Neutral'), nullable=False)
53
+ sentiment_score = db.Column(db.Float, nullable=False)
54
+ department_category = db.Column(db.String(100))
55
+ created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask>=3.0.0
2
+ Flask-SQLAlchemy>=3.1.1
3
+ Flask-Bcrypt>=1.0.1
4
+ Flask-Login>=0.6.3
5
+ pymysql>=1.1.0
6
+ pandas>=2.1.3
7
+ textblob>=0.17.1
8
+ nltk>=3.8.1
9
+ openpyxl>=3.1.2
10
+ xlrd>=2.0.1
11
+ reportlab>=4.0.7
12
+ python-dotenv>=1.0.0
13
+ Werkzeug>=3.0.1
14
+ transformers>=4.35.0
15
+ torch>=2.1.0
16
+ scipy>=1.11.0
reset_db.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from app import create_app
3
+ from models import db, User
4
+
5
+ app = create_app()
6
+
7
+ with app.app_context():
8
+ print("Dropping all existing tables...")
9
+ db.drop_all()
10
+ print("Creating all tables based on new models...")
11
+ db.create_all()
12
+
13
+ print("Seeding default Admin user...")
14
+ admin = User(
15
+ name="System Admin",
16
+ email="admin@neurosent.com",
17
+ role="Admin",
18
+ department="System Operations"
19
+ )
20
+ admin.set_password("admin123")
21
+ db.session.add(admin)
22
+ db.session.commit()
23
+ print("Database reset successfully! Default admin created (admin@neurosent.com / admin123).")
routes/admin.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app
2
+ from flask_login import login_required, current_user
3
+ import random
4
+ import string
5
+ from threading import Thread
6
+ from flask_mail import Message
7
+ from models import db, User
8
+
9
+ admin_bp = Blueprint('admin', __name__)
10
+
11
+ def send_async_email(app, msg):
12
+ with app.app_context():
13
+ try:
14
+ from app import mail
15
+ mail.send(msg)
16
+ print(f"Background email sent to {msg.recipients}")
17
+ except Exception as e:
18
+ print(f"Failed to send background email: {e}")
19
+
20
+ @admin_bp.route('/create-user', methods=['GET', 'POST'])
21
+ @login_required
22
+ def create_user():
23
+ # Security: Ensure ONLY Admins can access this page
24
+ if current_user.role != 'Admin':
25
+ flash('Access denied. You do not have permission to view this page.', 'danger')
26
+ return redirect(url_for('dashboard.index'))
27
+
28
+ if request.method == 'POST':
29
+ name = request.form.get('name')
30
+ email = request.form.get('email')
31
+ password = request.form.get('password')
32
+ role = request.form.get('role')
33
+ department = request.form.get('department')
34
+
35
+ # Validation
36
+ if not all([name, email, password, role]):
37
+ flash('Please fill in all required fields.', 'warning')
38
+ return redirect(url_for('admin.create_user'))
39
+
40
+ # Check if user already exists
41
+ if User.query.filter_by(email=email).first():
42
+ flash('A user with that email already exists.', 'danger')
43
+ return redirect(url_for('admin.create_user'))
44
+
45
+ # Create user
46
+ try:
47
+ new_user = User(name=name, email=email, role=role, department=department)
48
+ new_user.set_password(password)
49
+ db.session.add(new_user)
50
+ db.session.commit()
51
+
52
+ # Send Welcome Email asynchronously
53
+ msg = Message(
54
+ "Welcome to NeuroSent - Your Account is Ready",
55
+ recipients=[email]
56
+ )
57
+ msg.body = f"Hello {name},\n\nYour {role} account has been created.\nEmail: {email}\nPassword: {password}\n\nPlease login and change your password immediately."
58
+ msg.html = f"""
59
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 10px;">
60
+ <h2 style="color: #4f46e5;">Welcome to NeuroSent Hub!</h2>
61
+ <p>Hello <strong>{name}</strong>,</p>
62
+ <p>An administrator has provisioned a new <strong>{role}</strong> account for you.</p>
63
+ <div style="background-color: #f3f4f6; padding: 15px; border-radius: 5px; margin: 20px 0;">
64
+ <p style="margin: 0;"><strong>Email:</strong> {email}</p>
65
+ <p style="margin: 5px 0 0 0;"><strong>Temporary Password:</strong> {password}</p>
66
+ </div>
67
+ <p style="color: #ef4444; font-size: 0.9em;">For security reasons, please login and update your password immediately.</p>
68
+ <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;">
69
+ <p style="font-size: 0.8em; color: #6b7280;">This is an automated message. Please do not reply.</p>
70
+ </div>
71
+ """
72
+
73
+ Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start()
74
+
75
+ flash(f'Successfully created {role} account for {name}. Welcome email queued.', 'success')
76
+ return redirect(url_for('admin.create_user'))
77
+ except Exception as e:
78
+ db.session.rollback()
79
+ flash(f'An error occurred: {str(e)}', 'danger')
80
+
81
+ return render_template('admin/create_user.html')
82
+
83
+ @admin_bp.route('/manage-users', methods=['GET'])
84
+ @login_required
85
+ def manage_users():
86
+ if current_user.role != 'Admin':
87
+ flash('Access denied. Administrator privileges required.', 'danger')
88
+ return redirect(url_for('dashboard.index'))
89
+
90
+ users = User.query.all()
91
+ return render_template('admin/manage_users.html', users=users)
92
+
93
+ @admin_bp.route('/reset-password/<int:user_id>', methods=['POST'])
94
+ @login_required
95
+ def reset_password(user_id):
96
+ if current_user.role != 'Admin':
97
+ flash('Access denied. Administrator privileges required.', 'danger')
98
+ return redirect(url_for('dashboard.index'))
99
+
100
+ user = User.query.get_or_404(user_id)
101
+
102
+ custom_password = request.form.get('new_password', '').strip()
103
+
104
+ if custom_password:
105
+ new_password = custom_password
106
+ msg = f'Password for {user.email} has been manually changed successfully.'
107
+ else:
108
+ # Generate random 8-char password if none provided
109
+ new_password = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
110
+ msg = f'Password for {user.email} auto-generated successfully. New password: {new_password}'
111
+
112
+ try:
113
+ user.set_password(new_password)
114
+ db.session.commit()
115
+
116
+ # Send Notification Email asynchronously
117
+ email_msg = Message(
118
+ "NeuroSent Security: Password Reset",
119
+ recipients=[user.email]
120
+ )
121
+ email_msg.html = f"""
122
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 10px;">
123
+ <h2 style="color: #f59e0b;">Security Alert: Password Changed</h2>
124
+ <p>Hello <strong>{user.name}</strong>,</p>
125
+ <p>Your password has been successfully reset by an administrator.</p>
126
+ <div style="background-color: #fef3c7; padding: 15px; border-radius: 5px; border-left: 4px solid #f59e0b; margin: 20px 0;">
127
+ <p style="margin: 0;"><strong>New Password:</strong> {new_password}</p>
128
+ </div>
129
+ <p>If you did not request this change, please contact IT support immediately.</p>
130
+ </div>
131
+ """
132
+ Thread(target=send_async_email, args=(current_app._get_current_object(), email_msg)).start()
133
+
134
+ flash(msg + " An email notification has been queued.", 'success')
135
+ except Exception as e:
136
+ db.session.rollback()
137
+ flash(f'Error resetting password: {str(e)}', 'danger')
138
+
139
+ return redirect(url_for('admin.manage_users'))
140
+
141
+ @admin_bp.route('/delete-user/<int:user_id>', methods=['POST'])
142
+ @login_required
143
+ def delete_user(user_id):
144
+ if current_user.role != 'Admin':
145
+ flash('Access denied. Administrator privileges required.', 'danger')
146
+ return redirect(url_for('dashboard.index'))
147
+
148
+ user = User.query.get_or_404(user_id)
149
+
150
+ # Prevent admin from deleting themselves
151
+ if user.id == current_user.id:
152
+ flash('You cannot delete your own admin account.', 'danger')
153
+ return redirect(url_for('admin.manage_users'))
154
+
155
+ try:
156
+ db.session.delete(user)
157
+ db.session.commit()
158
+ flash(f'User {user.email} has been permanently deleted.', 'success')
159
+ except Exception as e:
160
+ db.session.rollback()
161
+ flash(f'Error deleting user: {str(e)}', 'danger')
162
+
163
+ return redirect(url_for('admin.manage_users'))
routes/auth.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, redirect, url_for, flash, request
2
+ from flask_login import login_user, logout_user, current_user
3
+ from models import db, User
4
+ from urllib.parse import urlsplit
5
+
6
+ auth_bp = Blueprint('auth', __name__)
7
+
8
+ @auth_bp.route('/login', methods=['GET', 'POST'])
9
+ def login():
10
+ if current_user.is_authenticated:
11
+ return redirect(url_for('dashboard.index'))
12
+
13
+ if request.method == 'POST':
14
+ email = request.form.get('email')
15
+ password = request.form.get('password')
16
+
17
+ user = User.query.filter_by(email=email).first()
18
+ if user and user.check_password(password):
19
+ login_user(user)
20
+ flash('Logged in successfully.', 'success')
21
+
22
+ # Redirect to next page if exists
23
+ next_page = request.args.get('next')
24
+ if not next_page or urlsplit(next_page).netloc != '':
25
+ next_page = url_for('dashboard.index')
26
+ return redirect(next_page)
27
+ else:
28
+ flash('Invalid email or password.', 'danger')
29
+
30
+ return render_template('auth/login.html')
31
+
32
+ @auth_bp.route('/register', methods=['GET', 'POST'])
33
+ def register():
34
+ if current_user.is_authenticated:
35
+ return redirect(url_for('dashboard.index'))
36
+
37
+ if request.method == 'POST':
38
+ name = request.form.get('name')
39
+ email = request.form.get('email')
40
+ password = request.form.get('password')
41
+ role = request.form.get('role', 'Student') # Default to Student
42
+ department = request.form.get('department')
43
+
44
+ # Check if user already exists
45
+ if User.query.filter_by(email=email).first():
46
+ flash('Email already registered. Please login.', 'danger')
47
+ return redirect(url_for('auth.login'))
48
+
49
+ new_user = User(name=name, email=email, role=role, department=department)
50
+ new_user.set_password(password)
51
+ db.session.add(new_user)
52
+ db.session.commit()
53
+
54
+ flash('Registration successful. You can now login.', 'success')
55
+ return redirect(url_for('auth.login'))
56
+
57
+ return render_template('auth/register.html')
58
+
59
+ @auth_bp.route('/logout')
60
+ def logout():
61
+ logout_user()
62
+ flash('You have been logged out.', 'info')
63
+ return redirect(url_for('auth.login'))
routes/dashboard.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, jsonify
2
+ from flask_login import login_required, current_user
3
+ from models import Feedback
4
+ from sqlalchemy import func
5
+ from sklearn.feature_extraction.text import TfidfVectorizer
6
+ import pandas as pd
7
+
8
+ dashboard_bp = Blueprint('dashboard', __name__)
9
+
10
+ @dashboard_bp.route('/')
11
+ @login_required
12
+ def index():
13
+ # Base query
14
+ query = Feedback.query
15
+
16
+ # Filter based on role
17
+ if current_user.role == 'Student' or current_user.role == 'Staff':
18
+ query = query.filter_by(user_id=current_user.id)
19
+ elif current_user.role == 'HOD':
20
+ query = query.filter_by(department_category=current_user.department)
21
+ # Admin sees all (no filter needed)
22
+
23
+ total_count = query.count()
24
+
25
+ # Sentiment distribution
26
+ positive = query.filter_by(sentiment='Positive').count()
27
+ negative = query.filter_by(sentiment='Negative').count()
28
+ neutral = query.filter_by(sentiment='Neutral').count()
29
+
30
+ # Get percentages
31
+ pct_pos = round((positive / total_count * 100) if total_count > 0 else 0, 1)
32
+ pct_neg = round((negative / total_count * 100) if total_count > 0 else 0, 1)
33
+ pct_neu = round((neutral / total_count * 100) if total_count > 0 else 0, 1)
34
+
35
+ # Department distribution
36
+ dept_distribution = dict(query.with_entities(
37
+ Feedback.department_category,
38
+ func.count(Feedback.id)
39
+ ).group_by(Feedback.department_category).all())
40
+
41
+ # Get recent feedbacks
42
+ recent_feedback = query.order_by(Feedback.created_at.desc()).limit(10).all()
43
+
44
+ # Pass the appropriate template based on role
45
+ template_name = 'dashboard/student.html'
46
+ if current_user.role == 'Admin':
47
+ template_name = 'dashboard/admin.html'
48
+ elif current_user.role in ['HOD', 'Staff']:
49
+ template_name = 'dashboard/hod_staff.html'
50
+
51
+ return render_template(
52
+ template_name,
53
+ total_count=total_count,
54
+ positive=positive,
55
+ negative=negative,
56
+ neutral=neutral,
57
+ pct_pos=pct_pos,
58
+ pct_neg=pct_neg,
59
+ pct_neu=pct_neu,
60
+ dept_distribution=dept_distribution,
61
+ recent_feedback=recent_feedback
62
+ )
63
+
64
+ @dashboard_bp.route('/api/dashboard_data')
65
+ @login_required
66
+ def get_dashboard_data():
67
+ """
68
+ AJAX endpoint to fetch filtered dashboard statistics based on date ranges
69
+ without reloading the entire page or restarting Vanta.js
70
+ """
71
+ from flask import request
72
+ from datetime import datetime
73
+
74
+ start_date_str = request.args.get('start_date')
75
+ end_date_str = request.args.get('end_date')
76
+
77
+ query = Feedback.query
78
+ if current_user.role == 'Student' or current_user.role == 'Staff':
79
+ query = query.filter_by(user_id=current_user.id)
80
+ elif current_user.role == 'HOD':
81
+ query = query.filter_by(department_category=current_user.department)
82
+
83
+ try:
84
+ # Apply date filters if provided
85
+ if start_date_str:
86
+ start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
87
+ query = query.filter(Feedback.created_at >= start_date)
88
+ if end_date_str:
89
+ # Add time to include the whole end date
90
+ end_date = datetime.strptime(f"{end_date_str} 23:59:59", '%Y-%m-%d %H:%M:%S')
91
+ query = query.filter(Feedback.created_at <= end_date)
92
+
93
+ total_count = query.count()
94
+ positive = query.filter_by(sentiment='Positive').count()
95
+ negative = query.filter_by(sentiment='Negative').count()
96
+ neutral = query.filter_by(sentiment='Neutral').count()
97
+
98
+ pct_pos = round((positive / total_count * 100) if total_count > 0 else 0, 1)
99
+ pct_neg = round((negative / total_count * 100) if total_count > 0 else 0, 1)
100
+ pct_neu = round((neutral / total_count * 100) if total_count > 0 else 0, 1)
101
+
102
+ dept_raw = query.with_entities(
103
+ Feedback.department_category,
104
+ func.count(Feedback.id)
105
+ ).group_by(Feedback.department_category).all()
106
+
107
+ dept_labels = []
108
+ dept_counts = []
109
+ for name, count in dept_raw:
110
+ dept_labels.append(name if name else "Uncategorized")
111
+ dept_counts.append(count)
112
+
113
+ return jsonify({
114
+ 'success': True,
115
+ 'total_count': total_count,
116
+ 'positive': positive, 'negative': negative, 'neutral': neutral,
117
+ 'pct_pos': pct_pos, 'pct_neg': pct_neg, 'pct_neu': pct_neu,
118
+ 'dept_labels': dept_labels,
119
+ 'dept_counts': dept_counts
120
+ })
121
+ except Exception as e:
122
+ print(f"AJAX Data Error: {e}")
123
+ return jsonify({'success': False, 'error': str(e)})
124
+
125
+ @dashboard_bp.route('/api/keywords')
126
+ @login_required
127
+ def get_keywords():
128
+ """
129
+ Asynchronous endpoint to extract trending keywords from negative feedback
130
+ using Term Frequency-Inverse Document Frequency (TF-IDF).
131
+ This doesn't block the initial page load.
132
+ """
133
+ # Only analyze negative feedback to find "pain points", or all feedback if preferred.
134
+ # We will analyze all, but you can filter by sentiment='Negative'
135
+ query = Feedback.query
136
+ if current_user.role == 'Student' or current_user.role == 'Staff':
137
+ query = query.filter_by(user_id=current_user.id)
138
+ elif current_user.role == 'HOD':
139
+ query = query.filter_by(department_category=current_user.department)
140
+
141
+ feedbacks = query.all()
142
+
143
+ if len(feedbacks) < 5:
144
+ return jsonify({"keywords": []}) # Not enough data
145
+
146
+ documents = [f.cleaned_text for f in feedbacks if f.cleaned_text]
147
+
148
+ if not documents:
149
+ return jsonify({"keywords": []})
150
+
151
+ try:
152
+ # Extract top 30 meaningful words, ignoring standard english stop words
153
+ vectorizer = TfidfVectorizer(stop_words='english', max_features=30)
154
+ tfidf_matrix = vectorizer.fit_transform(documents)
155
+
156
+ # Sum the TF-IDF scores for each word across all documents
157
+ word_scores = tfidf_matrix.sum(axis=0).A1
158
+ words = vectorizer.get_feature_names_out()
159
+
160
+ # wordcloud2.js expects an array of [word, size] arrays.
161
+ # We multiply the TF-IDF score by a factor (e.g., 20) to make the font size visually impactful
162
+ keywords = []
163
+ for word, score in zip(words, word_scores):
164
+ # Scale score for visual size mapping, ensure minimum size
165
+ size = max(float(score) * 20, 10)
166
+ keywords.append([str(word), size])
167
+
168
+ # Sort by size descending
169
+ keywords.sort(key=lambda x: x[1], reverse=True)
170
+
171
+ return jsonify({"keywords": keywords})
172
+ except Exception as e:
173
+ print(f"Keyword UI error: {e}")
174
+ return jsonify({"keywords": []})
routes/feedback.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, redirect, url_for, flash, render_template
2
+ from flask_login import login_required, current_user
3
+ from models import db, Feedback
4
+ from utils.nlp_utils import preprocess_text, analyze_sentiment
5
+
6
+ feedback_bp = Blueprint('feedback', __name__)
7
+
8
+ @feedback_bp.route('/submit', methods=['GET', 'POST'])
9
+ @login_required
10
+ def submit():
11
+ if request.method == 'POST':
12
+ original_text = request.form.get('text')
13
+ department = request.form.get('department') or current_user.department
14
+
15
+ if not original_text:
16
+ flash('Comment cannot be empty.', 'danger')
17
+ return redirect(url_for('feedback.submit'))
18
+
19
+ cleaned_text = preprocess_text(original_text)
20
+ sentiment, score = analyze_sentiment(cleaned_text)
21
+
22
+ new_feedback = Feedback(
23
+ user_id=current_user.id,
24
+ original_text=original_text,
25
+ cleaned_text=cleaned_text,
26
+ sentiment=sentiment,
27
+ sentiment_score=score,
28
+ department_category=department
29
+ )
30
+
31
+ db.session.add(new_feedback)
32
+ db.session.commit()
33
+
34
+ flash('Feedback submitted successfully. Thank you!', 'success')
35
+ return redirect(url_for('dashboard.index'))
36
+
37
+ return render_template('feedback/submit.html')
routes/profile.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template
2
+ from flask_login import login_required, current_user
3
+ from models import Feedback
4
+
5
+ profile_bp = Blueprint('profile', __name__)
6
+
7
+ @profile_bp.route('/', methods=['GET'])
8
+ @login_required
9
+ def index():
10
+ # Gather statistics for the user
11
+ total_feedbacks = Feedback.query.filter_by(user_id=current_user.id).count()
12
+
13
+ # Calculate their average sentiment score (if they've submitted any)
14
+ avg_score = 0
15
+ if total_feedbacks > 0:
16
+ feedbacks = Feedback.query.filter_by(user_id=current_user.id).all()
17
+ total_score = sum(f.sentiment_score for f in feedbacks)
18
+ avg_score = round(total_score / total_feedbacks, 2)
19
+
20
+ return render_template(
21
+ 'dashboard/profile.html',
22
+ total_feedbacks=total_feedbacks,
23
+ avg_score=avg_score
24
+ )
routes/reports.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from datetime import datetime
3
+ import pandas as pd
4
+ from flask import Blueprint, send_file, current_app, redirect, url_for, flash, make_response
5
+ from flask_login import login_required, current_user
6
+ from models import Feedback
7
+ from utils.decorators import requires_roles
8
+
9
+ # ReportLab imports for PDF generation
10
+ from reportlab.lib.pagesizes import letter
11
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
12
+ from reportlab.lib.styles import getSampleStyleSheet
13
+ from reportlab.lib import colors
14
+
15
+ reports_bp = Blueprint('reports', __name__)
16
+
17
+ def get_filtered_query():
18
+ """Helper to get feedback based on role"""
19
+ query = Feedback.query
20
+ if current_user.role == 'Student' or current_user.role == 'Staff':
21
+ query = query.filter_by(user_id=current_user.id)
22
+ elif current_user.role == 'HOD':
23
+ query = query.filter_by(department_category=current_user.department)
24
+ return query
25
+
26
+ @reports_bp.route('/excel')
27
+ @login_required
28
+ @requires_roles('Admin', 'HOD', 'Staff')
29
+ def download_excel():
30
+ feedbacks = get_filtered_query().all()
31
+ if not feedbacks:
32
+ flash('No data available to generate report.', 'warning')
33
+ return redirect(url_for('dashboard.index'))
34
+
35
+ # Prepare data for pandas
36
+ data = []
37
+ for f in feedbacks:
38
+ data.append({
39
+ 'ID': f.id,
40
+ 'Date': f.created_at.strftime('%Y-%m-%d %H:%M'),
41
+ 'Department/Category': f.department_category,
42
+ 'Original Text': f.original_text,
43
+ 'Cleaned Text': f.cleaned_text,
44
+ 'Sentiment': f.sentiment,
45
+ 'Score': f.sentiment_score
46
+ })
47
+
48
+ df = pd.DataFrame(data)
49
+
50
+ # Save to BytesIO
51
+ output = io.BytesIO()
52
+ with pd.ExcelWriter(output, engine='openpyxl') as writer:
53
+ df.to_excel(writer, index=False, sheet_name='Sentiment Report')
54
+
55
+ output.seek(0)
56
+
57
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
58
+ filename = f"Sentiment_Report_{timestamp}.xlsx"
59
+
60
+ response = make_response(output.getvalue())
61
+ response.headers['Content-Disposition'] = f'attachment; filename={filename}'
62
+ response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
63
+ return response
64
+
65
+ @reports_bp.route('/pdf')
66
+ @login_required
67
+ @requires_roles('Admin', 'HOD', 'Staff')
68
+ def download_pdf():
69
+ feedbacks = get_filtered_query().order_by(Feedback.created_at.desc()).limit(100).all() # Limit for PDF readability
70
+ if not feedbacks:
71
+ flash('No data available to generate report.', 'warning')
72
+ return redirect(url_for('dashboard.index'))
73
+
74
+ output = io.BytesIO()
75
+ doc = SimpleDocTemplate(output, pagesize=letter)
76
+ elements = []
77
+
78
+ styles = getSampleStyleSheet()
79
+ title_style = styles['Heading1']
80
+ normal_style = styles['Normal']
81
+
82
+ elements.append(Paragraph("AI-Based Sentiment Analysis Report", title_style))
83
+ elements.append(Spacer(1, 12))
84
+ elements.append(Paragraph(f"Generated by: {current_user.name} ({current_user.role})", normal_style))
85
+ elements.append(Paragraph(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}", normal_style))
86
+ elements.append(Spacer(1, 24))
87
+
88
+ # Table data
89
+ data = [['ID', 'Date', 'Sentiment', 'Score', 'Department']]
90
+ for f in feedbacks:
91
+ data.append([
92
+ str(f.id),
93
+ f.created_at.strftime('%Y-%m-%d'),
94
+ f.sentiment,
95
+ f"{f.sentiment_score:.2f}",
96
+ str(f.department_category or 'N/A')
97
+ ])
98
+
99
+ # Table styling
100
+ t = Table(data)
101
+ t.setStyle(TableStyle([
102
+ ('BACKGROUND', (0,0), (-1,0), colors.grey),
103
+ ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
104
+ ('ALIGN', (0,0), (-1,-1), 'CENTER'),
105
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
106
+ ('BOTTOMPADDING', (0,0), (-1,0), 12),
107
+ ('BACKGROUND', (0,1), (-1,-1), colors.beige),
108
+ ('GRID', (0,0), (-1,-1), 1, colors.black)
109
+ ]))
110
+
111
+ elements.append(t)
112
+ doc.build(elements)
113
+
114
+ output.seek(0)
115
+
116
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
117
+ filename = f"Sentiment_Report_{timestamp}.pdf"
118
+
119
+ response = make_response(output.getvalue())
120
+ response.headers['Content-Disposition'] = f'attachment; filename={filename}'
121
+ response.headers['Content-Type'] = 'application/pdf'
122
+ return response
routes/training.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import subprocess
4
+ from flask import Blueprint, render_template, jsonify, current_app, flash, redirect, url_for
5
+ from flask_login import login_required, current_user
6
+
7
+ training_bp = Blueprint('training', __name__)
8
+
9
+ STATUS_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "training_status.json")
10
+ TRAIN_SCRIPT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts", "train_model.py")
11
+ CUSTOM_MODEL_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "custom_model")
12
+
13
+ @training_bp.route('/training')
14
+ @login_required
15
+ def index():
16
+ if current_user.role != 'Admin':
17
+ flash('Access denied. Administrator privileges required.', 'danger')
18
+ return redirect(url_for('dashboard.index'))
19
+
20
+ # Check if a custom model already exists
21
+ has_custom = os.path.exists(CUSTOM_MODEL_DIR)
22
+
23
+ return render_template('admin/training.html', has_custom=has_custom)
24
+
25
+ @training_bp.route('/training/start', methods=['POST'])
26
+ @login_required
27
+ def start_training():
28
+ if current_user.role != 'Admin':
29
+ return jsonify({"success": False, "message": "Access denied"}), 403
30
+
31
+ # Reset the status file
32
+ with open(STATUS_FILE, "w") as f:
33
+ json.dump({"status": "Initializing", "progress": 0, "message": "Starting background trainer..."}, f)
34
+
35
+ # Launch training script as an asynchronous subprocess
36
+ try:
37
+ # Use python executable from sys or environment
38
+ subprocess.Popen(['python', TRAIN_SCRIPT],
39
+ stdout=subprocess.DEVNULL,
40
+ stderr=subprocess.DEVNULL)
41
+ return jsonify({"success": True})
42
+ except Exception as e:
43
+ return jsonify({"success": False, "message": str(e)})
44
+
45
+ @training_bp.route('/training/status')
46
+ @login_required
47
+ def check_status():
48
+ if current_user.role != 'Admin':
49
+ return jsonify({"status": "Error", "message": "Access denied"}), 403
50
+
51
+ if not os.path.exists(STATUS_FILE):
52
+ return jsonify({"status": "Idle", "progress": 0, "message": "Waiting to start..."})
53
+
54
+ try:
55
+ with open(STATUS_FILE, "r") as f:
56
+ data = json.load(f)
57
+ return jsonify(data)
58
+ except Exception:
59
+ return jsonify({"status": "Error", "progress": 0, "message": "Could not read status file."})
routes/upload.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
3
+ from flask_login import login_required, current_user
4
+ from werkzeug.utils import secure_filename
5
+ from models import db, Upload, Feedback
6
+ from utils.file_processor import allowed_file, process_uploaded_file
7
+ from utils.decorators import requires_roles
8
+
9
+ upload_bp = Blueprint('upload', __name__)
10
+
11
+ @upload_bp.route('/', methods=['GET', 'POST'])
12
+ @login_required
13
+ @requires_roles('Admin', 'HOD', 'Staff')
14
+ def index():
15
+ if request.method == 'POST':
16
+ if 'dataset' not in request.files:
17
+ flash('No file part in the request.', 'danger')
18
+ return redirect(request.url)
19
+
20
+ file = request.files['dataset']
21
+ if file.filename == '':
22
+ flash('No selected file.', 'danger')
23
+ return redirect(request.url)
24
+
25
+ if file and allowed_file(file.filename):
26
+ filename = secure_filename(file.filename)
27
+ filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
28
+ file.save(filepath)
29
+
30
+ # Create an upload record
31
+ new_upload = Upload(user_id=current_user.id, filename=filename, status='Processing')
32
+ db.session.add(new_upload)
33
+ db.session.commit()
34
+
35
+ # Process the file (Synchronous for now, ideally background task like Celery)
36
+ selected_column = request.form.get('text_column')
37
+ success, message = process_uploaded_file(filepath, new_upload.id, current_user.id, selected_column)
38
+
39
+ if success:
40
+ flash(message, 'success')
41
+ else:
42
+ flash(f"Error processing file: {message}", 'danger')
43
+
44
+ return redirect(url_for('dashboard.index'))
45
+ else:
46
+ flash('Invalid file type. Allowed: .csv, .xls, .xlsx', 'danger')
47
+
48
+ # GET: View all past uploads
49
+ uploads = Upload.query.order_by(Upload.upload_date.desc()).all()
50
+ return render_template('upload/index.html', uploads=uploads)
51
+
52
+ @upload_bp.route('/delete/<int:upload_id>', methods=['POST'])
53
+ @login_required
54
+ @requires_roles('Admin')
55
+ def delete_upload(upload_id):
56
+ upload = Upload.query.get_or_404(upload_id)
57
+ # The Feedbacks associated will be cascade deleted based on DB schema model
58
+ db.session.delete(upload)
59
+ db.session.commit()
60
+ flash('Upload and associated feedbacks deleted successfully.', 'success')
61
+ return redirect(url_for('upload.index'))
scripts/train_model.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import torch
5
+ from datasets import Dataset
6
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
7
+ # Add root path to access Flask app and db
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ root_dir = os.path.dirname(current_dir)
10
+ sys.path.append(root_dir)
11
+
12
+ from app import create_app
13
+ from models import Feedback
14
+
15
+ MODEL_NAME = "cardiffnlp/twitter-roberta-base-sentiment-latest"
16
+ CUSTOM_MODEL_DIR = os.path.join(root_dir, "custom_model")
17
+ STATUS_FILE = os.path.join(root_dir, "training_status.json")
18
+
19
+ def update_status(status, progress=0, message=""):
20
+ with open(STATUS_FILE, "w") as f:
21
+ json.dump({"status": status, "progress": progress, "message": message}, f)
22
+
23
+ def get_training_data():
24
+ app = create_app()
25
+ with app.app_context():
26
+ # Fetch feedbacks that aren't purely neutral/empty
27
+ feedbacks = Feedback.query.filter(Feedback.sentiment.in_(['Positive', 'Negative'])).all()
28
+
29
+ # Label mapping for CardiffNLP model
30
+ # 0: Negative, 1: Neutral, 2: Positive
31
+ label_map = {'Negative': 0, 'Positive': 2}
32
+
33
+ texts = []
34
+ labels = []
35
+ for f in feedbacks:
36
+ if f.cleaned_text:
37
+ texts.append(f.cleaned_text)
38
+ labels.append(label_map[f.sentiment])
39
+
40
+ return texts, labels
41
+
42
+ def main():
43
+ update_status("Starting", 5, "Extracting data from database...")
44
+
45
+ texts, labels = get_training_data()
46
+
47
+ if len(texts) < 50:
48
+ update_status("Error", 0, "Insufficient data for training. Need at least 50 positive/negative feedback entries.")
49
+ return
50
+
51
+ update_status("Processing", 20, f"Preparing dataset of {len(texts)} entries...")
52
+
53
+ # Load tokenizer
54
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
55
+
56
+ # Create HuggingFace dataset
57
+ dataset_dict = {
58
+ "text": texts,
59
+ "label": labels
60
+ }
61
+ raw_dataset = Dataset.from_dict(dataset_dict)
62
+
63
+ def tokenize_function(examples):
64
+ return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=128)
65
+
66
+ tokenized_dataset = raw_dataset.map(tokenize_function, batched=True)
67
+
68
+ # Split into train/eval
69
+ split_dataset = tokenized_dataset.train_test_split(test_size=0.1)
70
+ train_dataset = split_dataset["train"]
71
+ eval_dataset = split_dataset["test"]
72
+
73
+ update_status("Training", 40, "Downloading weights and initializing neural network...")
74
+
75
+ # We use num_labels=3 because the base model expects 3
76
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=3)
77
+
78
+ training_args = TrainingArguments(
79
+ output_dir="./trainer_logs",
80
+ learning_rate=2e-5,
81
+ per_device_train_batch_size=8,
82
+ per_device_eval_batch_size=8,
83
+ num_train_epochs=2,
84
+ weight_decay=0.01,
85
+ evaluation_strategy="epoch",
86
+ save_strategy="epoch",
87
+ load_best_model_at_end=True,
88
+ )
89
+
90
+ trainer = Trainer(
91
+ model=model,
92
+ args=training_args,
93
+ train_dataset=train_dataset,
94
+ eval_dataset=eval_dataset,
95
+ )
96
+
97
+ update_status("Training", 60, "Fine-tuning model weights... This may take a few minutes.")
98
+ trainer.train()
99
+
100
+ update_status("Saving", 90, "Saving local custom model...")
101
+ # Clean up old directory if exists
102
+ if not os.path.exists(CUSTOM_MODEL_DIR):
103
+ os.makedirs(CUSTOM_MODEL_DIR)
104
+
105
+ model.save_pretrained(CUSTOM_MODEL_DIR)
106
+ tokenizer.save_pretrained(CUSTOM_MODEL_DIR)
107
+
108
+ update_status("Completed", 100, "Successfully trained and exported custom AI model. Application is now using the enhanced AI.")
109
+
110
+ if __name__ == "__main__":
111
+ try:
112
+ main()
113
+ except Exception as e:
114
+ update_status("Error", 0, str(e))
sql/schema.sql ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Create Database
2
+ CREATE DATABASE IF NOT EXISTS sentiment_db;
3
+ USE sentiment_db;
4
+
5
+ -- Users Table
6
+ CREATE TABLE IF NOT EXISTS users (
7
+ id INT AUTO_INCREMENT PRIMARY KEY,
8
+ name VARCHAR(100) NOT NULL,
9
+ email VARCHAR(120) NOT NULL UNIQUE,
10
+ password_hash VARCHAR(255) NOT NULL,
11
+ role ENUM('Admin', 'HOD', 'Staff', 'Student') NOT NULL DEFAULT 'Student',
12
+ department VARCHAR(100),
13
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14
+ );
15
+
16
+ -- Feedback Table (Manual & Extracted)
17
+ CREATE TABLE IF NOT EXISTS feedback (
18
+ id INT AUTO_INCREMENT PRIMARY KEY,
19
+ user_id INT,
20
+ upload_id INT NULL,
21
+ original_text TEXT NOT NULL,
22
+ cleaned_text TEXT,
23
+ sentiment ENUM('Positive', 'Negative', 'Neutral') NOT NULL,
24
+ sentiment_score FLOAT NOT NULL,
25
+ department_category VARCHAR(100),
26
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
27
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
28
+ );
29
+
30
+ -- Uploads Table (Tracking Bulk Uploads)
31
+ CREATE TABLE IF NOT EXISTS uploads (
32
+ id INT AUTO_INCREMENT PRIMARY KEY,
33
+ user_id INT,
34
+ filename VARCHAR(255) NOT NULL,
35
+ total_rows INT DEFAULT 0,
36
+ processed_rows INT DEFAULT 0,
37
+ status ENUM('Pending', 'Processing', 'Completed', 'Failed') DEFAULT 'Pending',
38
+ upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
39
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
40
+ );
41
+
42
+ -- Add foreign key back to feedback
43
+ ALTER TABLE feedback
44
+ ADD CONSTRAINT fk_upload_id
45
+ FOREIGN KEY (upload_id) REFERENCES uploads(id) ON DELETE CASCADE;
46
+
47
+ -- Insert default Admin User (password: admin123)
48
+ -- Hash generated by bcrypt in Python: bcrypt.hashpw(b'admin123', bcrypt.gensalt())
49
+ -- Note: Replace with actual hash below or let application handle first startup
50
+ INSERT INTO users (name, email, password_hash, role, department)
51
+ VALUES ('System Admin', 'admin@example.com', '$2b$12$K8Z7J9vJ.5u9wD9/xS9T.O4a1xY7r/VjN6B2/5g3e4lq5vM1.bBki', 'Admin', 'Management')
52
+ ON DUPLICATE KEY UPDATE name='System Admin';
static/css/style.css ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap');
2
+
3
+ :root {
4
+ --bg-gradient-light: linear-gradient(135deg, #f0f4f8 0%, #e0e7ff 100%);
5
+ --bg-gradient-dark: linear-gradient(135deg, #090e17 0%, #151b29 100%);
6
+ --glass-bg-light: rgba(255, 255, 255, 0.7);
7
+ --glass-border-light: rgba(255, 255, 255, 0.4);
8
+ --glass-bg-dark: rgba(21, 27, 41, 0.65);
9
+ --glass-border-dark: rgba(255, 255, 255, 0.08);
10
+ }
11
+
12
+ body {
13
+ font-family: 'Outfit', sans-serif;
14
+ transition: background-color 0.4s ease-in-out, color 0.4s ease-in-out;
15
+ }
16
+
17
+ body.light {
18
+ background: var(--bg-gradient-light);
19
+ min-height: 100vh;
20
+ }
21
+
22
+ body.dark {
23
+ background: var(--bg-gradient-dark);
24
+ min-height: 100vh;
25
+ }
26
+
27
+ /* Glassmorphism Utilities */
28
+ .glass {
29
+ background: var(--glass-bg-light);
30
+ backdrop-filter: blur(12px);
31
+ -webkit-backdrop-filter: blur(12px);
32
+ border: 1px solid var(--glass-border-light);
33
+ }
34
+
35
+ .dark .glass {
36
+ background: var(--glass-bg-dark);
37
+ border: 1px solid var(--glass-border-dark);
38
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
39
+ }
40
+
41
+ /* Sidebar Custom Scrollbar */
42
+ .sidebar::-webkit-scrollbar {
43
+ width: 5px;
44
+ }
45
+ .sidebar::-webkit-scrollbar-track {
46
+ background: transparent;
47
+ }
48
+ .sidebar::-webkit-scrollbar-thumb {
49
+ background-color: rgba(156, 163, 175, 0.4);
50
+ border-radius: 10px;
51
+ }
52
+ .sidebar::-webkit-scrollbar-thumb:hover {
53
+ background-color: rgba(156, 163, 175, 0.7);
54
+ }
55
+
56
+ /* Card Hover Animations & Glow */
57
+ .hover-card {
58
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
59
+ }
60
+ .hover-card:hover {
61
+ transform: translateY(-6px) scale(1.01);
62
+ box-shadow: 0 20px 30px -10px rgba(59, 130, 246, 0.15), 0 10px 15px -5px rgba(0, 0, 0, 0.05);
63
+ border-color: rgba(59, 130, 246, 0.3);
64
+ }
65
+
66
+ .dark .hover-card:hover {
67
+ box-shadow: 0 20px 30px -10px rgba(99, 102, 241, 0.2), 0 10px 15px -5px rgba(0, 0, 0, 0.3);
68
+ border-color: rgba(99, 102, 241, 0.4);
69
+ }
70
+
71
+ /* Vibrant Gradient Text */
72
+ .text-gradient {
73
+ background-clip: text;
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-size: 200% auto;
77
+ background-image: linear-gradient(to right, #4facfe 0%, #00f2fe 50%, #4facfe 100%);
78
+ animation: shine 4s linear infinite;
79
+ }
80
+
81
+ .dark .text-gradient {
82
+ background-image: linear-gradient(to right, #a8c0ff 0%, #3f2b96 50%, #a8c0ff 100%);
83
+ }
84
+
85
+ @keyframes shine {
86
+ to {
87
+ background-position: 200% center;
88
+ }
89
+ }
90
+
91
+ /* Primary Button Glow */
92
+ .btn-primary {
93
+ background-image: linear-gradient(to right, #3b82f6, #6366f1);
94
+ color: white;
95
+ border-radius: 0.5rem;
96
+ transition: all 0.3s ease;
97
+ position: relative;
98
+ overflow: hidden;
99
+ z-index: 1;
100
+ }
101
+
102
+ .btn-primary::before {
103
+ content: '';
104
+ position: absolute;
105
+ top: 0; left: 0; right: 0; bottom: 0;
106
+ background-image: linear-gradient(to right, #2563eb, #4f46e5);
107
+ z-index: -1;
108
+ transition: opacity 0.3s ease;
109
+ opacity: 0;
110
+ }
111
+
112
+ .btn-primary:hover::before {
113
+ opacity: 1;
114
+ }
115
+
116
+ .btn-primary:hover {
117
+ transform: translateY(-2px);
118
+ box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.4);
119
+ }
120
+
121
+ /* Form Input Animations */
122
+ .form-input {
123
+ transition: all 0.3s ease;
124
+ background-color: var(--glass-bg-light);
125
+ border: 1px solid var(--glass-border-light);
126
+ }
127
+
128
+ .form-input:focus {
129
+ transform: scale(1.02);
130
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
131
+ }
132
+
133
+ .dark .form-input {
134
+ background-color: rgba(31, 41, 55, 0.5);
135
+ border-color: rgba(75, 85, 99, 0.4);
136
+ color: white;
137
+ }
138
+
139
+ .dark .form-input:focus {
140
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3);
141
+ border-color: rgba(99, 102, 241, 0.5);
142
+ }
143
+
144
+ /* Standardized Alerts */
145
+ .alert-success { background-color: rgba(209, 250, 229, 0.8); color: #065f46; border-left: 4px solid #10b981; backdrop-filter: blur(4px); }
146
+ .alert-danger { background-color: rgba(254, 226, 226, 0.8); color: #991b1b; border-left: 4px solid #ef4444; backdrop-filter: blur(4px); }
147
+ .alert-warning { background-color: rgba(254, 243, 199, 0.8); color: #92400e; border-left: 4px solid #f59e0b; backdrop-filter: blur(4px); }
148
+ .alert-info { background-color: rgba(219, 234, 254, 0.8); color: #1e40af; border-left: 4px solid #3b82f6; backdrop-filter: blur(4px); }
149
+
150
+ .dark .alert-success { background-color: rgba(6, 95, 70, 0.4); color: #34d399; border-left-color: #059669; }
151
+ .dark .alert-danger { background-color: rgba(153, 27, 27, 0.4); color: #f87171; border-left-color: #dc2626; }
152
+ .dark .alert-warning { background-color: rgba(146, 64, 14, 0.4); color: #fbbf24; border-left-color: #d97706; }
153
+ .dark .alert-info { background-color: rgba(30, 64, 175, 0.4); color: #60a5fa; border-left-color: #2563eb; }
154
+
155
+ /* Micro-animations */
156
+ .animate-fade-in {
157
+ animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
158
+ }
159
+
160
+ @keyframes fadeIn {
161
+ from { opacity: 0; transform: translateY(10px); }
162
+ to { opacity: 1; transform: translateY(0); }
163
+ }
164
+
165
+ .animation-delay-100 { animation-delay: 100ms; }
166
+ .animation-delay-200 { animation-delay: 200ms; }
167
+ .animation-delay-300 { animation-delay: 300ms; }
static/js/charts.js ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const chartConfig = {
2
+ // Colors matching our Tailwind theme with vibrant gradient-like values
3
+ colors: {
4
+ positive: '#10b981', // Emerald-500
5
+ negative: '#f43f5e', // Rose-500
6
+ neutral: '#8b5cf6', // Violet-500
7
+ primary: '#3b82f6', // Blue-500
8
+ purple: '#a855f7', // Purple-500
9
+ bgPositive: 'rgba(16, 185, 129, 0.15)',
10
+ bgNegative: 'rgba(244, 63, 94, 0.15)',
11
+ bgNeutral: 'rgba(139, 92, 246, 0.15)',
12
+ bgPrimary: 'rgba(59, 130, 246, 0.15)',
13
+ },
14
+ textColor: function() {
15
+ return document.documentElement.classList.contains('dark') ? '#e5e7eb' : '#374151';
16
+ },
17
+ gridColor: function() {
18
+ return document.documentElement.classList.contains('dark') ? '#374151' : '#e5e7eb';
19
+ }
20
+ };
21
+
22
+ function renderSentimentPieChart(ctxId, data) {
23
+ const ctx = document.getElementById(ctxId);
24
+ if (!ctx) return;
25
+
26
+ new Chart(ctx, {
27
+ type: 'doughnut',
28
+ data: {
29
+ labels: ['Positive', 'Negative', 'Neutral'],
30
+ datasets: [{
31
+ data: [data.positive, data.negative, data.neutral],
32
+ backgroundColor: [
33
+ chartConfig.colors.positive,
34
+ chartConfig.colors.negative,
35
+ chartConfig.colors.neutral
36
+ ],
37
+ borderWidth: 0,
38
+ hoverOffset: 12
39
+ }]
40
+ },
41
+ options: {
42
+ responsive: true,
43
+ maintainAspectRatio: false,
44
+ layout: {
45
+ padding: 10
46
+ },
47
+ plugins: {
48
+ legend: {
49
+ position: 'bottom',
50
+ labels: {
51
+ color: chartConfig.textColor(),
52
+ padding: 20,
53
+ usePointStyle: true,
54
+ pointStyle: 'circle'
55
+ }
56
+ },
57
+ tooltip: {
58
+ backgroundColor: 'rgba(15, 23, 42, 0.9)',
59
+ titleColor: '#fff',
60
+ bodyColor: '#cbd5e1',
61
+ borderColor: 'rgba(255, 255, 255, 0.1)',
62
+ borderWidth: 1,
63
+ padding: 12,
64
+ displayColors: true,
65
+ boxPadding: 6
66
+ }
67
+ },
68
+ cutout: '75%',
69
+ animation: { animateScale: true, animateRotate: true, duration: 1500, easing: 'easeOutQuart' }
70
+ }
71
+ });
72
+ }
73
+
74
+ function renderDepartmentBarChart(ctxId, labels, data) {
75
+ const ctx = document.getElementById(ctxId);
76
+ if (!ctx) return;
77
+
78
+ new Chart(ctx, {
79
+ type: 'bar',
80
+ data: {
81
+ labels: labels,
82
+ datasets: [{
83
+ label: 'Feedback Count',
84
+ data: data,
85
+ backgroundColor: chartConfig.colors.bgPrimary,
86
+ borderColor: chartConfig.colors.primary,
87
+ borderWidth: 2,
88
+ borderRadius: 6,
89
+ hoverBackgroundColor: chartConfig.colors.primary,
90
+ hoverBorderColor: '#60a5fa'
91
+ }]
92
+ },
93
+ options: {
94
+ responsive: true,
95
+ maintainAspectRatio: false,
96
+ layout: {
97
+ padding: { top: 10, right: 10, bottom: 0, left: 0 }
98
+ },
99
+ plugins: {
100
+ legend: { display: false },
101
+ tooltip: {
102
+ backgroundColor: 'rgba(15, 23, 42, 0.9)',
103
+ titleColor: '#fff',
104
+ bodyColor: '#cbd5e1',
105
+ borderColor: 'rgba(255, 255, 255, 0.1)',
106
+ borderWidth: 1,
107
+ padding: 12,
108
+ displayColors: false,
109
+ cornerRadius: 8
110
+ }
111
+ },
112
+ scales: {
113
+ y: {
114
+ beginAtZero: true,
115
+ ticks: { color: chartConfig.textColor(), precision: 0, padding: 10 },
116
+ grid: { color: chartConfig.gridColor(), drawBorder: false, borderDash: [5, 5] },
117
+ border: { display: false }
118
+ },
119
+ x: {
120
+ ticks: { color: chartConfig.textColor(), padding: 10 },
121
+ grid: { display: false },
122
+ border: { display: false }
123
+ }
124
+ },
125
+ animation: {
126
+ duration: 1200,
127
+ easing: 'easeOutQuart'
128
+ }
129
+ }
130
+ });
131
+ });
132
+ }
133
+ // Re-render charts on theme change to update text colors
134
+ document.getElementById('theme-toggle')?.addEventListener('click', () => {
135
+ // A quick hack is just to reload the page or we could store chart instances and update them.
136
+ // For simplicity, we delay and let the CSS transition finish, though proper way is updating chart config.
137
+ setTimeout(() => {
138
+ window.dispatchEvent(new Event('resize'));
139
+ // If we want actual color updates without reload, we need to loop Chart.instances
140
+ for (let id in Chart.instances) {
141
+ let chart = Chart.instances[id];
142
+
143
+ // Update Pie border color
144
+ if (chart.config.type === 'doughnut') {
145
+ chart.data.datasets[0].borderColor = document.documentElement.classList.contains('dark') ? '#1f2937' : '#ffffff';
146
+ }
147
+
148
+ // Update texts
149
+ if (chart.options.plugins?.legend?.labels) {
150
+ chart.options.plugins.legend.labels.color = chartConfig.textColor();
151
+ }
152
+ if (chart.options.scales?.y?.ticks) {
153
+ chart.options.scales.y.ticks.color = chartConfig.textColor();
154
+ chart.options.scales.x.ticks.color = chartConfig.textColor();
155
+ chart.options.scales.y.grid.color = chartConfig.gridColor();
156
+ }
157
+ chart.update();
158
+ }
159
+ }, 100);
160
+ });
static/js/main.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Dark Mode Toggle Logic
2
+ const themeToggleBtn = document.getElementById('theme-toggle');
3
+ const darkIcon = document.getElementById('theme-toggle-dark-icon');
4
+ const lightIcon = document.getElementById('theme-toggle-light-icon');
5
+
6
+ // Change the icons inside the button based on previous settings
7
+ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
8
+ darkIcon?.classList.add('hidden');
9
+ lightIcon?.classList.remove('hidden');
10
+ document.documentElement.classList.add('dark');
11
+ } else {
12
+ lightIcon?.classList.add('hidden');
13
+ darkIcon?.classList.remove('hidden');
14
+ document.documentElement.classList.remove('dark');
15
+ }
16
+
17
+ if (themeToggleBtn) {
18
+ themeToggleBtn.addEventListener('click', function() {
19
+ // toggle icons inside button
20
+ darkIcon.classList.toggle('hidden');
21
+ lightIcon.classList.toggle('hidden');
22
+
23
+ // if set via local storage previously
24
+ if (localStorage.getItem('color-theme')) {
25
+ if (localStorage.getItem('color-theme') === 'light') {
26
+ document.documentElement.classList.add('dark');
27
+ localStorage.setItem('color-theme', 'dark');
28
+ } else {
29
+ document.documentElement.classList.remove('dark');
30
+ localStorage.setItem('color-theme', 'light');
31
+ }
32
+
33
+ // if NOT set via local storage previously
34
+ } else {
35
+ if (document.documentElement.classList.contains('dark')) {
36
+ document.documentElement.classList.remove('dark');
37
+ localStorage.setItem('color-theme', 'light');
38
+ } else {
39
+ document.documentElement.classList.add('dark');
40
+ localStorage.setItem('color-theme', 'dark');
41
+ }
42
+ }
43
+ });
44
+ }
45
+
46
+ // Sidebar Toggle Mobile
47
+ const sidebarToggles = document.querySelectorAll('.sidebar-toggle-btn');
48
+ const sidebar = document.getElementById('sidebar');
49
+
50
+ if (sidebar) {
51
+ sidebarToggles.forEach(btn => {
52
+ btn.addEventListener('click', () => {
53
+ sidebar.classList.toggle('-translate-x-full');
54
+ });
55
+ });
56
+ }
57
+
58
+ // Fade out alerts
59
+ setTimeout(() => {
60
+ const alerts = document.querySelectorAll('.alert-auto-dismiss');
61
+ alerts.forEach(alert => {
62
+ alert.style.transition = "opacity 0.5s ease";
63
+ alert.style.opacity = "0";
64
+ setTimeout(() => alert.remove(), 500);
65
+ });
66
+ }, 5000);
templates/admin/create_user.html ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Create User - NeuroSent Admin{% endblock %}
3
+
4
+ {% block extra_head %}
5
+ <!-- Page specific head elements -->
6
+ {% endblock %}
7
+
8
+ {% block content %}
9
+ <!-- Vanta.js Background Container. Use absolute positioning to fill screen without disrupting flow -->
10
+ <div id="vanta-bg" class="fixed inset-0 z-0 pointer-events-none opacity-40 dark:opacity-30 mix-blend-screen transition-opacity duration-1000"></div>
11
+
12
+ <div class="relative z-10 max-w-2xl mx-auto pt-4 animate-fade-in">
13
+ <div class="mb-8 text-center">
14
+ <h1 class="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-indigo-600 dark:from-primary-400 dark:to-indigo-400 tracking-tight drop-shadow-sm">System Administration</h1>
15
+ <p class="text-gray-600 dark:text-gray-300 font-medium text-sm mt-2">Provision new staff, faculty, or student accounts securely.</p>
16
+ </div>
17
+
18
+ <!-- Stunning Glass Card over 3D background -->
19
+ <div class="glass rounded-3xl shadow-2xl p-8 sm:p-10 border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl relative overflow-hidden transition-all hover:shadow-[0_20px_60px_-15px_rgba(0,0,0,0.3)]">
20
+
21
+ <!-- Subtle internal glowing orb -->
22
+ <div class="absolute -top-20 -right-20 w-56 h-56 bg-primary-400/20 dark:bg-primary-500/10 rounded-full blur-3xl pointer-events-none"></div>
23
+
24
+ <form method="POST" action="{{ url_for('admin.create_user') }}" class="relative z-10 space-y-6">
25
+
26
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
27
+ <div class="space-y-2">
28
+ <label for="name" class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">Full Name</label>
29
+ <div class="relative">
30
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-primary-500">
31
+ <i class="fa-solid fa-id-badge"></i>
32
+ </div>
33
+ <input type="text" name="name" id="name" class="bg-white/70 border border-gray-300/50 text-gray-900 rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-800/70 dark:border-gray-600/50 dark:placeholder-gray-400 dark:text-white shadow-inner transition-all focus:bg-white dark:focus:bg-gray-900" placeholder="Jane Doe" required>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="space-y-2">
38
+ <label for="email" class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">Email Address</label>
39
+ <div class="relative">
40
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-primary-500">
41
+ <i class="fa-solid fa-at"></i>
42
+ </div>
43
+ <input type="email" name="email" id="email" class="bg-white/70 border border-gray-300/50 text-gray-900 rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-800/70 dark:border-gray-600/50 dark:placeholder-gray-400 dark:text-white shadow-inner transition-all focus:bg-white dark:focus:bg-gray-900" placeholder="jane@institution.edu" required>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
49
+ <div class="space-y-2">
50
+ <label for="password" class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">Secure Password</label>
51
+ <div class="relative">
52
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-primary-500">
53
+ <i class="fa-solid fa-key"></i>
54
+ </div>
55
+ <input type="text" name="password" id="password" class="bg-white/70 border border-gray-300/50 text-gray-900 rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-800/70 dark:border-gray-600/50 dark:placeholder-gray-400 dark:text-white shadow-inner transition-all focus:bg-white dark:focus:bg-gray-900 font-mono tracking-widest" value="{{ range(10000000, 99999999) | random | string }}" required>
56
+ </div>
57
+ <p class="text-xs text-gray-500 dark:text-gray-400 italic">Auto-generated password provided. Edit if necessary.</p>
58
+ </div>
59
+
60
+ <div class="space-y-2">
61
+ <label for="role" class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">Account Role</label>
62
+ <div class="relative">
63
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-primary-500 z-10">
64
+ <i class="fa-solid fa-user-shield"></i>
65
+ </div>
66
+ <select name="role" id="role" class="bg-white/70 border border-gray-300/50 text-gray-900 rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-800/70 dark:border-gray-600/50 dark:placeholder-gray-400 dark:text-white shadow-inner transition-all focus:bg-white dark:focus:bg-gray-900 appearance-none relative pr-10 cursor-pointer" required>
67
+ <option value="Student">Student (Default)</option>
68
+ <option value="Staff">Staff</option>
69
+ <option value="HOD">Head of Department (HOD)</option>
70
+ <option value="Admin">Administrator</option>
71
+ </select>
72
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-gray-500 dark:text-gray-400">
73
+ <i class="fa-solid fa-chevron-down text-sm"></i>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="space-y-2 pt-2">
80
+ <label for="department" class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">Assigned Department <span class="text-gray-500 dark:text-gray-400 font-normal lowercase">(Optional for Students)</span></label>
81
+ <div class="relative">
82
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-primary-500">
83
+ <i class="fa-solid fa-building-user"></i>
84
+ </div>
85
+ <input type="text" name="department" id="department" class="bg-white/70 border border-gray-300/50 text-gray-900 rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-800/70 dark:border-gray-600/50 dark:placeholder-gray-400 dark:text-white shadow-inner transition-all focus:bg-white dark:focus:bg-gray-900" placeholder="e.g. Computer Science, Human Resources">
86
+ </div>
87
+ </div>
88
+
89
+ <div class="pt-6">
90
+ <button type="submit" class="w-full text-white bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-extrabold text-base rounded-xl px-5 py-4 text-center dark:focus:ring-primary-800 shadow-xl hover:shadow-2xl transform transition-all hover:-translate-y-1 flex justify-center items-center gap-3 border border-white/20">
91
+ <i class="fa-solid fa-user-plus text-xl"></i> Create Account
92
+ </button>
93
+ </div>
94
+ </form>
95
+ </div>
96
+ </div>
97
+ {% endblock %}
98
+
99
+ {% block extra_scripts %}
100
+ <!-- Local scripts for Admin Create User page can go here ->
101
+ {% endblock %}
templates/admin/manage_users.html ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Manage Users - NeuroSent Admin{% endblock %}
3
+
4
+ {% block content %}
5
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
6
+ <style>
7
+ /* Glassmorphism overrides for DataTables */
8
+ .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate {
9
+ color: inherit;
10
+ padding: 1rem;
11
+ }
12
+ .dataTables_wrapper .dataTables_paginate .paginate_button {
13
+ border-radius: 0.5rem;
14
+ color: inherit !important;
15
+ }
16
+ .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
17
+ background: rgba(99, 102, 241, 0.2);
18
+ border: 1px solid rgba(99, 102, 241, 0.5);
19
+ color: inherit !important;
20
+ }
21
+ </style>
22
+ <div class="relative z-10 max-w-6xl mx-auto pt-4 animate-fade-in">
23
+ <div class="mb-8 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
24
+ <div>
25
+ <h1 class="text-3xl sm:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-indigo-600 dark:from-primary-400 dark:to-indigo-400 tracking-tight drop-shadow-sm">Manage Personnel</h1>
26
+ <p class="text-gray-600 dark:text-gray-300 font-medium text-sm mt-2">View, reset passwords, or delete registered user accounts.</p>
27
+ </div>
28
+ <a href="{{ url_for('admin.create_user') }}" class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 text-white text-sm font-bold rounded-xl shadow-lg hover:shadow-xl transition-all hover:-translate-y-0.5">
29
+ <i class="fa-solid fa-plus mr-2"></i> Add New User
30
+ </a>
31
+ </div>
32
+
33
+ <!-- Stunning Glass Container -->
34
+ <div class="glass rounded-3xl shadow-xl border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl relative overflow-hidden transition-all">
35
+ <!-- Subtle internal glowing orb -->
36
+ <div class="absolute -bottom-20 -left-20 w-64 h-64 bg-primary-400/20 dark:bg-primary-500/10 rounded-full blur-3xl pointer-events-none"></div>
37
+
38
+ <div class="w-full relative z-10">
39
+ <table id="usersTable" class="w-full text-sm text-left text-gray-700 dark:text-gray-300">
40
+ <thead class="text-xs text-gray-800 uppercase bg-primary-50/50 dark:bg-gray-800/80 dark:text-gray-300 border-b border-gray-200/50 dark:border-gray-700/50 backdrop-blur-md">
41
+ <tr>
42
+ <th scope="col" class="px-6 py-4 font-bold tracking-wider">User</th>
43
+ <th scope="col" class="px-6 py-4 font-bold tracking-wider">Role</th>
44
+ <th scope="col" class="px-6 py-4 font-bold tracking-wider">Department</th>
45
+ <th scope="col" class="px-6 py-4 font-bold tracking-wider">Status</th>
46
+ <th scope="col" class="px-6 py-4 font-bold tracking-wider text-right">Actions</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody class="divide-y divide-gray-200/50 dark:divide-gray-700/50">
50
+ {% for user in users %}
51
+ <tr class="bg-white/40 dark:bg-gray-800/30 hover:bg-white/60 dark:hover:bg-gray-800/60 transition-colors">
52
+ <td class="px-6 py-4 whitespace-nowrap">
53
+ <div class="flex items-center">
54
+ <div class="flex-shrink-0 h-10 w-10 flex items-center justify-center rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/40 dark:to-indigo-900/40 text-primary-600 dark:text-primary-400 font-bold border border-white/50 dark:border-gray-700 shadow-inner">
55
+ {{ user.name[0] | upper }}
56
+ </div>
57
+ <div class="ml-4">
58
+ <div class="text-sm font-bold text-gray-900 dark:text-white">{{ user.name }}</div>
59
+ <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">{{ user.email }}</div>
60
+ </div>
61
+ </div>
62
+ </td>
63
+ <td class="px-6 py-4 whitespace-nowrap">
64
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold {% if user.role == 'Admin' %}bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 border border-purple-200 dark:border-purple-800/50{% elif user.role == 'HOD' %}bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 border border-orange-200 dark:border-orange-800/50{% elif user.role == 'Staff' %}bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border border-blue-200 dark:border-blue-800/50{% else %}bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 border border-green-200 dark:border-green-800/50{% endif %}">
65
+ {{ user.role }}
66
+ </span>
67
+ </td>
68
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
69
+ {% if user.department %}
70
+ {{ user.department }}
71
+ {% else %}
72
+ <span class="text-gray-400 dark:text-gray-500 italic">None</span>
73
+ {% endif %}
74
+ </td>
75
+ <td class="px-6 py-4 whitespace-nowrap">
76
+ <span class="inline-flex items-center text-xs font-bold text-green-600 dark:text-green-400">
77
+ <span class="w-2 h-2 mr-1.5 bg-green-500 rounded-full animate-pulse"></span> Active
78
+ </span>
79
+ </td>
80
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
81
+ <div class="flex items-center justify-end space-x-3">
82
+ <!-- Reset Password Form -->
83
+ <form action="{{ url_for('admin.reset_password', user_id=user.id) }}" method="POST" onsubmit="return handlePasswordReset(event, '{{ user.email }}', this);">
84
+ <input type="hidden" name="new_password" class="new_password_input">
85
+ <button type="submit" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 p-2 rounded-lg transition-colors" title="Change / Reset Password">
86
+ <i class="fa-solid fa-key"></i>
87
+ </button>
88
+ </form>
89
+
90
+ <!-- Delete User Form -->
91
+ {% if user.id != current_user.id %}
92
+ <form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" onsubmit="return confirm('Are you strictly sure you want to permanently delete the account for {{ user.name }} ({{ user.role }})? This action cannot be undone.');">
93
+ <button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 p-2 rounded-lg transition-colors" title="Delete User">
94
+ <i class="fa-solid fa-trash-can"></i>
95
+ </button>
96
+ </form>
97
+ {% else %}
98
+ <button type="button" class="text-gray-300 dark:text-gray-600 p-2 rounded-lg cursor-not-allowed" title="Cannot delete yourself">
99
+ <i class="fa-solid fa-trash-can"></i>
100
+ </button>
101
+ {% endif %}
102
+ </div>
103
+ </td>
104
+ </tr>
105
+ {% else %}
106
+ <tr>
107
+ <td colspan="5" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
108
+ <i class="fa-solid fa-user-slash text-4xl mb-3 opacity-50"></i>
109
+ <p>No other users found in the system.</p>
110
+ </td>
111
+ </tr>
112
+ {% endfor %}
113
+ </tbody>
114
+ </table>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ {% endblock %}
119
+
120
+ {% block extra_scripts %}
121
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
122
+ <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
123
+ <script>
124
+ $(document).ready(function() {
125
+ $('#usersTable').DataTable({
126
+ "pageLength": 10,
127
+ "language": {
128
+ "search": "Search Personnel:",
129
+ "lengthMenu": "Show _MENU_ users per page"
130
+ }
131
+ });
132
+ });
133
+
134
+ function handlePasswordReset(event, email, form) {
135
+ event.preventDefault();
136
+ let newPass = prompt(`Enter new custom password for:\n${email}\n\n[Leave this blank and press OK to auto-generate a random 8-character password]`);
137
+
138
+ if (newPass === null) {
139
+ // User pressed Cancel
140
+ return false;
141
+ }
142
+
143
+ form.querySelector('.new_password_input').value = newPass;
144
+ form.submit();
145
+ }
146
+ </script>
147
+ {% endblock %}
templates/admin/training.html ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}AI Training Center - NeuroSent{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="relative z-10 max-w-5xl mx-auto pt-4 animate-fade-in">
6
+ <div class="mb-8 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
7
+ <div>
8
+ <h1 class="text-3xl sm:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-indigo-600 dark:from-primary-400 dark:to-indigo-400 tracking-tight drop-shadow-sm"><i class="fa-solid fa-brain mr-2"></i>AI Training Center</h1>
9
+ <p class="text-gray-600 dark:text-gray-300 font-medium text-sm mt-2">Fine-tune the neural network using your own proprietary feedback database.</p>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
14
+ <!-- Status Card -->
15
+ <div class="lg:col-span-2 glass rounded-3xl shadow-xl border border-white/40 dark:border-gray-700/60 p-8 relative overflow-hidden backdrop-blur-2xl">
16
+ <!-- Glowing Orb -->
17
+ <div class="absolute -top-20 -right-20 w-64 h-64 bg-indigo-500/20 dark:bg-indigo-600/20 rounded-full blur-3xl pointer-events-none"></div>
18
+
19
+ <h3 class="text-xl font-bold text-gray-800 dark:text-white mb-6">Training Engine Status</h3>
20
+
21
+ <div class="bg-white/40 dark:bg-gray-800/40 rounded-2xl p-6 border border-white/50 dark:border-gray-700/50 relative z-10">
22
+ <div class="flex items-center justify-between mb-2">
23
+ <span id="status-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
24
+ Checking Status...
25
+ </span>
26
+ <span id="progress-text" class="text-sm font-bold text-gray-600 dark:text-gray-400">0%</span>
27
+ </div>
28
+
29
+ <!-- Progress Bar -->
30
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 mb-4 overflow-hidden">
31
+ <div id="progress-bar" class="bg-gradient-to-r from-primary-500 to-indigo-600 h-3 rounded-full transition-all duration-500" style="width: 0%"></div>
32
+ </div>
33
+
34
+ <!-- Log Output terminal -->
35
+ <div class="bg-gray-900 border border-gray-700 rounded-xl p-4 font-mono text-sm text-green-400 shadow-inner h-32 overflow-y-auto">
36
+ <div id="terminal-output" class="whitespace-pre-line animate-pulse">Initializing connection...</div>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="mt-8 flex justify-end">
41
+ <button id="start-btn" onclick="startTraining()" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 text-white font-bold rounded-xl shadow-lg hover:shadow-xl transition-all hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed">
42
+ <i class="fa-solid fa-play mr-2"></i> Start Target Fine-Tuning
43
+ </button>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Info Card -->
48
+ <div class="glass rounded-3xl shadow-xl border border-white/40 dark:border-gray-700/60 p-6 relative overflow-hidden backdrop-blur-2xl">
49
+ <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">Current AI Brain</h3>
50
+
51
+ {% if has_custom %}
52
+ <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl p-4 mb-6">
53
+ <div class="flex items-start">
54
+ <i class="fa-solid fa-microchip text-green-600 dark:text-green-400 text-xl mt-0.5 mr-3"></i>
55
+ <div>
56
+ <h4 class="font-bold text-green-800 dark:text-green-300 text-sm">Custom Weights Active</h4>
57
+ <p class="text-xs text-green-600 dark:text-green-400 mt-1">The AI is currently processing data using your proprietary locally-trained neural network.</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ {% else %}
62
+ <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4 mb-6">
63
+ <div class="flex items-start">
64
+ <i class="fa-solid fa-globe text-blue-600 dark:text-blue-400 text-xl mt-0.5 mr-3"></i>
65
+ <div>
66
+ <h4 class="font-bold text-blue-800 dark:text-blue-300 text-sm">Global Base Model Active</h4>
67
+ <p class="text-xs text-blue-600 dark:text-blue-400 mt-1">The AI is running the standard Hugging Face CardiffNLP weights.</p>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ {% endif %}
72
+
73
+ <div class="text-sm text-gray-600 dark:text-gray-400 space-y-3">
74
+ <p><i class="fa-solid fa-circle-info mr-2 opacity-70"></i> <strong>How it works:</strong> Clicking Start extracts all human-reviewed Feedback from the database and compiles it into a Hugging Face Dataset.</p>
75
+ <p><i class="fa-solid fa-forward-fast mr-2 opacity-70"></i> It runs 2 Epochs through the RoBERTa Transformer architecture locally to update the attention weights.</p>
76
+ <p><i class="fa-solid fa-triangle-exclamation mr-2 opacity-70 text-orange-500"></i> Minimum 50 database rows required.</p>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ {% endblock %}
82
+
83
+ {% block extra_scripts %}
84
+ <script>
85
+ let statusInterval = null;
86
+
87
+ function updateBadge(status) {
88
+ const badge = document.getElementById('status-badge');
89
+ badge.textContent = status;
90
+
91
+ badge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ';
92
+ if (status === 'Idle') badge.className += 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300';
93
+ else if (status === 'Error') badge.className += 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400 border border-red-200 dark:border-red-800';
94
+ else if (status === 'Completed') badge.className += 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 border border-green-200 dark:border-green-800';
95
+ else badge.className += 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 border border-blue-200 dark:border-blue-800 animate-pulse';
96
+ }
97
+
98
+ function checkStatus() {
99
+ fetch('{{ url_for("training.check_status") }}')
100
+ .then(res => res.json())
101
+ .then(data => {
102
+ updateBadge(data.status);
103
+ document.getElementById('progress-bar').style.width = data.progress + '%';
104
+ document.getElementById('progress-text').textContent = data.progress + '%';
105
+
106
+ const term = document.getElementById('terminal-output');
107
+ if (term.innerText !== data.message) {
108
+ term.innerText += '\n> ' + data.message;
109
+ term.scrollTop = term.scrollHeight; // Auto-scroll to bottom
110
+ term.classList.remove('animate-pulse');
111
+ }
112
+
113
+ const startBtn = document.getElementById('start-btn');
114
+ if (['Initializing', 'Processing', 'Training', 'Saving', 'Starting'].includes(data.status)) {
115
+ startBtn.disabled = true;
116
+ startBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Engine Active...';
117
+ } else {
118
+ startBtn.disabled = false;
119
+ startBtn.innerHTML = '<i class="fa-solid fa-play mr-2"></i> Start Target Fine-Tuning';
120
+ if (data.status === 'Completed' || data.status === 'Error') {
121
+ clearInterval(statusInterval);
122
+ }
123
+ }
124
+ })
125
+ .catch(err => console.error(err));
126
+ }
127
+
128
+ function startTraining() {
129
+ if (!confirm("Start Neural Network Fine-Tuning? This will heavily utilize CPU/GPU for the duration of the training.")) return;
130
+
131
+ fetch('{{ url_for("training.start_training") }}', { method: 'POST' })
132
+ .then(res => res.json())
133
+ .then(data => {
134
+ if(data.success) {
135
+ document.getElementById('terminal-output').innerText = "> Launching external training script pipeline...";
136
+ checkStatus();
137
+ clearInterval(statusInterval);
138
+ statusInterval = setInterval(checkStatus, 3000);
139
+ } else {
140
+ alert("Failed to start training: " + data.message);
141
+ }
142
+ });
143
+ }
144
+
145
+ // Initial check
146
+ checkStatus();
147
+ statusInterval = setInterval(checkStatus, 3000);
148
+ </script>
149
+ {% endblock %}
templates/auth/login.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Login - NeuroSent{% endblock %}
3
+
4
+ {% block unauth_content %}
5
+ <div class="flex items-center justify-center min-h-[80vh] px-4 animate-fade-in">
6
+ <div class="w-full max-w-md p-8 sm:p-10 glass rounded-3xl shadow-2xl relative overflow-hidden backdrop-blur-xl border border-white/20 dark:border-gray-700/50">
7
+
8
+ <!-- Decorative gradient orb -->
9
+ <div class="absolute -top-16 -right-16 w-40 h-40 bg-primary-500/20 rounded-full blur-3xl pointer-events-none transform animate-pulse transition-all"></div>
10
+ <div class="absolute -bottom-16 -left-16 w-40 h-40 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none transform animate-pulse animation-delay-2000 transition-all"></div>
11
+
12
+ <div class="relative z-10 text-center mb-8">
13
+ <a href="#" class="inline-flex items-center space-x-3 text-4xl font-extrabold text-gradient mb-3 tracking-tight">
14
+ <i class="fa-solid fa-brain"></i>
15
+ <span>NeuroSent</span>
16
+ </a>
17
+ <p class="text-sm text-gray-500 dark:text-gray-400 font-medium">AI-Based Sentiment Analysis Platform</p>
18
+ </div>
19
+
20
+ <form method="POST" action="{{ url_for('auth.login') }}" class="relative z-10 space-y-6">
21
+ <div>
22
+ <label for="email" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Your email</label>
23
+ <div class="relative">
24
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
25
+ <i class="fa-solid fa-envelope text-primary-400"></i>
26
+ </div>
27
+ <input type="email" name="email" id="email" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" placeholder="name@company.com" required="">
28
+ </div>
29
+ </div>
30
+ <div>
31
+ <label for="password" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Password</label>
32
+ <div class="relative">
33
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
34
+ <i class="fa-solid fa-lock text-primary-400"></i>
35
+ </div>
36
+ <input type="password" name="password" id="password" placeholder="••••••••" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" required="">
37
+ </div>
38
+ </div>
39
+
40
+ <button type="submit" class="w-full text-white bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-bold rounded-xl text-sm px-5 py-3.5 text-center dark:focus:ring-primary-900 shadow-lg hover:shadow-xl transform transition-all hover:-translate-y-0.5 mt-2">
41
+ Sign in to your account
42
+ </button>
43
+ <p class="text-sm font-medium text-center text-gray-600 dark:text-gray-400 mt-6">
44
+ Don't have an account yet? <a href="{{ url_for('auth.register') }}" class="font-bold text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 transition-colors">Sign up</a>
45
+ </p>
46
+ </form>
47
+ </div>
48
+ </div>
49
+ {% endblock %}
templates/auth/register.html ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Register - NeuroSent{% endblock %}
3
+
4
+ {% block unauth_content %}
5
+ <div class="flex items-center justify-center min-h-[85vh] py-8 px-4 animate-fade-in">
6
+ <div class="w-full max-w-md p-8 sm:p-10 glass rounded-3xl shadow-2xl relative overflow-hidden backdrop-blur-xl border border-white/20 dark:border-gray-700/50">
7
+
8
+ <!-- Decorative gradient orb -->
9
+ <div class="absolute -bottom-16 -right-16 w-40 h-40 bg-pink-500/20 rounded-full blur-3xl pointer-events-none transform animate-pulse transition-all"></div>
10
+ <div class="absolute -top-16 -left-16 w-40 h-40 bg-blue-500/20 rounded-full blur-3xl pointer-events-none transform animate-pulse animation-delay-2000 transition-all"></div>
11
+
12
+ <div class="relative z-10 text-center mb-8">
13
+ <h2 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-2 tracking-tight">Create an Account</h2>
14
+ <p class="text-sm text-gray-500 dark:text-gray-400 font-medium">Join the NeuroSent analytics platform</p>
15
+ </div>
16
+
17
+ <form method="POST" action="{{ url_for('auth.register') }}" class="relative z-10 space-y-5">
18
+
19
+ <div>
20
+ <label for="name" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Full Name</label>
21
+ <div class="relative">
22
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
23
+ <i class="fa-solid fa-user text-primary-400"></i>
24
+ </div>
25
+ <input type="text" name="name" id="name" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" placeholder="John Doe" required="">
26
+ </div>
27
+ </div>
28
+
29
+ <div>
30
+ <label for="email" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Email address</label>
31
+ <div class="relative">
32
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
33
+ <i class="fa-solid fa-envelope text-primary-400"></i>
34
+ </div>
35
+ <input type="email" name="email" id="email" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" placeholder="name@company.com" required="">
36
+ </div>
37
+ </div>
38
+
39
+ <div>
40
+ <label for="password" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Password</label>
41
+ <div class="relative">
42
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
43
+ <i class="fa-solid fa-lock text-primary-400"></i>
44
+ </div>
45
+ <input type="password" name="password" id="password" placeholder="••••••••" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" required="">
46
+ </div>
47
+ </div>
48
+
49
+ <div class="grid grid-cols-2 gap-4">
50
+ <div class="col-span-2">
51
+ <label for="department" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Department</label>
52
+ <input type="text" name="department" id="department" class="bg-white/60 border border-gray-300/50 text-gray-900 sm:text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" placeholder="e.g. IT, HR" required="">
53
+ </div>
54
+ </div>
55
+
56
+ <button type="submit" class="w-full text-white bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-bold rounded-xl text-sm px-5 py-3.5 text-center dark:focus:ring-primary-900 shadow-lg hover:shadow-xl transform transition-all hover:-translate-y-0.5 mt-6">
57
+ Create Account
58
+ </button>
59
+ <p class="text-sm font-medium text-center text-gray-600 dark:text-gray-400 mt-6">
60
+ Already have an account? <a href="{{ url_for('auth.login') }}" class="font-bold text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 transition-colors">Login here</a>
61
+ </p>
62
+ </form>
63
+ </div>
64
+ </div>
65
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}AI Sentiment Analysis{% endblock %}</title>
7
+ <!-- Tailwind CSS (via CDN for setup, preferably local build in prod) -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: {"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a","950":"#172554"}
16
+ }
17
+ }
18
+ }
19
+ }
20
+ // Early dark mode script to prevent FOUC
21
+ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
22
+ document.documentElement.classList.add('dark');
23
+ } else {
24
+ document.documentElement.classList.remove('dark')
25
+ }
26
+ </script>
27
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
28
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
29
+ <!-- Load Three.js and Vanta.js globally for extreme aesthetic flow -->
30
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js"></script>
32
+ {% block extra_head %}{% endblock %}
33
+ </head>
34
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-200 transition-colors duration-300">
35
+ <!-- Global Vanta.js Background Container. Use absolute positioning to fill screen without disrupting flow -->
36
+ <div id="vanta-bg" class="fixed inset-0 z-0 pointer-events-none opacity-40 dark:opacity-30 mix-blend-screen transition-opacity duration-1000"></div>
37
+
38
+ {% if current_user.is_authenticated %}
39
+ <!-- Sidebar Setup -->
40
+ <div class="flex h-screen overflow-hidden">
41
+
42
+ <!-- Sidebar -->
43
+ <aside id="sidebar" class="sidebar absolute z-50 flex flex-col w-64 h-screen px-4 py-8 overflow-y-auto bg-white/50 glass border-r border-gray-200/50 -translate-x-full lg:relative lg:translate-x-0 dark:bg-gray-800/40 dark:border-gray-700/50 transition-all duration-300 ease-in-out shadow-2xl">
44
+ <div class="flex items-center justify-between mb-6">
45
+ <a href="{{ url_for('dashboard.index') }}" class="flex items-center space-x-2 text-2xl font-bold text-gradient tracking-tight">
46
+ <i class="fa-solid fa-brain"></i>
47
+ <span>NeuroSent</span>
48
+ </a>
49
+ <button class="sidebar-toggle-btn lg:hidden text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none transition-transform hover:scale-110">
50
+ <i class="fa-solid fa-times text-xl"></i>
51
+ </button>
52
+ </div>
53
+
54
+ <div class="flex items-center px-4 py-3 mb-6 bg-white/40 glass rounded-xl dark:bg-gray-800/40 shadow-sm hover:shadow-md transition-shadow">
55
+ <div class="flex-shrink-0">
56
+ <div class="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-indigo-600 flex items-center justify-center text-white font-bold text-lg shadow-inner ring-2 ring-white/20">
57
+ {{ current_user.name[0] }}
58
+ </div>
59
+ </div>
60
+ <div class="ml-3">
61
+ <p class="text-sm font-semibold text-gray-800 dark:text-gray-100">{{ current_user.name }}</p>
62
+ <p class="text-xs text-primary-600 dark:text-primary-400 font-medium">{{ current_user.role }}</p>
63
+ </div>
64
+ </div>
65
+
66
+ <nav class="flex-1 space-y-2">
67
+ <a href="{{ url_for('dashboard.index') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'dashboard.index' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
68
+ <i class="fa-solid fa-chart-line w-5"></i>
69
+ <span class="ml-3">Dashboard</span>
70
+ </a>
71
+
72
+ <a href="{{ url_for('profile.index') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'profile.index' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
73
+ <i class="fa-solid fa-user-circle w-5"></i>
74
+ <span class="ml-3">My Profile</span>
75
+ </a>
76
+
77
+ {% if current_user.role == 'Admin' %}
78
+ <a href="{{ url_for('admin.create_user') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'admin.create_user' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
79
+ <i class="fa-solid fa-user-plus w-5"></i>
80
+ <span class="ml-3">Create User</span>
81
+ </a>
82
+
83
+ <a href="{{ url_for('admin.manage_users') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'admin.manage_users' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
84
+ <i class="fa-solid fa-users-gear w-5"></i>
85
+ <span class="ml-3">Manage Users</span>
86
+ </a>
87
+
88
+ <a href="{{ url_for('training.index') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if 'training' in request.endpoint %}bg-primary-50 text-indigo-600 dark:bg-gray-700 dark:text-indigo-400 font-bold{% endif %}">
89
+ <i class="fa-solid fa-microchip w-5"></i>
90
+ <span class="ml-3">AI Training Center</span>
91
+ </a>
92
+ {% endif %}
93
+
94
+ {% if current_user.role in ['Admin', 'HOD', 'Staff'] %}
95
+ <a href="{{ url_for('upload.index') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'upload.index' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
96
+ <i class="fa-solid fa-cloud-upload-alt w-5"></i>
97
+ <span class="ml-3">Upload Data</span>
98
+ </a>
99
+ {% endif %}
100
+
101
+ <a href="{{ url_for('feedback.submit') }}" class="flex items-center px-4 py-2.5 text-sm font-medium transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 {% if request.endpoint == 'feedback.submit' %}bg-primary-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400{% endif %}">
102
+ <i class="fa-solid fa-pen-to-square w-5"></i>
103
+ <span class="ml-3">Submit Feedback</span>
104
+ </a>
105
+ </nav>
106
+
107
+ <div class="mt-8 border-t border-gray-200/50 dark:border-gray-700/50 pt-4">
108
+ <button id="theme-toggle" type="button" class="w-full flex items-center px-4 py-2.5 text-sm font-medium transition-all duration-200 rounded-lg hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:pl-5">
109
+ <i id="theme-toggle-dark-icon" class="fa-solid fa-moon w-5 hidden text-indigo-400"></i>
110
+ <i id="theme-toggle-light-icon" class="fa-solid fa-sun w-5 hidden text-yellow-500"></i>
111
+ <span class="ml-3">Toggle Theme</span>
112
+ </button>
113
+ <a href="{{ url_for('auth.logout') }}" class="flex items-center px-4 py-2.5 mt-2 text-sm font-medium text-red-600 transition-all duration-200 rounded-lg hover:bg-red-50/50 dark:hover:bg-red-900/20 dark:text-red-400 hover:pl-5">
114
+ <i class="fa-solid fa-sign-out-alt w-5"></i>
115
+ <span class="ml-3">Logout</span>
116
+ </a>
117
+ </div>
118
+ </aside>
119
+
120
+ <!-- Main content -->
121
+ <main class="flex-1 flex flex-col h-screen overflow-hidden bg-transparent relative z-10">
122
+ <!-- Mobile Header -->
123
+ <header class="flex items-center justify-between px-6 py-4 glass border-b border-gray-200/50 lg:hidden shadow-sm">
124
+ <a href="{{ url_for('dashboard.index') }}" class="text-xl font-bold text-gradient">
125
+ <i class="fa-solid fa-brain"></i> NeuroSent
126
+ </a>
127
+ <button class="sidebar-toggle-btn text-gray-500 focus:outline-none focus:text-gray-700 dark:text-gray-400 dark:focus:text-gray-200">
128
+ <i class="fa-solid fa-bars text-xl"></i>
129
+ </button>
130
+ </header>
131
+
132
+ <div class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8 animate-fade-in relative z-20">
133
+ <!-- Flash Messages -->
134
+ {% with messages = get_flashed_messages(with_categories=true) %}
135
+ {% if messages %}
136
+ <div class="mb-6 space-y-3 z-50">
137
+ {% for category, message in messages %}
138
+ <div class="alert-{{ category }} alert-auto-dismiss px-5 py-4 rounded-xl shadow-lg border-l-4 flex items-center justify-between animate-fade-in">
139
+ <div><i class="fa-solid fa-circle-info mr-2 opacity-80"></i> {{ message }}</div>
140
+ </div>
141
+ {% endfor %}
142
+ </div>
143
+ {% endif %}
144
+ {% endwith %}
145
+
146
+ <div class="animation-delay-100">
147
+ {% block content %}{% endblock %}
148
+ </div>
149
+ </div>
150
+ </main>
151
+ </div>
152
+ {% else %}
153
+ <!-- No Sidebar for Unauthenticated Users -->
154
+ <div class="min-h-screen flex flex-col">
155
+ <div class="flex-1">
156
+ <!-- Flash Messages -->
157
+ <div class="max-w-md mx-auto mt-4 px-4">
158
+ {% with messages = get_flashed_messages(with_categories=true) %}
159
+ {% if messages %}
160
+ {% for category, message in messages %}
161
+ <div class="alert-{{ category }} alert-auto-dismiss px-4 py-3 rounded-lg shadow-sm mb-2">
162
+ <i class="fa-solid fa-circle-info mr-2"></i> {{ message }}
163
+ </div>
164
+ {% endfor %}
165
+ {% endif %}
166
+ {% endwith %}
167
+ </div>
168
+
169
+ {% block unauth_content %}{% endblock %}
170
+ </div>
171
+
172
+ <button id="theme-toggle" type="button" class="fixed bottom-4 right-4 p-3 rounded-full bg-white dark:bg-gray-800 shadow-lg text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 focus:outline-none transition-colors">
173
+ <i id="theme-toggle-dark-icon" class="fa-solid fa-moon text-xl hidden"></i>
174
+ <i id="theme-toggle-light-icon" class="fa-solid fa-sun text-xl hidden"></i>
175
+ </button>
176
+ </div>
177
+ {% endif %}
178
+
179
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
180
+ <script>
181
+ // Global Vanta.js Initialization for supreme aesthetics
182
+ document.addEventListener('DOMContentLoaded', () => {
183
+ const isDark = document.documentElement.classList.contains('dark');
184
+
185
+ // Deeper blue for light mode network lines, brighter blue for dark mode
186
+ const vantaEffect = VANTA.NET({
187
+ el: "#vanta-bg",
188
+ mouseControls: true,
189
+ touchControls: true,
190
+ gyroControls: false,
191
+ minHeight: 200.00,
192
+ minWidth: 200.00,
193
+ scale: 1.00,
194
+ scaleMobile: 1.00,
195
+ color: isDark ? 0x60a5fa : 0x1d4ed8, // primary-400 (Dark) / primary-700 (Light)
196
+ backgroundColor: isDark ? 0x111827 : 0xf9fafb, // gray-900 / gray-50
197
+ points: 10.00,
198
+ maxDistance: 20.00,
199
+ spacing: 18.00,
200
+ showDots: true
201
+ });
202
+
203
+ // Handle theme toggling seamlessly without reloading
204
+ const observer = new MutationObserver((mutations) => {
205
+ mutations.forEach((mutation) => {
206
+ if (mutation.attributeName === 'class') {
207
+ const isDarkTheme = document.documentElement.classList.contains('dark');
208
+ vantaEffect.setOptions({
209
+ color: isDarkTheme ? 0x60a5fa : 0x1d4ed8,
210
+ backgroundColor: isDarkTheme ? 0x111827 : 0xf9fafb
211
+ });
212
+ }
213
+ });
214
+ });
215
+
216
+ observer.observe(document.documentElement, { attributes: true });
217
+ });
218
+ </script>
219
+ {% block extra_scripts %}{% endblock %}
220
+ </body>
221
+ </html>
templates/dashboard/admin.html ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Admin Dashboard - NeuroSent{% endblock %}
3
+
4
+ {% block extra_scripts %}
5
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
6
+ <style>
7
+ /* Glassmorphism overrides for DataTables */
8
+ .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate {
9
+ color: inherit;
10
+ padding: 1rem;
11
+ }
12
+ .dataTables_wrapper .dataTables_paginate .paginate_button {
13
+ border-radius: 0.5rem;
14
+ color: inherit !important;
15
+ }
16
+ .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
17
+ background: rgba(99, 102, 241, 0.2);
18
+ border: 1px solid rgba(99, 102, 241, 0.5);
19
+ color: inherit !important;
20
+ }
21
+ table.dataTable.no-footer {
22
+ border-bottom: 1px solid rgba(255,255,255,0.1);
23
+ }
24
+ </style>
25
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
26
+ <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
27
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.2.2/wordcloud2.min.js"></script>
28
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
29
+ <script src="{{ url_for('static', filename='js/charts.js') }}"></script>
30
+ <script>
31
+ document.addEventListener('DOMContentLoaded', function() {
32
+ renderSentimentPieChart('sentimentPieChart', {
33
+ positive: {{ positive|default(0) }},
34
+ negative: {{ negative|default(0) }},
35
+ neutral: {{ neutral|default(0) }}
36
+ });
37
+
38
+ // Process department dictionary to arrays
39
+ const deptLabels = [];
40
+ const deptData = [];
41
+ {% for dept, count in dept_distribution.items() %}
42
+ deptLabels.push("{{ dept|default('Uncategorized', true) }}");
43
+ deptData.push({{ count }});
44
+ {% endfor %}
45
+
46
+ renderDepartmentBarChart('deptBarChart', deptLabels, deptData);
47
+
48
+ // Initialize DataTable
49
+ $('#recentFeedbackTable').DataTable({
50
+ "pageLength": 5,
51
+ "lengthMenu": [5, 10, 25, 50],
52
+ "language": {
53
+ "search": "Filter Feedback:",
54
+ "info": "Showing _START_ to _END_ of _TOTAL_ entries"
55
+ }
56
+ });
57
+
58
+ // Fetch and render asynchronous Word Cloud
59
+ fetch('{{ url_for("dashboard.get_keywords") }}')
60
+ .then(response => response.json())
61
+ .then(data => {
62
+ if (data.keywords && data.keywords.length > 0) {
63
+ // WordCloud2 options
64
+ WordCloud(document.getElementById('keywordCloud'), {
65
+ list: data.keywords,
66
+ weightFactor: 8,
67
+ fontFamily: 'Inter, sans-serif',
68
+ color: function (word, weight, fontSize, distance, theta) {
69
+ return weight > 10 ? '#ef4444' : (weight > 5 ? '#f59e0b' : '#3b82f6');
70
+ },
71
+ backgroundColor: 'transparent'
72
+ });
73
+ } else {
74
+ document.getElementById('keywordCloud').parentElement.innerHTML = '<div class="flex h-full items-center justify-center text-gray-400 italic">Not enough text data to extract keywords</div>';
75
+ }
76
+ });
77
+
78
+ // Date Range Filter Logic
79
+ document.getElementById('applyDateFilter').addEventListener('click', function() {
80
+ const startDate = document.getElementById('startDate').value;
81
+ const endDate = document.getElementById('endDate').value;
82
+ const btn = this;
83
+
84
+ if(!startDate && !endDate) return;
85
+
86
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
87
+
88
+ fetch(`{{ url_for("dashboard.get_dashboard_data") }}?start_date=${startDate}&end_date=${endDate}`)
89
+ .then(res => res.json())
90
+ .then(data => {
91
+ btn.innerHTML = '<i class="fa-solid fa-filter"></i> Apply';
92
+ if(data.success) {
93
+ // Update Stat Cards (with basic rolling animation simulation)
94
+ document.getElementById('statTotal').innerText = data.total_count;
95
+ document.getElementById('statPos').innerText = data.positive;
96
+ document.getElementById('statNeg').innerText = data.negative;
97
+ document.getElementById('statNeu').innerText = data.neutral;
98
+
99
+ document.getElementById('pctPos').innerText = data.pct_pos + '%';
100
+ document.getElementById('pctNeg').innerText = data.pct_neg + '%';
101
+ document.getElementById('pctNeu').innerText = data.pct_neu + '%';
102
+
103
+ // Update Charts
104
+ const pieChart = Chart.instances[0];
105
+ if(pieChart) {
106
+ pieChart.data.datasets[0].data = [data.positive, data.negative, data.neutral];
107
+ pieChart.update();
108
+ }
109
+
110
+ const barChart = Chart.instances[1];
111
+ if(barChart) {
112
+ barChart.data.labels = data.dept_labels;
113
+ barChart.data.datasets[0].data = data.dept_counts;
114
+ barChart.update();
115
+ }
116
+ }
117
+ })
118
+ .catch(err => {
119
+ btn.innerHTML = '<i class="fa-solid fa-filter"></i> Apply';
120
+ console.error("Date filter error:", err);
121
+ });
122
+ });
123
+ });
124
+ </script>
125
+ {% endblock %}
126
+
127
+ {% block content %}
128
+ <div class="mb-8 flex flex-col md:flex-row justify-between items-start md:items-end animate-fade-in">
129
+ <div>
130
+ <h1 class="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">Admin Dashboard</h1>
131
+ <p class="text-gray-500 dark:text-gray-400 text-sm mt-2">System-wide sentiment overview and data management.</p>
132
+ </div>
133
+ </div>
134
+ <div class="mt-4 md:mt-0 flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-3 items-end">
135
+ <!-- Date Filter UI -->
136
+ <div class="flex items-center space-x-2 bg-white/20 dark:bg-gray-800/40 p-1.5 rounded-xl border border-white/20 dark:border-gray-700/50 backdrop-blur-md">
137
+ <input type="date" id="startDate" class="bg-transparent border-none text-xs text-gray-700 dark:text-gray-300 focus:ring-0 cursor-pointer" title="Start Date">
138
+ <span class="text-gray-400 text-xs">to</span>
139
+ <input type="date" id="endDate" class="bg-transparent border-none text-xs text-gray-700 dark:text-gray-300 focus:ring-0 cursor-pointer" title="End Date">
140
+ <button id="applyDateFilter" class="px-3 py-1.5 text-xs font-bold text-white bg-indigo-500 hover:bg-indigo-600 rounded-lg transition-colors shadow-sm">
141
+ <i class="fa-solid fa-filter"></i> Apply
142
+ </button>
143
+ </div>
144
+
145
+ <div class="flex space-x-2">
146
+ <a href="{{ url_for('reports.download_excel') }}" class="px-4 py-2 text-sm font-semibold text-emerald-700 bg-emerald-100/80 hover:bg-emerald-200 dark:bg-emerald-500/20 dark:text-emerald-300 dark:hover:bg-emerald-500/30 rounded-xl transition-all shadow-sm hover:shadow" title="Export Excel">
147
+ <i class="fa-solid fa-file-excel"></i>
148
+ </a>
149
+ <a href="{{ url_for('reports.download_pdf') }}" class="px-4 py-2 text-sm font-semibold text-rose-700 bg-rose-100/80 hover:bg-rose-200 dark:bg-rose-500/20 dark:text-rose-300 dark:hover:bg-rose-500/30 rounded-xl transition-all shadow-sm hover:shadow" title="Export PDF">
150
+ <i class="fa-solid fa-file-pdf"></i>
151
+ </a>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Stats Overview -->
157
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 animate-fade-in animation-delay-100">
158
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
159
+ <div class="flex items-center">
160
+ <div class="p-4 rounded-xl bg-blue-500/10 text-blue-600 dark:text-blue-400">
161
+ <i class="fa-solid fa-database text-2xl"></i>
162
+ </div>
163
+ <div class="ml-5">
164
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Total Feedback</p>
165
+ <h3 id="statTotal" class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_count }}</h3>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
170
+ <div class="flex items-center">
171
+ <div class="p-4 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
172
+ <i class="fa-solid fa-face-smile text-2xl"></i>
173
+ </div>
174
+ <div class="ml-5">
175
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Positive</p>
176
+ <div class="flex items-baseline space-x-2">
177
+ <h3 id="statPos" class="text-3xl font-bold text-gray-900 dark:text-white">{{ positive }}</h3>
178
+ <span id="pctPos" class="text-sm font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full">{{ pct_pos }}%</span>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
184
+ <div class="flex items-center">
185
+ <div class="p-4 rounded-xl bg-rose-500/10 text-rose-600 dark:text-rose-400">
186
+ <i class="fa-solid fa-face-frown text-2xl"></i>
187
+ </div>
188
+ <div class="ml-5">
189
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Negative</p>
190
+ <div class="flex items-baseline space-x-2">
191
+ <h3 id="statNeg" class="text-3xl font-bold text-gray-900 dark:text-white">{{ negative }}</h3>
192
+ <span id="pctNeg" class="text-sm font-bold text-rose-500 bg-rose-500/10 px-2 py-0.5 rounded-full">{{ pct_neg }}%</span>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
198
+ <div class="flex items-center">
199
+ <div class="p-4 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400">
200
+ <i class="fa-solid fa-face-meh text-2xl"></i>
201
+ </div>
202
+ <div class="ml-5">
203
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Neutral</p>
204
+ <div class="flex items-baseline space-x-2">
205
+ <h3 id="statNeu" class="text-3xl font-bold text-gray-900 dark:text-white">{{ neutral }}</h3>
206
+ <span id="pctNeu" class="text-sm font-bold text-violet-500 bg-violet-500/10 px-2 py-0.5 rounded-full">{{ pct_neu }}%</span>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Charts Row -->
214
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8 animate-fade-in animation-delay-200">
215
+ <div class="glass rounded-2xl shadow-sm p-6 lg:col-span-1 border border-white/20 dark:border-gray-700/50">
216
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4 tracking-tight">Sentiment Distribution</h3>
217
+ <div class="relative h-64">
218
+ {% if total_count > 0 %}
219
+ <canvas id="sentimentPieChart"></canvas>
220
+ {% else %}
221
+ <div class="flex h-full items-center justify-center text-gray-400 italic">No data available</div>
222
+ {% endif %}
223
+ </div>
224
+ </div>
225
+
226
+ <div class="glass rounded-2xl shadow-sm p-6 lg:col-span-2 border border-white/20 dark:border-gray-700/50">
227
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4 tracking-tight">Volume by Department</h3>
228
+ <div class="relative h-64">
229
+ {% if total_count > 0 %}
230
+ <canvas id="deptBarChart"></canvas>
231
+ {% else %}
232
+ <div class="flex h-full items-center justify-center text-gray-400 italic">No data available</div>
233
+ {% endif %}
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Keyword Cloud Row -->
239
+ <div class="glass rounded-2xl shadow-sm p-6 mb-8 border border-white/20 dark:border-gray-700/50 animate-fade-in animation-delay-250">
240
+ <div class="flex justify-between items-center mb-4">
241
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white tracking-tight"><i class="fa-solid fa-cloud-word mr-2 text-indigo-500"></i>AI Trending Keywords</h3>
242
+ <span class="text-xs font-semibold px-2 py-1 bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400 rounded-lg">Real-time Extraction</span>
243
+ </div>
244
+ <div class="relative h-64 w-full flex items-center justify-center overflow-hidden">
245
+ <canvas id="keywordCloud" class="w-full h-full"></canvas>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Recent Feedback Table -->
250
+ <div class="glass rounded-2xl shadow-lg border border-white/20 dark:border-gray-700/50 overflow-hidden animate-fade-in animation-delay-300">
251
+ <div class="px-6 py-5 border-b border-gray-200/50 dark:border-gray-700/50 bg-white/30 dark:bg-gray-800/30 flex justify-between items-center backdrop-blur-md">
252
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white tracking-tight">Recent Feedback</h3>
253
+ </div>
254
+ <div class="w-full">
255
+ <table id="recentFeedbackTable" class="w-full text-sm text-left text-gray-600 dark:text-gray-300">
256
+ <thead class="text-xs text-gray-500 uppercase bg-black/5 dark:bg-white/5 dark:text-gray-400 tracking-wider">
257
+ <tr>
258
+ <th scope="col" class="px-6 py-4 font-semibold">Department</th>
259
+ <th scope="col" class="px-6 py-4 font-semibold">Text Fragment</th>
260
+ <th scope="col" class="px-6 py-4 font-semibold">Sentiment</th>
261
+ <th scope="col" class="px-6 py-4 font-semibold">Score</th>
262
+ <th scope="col" class="px-6 py-4 font-semibold tracking-wider">Date</th>
263
+ </tr>
264
+ </thead>
265
+ <tbody class="divide-y divide-gray-200/50 dark:divide-gray-700/50">
266
+ {% for item in recent_feedback %}
267
+ <tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors duration-150">
268
+ <td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
269
+ {{ item.department_category or 'N/A' }}
270
+ </td>
271
+ <td class="px-6 py-4 truncate max-w-xs text-gray-600 dark:text-gray-300" title="{{ item.original_text }}">
272
+ {{ item.original_text[:50] }}{% if item.original_text|length > 50 %}...{% endif %}
273
+ </td>
274
+ <td class="px-6 py-4">
275
+ {{ item.sentiment | sentiment_badge }}
276
+ </td>
277
+ <td class="px-6 py-4 font-mono text-xs">
278
+ <span class="bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 px-2 py-1 rounded-md">{{ "%.2f"|format(item.sentiment_score) }}</span>
279
+ </td>
280
+ <td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
281
+ {{ item.created_at.strftime('%b %d, %Y %H:%M') }}
282
+ </td>
283
+ </tr>
284
+ {% else %}
285
+ <tr>
286
+ <td colspan="5" class="px-6 py-10 text-center text-gray-500 italic">
287
+ No feedback entries found in the system yet.
288
+ </td>
289
+ </tr>
290
+ {% endfor %}
291
+ </tbody>
292
+ </table>
293
+ </div>
294
+ </div>
295
+ {% endblock %}
templates/dashboard/hod_staff.html ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ current_user.role }} Dashboard - NeuroSent{% endblock %}
3
+
4
+ {% block extra_scripts %}
5
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
+ <script src="{{ url_for('static', filename='js/charts.js') }}"></script>
7
+ <script>
8
+ document.addEventListener('DOMContentLoaded', function() {
9
+ renderSentimentPieChart('sentimentPieChart', {
10
+ positive: {{ positive|default(0) }},
11
+ negative: {{ negative|default(0) }},
12
+ neutral: {{ neutral|default(0) }}
13
+ });
14
+
15
+ // Date Range Filter Logic
16
+ document.getElementById('applyDateFilter').addEventListener('click', function() {
17
+ const startDate = document.getElementById('startDate').value;
18
+ const endDate = document.getElementById('endDate').value;
19
+ const btn = this;
20
+
21
+ if(!startDate && !endDate) return;
22
+
23
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
24
+
25
+ fetch(`{{ url_for("dashboard.get_dashboard_data") }}?start_date=${startDate}&end_date=${endDate}`)
26
+ .then(res => res.json())
27
+ .then(data => {
28
+ btn.innerHTML = '<i class="fa-solid fa-filter"></i> Apply';
29
+ if(data.success) {
30
+ document.getElementById('statTotal').innerText = data.total_count;
31
+ document.getElementById('statPos').innerText = data.positive;
32
+ document.getElementById('statNeg').innerText = data.negative;
33
+ document.getElementById('statNeu').innerText = data.neutral;
34
+
35
+ document.getElementById('pctPos').innerText = data.pct_pos + '%';
36
+ document.getElementById('pctNeg').innerText = data.pct_neg + '%';
37
+ document.getElementById('pctNeu').innerText = data.pct_neu + '%';
38
+
39
+ const pieChart = Chart.instances[0];
40
+ if(pieChart) {
41
+ pieChart.data.datasets[0].data = [data.positive, data.negative, data.neutral];
42
+ pieChart.update();
43
+ }
44
+ }
45
+ })
46
+ .catch(err => {
47
+ btn.innerHTML = '<i class="fa-solid fa-filter"></i> Apply';
48
+ console.error("Date filter error:", err);
49
+ });
50
+ });
51
+ });
52
+ </script>
53
+ {% endblock %}
54
+
55
+ {% block content %}
56
+ <div class="mb-8 flex flex-col md:flex-row justify-between items-start md:items-end animate-fade-in">
57
+ <div>
58
+ <h1 class="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">{{ current_user.department or 'Your' }} Department Dashboard</h1>
59
+ <p class="text-gray-500 dark:text-gray-400 text-sm mt-2">Viewing analytics for {{ current_user.department or 'your specific scope' }}.</p>
60
+ </div>
61
+ </div>
62
+ <div class="mt-4 md:mt-0 flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-3 items-end">
63
+ <!-- Date Filter UI -->
64
+ <div class="flex items-center space-x-2 bg-white/20 dark:bg-gray-800/40 p-1.5 rounded-xl border border-white/20 dark:border-gray-700/50 backdrop-blur-md">
65
+ <input type="date" id="startDate" class="bg-transparent border-none text-xs text-gray-700 dark:text-gray-300 focus:ring-0 cursor-pointer" title="Start Date">
66
+ <span class="text-gray-400 text-xs">to</span>
67
+ <input type="date" id="endDate" class="bg-transparent border-none text-xs text-gray-700 dark:text-gray-300 focus:ring-0 cursor-pointer" title="End Date">
68
+ <button id="applyDateFilter" class="px-3 py-1.5 text-xs font-bold text-white bg-primary-500 hover:bg-primary-600 rounded-lg transition-colors shadow-sm">
69
+ <i class="fa-solid fa-filter"></i> Apply
70
+ </button>
71
+ </div>
72
+ <a href="{{ url_for('reports.download_pdf') }}" class="px-5 py-2.5 text-sm font-semibold text-primary-700 bg-primary-100/80 hover:bg-primary-200 dark:bg-primary-500/20 dark:text-primary-300 dark:hover:bg-primary-500/30 rounded-xl transition-all shadow-sm hover:shadow">
73
+ <i class="fa-solid fa-download mr-2"></i> Export Report
74
+ </a>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Stats Overview -->
79
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 animate-fade-in animation-delay-100">
80
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
81
+ <div class="flex items-center">
82
+ <div class="p-4 rounded-xl bg-blue-500/10 text-blue-600 dark:text-blue-400"><i class="fa-solid fa-filter text-2xl"></i></div>
83
+ <div class="ml-5">
84
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Total Feedback</p>
85
+ <h3 id="statTotal" class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_count }}</h3>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
90
+ <div class="flex items-center">
91
+ <div class="p-4 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"><i class="fa-solid fa-thumbs-up text-2xl"></i></div>
92
+ <div class="ml-5">
93
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Positive</p>
94
+ <div class="flex items-baseline space-x-2">
95
+ <h3 id="statPos" class="text-3xl font-bold text-gray-900 dark:text-white">{{ positive }}</h3>
96
+ <span id="pctPos" class="text-sm font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full">{{ pct_pos }}%</span>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
102
+ <div class="flex items-center">
103
+ <div class="p-4 rounded-xl bg-rose-500/10 text-rose-600 dark:text-rose-400"><i class="fa-solid fa-thumbs-down text-2xl"></i></div>
104
+ <div class="ml-5">
105
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Negative</p>
106
+ <div class="flex items-baseline space-x-2">
107
+ <h3 id="statNeg" class="text-3xl font-bold text-gray-900 dark:text-white">{{ negative }}</h3>
108
+ <span id="pctNeg" class="text-sm font-bold text-rose-500 bg-rose-500/10 px-2 py-0.5 rounded-full">{{ pct_neg }}%</span>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ <div class="glass rounded-2xl shadow-sm p-6 hover-card">
114
+ <div class="flex items-center">
115
+ <div class="p-4 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400"><i class="fa-solid fa-minus text-2xl"></i></div>
116
+ <div class="ml-5">
117
+ <p class="mb-1 text-sm text-gray-500 dark:text-gray-400 font-medium uppercase tracking-wider">Neutral</p>
118
+ <div class="flex items-baseline space-x-2">
119
+ <h3 id="statNeu" class="text-3xl font-bold text-gray-900 dark:text-white">{{ neutral }}</h3>
120
+ <span id="pctNeu" class="text-sm font-bold text-violet-500 bg-violet-500/10 px-2 py-0.5 rounded-full">{{ pct_neu }}%</span>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8 animate-fade-in animation-delay-200">
128
+ <div class="glass rounded-2xl shadow-sm p-6 lg:col-span-1 border border-white/20 dark:border-gray-700/50">
129
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4 tracking-tight">Sentiment Distribution</h3>
130
+ <div class="relative h-64">
131
+ {% if total_count > 0 %}
132
+ <canvas id="sentimentPieChart"></canvas>
133
+ {% else %}
134
+ <div class="flex h-full items-center justify-center text-gray-400 italic">No data available</div>
135
+ {% endif %}
136
+ </div>
137
+ </div>
138
+ <div class="glass rounded-2xl shadow-sm p-6 lg:col-span-2 border border-white/20 dark:border-gray-700/50 flex flex-col">
139
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4 tracking-tight">Recent Comments</h3>
140
+ <div class="flex-1 overflow-y-auto pr-2 space-y-4">
141
+ {% for item in recent_feedback %}
142
+ <div class="p-5 rounded-xl bg-white/40 dark:bg-gray-800/40 border border-gray-100/50 dark:border-gray-600/30 shadow-sm hover:shadow-md transition-shadow">
143
+ <div class="flex justify-between items-start mb-3">
144
+ <div class="flex items-center space-x-3">
145
+ {{ item.sentiment | sentiment_badge }}
146
+ <span class="text-xs font-medium text-gray-500">{{ item.created_at.strftime('%b %d, %Y %H:%M') }}</span>
147
+ </div>
148
+ </div>
149
+ <p class="text-gray-800 dark:text-gray-200 text-sm leading-relaxed tracking-wide">"{{ item.original_text }}"</p>
150
+ </div>
151
+ {% else %}
152
+ <div class="flex h-full items-center justify-center py-8">
153
+ <p class="text-gray-500 italic">No feedback recorded yet in this department.</p>
154
+ </div>
155
+ {% endfor %}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ {% endblock %}
templates/dashboard/profile.html ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}My Profile - NeuroSent{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="max-w-4xl mx-auto pt-4 animate-fade-in relative z-10">
6
+ <div class="mb-8 flex items-center justify-between">
7
+ <div>
8
+ <h1 class="text-3xl sm:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-indigo-600 dark:from-primary-400 dark:to-indigo-400 tracking-tight drop-shadow-sm">My Profile</h1>
9
+ <p class="text-gray-600 dark:text-gray-300 font-medium text-sm mt-1">Manage your account and view key statistics.</p>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
14
+
15
+ <!-- Left Column: User Identity Card -->
16
+ <div class="md:col-span-1">
17
+ <div class="glass rounded-3xl shadow-xl p-8 border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl relative overflow-hidden transition-all hover:shadow-2xl text-center h-full flex flex-col items-center justify-center">
18
+ <!-- Decorative Orb -->
19
+ <div class="absolute -top-10 -left-10 w-32 h-32 bg-primary-400/20 dark:bg-primary-500/10 rounded-full blur-3xl pointer-events-none"></div>
20
+
21
+ <div class="w-32 h-32 mb-6 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-black text-5xl shadow-inner ring-4 ring-white/30 dark:ring-gray-800/50 transform transition hover:scale-105 relative z-10">
22
+ {{ current_user.name[0] }}
23
+ </div>
24
+
25
+ <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-1 relative z-10">{{ current_user.name }}</h2>
26
+
27
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold leading-5 bg-primary-100 text-primary-800 dark:bg-primary-900/40 dark:text-primary-300 mb-4 relative z-10 border border-primary-200 dark:border-primary-800/60">
28
+ <i class="fa-solid fa-shield-halved mr-1.5"></i> {{ current_user.role }}
29
+ </span>
30
+
31
+ <p class="text-gray-500 dark:text-gray-400 text-sm font-medium relative z-10 flex items-center justify-center gap-2">
32
+ <i class="fa-solid fa-building text-gray-400"></i>
33
+ {{ current_user.department if current_user.department else 'No Department Assigned' }}
34
+ </p>
35
+
36
+ <div class="mt-8 pt-6 border-t border-gray-200/50 dark:border-gray-700/50 w-full relative z-10">
37
+ <p class="text-xs text-gray-500 dark:text-gray-400 font-medium">Joined NeuroSent on</p>
38
+ <p class="text-sm text-gray-800 dark:text-gray-200 font-bold mt-1">{{ current_user.created_at.strftime('%B %d, %Y') }}</p>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Right Column: Settings & Stats -->
44
+ <div class="md:col-span-2 space-y-8">
45
+
46
+ <!-- Statistics Card -->
47
+ <div class="glass rounded-3xl shadow-xl p-8 border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl relative overflow-hidden transition-all hover:shadow-2xl">
48
+ <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center">
49
+ <i class="fa-solid fa-chart-pie mr-3 text-primary-500"></i> Activity Overview
50
+ </h3>
51
+
52
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
53
+ <div class="bg-white/40 dark:bg-gray-800/40 rounded-2xl p-6 border border-white/50 dark:border-gray-700/50 shadow-sm flex items-center gap-5 transition hover:bg-white/60 dark:hover:bg-gray-800/60">
54
+ <div class="w-14 h-14 rounded-full bg-blue-100 dark:bg-blue-900/40 flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl shadow-inner">
55
+ <i class="fa-solid fa-comment-dots"></i>
56
+ </div>
57
+ <div>
58
+ <p class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Total Feedbacks</p>
59
+ <p class="text-3xl font-black text-gray-900 dark:text-white mt-1">{{ total_feedbacks }}</p>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="bg-white/40 dark:bg-gray-800/40 rounded-2xl p-6 border border-white/50 dark:border-gray-700/50 shadow-sm flex items-center gap-5 transition hover:bg-white/60 dark:hover:bg-gray-800/60">
64
+ <div class="w-14 h-14 rounded-full {% if avg_score > 0 %}bg-green-100 dark:bg-green-900/40 text-green-600 dark:text-green-400{% elif avg_score < 0 %}bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400{% else %}bg-gray-100 dark:bg-gray-700/40 text-gray-600 dark:text-gray-400{% endif %} flex items-center justify-center text-xl shadow-inner">
65
+ <i class="fa-solid fa-scale-balanced"></i>
66
+ </div>
67
+ <div>
68
+ <p class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Avg Sentiment</p>
69
+ <p class="text-3xl font-black text-gray-900 dark:text-white mt-1">{{ avg_score }}</p>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Account Details Form (Read-only for now) -->
76
+ <div class="glass rounded-3xl shadow-xl p-8 border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl relative overflow-hidden transition-all hover:shadow-2xl">
77
+ <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center">
78
+ <i class="fa-solid fa-id-card flex-shrink-0 mr-3 text-primary-500"></i> Account Details
79
+ </h3>
80
+
81
+ <div class="space-y-5">
82
+ <div>
83
+ <label class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide mb-2">Email Address</label>
84
+ <div class="relative">
85
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-gray-400">
86
+ <i class="fa-solid fa-envelope"></i>
87
+ </div>
88
+ <input type="email" value="{{ current_user.email }}" readonly class="bg-gray-100/50 border border-gray-300/30 text-gray-500 rounded-xl block w-full pl-11 p-3.5 dark:bg-gray-800/40 dark:border-gray-700/30 dark:text-gray-400 shadow-inner cursor-not-allowed font-medium">
89
+ </div>
90
+ </div>
91
+
92
+ <div>
93
+ <label class="block text-sm font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide mb-2">System Passkey</label>
94
+ <div class="relative">
95
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none text-gray-400">
96
+ <i class="fa-solid fa-lock"></i>
97
+ </div>
98
+ <!-- Display a fake masked password since we hash them -->
99
+ <input type="password" value="••••••••••••••" readonly class="bg-gray-100/50 border border-gray-300/30 text-gray-500 rounded-xl block w-full pl-11 p-3.5 dark:bg-gray-800/40 dark:border-gray-700/30 dark:text-gray-400 shadow-inner cursor-not-allowed tracking-widest">
100
+ </div>
101
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-2 italic"><i class="fa-solid fa-circle-info mr-1"></i> Passwords are one-way hashed and cannot be viewed. Contact administration to execute a password reset.</p>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ </div>
107
+ </div>
108
+ </div>
109
+ {% endblock %}
templates/dashboard/student.html ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}My Feedback - NeuroSent{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="mb-8 flex flex-col md:flex-row justify-between items-start md:items-end animate-fade-in">
6
+ <div>
7
+ <h1 class="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">My Submissions</h1>
8
+ <p class="text-gray-500 dark:text-gray-400 text-sm mt-2">History of your submitted feedback and its AI analysis.</p>
9
+ </div>
10
+ <div class="mt-4 md:mt-0">
11
+ <a href="{{ url_for('feedback.submit') }}" class="px-5 py-2.5 text-sm font-semibold text-white bg-gradient-to-r from-primary-600 to-indigo-600 rounded-xl hover:from-primary-700 hover:to-indigo-700 transition-all shadow-md hover:shadow-lg transform hover:-translate-y-0.5 inline-flex items-center">
12
+ <i class="fa-solid fa-plus mr-2"></i> New Feedback
13
+ </a>
14
+ </div>
15
+ </div>
16
+
17
+ <!-- Stats -->
18
+ <div class="flex space-x-6 mb-8 animate-fade-in animation-delay-100">
19
+ <div class="glass px-6 py-5 rounded-2xl shadow-sm hover-card border border-white/20 dark:border-gray-700/50 min-w-[200px]">
20
+ <div class="flex items-center space-x-4">
21
+ <div class="p-3 rounded-xl bg-primary-500/10 text-primary-600 dark:text-primary-400">
22
+ <i class="fa-solid fa-file-lines text-2xl"></i>
23
+ </div>
24
+ <div>
25
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider block mb-1">Total Submitted</span>
26
+ <div class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_count }}</div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="glass rounded-2xl shadow-lg border border-white/20 dark:border-gray-700/50 overflow-hidden animate-fade-in animation-delay-200">
33
+ <div class="px-6 py-5 border-b border-gray-200/50 dark:border-gray-700/50 bg-white/30 dark:bg-gray-800/30 flex justify-between items-center backdrop-blur-md">
34
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white tracking-tight">Submission History</h3>
35
+ </div>
36
+ {% if total_count > 0 %}
37
+ <div class="overflow-x-auto">
38
+ <table class="w-full text-sm text-left text-gray-600 dark:text-gray-300">
39
+ <thead class="text-xs text-gray-500 uppercase bg-black/5 dark:bg-white/5 dark:text-gray-400 tracking-wider">
40
+ <tr>
41
+ <th scope="col" class="px-6 py-4 font-semibold tracking-wider">Date</th>
42
+ <th scope="col" class="px-6 py-4 font-semibold w-1/2">Your Feedback</th>
43
+ <th scope="col" class="px-6 py-4 font-semibold">AI Sentiment</th>
44
+ </tr>
45
+ </thead>
46
+ <tbody class="divide-y divide-gray-200/50 dark:divide-gray-700/50">
47
+ {% for item in recent_feedback %}
48
+ <tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors duration-150">
49
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-medium text-gray-500 dark:text-gray-400">
50
+ {{ item.created_at.strftime('%b %d, %Y %H:%M') }}
51
+ </td>
52
+ <td class="px-6 py-4 text-gray-800 dark:text-gray-200 leading-relaxed tracking-wide">
53
+ {{ item.original_text }}
54
+ </td>
55
+ <td class="px-6 py-4">
56
+ {{ item.sentiment | sentiment_badge }}
57
+ </td>
58
+ </tr>
59
+ {% endfor %}
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+ {% else %}
64
+ <div class="text-center py-16">
65
+ <div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary-50 dark:bg-gray-800/50 mb-6 shadow-inner ring-4 ring-white/50 dark:ring-gray-700/50">
66
+ <i class="fa-solid fa-folder-open text-3xl text-primary-300 dark:text-gray-500 transform transition-transform hover:scale-110"></i>
67
+ </div>
68
+ <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight">No feedback yet</h3>
69
+ <p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">You haven't submitted any feedback manually. Start by sharing your thoughts.</p>
70
+ <div class="mt-8">
71
+ <a href="{{ url_for('feedback.submit') }}" class="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-gradient-to-r from-primary-600 to-indigo-600 rounded-xl hover:from-primary-700 hover:to-indigo-700 transition-all shadow hover:shadow-lg transform hover:-translate-y-0.5">
72
+ Submit your first feedback <i class="fa-solid fa-arrow-right ml-2"></i>
73
+ </a>
74
+ </div>
75
+ </div>
76
+ {% endif %}
77
+ </div>
78
+ {% endblock %}
templates/feedback/submit.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Submit Feedback - NeuroSent{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="mb-8 animate-fade-in">
6
+ <h1 class="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">Submit Feedback</h1>
7
+ <p class="text-gray-500 dark:text-gray-400 text-sm mt-2">Share your thoughts on products, services, or general experience.</p>
8
+ </div>
9
+
10
+ <div class="max-w-3xl mx-auto animate-fade-in animation-delay-100">
11
+ <div class="glass rounded-2xl shadow-xl p-8 sm:p-10 hover-card border border-white/20 dark:border-gray-700/50 relative overflow-hidden">
12
+ <!-- Decorative glowing orb -->
13
+ <div class="absolute -top-16 -right-16 w-32 h-32 bg-primary-500/20 rounded-full blur-2xl pointer-events-none"></div>
14
+
15
+ <form action="{{ url_for('feedback.submit') }}" method="POST" class="space-y-7 relative z-10">
16
+
17
+ {% if current_user.role in ['Admin', 'HOD', 'Staff'] %}
18
+ <div>
19
+ <label for="department" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Department / Category</label>
20
+ <div class="relative">
21
+ <div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
22
+ <i class="fa-solid fa-building text-primary-400"></i>
23
+ </div>
24
+ <input type="text" id="department" name="department" value="{{ current_user.department }}" class="bg-white/60 border border-gray-300/50 text-gray-900 text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full pl-11 p-3.5 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all focus:bg-white dark:focus:bg-gray-800" placeholder="e.g. Facilities, IT Support, etc.">
25
+ </div>
26
+ </div>
27
+ {% endif %}
28
+
29
+ <div>
30
+ <label for="text" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Your Feedback</label>
31
+ <textarea id="text" name="text" rows="6" class="block p-4 w-full text-sm text-gray-900 bg-white/60 rounded-xl border border-gray-300/50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700/60 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 transition-all shadow-sm focus:bg-white dark:focus:bg-gray-800" placeholder="Write your comments here... Please be as detailed as possible." required></textarea>
32
+ </div>
33
+
34
+ <div class="p-4 mb-4 text-sm text-indigo-800 rounded-xl bg-indigo-50/80 dark:bg-indigo-900/30 dark:text-indigo-300 border border-indigo-200/50 dark:border-indigo-800/50 shadow-sm backdrop-blur-sm" role="alert">
35
+ <div class="flex items-center font-bold mb-1 tracking-wide">
36
+ <i class="fa-solid fa-robot mr-2 text-indigo-500 dark:text-indigo-400"></i> AI Processing
37
+ </div>
38
+ Your feedback will be automatically analyzed by our advanced NLP sentiment engine once submitted.
39
+ </div>
40
+
41
+ <button type="submit" class="w-full text-white bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 focus:ring-4 focus:ring-primary-300 font-bold rounded-xl text-sm px-5 py-3.5 text-center dark:focus:ring-primary-900 transition-all shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 inline-flex items-center justify-center">
42
+ <i class="fa-solid fa-paper-plane mr-2 text-lg"></i> Submit Feedback
43
+ </button>
44
+ </form>
45
+ </div>
46
+ </div>
47
+ {% endblock %}
templates/upload/index.html ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Bulk Upload - NeuroSent{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="mb-8 animate-fade-in">
6
+ <h1 class="text-3xl font-extrabold text-gray-900 dark:text-white tracking-tight">Dataset Upload</h1>
7
+ <p class="text-gray-500 dark:text-gray-400 text-sm mt-2">Upload CSV or Excel files containing feedback for bulk sentiment processing.</p>
8
+ </div>
9
+
10
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
11
+
12
+ <!-- Upload Form -->
13
+ <div class="lg:col-span-1 animate-fade-in animation-delay-100">
14
+ <div class="glass rounded-2xl shadow-lg p-6 hover-card border border-white/20 dark:border-gray-700/50">
15
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-5 tracking-tight">Upload File</h3>
16
+
17
+ <form action="{{ url_for('upload.index') }}" method="POST" enctype="multipart/form-data" class="space-y-5 shadow-inner border border-gray-100/50 dark:border-gray-700/50 p-5 rounded-xl bg-white/40 dark:bg-gray-800/40 backdrop-blur-sm">
18
+ <div class="flex items-center justify-center w-full">
19
+ <label for="dataset" class="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-primary-300 dark:border-primary-500/30 rounded-xl cursor-pointer bg-white/50 dark:hover:bg-gray-800/60 dark:bg-gray-700/40 hover:bg-primary-50 hover:border-primary-400 transition-all duration-300 group">
20
+ <div class="flex flex-col items-center justify-center pt-5 pb-6 transform group-hover:-translate-y-1 transition-transform">
21
+ <i class="fa-solid fa-cloud-arrow-up text-5xl text-primary-400 group-hover:text-primary-500 mb-4 transition-colors"></i>
22
+ <p class="mb-2 text-sm text-gray-600 dark:text-gray-300"><span class="font-bold text-primary-600 dark:text-primary-400">Click to upload</span> or drag and drop</p>
23
+ <p class="text-xs text-gray-500 dark:text-gray-400">CSV, XLS, or XLSX (MAX. 16MB)</p>
24
+ </div>
25
+ <input id="dataset" name="dataset" type="file" class="hidden" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" required />
26
+ </label>
27
+ </div>
28
+
29
+ <div id="file-name-display" class="hidden text-sm text-center font-medium text-primary-700 dark:text-primary-300 bg-primary-100/50 dark:bg-primary-900/40 p-3 rounded-lg border border-primary-200/50 dark:border-primary-800/50 truncate shadow-sm"></div>
30
+
31
+ <div>
32
+ <label for="text_column" class="block mb-2 text-sm font-semibold text-gray-900 dark:text-gray-300">Target Text Column <span class="text-gray-400 font-normal">(Optional)</span></label>
33
+ <input type="text" id="text_column" name="text_column" class="bg-white/70 border border-gray-300/50 text-gray-900 text-sm rounded-xl focus:ring-primary-500 focus:border-primary-500 block w-full p-3 dark:bg-gray-700/70 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white shadow-sm transition-all" placeholder="e.g. 'review_text'">
34
+ <p class="mt-2 text-xs text-gray-500 dark:text-gray-400 italic">Leave blank to auto-detect the text column.</p>
35
+ </div>
36
+
37
+ <button type="submit" id="upload-btn" class="w-full text-white bg-gradient-to-r from-primary-600 to-indigo-600 hover:from-primary-700 hover:to-indigo-700 focus:ring-4 focus:ring-primary-300 font-semibold rounded-xl text-sm px-5 py-3 text-center dark:focus:ring-primary-800 transition-all shadow-md hover:shadow-lg transform hover:-translate-y-0.5 flex justify-center items-center">
38
+ <i class="fa-solid fa-gears mr-2"></i> Process Dataset
39
+ </button>
40
+ </form>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Upload History -->
45
+ <div class="lg:col-span-2 animate-fade-in animation-delay-200">
46
+ <div class="glass rounded-2xl shadow-lg border border-white/20 dark:border-gray-700/50 overflow-hidden">
47
+ <div class="px-6 py-5 border-b border-gray-200/50 dark:border-gray-700/50 bg-white/30 dark:bg-gray-800/30 flex justify-between items-center backdrop-blur-md">
48
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white tracking-tight">Upload History</h3>
49
+ </div>
50
+
51
+ <div class="overflow-x-auto">
52
+ <table class="w-full text-sm text-left text-gray-600 dark:text-gray-300">
53
+ <thead class="text-xs text-gray-500 uppercase bg-black/5 dark:bg-white/5 dark:text-gray-400 tracking-wider">
54
+ <tr>
55
+ <th scope="col" class="px-6 py-4 font-semibold">Filename</th>
56
+ <th scope="col" class="px-6 py-4 font-semibold">Status</th>
57
+ <th scope="col" class="px-6 py-4 font-semibold">Processed</th>
58
+ <th scope="col" class="px-6 py-4 font-semibold tracking-wider">Date</th>
59
+ <th scope="col" class="px-6 py-4 font-semibold text-right">Actions</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody class="divide-y divide-gray-200/50 dark:divide-gray-700/50">
63
+ {% for upload in uploads %}
64
+ <tr class="hover:bg-black/5 dark:hover:bg-white/5 transition-colors duration-150">
65
+ <td class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
66
+ <i class="fa-solid fa-file-csv text-primary-400 mr-2 text-lg"></i> {{ upload.filename }}
67
+ </td>
68
+ <td class="px-6 py-4">
69
+ {% if upload.status == 'Completed' %}
70
+ <span class="bg-emerald-100/80 text-emerald-800 border border-emerald-200 dark:border-emerald-800/50 text-xs font-semibold px-2.5 py-1 rounded-md dark:bg-emerald-900/50 dark:text-emerald-300 shadow-sm">Completed</span>
71
+ {% elif upload.status == 'Failed' %}
72
+ <span class="bg-rose-100/80 text-rose-800 border border-rose-200 dark:border-rose-800/50 text-xs font-semibold px-2.5 py-1 rounded-md dark:bg-rose-900/50 dark:text-rose-300 shadow-sm">Failed</span>
73
+ {% else %}
74
+ <span class="bg-amber-100/80 text-amber-800 border border-amber-200 dark:border-amber-800/50 text-xs font-semibold px-2.5 py-1 rounded-md dark:bg-amber-900/50 dark:text-amber-300 shadow-sm">
75
+ <i class="fa-solid fa-circle-notch fa-spin mr-1"></i> Processing
76
+ </span>
77
+ {% endif %}
78
+ </td>
79
+ <td class="px-6 py-4 font-mono text-xs">
80
+ <span class="bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 px-2 py-1 rounded-md">{{ upload.processed_rows }} / {{ upload.total_rows }}</span>
81
+ </td>
82
+ <td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
83
+ {{ upload.upload_date.strftime('%b %d, %Y %H:%M') }}
84
+ </td>
85
+ <td class="px-6 py-4 text-right">
86
+ {% if current_user.role == 'Admin' %}
87
+ <form action="{{ url_for('upload.delete_upload', upload_id=upload.id) }}" method="POST" class="inline" onsubmit="return confirm('Are you sure you want to delete this upload and ALL its associated feedback records?');">
88
+ <button type="submit" class="text-rose-500 hover:text-rose-700 dark:text-rose-400 dark:hover:text-rose-300 p-2 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-lg transition-colors" title="Delete Upload">
89
+ <i class="fa-solid fa-trash-can text-lg"></i>
90
+ </button>
91
+ </form>
92
+ {% endif %}
93
+ </td>
94
+ </tr>
95
+ {% else %}
96
+ <tr>
97
+ <td colspan="5" class="px-6 py-12 text-center text-gray-500">
98
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 mb-4 shadow-inner">
99
+ <i class="fa-solid fa-inbox text-2xl text-gray-400"></i>
100
+ </div>
101
+ <p class="italic">No dataset uploads found.</p>
102
+ </td>
103
+ </tr>
104
+ {% endfor %}
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ {% endblock %}
113
+
114
+ {% block extra_scripts %}
115
+ <script>
116
+ const fileInput = document.getElementById('dataset');
117
+ const fileNameDisplay = document.getElementById('file-name-display');
118
+ const uploadBtn = document.getElementById('upload-btn');
119
+
120
+ fileInput.addEventListener('change', function(e) {
121
+ if(e.target.files.length > 0) {
122
+ fileNameDisplay.textContent = 'Selected: ' + e.target.files[0].name;
123
+ fileNameDisplay.classList.remove('hidden');
124
+ }
125
+ });
126
+
127
+ // Optional: show loading state on form submit
128
+ const form = document.querySelector('form');
129
+ form.addEventListener('submit', function() {
130
+ if(fileInput.files.length > 0) {
131
+ uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i>Processing...';
132
+ uploadBtn.disabled = true;
133
+ uploadBtn.classList.add('opacity-75');
134
+ // Form submission will continue normally
135
+ }
136
+ });
137
+ </script>
138
+ {% endblock %}
test_app.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from app import create_app
3
+ from models import db, User
4
+
5
+ @pytest.fixture
6
+ def test_client():
7
+ # Configure app for testing
8
+ app = create_app()
9
+ app.config['TESTING'] = True
10
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' # Use in-memory SQLite for tests
11
+ app.config['WTF_CSRF_ENABLED'] = False
12
+
13
+ with app.test_client() as testing_client:
14
+ with app.app_context():
15
+ db.create_all()
16
+
17
+ # Seed test users
18
+ admin = User(name="Admin test", email="admin@test.com", role="Admin")
19
+ admin.set_password("pass123")
20
+
21
+ student = User(name="Student test", email="student@test.com", role="Student")
22
+ student.set_password("pass123")
23
+
24
+ db.session.add_all([admin, student])
25
+ db.session.commit()
26
+
27
+ yield testing_client
28
+
29
+ db.session.remove()
30
+ db.drop_all()
31
+
32
+ def test_login_page_loads(test_client):
33
+ response = test_client.get('/auth/login')
34
+ assert response.status_code == 200
35
+
36
+ def test_register_page_loads(test_client):
37
+ response = test_client.get('/auth/register')
38
+ assert response.status_code == 200
39
+
40
+ def login(client, email, password):
41
+ return client.post('/auth/login', data=dict(
42
+ email=email,
43
+ password=password
44
+ ), follow_redirects=True)
45
+
46
+ def logout(client):
47
+ return client.get('/auth/logout', follow_redirects=True)
48
+
49
+ def test_dashboard_access(test_client):
50
+ # Unauthenticated should redirect
51
+ response = test_client.get('/dashboard/', follow_redirects=False)
52
+ assert response.status_code == 302
53
+ assert '/auth/login' in response.headers.get('Location')
54
+
55
+ # Authenticated should succeed
56
+ login(test_client, 'student@test.com', 'pass123')
57
+ response2 = test_client.get('/dashboard/')
58
+ assert response2.status_code == 200
59
+
60
+ def test_admin_routes_security(test_client):
61
+ # Login as student
62
+ login(test_client, 'student@test.com', 'pass123')
63
+
64
+ # Try access admin route
65
+ response = test_client.get('/admin/create-user', follow_redirects=True)
66
+ # Should be redirected to dashboard
67
+ assert b'Access denied' in response.data
68
+
69
+ logout(test_client)
70
+
71
+ # Login as admin
72
+ login(test_client, 'admin@test.com', 'pass123')
73
+ response2 = test_client.get('/admin/create-user')
74
+ assert response2.status_code == 200
75
+
76
+ response3 = test_client.get('/admin/manage-users')
77
+ assert response3.status_code == 200
78
+
79
+ def test_profile_route(test_client):
80
+ login(test_client, 'student@test.com', 'pass123')
81
+ response = test_client.get('/profile/')
82
+ assert response.status_code == 200
83
+
84
+ def test_submit_feedback_page(test_client):
85
+ login(test_client, 'student@test.com', 'pass123')
86
+ response = test_client.get('/feedback/submit')
87
+ assert response.status_code == 200
88
+
89
+ def test_upload_route_security(test_client):
90
+ # Student cannot upload
91
+ login(test_client, 'student@test.com', 'pass123')
92
+ response = test_client.get('/upload/', follow_redirects=True)
93
+ assert b'permission to access' in b''.join(response.data.split()) or b'permission' in response.data
94
+ logout(test_client)
95
+
96
+ # Admin can upload
97
+ login(test_client, 'admin@test.com', 'pass123')
98
+ response2 = test_client.get('/upload/')
99
+ assert response2.status_code == 200
test_model.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.nlp_utils import analyze_sentiment
2
+
3
+ print("Testing Positive:")
4
+ print(analyze_sentiment("This is an absolutely fantastic product, highly recommend!"))
5
+
6
+ print("\nTesting Negative:")
7
+ print(analyze_sentiment("What a terrible waste of money. Completely broken."))
8
+
9
+ print("\nTesting Neutral:")
10
+ print(analyze_sentiment("The package arrived on Tuesday."))
utils/decorators.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import wraps
2
+ from flask import redirect, url_for, flash
3
+ from flask_login import current_user
4
+
5
+ def requires_roles(*roles):
6
+ """
7
+ Decorator to restrict access to endpoints based on user roles.
8
+ """
9
+ def wrapper(f):
10
+ @wraps(f)
11
+ def wrapped(*args, **kwargs):
12
+ if not current_user.is_authenticated:
13
+ return redirect(url_for('auth.login'))
14
+ if current_user.role not in roles:
15
+ flash("You do not have permission to access this page.", "danger")
16
+ return redirect(url_for('dashboard.index'))
17
+ return f(*args, **kwargs)
18
+ return wrapped
19
+ return wrapper
utils/file_processor.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from werkzeug.utils import secure_filename
4
+ from utils.nlp_utils import preprocess_text, analyze_sentiment
5
+ from models import db, Feedback, Upload
6
+ from flask import current_app
7
+
8
+ def allowed_file(filename):
9
+ return '.' in filename and \
10
+ filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
11
+
12
+ def identify_text_column(df):
13
+ """
14
+ Heuristic to find the column most likely containing the feedback text.
15
+ Looks for keywords or selects the column with the longest average string length.
16
+ """
17
+ # Lowercase column names for easier checking
18
+ col_names = [str(c).lower().strip() for c in df.columns]
19
+
20
+ # Expanded heuristic matching for academic, social media, and product reviews
21
+ keywords = [
22
+ 'review_text', 'feedback_text', 'text', 'feedback', 'review',
23
+ 'comment', 'description', 'message', 'body', 'content', 'tweet', 'post'
24
+ ]
25
+ for kw in keywords:
26
+ for i, col in enumerate(col_names):
27
+ if kw == col or kw in col:
28
+ return df.columns[i]
29
+
30
+ # Fallback: Find column with string type and highest max length
31
+ text_cols = df.select_dtypes(include=['object', 'string']).columns
32
+ if len(text_cols) == 0:
33
+ return None
34
+
35
+ max_len_col = text_cols[0]
36
+ max_len = 0
37
+
38
+ for col in text_cols:
39
+ # Get mean length of strings in this column (sample first 50 rows for speed)
40
+ avg_len = df[col].astype(str).head(50).apply(len).mean()
41
+ if avg_len > max_len:
42
+ max_len = avg_len
43
+ max_len_col = col
44
+
45
+ return max_len_col
46
+
47
+ def process_uploaded_file(filepath, upload_record_id, user_id, selected_column=None):
48
+ """
49
+ Reads CSV/Excel, processes rows for sentiment, and stores to database.
50
+ """
51
+ try:
52
+ # Read file
53
+ if filepath.endswith('.csv'):
54
+ df = pd.read_csv(filepath)
55
+ elif filepath.endswith(('.xls', '.xlsx')):
56
+ df = pd.read_excel(filepath)
57
+ else:
58
+ return False, "Unsupported file format"
59
+
60
+ if df.empty:
61
+ return False, "The uploaded file is empty."
62
+
63
+ # Determine target text column
64
+ text_col = selected_column if selected_column and selected_column in df.columns else identify_text_column(df)
65
+ if not text_col:
66
+ return False, "Could not identify a text/feedback column in the dataset."
67
+
68
+ # Look for a department/category column if it exists
69
+ dept_col = None
70
+ dept_keywords = [
71
+ 'department', 'category', 'product', 'dept', 'course',
72
+ 'branch', 'faculty', 'subject', 'program', 'unit'
73
+ ]
74
+ for col in df.columns:
75
+ if any(kw in str(col).lower().strip() for kw in dept_keywords):
76
+ dept_col = col
77
+ break
78
+
79
+ # Process and prepare bulk insert data
80
+ feedbacks = []
81
+ total_rows = len(df)
82
+
83
+ # We can drop NA from the text column to avoid processing empties
84
+ df = df.dropna(subset=[text_col])
85
+
86
+ for index, row in df.iterrows():
87
+ orig_text = str(row[text_col])
88
+
89
+ # Skip very empty rows
90
+ if not orig_text.strip() or orig_text.lower() == 'nan':
91
+ continue
92
+
93
+ cleaned_text = preprocess_text(orig_text)
94
+ sentiment, score = analyze_sentiment(cleaned_text)
95
+
96
+ department = str(row[dept_col]) if dept_col and pd.notna(row[dept_col]) else None
97
+
98
+ feedback = Feedback(
99
+ user_id=user_id,
100
+ upload_id=upload_record_id,
101
+ original_text=orig_text,
102
+ cleaned_text=cleaned_text,
103
+ sentiment=sentiment,
104
+ sentiment_score=score,
105
+ department_category=department
106
+ )
107
+ feedbacks.append(feedback)
108
+
109
+ # Update upload tally
110
+ upload_record = Upload.query.get(upload_record_id)
111
+ if upload_record:
112
+ upload_record.total_rows = total_rows
113
+ upload_record.processed_rows = len(feedbacks)
114
+ upload_record.status = 'Completed'
115
+
116
+ # Bulk save
117
+ db.session.bulk_save_objects(feedbacks)
118
+ db.session.commit()
119
+
120
+ return True, f"Successfully processed {len(feedbacks)} out of {total_rows} rows."
121
+
122
+ except Exception as e:
123
+ db.session.rollback()
124
+ # Mark as failed
125
+ upload_record = Upload.query.get(upload_record_id)
126
+ if upload_record:
127
+ upload_record.status = 'Failed'
128
+ db.session.commit()
129
+ return False, str(e)
utils/nlp_utils.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import nltk
3
+ from transformers import pipeline
4
+ import torch
5
+
6
+ import os
7
+ # Define custom model path relative to this file
8
+ CUSTOM_MODEL_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "custom_model")
9
+ BASE_MODEL = "cardiffnlp/twitter-roberta-base-sentiment-latest"
10
+
11
+ try:
12
+ # We use device=0 if GPU is available, else -1 for CPU
13
+ device = 0 if torch.cuda.is_available() else -1
14
+
15
+ # Check if we have a locally fine-tuned model
16
+ if os.path.exists(CUSTOM_MODEL_DIR):
17
+ print("Detected local completely CUSTOM fine-tuned AI model! Loading...")
18
+ sentiment_pipeline = pipeline(
19
+ "sentiment-analysis",
20
+ model=CUSTOM_MODEL_DIR,
21
+ tokenizer=CUSTOM_MODEL_DIR,
22
+ device=device
23
+ )
24
+ else:
25
+ print("Loading default Hugging Face Transformers model.")
26
+ sentiment_pipeline = pipeline(
27
+ "sentiment-analysis",
28
+ model=BASE_MODEL,
29
+ device=device
30
+ )
31
+ print("AI Engine Online.")
32
+ except Exception as e:
33
+ print(f"Failed to load Transformers model: {e}")
34
+ sentiment_pipeline = None
35
+
36
+ # Basic stop words fallback if NLTK fails to load
37
+ FALLBACK_STOPWORDS = {"i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your", "yours", "yourself", "yourselves", "he", "him", "his", "himself", "she", "her", "hers", "herself", "it", "its", "itself", "they", "them", "their", "theirs", "themselves", "what", "which", "who", "whom", "this", "that", "these", "those", "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "having", "do", "does", "did", "doing", "a", "an", "the", "and", "but", "if", "or", "because", "as", "until", "while", "of", "at", "by", "for", "with", "about", "against", "between", "into", "through", "during", "before", "after", "above", "below", "to", "from", "up", "down", "in", "out", "on", "off", "over", "under", "again", "further", "then", "once", "here", "there", "when", "where", "why", "how", "all", "any", "both", "each", "few", "more", "most", "other", "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "s", "t", "can", "will", "just", "don", "should", "now"}
38
+
39
+ # Attempt to load NLTK corpora gracefully
40
+ stop_words = FALLBACK_STOPWORDS
41
+ use_nltk_tokenize = False
42
+
43
+ try:
44
+ try:
45
+ nltk.data.find('corpora/stopwords')
46
+ except LookupError:
47
+ nltk.download('stopwords', quiet=True)
48
+ from nltk.corpus import stopwords
49
+ stop_words = set(stopwords.words('english'))
50
+
51
+ try:
52
+ nltk.data.find('tokenizers/punkt')
53
+ except LookupError:
54
+ nltk.download('punkt', quiet=True)
55
+ nltk.download('punkt_tab', quiet=True)
56
+ use_nltk_tokenize = True
57
+ except Exception as e:
58
+ # Environment blocked NLTK Zip extraction (e.g. Zip Slip blocked)
59
+ print(f"NLTK initialization encountered an error: {e}. Falling back to basic tokenization.")
60
+
61
+ def preprocess_text(text):
62
+ """
63
+ Cleans text by removing punctuation, converting to lowercase,
64
+ and removing stopwords.
65
+ """
66
+ if not isinstance(text, str):
67
+ return ""
68
+
69
+ # Lowercase
70
+ text = text.lower()
71
+
72
+ # Remove punctuation & special characters
73
+ text = re.sub(r'[^\w\s]', '', text)
74
+
75
+ # Tokenization and Stopword removal
76
+ if use_nltk_tokenize:
77
+ try:
78
+ from nltk.tokenize import word_tokenize
79
+ tokens = word_tokenize(text)
80
+ except Exception:
81
+ tokens = text.split()
82
+ else:
83
+ tokens = text.split()
84
+
85
+ filtered_tokens = [w for w in tokens if w not in stop_words]
86
+
87
+ return ' '.join(filtered_tokens)
88
+
89
+ def analyze_sentiment(text):
90
+ """
91
+ Calculates polarity and categorizes sentiment using huggingface pipeline.
92
+ Returns (sentiment_category, polarity_score)
93
+ """
94
+ if not text or not sentiment_pipeline:
95
+ # Fallback to neutral if pipeline fails or text is empty
96
+ return 'Neutral', 0.0
97
+
98
+ try:
99
+ # The pipeline may fail on texts that are too long (e.g., > 512 tokens)
100
+ # We truncate the input string to roughly 2000 characters as a safe fallback
101
+ safe_text = text[:2000]
102
+
103
+ result = sentiment_pipeline(safe_text)[0]
104
+ label = result['label'].lower()
105
+ score = result['score'] # Confidence score between 0 and 1
106
+
107
+ # Convert label to our standard format and map score to [-1, 1] range to match DB expectations
108
+ # Support both direct string evaluation and underlying LABEL_id mappings (0: Neg, 1: Neu, 2: Pos for CardiffNLP)
109
+ if label == 'positive' or label == 'label_2':
110
+ return 'Positive', float(score)
111
+ elif label == 'negative' or label == 'label_0':
112
+ return 'Negative', -float(score)
113
+ else:
114
+ return 'Neutral', 0.0
115
+
116
+ except Exception as e:
117
+ print(f"Error in sentiment pipeline: {e}")
118
+ return 'Neutral', 0.0
119
+