Quivara commited on
Commit
bdb271a
·
0 Parent(s):

Fresh upload with LFS

Browse files
Files changed (36) hide show
  1. .gitattributes +2 -0
  2. .gitignore +13 -0
  3. alisto_project/Dockerfile +24 -0
  4. alisto_project/backend/alisto.db +0 -0
  5. alisto_project/backend/app.py +210 -0
  6. alisto_project/backend/augment_data.py +172 -0
  7. alisto_project/backend/images/alert.png +0 -0
  8. alisto_project/backend/images/alert1.png +0 -0
  9. alisto_project/backend/images/bg.jpg +3 -0
  10. alisto_project/backend/images/bg1.jpg +3 -0
  11. alisto_project/backend/images/bg2-bw.jpg +3 -0
  12. alisto_project/backend/images/bg2-logan.jpg +3 -0
  13. alisto_project/backend/images/bg2.jpg +3 -0
  14. alisto_project/backend/images/earthquake.png +0 -0
  15. alisto_project/backend/images/earthquake1.png +0 -0
  16. alisto_project/backend/index.html +260 -0
  17. alisto_project/backend/ingest_reddit.py +423 -0
  18. alisto_project/backend/init_db.py +28 -0
  19. alisto_project/backend/login.html +92 -0
  20. alisto_project/backend/map.html +44 -0
  21. alisto_project/backend/map_script.js +219 -0
  22. alisto_project/backend/models.py +64 -0
  23. alisto_project/backend/models/tfidf_ensemble.pkl +3 -0
  24. alisto_project/backend/my_generator.py +72 -0
  25. alisto_project/backend/ner_extractor.py +105 -0
  26. alisto_project/backend/script.js +594 -0
  27. alisto_project/backend/simulate_feed.py +127 -0
  28. alisto_project/backend/style.css +763 -0
  29. alisto_project/backend/templates.py +43 -0
  30. alisto_project/backend/train_ensemble.py +71 -0
  31. alisto_project/backend/train_model.py +136 -0
  32. alisto_project/data/reddit_disaster_posts.csv +0 -0
  33. alisto_project/data/seed_data.txt +0 -0
  34. alisto_project/requirements.txt +17 -0
  35. alisto_project/start.sh +9 -0
  36. instance/alisto.db +0 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
2
+ *.pkl filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore the virtual environment folder
2
+ .venv/
3
+ venv/
4
+ env/
5
+
6
+ # Ignore compiled python files (junk)
7
+ __pycache__/
8
+ *.pyc
9
+
10
+ # Ignore environment variables (sensitive passwords/keys)
11
+ .env
12
+
13
+ alisto_project/backend/models/roberta_model/
alisto_project/Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9
2
+ FROM python:3.9
3
+
4
+ # Set up the folder
5
+ WORKDIR /code
6
+
7
+ # Copy requirements and install them
8
+ COPY ./requirements.txt /code/requirements.txt
9
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
10
+
11
+ # Copy all your code files
12
+ COPY . .
13
+
14
+ # Create a writable folder for the AI model to download into
15
+ # (Hugging Face requires this permission step)
16
+ RUN mkdir -p /tmp/cache
17
+ RUN chmod 777 /tmp/cache
18
+ ENV TRANSFORMERS_CACHE=/tmp/cache
19
+
20
+ # Give permission to run the start script
21
+ RUN chmod +x /code/start.sh
22
+
23
+ # Start the "All-in-One" script
24
+ CMD ["/code/start.sh"]
alisto_project/backend/alisto.db ADDED
Binary file (36.9 kB). View file
 
alisto_project/backend/app.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import csv
3
+ import io
4
+ from flask import Flask, render_template, jsonify, request, Response, redirect, url_for
5
+ from flask_cors import CORS
6
+ from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
7
+ from werkzeug.security import generate_password_hash, check_password_hash
8
+ from sqlalchemy import func
9
+ from models import db, DisasterPost
10
+
11
+ # 1. CONFIG: FLAT STRUCTURE (Look in current directory '.')
12
+ # initializes the Flask application
13
+ app = Flask(__name__, static_url_path='', static_folder='.', template_folder='.')
14
+ # sets a secret key for session management and security
15
+ app.secret_key = "alisto_secret_key_secure"
16
+
17
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ DB_PATH = os.path.join(BASE_DIR, 'alisto.db')
19
+ # configures the application to use the SQLite database
20
+ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
21
+ # disables modification tracking to save resources
22
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
23
+ # enables Cross-Origin Resource Sharing for API requests
24
+ CORS(app)
25
+ # initializes the SQLAlchemy database object with the Flask app
26
+ db.init_app(app)
27
+
28
+ # 2. AUTH CONFIG
29
+ # initializes Flask-Login manager for user session handling
30
+ login_manager = LoginManager()
31
+ login_manager.init_app(app)
32
+ # sets the default view for unauthenticated users
33
+ login_manager.login_view = 'login_page'
34
+
35
+ # defines the User model for database and login functionality
36
+ class User(UserMixin, db.Model):
37
+ id = db.Column(db.Integer, primary_key=True)
38
+ username = db.Column(db.String(100), unique=True)
39
+ password = db.Column(db.String(100))
40
+
41
+ # flask-Login callback to reload the user object from the user ID stored in the session
42
+ @login_manager.user_loader
43
+ def load_user(user_id):
44
+ return User.query.get(int(user_id))
45
+
46
+ # creates the default 'admin' user if it does not already exist in the database
47
+ def create_admin():
48
+ with app.app_context():
49
+ db.create_all()
50
+ if not User.query.filter_by(username='admin').first():
51
+ hashed = generate_password_hash('admin123', method='pbkdf2:sha256')
52
+ db.session.add(User(username='admin', password=hashed))
53
+ db.session.commit()
54
+ print("✅ Admin Created: admin / admin123")
55
+
56
+ # 3. ROUTES
57
+
58
+ # route to display the login page template
59
+ @app.route('/login')
60
+ def login_page():
61
+ if current_user.is_authenticated:
62
+ return redirect(url_for('index'))
63
+ return render_template('login.html')
64
+
65
+ # route to display the main dashboard (requires login)
66
+ @app.route('/')
67
+ @login_required
68
+ def index():
69
+ return render_template('index.html')
70
+
71
+ # route to display the live map view (requires login)
72
+ @app.route('/map')
73
+ @login_required
74
+ def map_view():
75
+ return render_template('map.html')
76
+
77
+ # --- API ROUTES ---
78
+ # api endpoint for user authentication via POST request
79
+ @app.route('/api/login', methods=['POST'])
80
+ def login_api():
81
+ data = request.get_json()
82
+ user = User.query.filter_by(username=data.get('username')).first()
83
+ # checks hashed password and logs the user in on success
84
+ if user and check_password_hash(user.password, data.get('password')):
85
+ login_user(user)
86
+ return jsonify({"message": "Success"})
87
+ return jsonify({"message": "Invalid credentials"}), 401
88
+
89
+ # api endpoint for logging out the current user
90
+ @app.route('/api/logout')
91
+ @login_required
92
+ def logout():
93
+ logout_user()
94
+ return redirect(url_for('login_page'))
95
+
96
+ # api endpoint to fetch filtered, sorted, and paginated disaster posts
97
+ @app.route('/api/posts')
98
+ @login_required
99
+ def get_posts():
100
+ search_query = request.args.get('query')
101
+ sort_order = request.args.get('sort', 'newest')
102
+ view_mode = request.args.get('view', 'active')
103
+ urgency_filter = request.args.get('urgency', 'all')
104
+ type_filter = request.args.get('type', 'all')
105
+ assist_filter = request.args.get('assist', 'all')
106
+
107
+ posts_query = DisasterPost.query
108
+ posts_query = DisasterPost.query
109
+
110
+ # applies full-text search filter across title, content, and location
111
+ if search_query:
112
+ pattern = f"%{search_query}%"
113
+ posts_query = posts_query.filter(DisasterPost.title.ilike(pattern) | DisasterPost.content.ilike(pattern) | DisasterPost.location.ilike(pattern))
114
+
115
+ # --- MODIFIED VIEW MODE FILTER LOGIC ---
116
+ # filters to show only resolved/archived posts
117
+ if view_mode == 'archived':
118
+ posts_query = posts_query.filter(DisasterPost.status == 'Resolved')
119
+
120
+ # filters to show only active (New or Verified) posts
121
+ elif view_mode == 'active':
122
+ posts_query = posts_query.filter(DisasterPost.status.in_(['New', 'Verified']))
123
+
124
+ # applies filter based on the 'urgency_level' parameter
125
+ if urgency_filter != 'all': posts_query = posts_query.filter(DisasterPost.urgency_level == urgency_filter)
126
+
127
+ # applies sorting by timestamp (newest or oldest)
128
+ if sort_order == 'oldest': posts_query = posts_query.order_by(DisasterPost.timestamp.asc())
129
+ else: posts_query = posts_query.order_by(DisasterPost.timestamp.desc())
130
+
131
+ # applies filter based on the 'urgency_level' parameter
132
+ if urgency_filter != 'all': posts_query = posts_query.filter(DisasterPost.urgency_level == urgency_filter)
133
+
134
+ # applies filter based on the 'disaster_type' parameter
135
+ if type_filter != 'all': posts_query = posts_query.filter(DisasterPost.disaster_type == type_filter)
136
+ # applies filter based on the 'assistance_type' parameter
137
+ if assist_filter != 'all': posts_query = posts_query.filter(DisasterPost.assistance_type == assist_filter)
138
+
139
+ # returns the first 100 posts matching the filters as a JSON array
140
+ return jsonify([p.to_dict() for p in posts_query.limit(100).all()])
141
+
142
+ # api endpoint to update the status (New, Verified, Resolved) of a specific post ID
143
+ @app.route('/api/posts/<int:post_id>/status', methods=['POST'])
144
+ @login_required
145
+ def update_status(post_id):
146
+ data = request.get_json()
147
+ post = DisasterPost.query.get(post_id)
148
+ # finds the post and updates its 'status' field
149
+ if post:
150
+ post.status = data.get('status')
151
+ db.session.commit()
152
+ return jsonify({"success": True})
153
+ return jsonify({"error": "Not found"}), 404
154
+
155
+ # api endpoint to fetch statistics (counts by disaster type and urgency level) for charts
156
+ @app.route('/api/stats')
157
+ @login_required
158
+ def get_stats():
159
+ # queries the count of active posts grouped by disaster type
160
+ disaster_counts = db.session.query(DisasterPost.disaster_type, func.count(DisasterPost.id)).filter(DisasterPost.status.in_(['New', 'Verified'])).group_by(DisasterPost.disaster_type).all()
161
+ # queries the count of active posts grouped by urgency level
162
+ urgency_counts = db.session.query(DisasterPost.urgency_level, func.count(DisasterPost.id)).filter(DisasterPost.status.in_(['New', 'Verified'])).group_by(DisasterPost.urgency_level).all()
163
+ # returns both sets of counts as a single JSON object
164
+ return jsonify({"disaster_types": dict(disaster_counts), "urgency_levels": dict(urgency_counts)})
165
+
166
+ # api endpoint to export the full post database as a CSV file
167
+ @app.route('/api/export')
168
+ @login_required
169
+ def export_csv():
170
+ # queries ALL posts in the database, regardless of status
171
+ posts = DisasterPost.query.order_by(DisasterPost.timestamp.desc()).all()
172
+
173
+ output = io.StringIO()
174
+ writer = csv.writer(output)
175
+
176
+ # writes the header row with all necessary triage columns
177
+ writer.writerow([
178
+ 'ID', 'Time', 'Location', 'Contact Number',
179
+ 'Disaster Type', 'Assistance Type', 'Urgency', 'Status', 'Content'
180
+ ])
181
+
182
+ # iterates through posts and writes data rows to the CSV output stream
183
+ for p in posts:
184
+ writer.writerow([
185
+ p.id,
186
+ p.timestamp,
187
+ p.location,
188
+ p.contact_number,
189
+ p.disaster_type,
190
+ p.assistance_type,
191
+ p.urgency_level,
192
+ p.status,
193
+ p.content
194
+ ])
195
+
196
+ output.seek(0)
197
+ # returns the CSV data as an attachment for download
198
+ return Response(output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=alisto_full_report.csv"})
199
+
200
+ # api endpoint to check the current user's authentication status and return their username
201
+ @app.route('/api/user_status')
202
+ def user_status():
203
+ if current_user.is_authenticated:
204
+ return jsonify({"is_logged_in": True, "username": current_user.username})
205
+ return jsonify({"is_logged_in": False})
206
+
207
+ # runs the application on host 0.0.0.0 and creates the admin user on startup
208
+ if __name__ == '__main__':
209
+ create_admin()
210
+ app.run(debug=True, port=5000)
alisto_project/backend/augment_data.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import random
4
+ import re
5
+ import my_generator as gen
6
+
7
+ # Config
8
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
+ SEED_PATH = os.path.join(BASE_DIR, '../data/seed_data.txt')
10
+ CSV_PATH = os.path.join(BASE_DIR, '../data/reddit_disaster_posts.csv')
11
+ # sets the target total size of the final dataset
12
+ TOTAL_ROWS = 5000
13
+
14
+ # ---------------------------------------------------------
15
+ # 1. SLANG & AUGMENTATION LIBRARY
16
+ # ---------------------------------------------------------
17
+ # defines common Taglish words and their text-speak/slang equivalents
18
+ SLANG_MAP = {
19
+ "tulong": ["help", "saklolo", "tulong po", "help pls"],
20
+ "kami": ["kmi", "kme", "tayo"],
21
+ "dito": ["d2", "dto", "here"],
22
+ "baha": ["flood", "tubig", "pagbaha"],
23
+ "rescue": ["save us", "pasundo", "saklolo"],
24
+ "please": ["pls", "plz", "paki", "parang awa nyo na"],
25
+ "wala": ["la", "wla", "zero"],
26
+ "sa": ["s", "sa may"],
27
+ "ang": ["ung", "yung", "ang"],
28
+ "hindi": ["di", "d", "hndi"],
29
+ "kayo": ["kau", "nyo"],
30
+ "may": ["meron", "my"],
31
+ }
32
+
33
+ # replaces common words with text-speak or slang based on SLANG_MAP
34
+ def apply_slang(text):
35
+ words = text.split()
36
+ new_words = []
37
+ for word in words:
38
+ lower_word = word.lower().replace(".", "").replace("!", "")
39
+ if lower_word in SLANG_MAP and random.random() > 0.5:
40
+ new_words.append(random.choice(SLANG_MAP[lower_word]))
41
+ else:
42
+ new_words.append(word)
43
+ return " ".join(new_words)
44
+
45
+ # randomly swaps two adjacent words to simulate panic typing errors
46
+ def shuffle_sentence(text):
47
+ words = text.split()
48
+ if len(words) > 3:
49
+ idx = random.randint(0, len(words) - 2)
50
+ words[idx], words[idx+1] = words[idx+1], words[idx]
51
+ return " ".join(words)
52
+
53
+ # adds formatting noise (random caps, repetition, punctuation spam) to simulate distress
54
+ def add_noise(text):
55
+ # 1. Random Caps
56
+ if random.random() > 0.7:
57
+ text = text.upper()
58
+ elif random.random() > 0.7:
59
+ text = text.lower()
60
+
61
+ # 2. Urgent Repetition (for positives)
62
+ if "help" in text.lower() or "tulong" in text.lower():
63
+ if random.random() > 0.7:
64
+ text = text + " TULONG!"
65
+
66
+ # 3. Punctuation Spam
67
+ if random.random() > 0.6:
68
+ text += "!!" if random.random() > 0.5 else "..."
69
+
70
+ return text
71
+
72
+ # creates multiple variations of a single input text using all augmentation methods
73
+ def generate_variations(text, num_variations=3):
74
+ variations = [text] # Keep original
75
+
76
+ for _ in range(num_variations):
77
+ # Method A: Slang
78
+ var = apply_slang(text)
79
+ # Method B: Noise
80
+ var = add_noise(var)
81
+ # Method C: Shuffle (rarely)
82
+ if random.random() > 0.8:
83
+ var = shuffle_sentence(var)
84
+
85
+ variations.append(var)
86
+
87
+ return list(set(variations)) # Unique only
88
+
89
+ # ---------------------------------------------------------
90
+ # 2. CORE LOGIC
91
+ # ---------------------------------------------------------
92
+ # cleans and loads the initial seed data from the text file
93
+ def clean_and_load_seed_data(filepath):
94
+ if not os.path.exists(filepath):
95
+ print(f"⚠️ Warning: {filepath} not found.")
96
+ return []
97
+
98
+ with open(filepath, 'r', encoding='utf-8') as f:
99
+ content = f.read()
100
+
101
+ # removes internal tags or bracketed information
102
+ pattern = re.compile(r"\[.*?\]")
103
+ content = pattern.sub("", content)
104
+
105
+ clean_rows = []
106
+ raw_lines = content.split('\n')
107
+ buffer = ""
108
+
109
+ for line in raw_lines:
110
+ line = line.strip()
111
+ if not line: continue
112
+
113
+ # checks for the classification label at the end of the line
114
+ if line.endswith('|0') or line.endswith('|1'):
115
+ full_line = (buffer + " " + line).strip()
116
+ try:
117
+ text, label = full_line.rsplit('|', 1)
118
+ clean_rows.append({'text': text.strip(), 'label': int(label)})
119
+ except: pass
120
+ buffer = ""
121
+ else:
122
+ buffer += line + " "
123
+
124
+ return clean_rows
125
+
126
+ # main function to orchestrate the data augmentation and saving process
127
+ def create_database():
128
+ print("--- ALISTO: Phase 2 Data Augmentation (Lean & Mean Mode) ---")
129
+
130
+ # 1. Load Seed Data
131
+ seed_rows = clean_and_load_seed_data(SEED_PATH)
132
+ print(f"🌱 Loaded {len(seed_rows)} original seed rows.")
133
+
134
+ # 2. MULTIPLY SEED DATA
135
+ final_rows = []
136
+ print("🧬 Cloning and mutating seed data...")
137
+ # generates multiple variations for each original seed row
138
+ for row in seed_rows:
139
+ # Generate 8 variations per real row
140
+ variations = generate_variations(row['text'], num_variations=8)
141
+ for var in variations:
142
+ final_rows.append({'text': var, 'label': row['label']})
143
+
144
+ print(f"   ↳ Expanded seed data to {len(final_rows)} rows.")
145
+
146
+ # 3. Fill the rest with Synthetic Templates
147
+ # calculates how many synthetic rows are needed to meet the TOTAL_ROWS target
148
+ remaining = TOTAL_ROWS - len(final_rows)
149
+
150
+ # generates synthetic positive and negative posts using my_generator
151
+ if remaining > 0:
152
+ print(f"🤖 Generating {remaining} TRICKY synthetic rows to fill dataset...")
153
+ for _ in range(remaining // 2):
154
+ final_rows.append({'text': add_noise(gen.build_positive()), 'label': 1})
155
+ final_rows.append({'text': add_noise(gen.build_negative()), 'label': 0})
156
+ else:
157
+ print("🤖 Seed data expansion is sufficient. Skipping synthetic generation.")
158
+
159
+ # 4. Save
160
+ df = pd.DataFrame(final_rows)
161
+ # shuffles the dataset and removes duplicates before saving
162
+ final_df = df.sample(frac=1).reset_index(drop=True)
163
+ final_df.drop_duplicates(subset=['text'], inplace=True)
164
+
165
+ # saves the final dataset to a CSV file
166
+ final_df.to_csv(CSV_PATH, index=False)
167
+ print(f"✅ Success! Saved {len(final_df)} rows to {CSV_PATH}")
168
+ print("   (Note: Dataset is optimized for quality over quantity)")
169
+
170
+ # executes the main function when the script is run
171
+ if __name__ == "__main__":
172
+ create_database()
alisto_project/backend/images/alert.png ADDED
alisto_project/backend/images/alert1.png ADDED
alisto_project/backend/images/bg.jpg ADDED

Git LFS Details

  • SHA256: 458ee3732d3eacd658b4cd0fcaf245f89c1b491fd5f0917d85fc8cf8116772e5
  • Pointer size: 131 Bytes
  • Size of remote file: 231 kB
alisto_project/backend/images/bg1.jpg ADDED

Git LFS Details

  • SHA256: ebe7d968ea74e3646a8f22d6f93ed86400db8e6382ade8ac7381549e1113676a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
alisto_project/backend/images/bg2-bw.jpg ADDED

Git LFS Details

  • SHA256: 8b6e6ee4c61421c0d19a8ac5d774427403ff5ff77defecc522a683d638d18621
  • Pointer size: 131 Bytes
  • Size of remote file: 778 kB
alisto_project/backend/images/bg2-logan.jpg ADDED

Git LFS Details

  • SHA256: ca90384e2ccf5c223807504561a4735bacdf5ddd5a635ae106b28d4ed09c2ea4
  • Pointer size: 131 Bytes
  • Size of remote file: 550 kB
alisto_project/backend/images/bg2.jpg ADDED

Git LFS Details

  • SHA256: c352e7e37e9c25df7f5c29dd9e5b97b80e26760caebd0dc6e1217b223f274cd6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.68 MB
alisto_project/backend/images/earthquake.png ADDED
alisto_project/backend/images/earthquake1.png ADDED
alisto_project/backend/index.html ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ALISTO</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700;900&family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ <link rel="stylesheet" href="style.css">
11
+ </head>
12
+ <body>
13
+ <div id="login-modal" class="modal-overlay hidden">
14
+ <div class="modal-content small-modal">
15
+ <div class="modal-header">
16
+ <h2>Responder Login</h2>
17
+ <button id="close-login-btn" class="icon-btn-ghost"><i class="fa-solid fa-xmark"></i></button>
18
+ </div>
19
+ <div class="login-form">
20
+ <input type="text" id="username" placeholder="Username" class="login-input">
21
+ <input type="password" id="password" placeholder="Password" class="login-input">
22
+ <button id="login-submit-btn" class="action-btn resolve-btn full-width">Log In</button>
23
+ <p id="login-error" style="color: #ff4444; margin-top: 10px; font-size: 0.9em;"></p>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div id="logout-modal" class="modal-overlay hidden">
29
+ <div class="modal-content" style="max-width: 400px; text-align: center;">
30
+ <div class="modal-header" style="justify-content: center; margin-bottom: 10px;">
31
+ <h2 style="color: #ed4801;">Confirm Logout</h2>
32
+ </div>
33
+ <p style="color: #ccc; margin-bottom: 30px;">Are you sure you want to end your session?</p>
34
+
35
+ <div class="action-buttons" style="justify-content: center; gap: 15px;">
36
+ <button id="cancel-logout-btn" class="action-btn" style="background: #444; padding: 10px 20px;">Cancel</button>
37
+ <button id="confirm-logout-btn" class="action-btn confirm-btn" style="padding: 10px 20px;">Yes, Logout</button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div id="new-alert-notification" class="alert-popup hidden">
43
+ <i class="fa-solid fa-bell"></i>
44
+ <span id="notification-message">New URGENT Alert Received!</span>
45
+ </div>
46
+
47
+ <audio id="alert-sound" src="https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3" preload="auto"></audio>
48
+
49
+ <div id="stats-modal" class="modal-overlay hidden">
50
+ <div class="modal-content">
51
+ <div class="modal-header">
52
+ <h2>Situation Report</h2>
53
+ <button id="close-stats-btn" class="icon-btn-ghost"><i class="fa-solid fa-xmark"></i></button>
54
+ </div>
55
+ <div class="charts-container">
56
+ <div class="chart-wrapper">
57
+ <h3>Active Incidents by Type</h3>
58
+ <canvas id="typeChart"></canvas>
59
+ </div>
60
+ <div class="chart-wrapper">
61
+ <h3>Urgency Breakdown</h3>
62
+ <canvas id="urgencyChart"></canvas>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <header class="main-header">
69
+ <div class="logo">
70
+ <span class="logo-main">ALISTO</span>
71
+ <span class="logo-sub">Alert System</span>
72
+ </div>
73
+
74
+ <div class="search-container">
75
+ <i class="fa-solid fa-xmark clear-icon hidden"></i>
76
+ <input type="text" placeholder="Search incidents..." class="search-input">
77
+ <i class="fa-solid fa-magnifying-glass search-icon clickable" data_tooltip="Search"></i>
78
+ </div>
79
+
80
+ <nav class="main-nav">
81
+ <a href="index.html" class="nav-link active" data_tooltip="Go to Posts">Dashboard</a>
82
+ <a href="map.html" class="nav-link" data_tooltip="Go to Live Map">Live Map</a>
83
+ <a href="#" id="nav-login-btn" class="nav-link" style="color: #ed4801;">Login</a>
84
+
85
+ <div class="profile-container" id="profile-container-wrap" style="display: none;">
86
+
87
+ <div id="profile-toggle" class="profile-icon">
88
+ <i class="fa-solid fa-user"></i>
89
+ </div>
90
+
91
+ <div id="profile-dropdown" class="profile-dropdown hidden">
92
+ <div class="profile-info-header">
93
+ <div class="profile-icon-large">
94
+ <i class="fa-solid fa-user"></i>
95
+ </div>
96
+ <div id="dropdown-username" class="dropdown-username">Officer ID</div>
97
+ </div>
98
+
99
+ <hr class="dropdown-separator">
100
+
101
+ <button id="dropdown-logout-btn" class="dropdown-logout-btn">
102
+ <i class="fa-solid fa-right-from-bracket"></i> Logout
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </nav>
107
+ </header>
108
+
109
+ <section class="hero">
110
+
111
+ <div class="sidebar-wrapper">
112
+ <div class="filter-bar">
113
+
114
+ <button id="show-stats-btn" class="pulse-btn" data_tooltip="View Real-time Alert Statistics" data_tooltip_align="left">
115
+ <i class="fa-solid fa-chart-pie"></i>
116
+ </button>
117
+
118
+ <div class="separator"></div>
119
+
120
+ <div class="filter-bar-c">
121
+ <div class="filter-bar-r">
122
+
123
+ <div class="filter-wrap1" data_tooltip="Sort alerts by Time">
124
+ <select id="sort-select" class="filter-select">
125
+ <option value="newest" title="Hi">Newest</option>
126
+ <option value="oldest">Oldest</option>
127
+ </select>
128
+ </div>
129
+
130
+ <div class="filter-wrap2" data_tooltip="Filter by Urgency Level">
131
+ <select id="urgency-select" class="filter-select">
132
+ <option value="all">All Levels</option>
133
+ <option value="High">High</option>
134
+ <option value="Medium">Medium</option>
135
+ <option value="Low">Low</option>
136
+ </select>
137
+ </div>
138
+
139
+ <div class="filter-wrap3" data_tooltip="Filter alerts by status">
140
+ <select id="view-select" class="filter-select">
141
+ <option value="all">All Status</option>
142
+ <option value="active">Active</option>
143
+ <!-- <option value="active">Verified</option> -->
144
+ <option value="archived">Resolved</option>
145
+ </select>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="filter-bar-r">
150
+ <div class="filter-wrap4" data_tooltip="Filter by Disaster Type">
151
+ <select id="type-select" class="filter-select">
152
+ <option value="all">All Disaster Types</option>
153
+ <option value="Flood">Flood</option>
154
+ <option value="Typhoon">Typhoon</option>
155
+ <option value="Earthquake">Earthquake</option>
156
+ <option value="Fire">Fire</option>
157
+ <option value="Volcano">Volcano</option>
158
+ <option value="Landslide">Landslide</option>
159
+ <option value="General Emergency">General</option>
160
+ </select>
161
+ </div>
162
+
163
+ <div class="filter-wrap5" data_tooltip="Filter by Assistance Needed">
164
+ <select id="assist-select" class="filter-select">
165
+ <option value="all">All Assistances</option>
166
+ <option value="Rescue">Rescue</option>
167
+ <option value="Medical">Medical</option>
168
+ <option value="Evacuation">Evacuation</option>
169
+ <option value="Food/Water">Food/Water</option>
170
+ <option value="General Assistance">General</option>
171
+ </select>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <button id="export-btn" class="icon-btn" data_tooltip="Download Report (CSV)">
177
+ <i class="fa-solid fa-download"></i>
178
+ </button>
179
+
180
+ </div>
181
+
182
+ <div class="sidebar" id="incident-feed">
183
+ <p style="color: white; padding: 20px; text-align: center;">Loading alerts...</p>
184
+ </div>
185
+
186
+ <div class="active-alert-counter">
187
+ <span style="font-size: 0.9em; color: #ccc;">Active Alerts: </span>
188
+ <span id="dashboard-alert-count" style="font-weight: 700; color: #ffc107;">0</span>
189
+ </div>
190
+
191
+ </div>
192
+
193
+ <div class="detail-box" id="postInfo">
194
+ <div class="detail-header-r">
195
+ <h2 class="detail-title" id="detail-title">Select an Alert</h2>
196
+
197
+ <div class="action-buttons">
198
+ <button id="verify-btn" class="action-btn1 verify-btn" onclick="updateStatus('Verified')" data_tooltip="Mark this alert as Verified (Confirmed)">
199
+ <i class="fa-solid fa-check"></i>
200
+ <span class="btn-text">Verify</span>
201
+ </button>
202
+ <button id="resolve-btn" class="action-btn1 resolve-btn" onclick="updateStatus('Resolved')" data_tooltip="Mark this alert as Resolved and dismiss">
203
+ <i class="fa-solid fa-box-archive"></i>
204
+ <span class="btn-text">Resolve</span>
205
+ </button>
206
+ <a href="#" id="detail-link" target="_blank" class="detail-redirect" data_tooltip="Go to Reddit Post">
207
+ <i class="fa-brands fa-reddit-alien"></i> Post
208
+ </a>
209
+ </div>
210
+ </div>
211
+
212
+ <hr class="detail-line">
213
+
214
+ <div class="detail-important-r">
215
+ <div class="detail-row1-c">
216
+ <div>
217
+ <div class="detail-label" style="margin-top: 10px;">Assistance Needed</div>
218
+ <div id="detail-assistance" style="color: #ed4801; font-weight: bold; text-transform: uppercase;">-</div>
219
+ </div>
220
+ <div>
221
+ <div class="detail-label">Location / Address</div>
222
+ <span id="detail-location" style="color: white;">-</span>
223
+ </div>
224
+
225
+ <div class="detail-row1-col2-r">
226
+ <div class="contact-c">
227
+ <div class="detail-label">Contact Person</div>
228
+ <span id="detail-contact-name" style="color: white;">-</span>
229
+ </div>
230
+
231
+ <div class="contact-c">
232
+ <div class="detail-label">Contact Number</div>
233
+ <span id="detail-contact-number" style="color: white;">-</span>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <div style="text-align: right;">
239
+ <div class="detail-label">Posted</div>
240
+ <div class="time" id="detail-time">_ mins ago at __:__ __</div>
241
+
242
+ <!-- <div class="detail-label" style="margin-top: 10px;">Assistance Needed</div>
243
+ <div id="detail-assistance" style="color: #ed4801; font-weight: bold; text-transform: uppercase;">-</div> -moved above -->
244
+
245
+ <div class="detail-label" style="margin-top: 10px;">Status</div>
246
+ <div id="detail-urgency" style="color: white;">-</div>
247
+ <div id="detail-status" class="status-badge">New</div>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="detail-content" id="detail-body">
252
+ Click on an alert from the sidebar to view full details here.
253
+ </div>
254
+ </div>
255
+
256
+ </section>
257
+
258
+ <script src="script.js"></script>
259
+ </body>
260
+ </html>
alisto_project/backend/ingest_reddit.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncpraw
2
+ import asyncio
3
+ import os
4
+ import torch
5
+ import pickle
6
+ import numpy as np
7
+ import torch.nn.functional as F
8
+ from datetime import datetime
9
+ from dotenv import load_dotenv
10
+ from flask import Flask
11
+ from models import db, DisasterPost
12
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
13
+ from ner_extractor import extract_entities
14
+
15
+ # 1. Config & Setup
16
+ # defines the subreddits to be monitored by the scraper
17
+ SUBREDDITS = "AlistoSimulation"
18
+ # SUBREDDITS = "Philippines+NaturalDisasters+DisasterUpdatePH+Assistance+Typhoon+AlistoSimulation"
19
+
20
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
21
+ # loads environment variables from .env file
22
+ load_dotenv(os.path.join(BASE_DIR, '../.env'))
23
+
24
+ # initializes the Flask application context for database access
25
+ app = Flask(__name__)
26
+ DB_PATH = os.path.join(BASE_DIR, 'alisto.db')
27
+ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
28
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
29
+ # sets a timeout for stable database connection
30
+ app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'connect_args': {'timeout': 15}}
31
+ db.init_app(app)
32
+
33
+ # 2. Load Models
34
+ print("Loading ALISTO Brains...")
35
+ MODEL_DIR = os.path.join(BASE_DIR, 'models')
36
+ ROBERTA_DIR = os.path.join(MODEL_DIR, 'roberta_model')
37
+ TFIDF_PATH = os.path.join(MODEL_DIR, 'tfidf_ensemble.pkl')
38
+
39
+ # A. RoBERTa (XLM-R Multilingual)
40
+ # loads the RoBERTa tokenizer and sequence classification model (Context Expert)
41
+ try:
42
+ tokenizer = AutoTokenizer.from_pretrained(ROBERTA_DIR)
43
+ roberta_model = AutoModelForSequenceClassification.from_pretrained(ROBERTA_DIR)
44
+ device = torch.device("cpu") # determines the device (CPU/GPU) for model execution
45
+ roberta_model.to(device)
46
+ roberta_model.eval()
47
+ print("✅ Context Expert (XLM-R) loaded")
48
+ except Exception as e:
49
+ print(f"❌ Error loading RoBERTa: {e}")
50
+ exit()
51
+
52
+ # B. TF-IDF (The Gatekeeper)
53
+ # loads the pre-trained TF-IDF vectorizer and ensemble model (Gatekeeper)
54
+ try:
55
+ with open(TFIDF_PATH, 'rb') as f:
56
+ tfidf_model = pickle.load(f)
57
+ print("✅ Gatekeeper (TF-IDF) loaded")
58
+ except Exception as e:
59
+ print(f"❌ Error loading TF-IDF: {e}")
60
+ tfidf_model = None
61
+
62
+ # 3. Reference Lists (Kept from your original)
63
+ # list of Philippine locations used for basic geo-validation
64
+ PHILIPPINE_LOCATIONS = [
65
+ "Philippines", "PH", "Luzon", "Visayas", "Mindanao", "Metro Manila", "NCR",
66
+ "Manila", "Quezon City", "Makati", "Taguig", "Pasig", "Mandaluyong",
67
+ "Marikina", "Las Pinas", "Las Piñas", "Muntinlupa", "Caloocan",
68
+ "Paranaque", "Parañaque", "Valenzuela", "Pasay", "Malabon",
69
+ "Navotas", "San Juan", "Pateros",
70
+ "Cavite", "Naic", "Bacoor", "Imus", "Dasmarinas", "Dasmariñas",
71
+ "General Trias", "Tagaytay", "Kawit", "Noveleta", "Rosario", "Tanza",
72
+ "Silang", "Trece Martires", "Laguna", "Calamba", "Santa Rosa", "Binan",
73
+ "Biñan", "San Pedro", "Cabuyao", "Los Banos", "Los Baños", "Rizal",
74
+ "Antipolo", "Cainta", "Taytay", "San Mateo", "Binangonan", "Batangas",
75
+ "Bulacan", "Pampanga", "Tarlac", "Cebu", "Iloilo", "Tacloban",
76
+ "Davao", "Cagayan", "Bicol", "Albay", "Isabela"
77
+ ]
78
+
79
+ # function to process a single Reddit submission through all filters and save it
80
+ async def process_post(post):
81
+ """handles logic for a single Reddit submission (filtering, AI, saving)"""
82
+ try:
83
+ full_text = f"{post.title} {post.selftext}"
84
+
85
+ # A. Check for Duplicates & Credibility (Unchanged logic)
86
+ # checks for existing post ID in the database
87
+ with app.app_context():
88
+ exists = DisasterPost.query.filter_by(reddit_id=post.id).first()
89
+ if exists: return
90
+ # blocks posts from suspicious new/low-karma accounts
91
+ if not is_credible_user(post):
92
+ print(f"\n------------------- DEBUG REJECTION -------------------")
93
+ print(f"❌ REJECTED POST ID: {post.id} (Title: {post.title[:30]})")
94
+ print(f"REASON: Credibility Check (Account too new/Low Karma)")
95
+ print(f"---------------------------------------------------------\n")
96
+ return
97
+
98
+ # B. Logic Filter (First Defense) (Unchanged logic)
99
+ # runs simple keyword checks to filter news/financial/irrelevant content
100
+ is_bad, reason = is_news_or_irrelevant(full_text)
101
+ if is_bad:
102
+ print(f"\n------------------- DEBUG REJECTION -------------------")
103
+ print(f"❌ REJECTED POST ID: {post.id} (Title: {post.title[:30]})")
104
+ print(f"REASON: Logic Filter (Common Sense Layer) Categorized as: {reason}")
105
+ print(f"---------------------------------------------------------\n")
106
+ return
107
+
108
+ # C. AI Analysis (Unchanged logic)
109
+ # runs the cascade AI check (TF-IDF then RoBERTa)
110
+ is_urgent, score, source = predict_urgency(full_text)
111
+ if not is_urgent:
112
+ print(f"\n------------------- DEBUG REJECTION -------------------")
113
+ print(f"❌ REJECTED POST ID: {post.id} (Title: {post.title[:30]})")
114
+ print(f"REASON: AI Confidence too low Score: {score:.2%} (Source: {source})")
115
+ print(f"---------------------------------------------------------\n")
116
+ return
117
+
118
+ # D. Entity Extraction
119
+ # extracts location, contact number, and contact person name
120
+ ner_results = extract_entities(full_text)
121
+ locations = ner_results.get('locations', [])
122
+ contact_num = ner_results.get('contact', None)
123
+ contact_person_name = ner_results.get('contact_person_name', None)
124
+
125
+ # E. Final Triage and Data Preparation
126
+ # assigns location and determines disaster/assistance type
127
+ location = locations[0] if locations else "Unknown Location"
128
+ disaster_type = get_disaster_type(full_text)
129
+ assistance_type = get_assistance_type(full_text)
130
+
131
+ # 1. Calculate Dynamic Urgency (NEW)
132
+ # assigns High, Medium, or Low urgency based on severity keywords
133
+ dynamic_urgency = assign_dynamic_urgency(full_text)
134
+
135
+ # 2. Finalize Author (Fallback Logic)
136
+ # defaults to Reddit username if no contact name is explicitly extracted
137
+ reddit_username = str(post.author) if post.author else "Unknown"
138
+ final_author = contact_person_name if contact_person_name else reddit_username
139
+
140
+ # 3. Print Final Alert Confirmation
141
+ print(f"""------------------- ALERT SAVED -------------------\n🚨 ALERT ({score:.2%}): {disaster_type} in {location} Urgency: {dynamic_urgency} \n---------------------------------------------------------""")
142
+
143
+ # F. Single Database Creation and Commit
144
+ # creates and commits the final DisasterPost object to the database
145
+ new_post = DisasterPost(
146
+ reddit_id=post.id,
147
+ title=post.title,
148
+ content=post.selftext or post.title,
149
+ author=final_author,
150
+ location=location,
151
+ contact_number=contact_num,
152
+ disaster_type=disaster_type,
153
+ assistance_type=assistance_type,
154
+ urgency_level=dynamic_urgency,
155
+ is_help_request=True,
156
+ timestamp=datetime.utcfromtimestamp(post.created_utc)
157
+ )
158
+
159
+ with app.app_context():
160
+ db.session.add(new_post)
161
+ db.session.commit()
162
+
163
+ except Exception as e:
164
+ print(f"Post Processing Error for {post.id}: {e}")
165
+
166
+ # validates if the extracted location is relevant to the Philippines
167
+ def check_for_philippine_location(location_list):
168
+ if not location_list: return False
169
+ ph_locations = [loc.lower() for loc in PHILIPPINE_LOCATIONS]
170
+ for extracted_loc in location_list:
171
+ # Check partial match (e.g., "Marikina City" matches "Marikina")
172
+ for known_loc in ph_locations:
173
+ if known_loc in extracted_loc.lower() or extracted_loc.lower() in known_loc:
174
+ return True
175
+ return False
176
+
177
+ # classifies the type of disaster based on severity keywords
178
+ def get_disaster_type(text):
179
+ text_lower = text.lower()
180
+ mapping = {
181
+ "Earthquake": ["quake", "lindol", "shake", "aftershock"],
182
+ "Landslide": ["landslide", "guho", "mudslide", "natabunan"],
183
+ "Volcano": ["volcano", "lava", "ash", "magma", "taal", "mayon"],
184
+ "Fire": ["fire", "sunog", "burn", "smoke"],
185
+ "Typhoon": ["typhoon", "bagyo", "storm", "wind", "signal", "ulysses", "odette"],
186
+ "Flood": ["flood", "baha", "water", "river", "drown", "lubog", "taas ng tubig"]
187
+ }
188
+
189
+ for dtype, keywords in mapping.items():
190
+ if any(k in text_lower for k in keywords):
191
+ return dtype
192
+ return "General Emergency"
193
+
194
+ # classifies the specific type of assistance needed (e.g., Medical, Rescue, Food)
195
+ def get_assistance_type(text):
196
+ """determines the specific help needed using Nested Priority"""
197
+ text = text.lower()
198
+
199
+ # --- 1. IMMEDIATE RESCUE (Life Threatening) ---
200
+ rescue_kw = [
201
+ "rescue", "saklolo", "trapped", "stuck", "stranded",
202
+ "bubong", "roof", "boat", "bangka", "drowning", "lunod",
203
+ "di makalabas", "unable to leave"
204
+ ]
205
+ if any(k in text for k in rescue_kw):
206
+
207
+ critical_medical_override_kw = [
208
+ "bleeding", "unconscious", "head injury", "head wound",
209
+ "severely bleeding", "stroke", "heart attack", "trauma"
210
+ ]
211
+ if any(k in text for k in critical_medical_override_kw):
212
+ return "Medical"
213
+
214
+ return "Rescue" # if no critical medical keywords found
215
+
216
+ # --- 2. MEDICAL (Specific Needs/Ambulance) ---
217
+ # handles standalone medical needs if no rescue keywords were found
218
+ medical_kw = [
219
+ "medical", "doctor", "gamot", "medicine", "insulin", "dialysis",
220
+ "hospital", "oxygen", "pregnant", "labor", "manganganak", "ambulance",
221
+ "first aid", "pills", "medication"
222
+ ]
223
+ if any(k in text for k in medical_kw):
224
+ return "Medical"
225
+
226
+ # --- 3. EVACUATION (Shelter/Transport) ---
227
+ # classifies the need for temporary shelter or transport
228
+ evac_kw = [
229
+ "evacuate", "evacuation", "shelter", "center", "likas", "tents",
230
+ "matutuluyan", "alis", "transportation", "walang matutuluyan"
231
+ ]
232
+ if any(k in text for k in evac_kw):
233
+ return "Evacuation"
234
+
235
+ # --- 4. FOOD & WATER (Logistics) ---
236
+ # classifies the need for essential supplies (food, water, formula)
237
+ food_kw = [
238
+ "food", "pagkain", "water", "tubig", "gutom", "hungry", "relief",
239
+ "goods", "makakain", "inumin", "groceries", "supplies", "supply", "wala ng stock",
240
+ "gatas", "milk", "formula", "baby supplies", "ubos na", "wala na", "stock", "stock ng"
241
+ ]
242
+ if any(k in text for k in food_kw):
243
+ return "Food/Water"
244
+
245
+ return "General Assistance"
246
+
247
+
248
+ # --- LOGIC FILTERS (The "Common Sense" Layer) ---
249
+ # runs simple logic checks to filter out news reports and non-urgent context
250
+ def is_news_or_irrelevant(text):
251
+ text_lower = text.lower()
252
+
253
+ # 1. NEWS & REPORTS
254
+ news_indicators = [
255
+ "breaking:", "just in:", "news:", "update:", "report:",
256
+ "casualties", "death toll", "according to", "reported that",
257
+ "suspension", "declared", "signal no", "public advisory",
258
+ "weather update", "volcano alert", "mmda", "pagasa"
259
+ ]
260
+
261
+ # 2. MONEY / SELLING
262
+ financial_indicators = [
263
+ "gcash", "paypal", "budget", "loan", "selling",
264
+ "fundraising", "donate", "send funds"
265
+ ]
266
+
267
+ # 3. IRRELEVANT CONTEXT
268
+ irrelevant_contexts = [
269
+ "how can i help", "where to donate", "thoughts and prayers",
270
+ "keep safe", "god bless", "praying for", "discussion:", "opinion:"
271
+ ]
272
+
273
+ # Logic Checks
274
+ if any(ind in text_lower for ind in news_indicators):
275
+ return True, "News/Report"
276
+
277
+ # blocks financial requests unless life-threatening keywords are also present
278
+ has_financial = any(ind in text_lower for ind in financial_indicators)
279
+ is_life_death = any(k in text_lower for k in ["trapped", "lubog", "roof", "rescue", "drowning", "stuck"])
280
+
281
+ if has_financial and not is_life_death:
282
+ return True, "Financial/Non-Urgent"
283
+
284
+ # blocks posts containing non-urgent discussion or commentary
285
+ if any(ctx in text_lower for ctx in irrelevant_contexts):
286
+ return True, "Context/NotUrgent"
287
+
288
+ return False, None
289
+
290
+ # runs the two-stage AI classification check (TF-IDF then RoBERTa)
291
+ def predict_urgency(text):
292
+
293
+ # 1. Gatekeeper (TF-IDF)
294
+ # quickly rejects posts with extremely low urgency confidence (below 10%)
295
+ if tfidf_model:
296
+ tfidf_probs = tfidf_model.predict_proba([text])[0]
297
+ tfidf_conf = tfidf_probs[1]
298
+
299
+ # If the fast model is sure it's junk, skip the heavy lifting
300
+ if tfidf_conf < 0.20:
301
+ return False, tfidf_conf, "TF-IDF Reject"
302
+
303
+ # 2. Context Expert (RoBERTa)
304
+ # runs the slower, context-aware model for final classification
305
+ inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128)
306
+ with torch.no_grad():
307
+ outputs = roberta_model(**inputs)
308
+ probs = F.softmax(outputs.logits, dim=-1)
309
+ roberta_conf = probs[0][1].item() # Probability of 'Rescue Request'
310
+
311
+ # final acceptance threshold (40%) for the RoBERTa model
312
+ return (roberta_conf > 0.4), roberta_conf, "RoBERTa"
313
+
314
+ # assigns the final severity level (High, Medium, Low) based on severity keywords
315
+ def assign_dynamic_urgency(text):
316
+ text_lower = text.lower()
317
+
318
+ # 1. HIGH URGENCY (Immediate Life-Threatening Event or Critical Medical Need)
319
+ high_keywords = [
320
+ "bleeding", "unconscious", "severely injured", "severe injury", "life threatening",
321
+ "insulin", "oxygen", "ambulance", "urgent medicine", "doctor", "hospital",
322
+
323
+ "trap", "trapped", "bubong", "collapsed", "di mapigilan", "drowning",
324
+ "lampas tao", "lubog", "delikado", "baha na", "mamatay"
325
+ ]
326
+ if any(k in text_lower for k in high_keywords):
327
+ return "High"
328
+
329
+ # 2. MEDIUM URGENCY (Time-Sensitive, Logistical Crisis)
330
+ medium_keywords = [
331
+ "stranded", "running out", "evacuate", "kailangan agad", "lowbat",
332
+ "paubos", "senior", "bedridden", "disabled", "gatas", "formula"
333
+ ]
334
+ if any(k in text_lower for k in medium_keywords):
335
+ return "Medium"
336
+
337
+ # 3. LOW URGENCY (General Supplies/Warning)
338
+ # posts that pass the AI but lack the above severity indicators fall here
339
+ return "Low"
340
+
341
+ # blocks posts from accounts created less than 2 days ago or with negative karma
342
+ def is_credible_user(post):
343
+ try:
344
+ author = post.author
345
+
346
+ # checks if author is deleted or unknown
347
+ if not author:
348
+ return False
349
+
350
+ # 1. Check Account Age (Must be older than 2 days)
351
+ created_time = datetime.utcfromtimestamp(author.created_utc)
352
+ account_age = datetime.utcnow() - created_time
353
+
354
+ if account_age.days < 2:
355
+ print(f"   ⚠️ Blocked: Account too new ({account_age.days} days)")
356
+ return False
357
+
358
+ # 2. Check Karma (Must not be negative)
359
+ total_karma = author.comment_karma + author.link_karma
360
+ if total_karma < -5:
361
+ print(f"   ⚠️ Blocked: Negative Karma ({total_karma})")
362
+ return False
363
+
364
+ return True
365
+
366
+ except Exception as e:
367
+ # allows posts to pass if Reddit API fails to get user info
368
+ return True
369
+
370
+
371
+ # 4. Main Scraper Loop
372
+ # orchestrates the entire scraping process (historical scan + real-time stream)
373
+ async def scrape_reddit():
374
+ print("Connecting to Reddit API...")
375
+
376
+ client_id = os.getenv("REDDIT_CLIENT_ID")
377
+ client_secret = os.getenv("REDDIT_CLIENT_SECRET")
378
+
379
+ if not client_id or not client_secret:
380
+ print("❌ Error: Client ID or Secret missing in .env")
381
+ return
382
+
383
+ # initializes PRAW using the secure Client Credentials Flow (read-only)
384
+ reddit = asyncpraw.Reddit(
385
+ client_id=client_id,
386
+ client_secret=client_secret,
387
+ user_agent=os.getenv("REDDIT_USER_AGENT", "script:alisto_bot:v3.0")
388
+ )
389
+
390
+ try:
391
+ subreddit = await reddit.subreddit(SUBREDDITS)
392
+ print(f"👁️  ALISTO ACTIVE: Monitoring r/{SUBREDDITS}...")
393
+
394
+ # --- PHASE 1: FETCH LATEST EXISTING POSTS (e.g., last 500) ---
395
+ print("🔍 Scanning last 500 posts for missed alerts...")
396
+ # iterates over the last 500 posts asynchronously
397
+ async for post in subreddit.new(limit=500):
398
+ await process_post(post)
399
+
400
+ print("✅ Historical scan complete")
401
+
402
+ # --- PHASE 2: START REAL-TIME STREAM (Forever Loop) ---
403
+ print("📡 Starting real-time stream for new submissions...")
404
+
405
+ # starts the continuous loop to monitor for new submissions
406
+ async for post in subreddit.stream.submissions(skip_existing=False):
407
+ await process_post(post)
408
+
409
+ except Exception as e:
410
+ print(f"Global Scraper Error: {e}")
411
+ finally:
412
+ await reddit.close()
413
+ print("Scraper stopped")
414
+
415
+
416
+ # executes the main scraping loop when the script is run
417
+ if __name__ == "__main__":
418
+ try:
419
+ loop = asyncio.new_event_loop()
420
+ asyncio.set_event_loop(loop)
421
+ loop.run_until_complete(scrape_reddit())
422
+ except KeyboardInterrupt:
423
+ print("\n🛑 Stopped by user")
alisto_project/backend/init_db.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask
3
+ from models import db
4
+
5
+ app = Flask(__name__)
6
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
7
+ DB_PATH = os.path.join(BASE_DIR, 'alisto.db')
8
+ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
9
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
10
+ db.init_app(app)
11
+
12
+ def reset_database():
13
+ print("--- RESETTING DATABASE ---")
14
+
15
+ # 1. Delete the old file if it exists
16
+ if os.path.exists(DB_PATH):
17
+ os.remove(DB_PATH)
18
+ print(f"Deleted old database: {DB_PATH}")
19
+ else:
20
+ print("No old database found.")
21
+
22
+ # 2. Create fresh tables
23
+ with app.app_context():
24
+ db.create_all()
25
+ print("✅ Success: Created new empty 'alisto.db' with correct columns.")
26
+
27
+ if __name__ == "__main__":
28
+ reset_database()
alisto_project/backend/login.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ALISTO | Command Login</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700;900&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="style.css">
9
+ <style>
10
+ /* Specific overrides for Login Page centering */
11
+ body.login-page {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ height: 100vh;
16
+ background: #121212 url('images/bg2-logan.jpg') center/cover no-repeat;
17
+ }
18
+ .login-card {
19
+ position: relative; z-index: 2;
20
+ background: rgba(30, 30, 30, 0.9);
21
+ padding: 40px;
22
+ border-radius: 15px;
23
+ border: 1px solid #ed4801;
24
+ width: 100%; max-width: 400px;
25
+ text-align: center;
26
+ box-shadow: 0 20px 50px rgba(0,0,0,0.8);
27
+ }
28
+ .overlay {
29
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
30
+ background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(5px); z-index: 1;
31
+ }
32
+ input {
33
+ width: 100%; padding: 15px; margin-bottom: 15px;
34
+ background: rgba(255,255,255,0.1); border: 1px solid #444;
35
+ border-radius: 8px; color: white; font-size: 1rem; outline: none;
36
+ }
37
+ button {
38
+ width: 100%; padding: 15px;
39
+ background: linear-gradient(135deg, #ed4801, #ff7e42);
40
+ color: white; border: none; border-radius: 8px;
41
+ font-size: 1.1rem; font-weight: bold; cursor: pointer;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body class="login-page">
46
+ <div class="overlay"></div>
47
+
48
+ <div class="login-card">
49
+ <h1 style="color: #ed4801; font-family: 'Montserrat'; margin: 0;">ALISTO</h1>
50
+ <p style="color: #ccc; letter-spacing: 2px; font-size: 0.8em; text-transform: uppercase; margin-bottom: 30px;">Command Center</p>
51
+
52
+ <p id="error-msg" style="color: #ff4444; font-size: 0.9em; min-height: 20px;"></p>
53
+
54
+ <form id="loginForm">
55
+ <input type="text" id="username" placeholder="Officer ID" required>
56
+ <input type="password" id="password" placeholder="Password" required>
57
+ <button type="submit">Login</button>
58
+ </form>
59
+ </div>
60
+
61
+ <script>
62
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
63
+ e.preventDefault();
64
+ const u = document.getElementById('username').value;
65
+ const p = document.getElementById('password').value;
66
+ const btn = document.querySelector('button');
67
+ const err = document.getElementById('error-msg');
68
+
69
+ btn.innerText = "Authenticating...";
70
+
71
+ try {
72
+ const res = await fetch('/api/login', {
73
+ method: 'POST',
74
+ headers: {'Content-Type': 'application/json'},
75
+ body: JSON.stringify({username: u, password: p})
76
+ });
77
+
78
+ if (res.ok) {
79
+ window.location.href = "/";
80
+ } else {
81
+ const data = await res.json();
82
+ err.innerText = data.message || "Access Denied";
83
+ btn.innerText = "ACCESS SYSTEM";
84
+ }
85
+ } catch (e) {
86
+ err.innerText = "Connection Failed";
87
+ btn.innerText = "ACCESS SYSTEM";
88
+ }
89
+ });
90
+ </script>
91
+ </body>
92
+ </html>
alisto_project/backend/map.html ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ALISTO | Live Map</title>
7
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700;900&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="style.css">
10
+
11
+
12
+ <style>
13
+ /* Specific Map Page Styles */
14
+ body { overflow: hidden; }
15
+
16
+ #map {
17
+ width: 100%;
18
+ height: 100vh;
19
+ z-index: 1;
20
+ }
21
+
22
+ </style>
23
+ </head>
24
+ <body>
25
+
26
+ <div class="map-sidebar">
27
+ <a href="index.html" class="back-btn">← Back to Dashboard</a>
28
+ <h1 style="font-family: Montserrat; color: #ed4801; margin-bottom: 5px;">LIVE MAP</h1>
29
+ <p style="font-size: 0.8em; color: #ccc;">Real-time disaster tracking</p>
30
+ <hr style="border: 0; border-top: 1px solid #555; margin: 15px 0;">
31
+
32
+ <div id="status-text">Connecting to Alisto...</div>
33
+
34
+ <div class="stat-item">
35
+ <span class="stat-highlight" id="alert-count">0</span> Active Alerts
36
+ </div>
37
+ </div>
38
+
39
+ <div id="map"></div>
40
+
41
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
42
+ <script src="map_script.js"></script>
43
+ </body>
44
+ </html>
alisto_project/backend/map_script.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 1. EXTENDED COORDINATE DATABASE
2
+ // defines a static database of coordinates for Philippine cities and provinces
3
+ const cityCoords = {
4
+     // --- NCR ---
5
+     "Manila": [14.5995, 120.9842],
6
+     "Quezon City": [14.6760, 121.0437],
7
+     "Makati": [14.5547, 121.0244],
8
+     "Taguig": [14.5176, 121.0509],
9
+     "Pasig": [14.5763, 121.0851],
10
+     "Mandaluyong": [14.5794, 121.0359],
11
+     "Marikina": [14.6333, 121.0980],
12
+     "Las Pinas": [14.4445, 120.9939],
13
+     "Muntinlupa": [14.4081, 121.0415],
14
+     "Caloocan": [14.6401, 120.9745],
15
+     "Parañaque": [14.4793, 121.0198],
16
+     "Valenzuela": [14.7011, 120.9830],
17
+     "Pasay": [14.5378, 121.0014],
18
+     "Malabon": [14.6625, 120.9512],
19
+     "Navotas": [14.6732, 120.9350],
20
+     "San Juan": [14.6019, 121.0355],
21
+     "Pateros": [14.5454, 121.0687],
22
+
23
+     // --- CAVITE ---
24
+     "Cavite": [14.2831, 120.9168],
25
+     "Naic": [14.3168, 120.7628],
26
+     "Bacoor": [14.4624, 120.9645],
27
+     "Imus": [14.4297, 120.9367],
28
+     "Dasmarinas": [14.3294, 120.9367],
29
+     "General Trias": [14.3876, 120.8842],
30
+     "Tagaytay": [14.1153, 120.9621],
31
+     "Kawit": [14.4448, 120.9022],
32
+     "Noveleta": [14.4263, 120.8820],
33
+     "Rosario": [14.4153, 120.8532],
34
+     "Tanza": [14.3949, 120.8532],
35
+     "Silang": [14.2312, 120.9746],
36
+     "Trece Martires": [14.2883, 120.8677],
37
+
38
+     // --- LAGUNA ---
39
+     "Laguna": [14.2166, 121.1667],
40
+     "Calamba": [14.2142, 121.1553],
41
+     "Santa Rosa": [14.3121, 121.1132],
42
+     "Binan": [14.3400, 121.0827],
43
+     "San Pedro": [14.3644, 121.0370],
44
+     "Cabuyao": [14.2796, 121.1219],
45
+     "Los Banos": [14.1708, 121.2413],
46
+
47
+     // --- RIZAL ---
48
+     "Rizal": [14.5906, 121.2236],
49
+     "Antipolo": [14.5844, 121.1763],
50
+     "Cainta": [14.5760, 121.1213],
51
+     "Taytay": [14.5623, 121.1376],
52
+     "San Mateo": [14.6963, 121.1215],
53
+     "Binangonan": [14.4759, 121.1893],
54
+
55
+     // --- BULACAN ---
56
+     "Bulacan": [14.8524, 120.8228],
57
+     "Malolos": [14.8527, 120.8160],
58
+     "Meycauayan": [14.7356, 120.9622],
59
+     "San Jose del Monte": [14.8143, 121.0427],
60
+     "Bocaue": [14.8066, 120.9256],
61
+
62
+     // --- MAJOR PROVINCES / CITIES ---
63
+     "Pampanga": [15.0359, 120.6924],
64
+     "Tarlac": [15.4802, 120.5979],
65
+     "Batangas": [13.7565, 121.0583],
66
+     "Baguio": [16.4023, 120.5960],
67
+     "Cebu": [10.3157, 123.8854],
68
+     "Iloilo": [10.7202, 122.5621],
69
+     "Davao": [7.1907, 125.4553],
70
+     "Cagayan": [17.6133, 121.7302],
71
+     "Bicol": [13.4350, 123.4100],
72
+     "Albay": [13.1391, 123.7438],
73
+     "Tacloban": [11.2442, 125.0039],
74
+     "Zamboanga": [6.9214, 122.0790],
75
+     "Palawan": [9.8349, 118.7384],
76
+     "Mindoro": [13.0264, 121.2227],
77
+     "Isabela": [16.9754, 121.8107],
78
+     "Pangasinan": [15.9236, 120.3392],
79
+     "Philippines": [12.8797, 121.7740] // CENTER OF PH
80
+ };
81
+
82
+ let map;
83
+ let markers = [];
84
+
85
+ // initializes the map view and starts fetching data
86
+ document.addEventListener("DOMContentLoaded", () => {
87
+     // sets the initial map center on the CALABARZON/NCR area
88
+     map = L.map('map').setView([14.40, 121.00], 10);
89
+
90
+     // adds the dark-themed tile layer to the map
91
+     L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
92
+         attribution: '&copy; OpenStreetMap &copy; CARTO',
93
+         subdomains: 'abcd',
94
+         maxZoom: 19
95
+     }).addTo(map);
96
+
97
+     console.log("ALISTO Map: Loaded");
98
+     // fetches the first set of post data
99
+     fetchDataAndPlot();
100
+     // sets a timer to refresh data every 30 seconds
101
+     setInterval(fetchDataAndPlot, 30000);
102
+ });
103
+
104
+ // fetches the list of active disaster posts from the API
105
+ function fetchDataAndPlot() {
106
+     fetch('/api/posts')
107
+         .then(response => response.json())
108
+         .then(data => {
109
+             // updates the alert count in the map sidebar
110
+             updateSidebar(data);
111
+             // plots the markers on the map
112
+             plotMarkers(data);
113
+         })
114
+         .catch(err => console.error("Map Fetch Error:", err));
115
+ }
116
+
117
+ // updates the displayed alert count and system status in the floating sidebar
118
+ function updateSidebar(data) {
119
+     const countEl = document.getElementById('alert-count');
120
+     const statusEl = document.getElementById('status-text');
121
+     if(countEl) countEl.innerText = data.length;
122
+     if(statusEl) statusEl.innerText = "System Active";
123
+ }
124
+
125
+ // clears existing markers and plots new circular markers for each post
126
+ function plotMarkers(posts) {
127
+     // removes all existing markers from the map
128
+     markers.forEach(m => map.removeLayer(m));
129
+     markers = [];
130
+
131
+     // iterates through all posts to find coordinates and draw markers
132
+     posts.forEach(async post => {
133
+         // asynchronously finds the latitude and longitude for the location
134
+         let coords = await getCoordinatesSmart(post.location);
135
+
136
+         if (coords) {
137
+             // sets color and size based on post urgency level
138
+             const urgencyColor = post.urgency_level === 'High' ? '#ff4444' : '#ed4801';
139
+             const radius = post.urgency_level === 'High' ? 14 : 8;
140
+
141
+             // creates and adds a circular marker (L.circleMarker) to the map
142
+             const circle = L.circleMarker(coords, {
143
+                 color: urgencyColor,
144
+                 fillColor: urgencyColor,
145
+                 fillOpacity: 0.7,
146
+                 radius: radius
147
+             }).addTo(map);
148
+
149
+             const timeStr = new Date(post.timestamp).toLocaleTimeString();
150
+            
151
+             // binds a detailed pop-up box to the circular marker
152
+             circle.bindPopup(`
153
+                 <div style="font-family: 'Roboto', sans-serif; color: #333; min-width: 200px;">
154
+                     <strong style="text-transform:uppercase; color: #d32f2f; font-size: 1.1em;">
155
+                         ${post.disaster_type}
156
+                     </strong>
157
+                    
158
+                     <div style="color: #ed4801; font-weight: 700; font-size: 0.9em; margin-bottom: 4px; text-transform: uppercase;">
159
+                         ⚠ ${post.assistance_type || "General Help"}
160
+                     </div>
161
+
162
+                     <span style="font-size: 0.9em; color: #555;">📍 ${post.location}</span>
163
+                    
164
+                     <hr style="margin:8px 0; border:0; border-top:1px solid #ccc;">
165
+                    
166
+                     <div style="font-size: 0.9em; margin-bottom: 5px; font-weight: 500;">
167
+                         "${post.title}"
168
+                     </div>
169
+                    
170
+                     <small style="color: #888;">${timeStr}</small>
171
+                 </div>
172
+             `);
173
+             // adds the new marker to the global array
174
+             markers.push(circle);
175
+         }
176
+     });
177
+ }
178
+
179
+ // --- NEW SMART FINDER (Hybrid: Static List + API) ---
180
+ // function to look up coordinates, prioritizing the static list then Nominatim API
181
+ async function getCoordinatesSmart(locationStr) {
182
+     if (!locationStr) return cityCoords["Philippines"];
183
+
184
+     // 1. Try Static List (Instant)
185
+     // direct match check
186
+     const exactMatch = Object.keys(cityCoords).find(k => k.toLowerCase() === locationStr.toLowerCase());
187
+     if (exactMatch) return cityCoords[exactMatch];
188
+
189
+     // fuzzy match check
190
+     const fuzzyKey = Object.keys(cityCoords).find(city => locationStr.toLowerCase().includes(city.toLowerCase()));
191
+     if (fuzzyKey) return cityCoords[fuzzyKey];
192
+
193
+     // 2. Try OpenStreetMap API (Dynamic)
194
+     // checks local cache before making a network request
195
+     if (!window.coordCache) window.coordCache = {};
196
+     if (window.coordCache[locationStr]) return window.coordCache[locationStr];
197
+
198
+     try {
199
+         console.log(`Fetching coords for: ${locationStr}...`);
200
+         // fetches coordinates from the Nominatim API
201
+         const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${locationStr}, Philippines`);
202
+         const data = await response.json();
203
+        
204
+         // processes and caches the result
205
+         if (data && data.length > 0) {
206
+             const lat = parseFloat(data[0].lat);
207
+             const lon = parseFloat(data[0].lon);
208
+             const result = [lat, lon];
209
+             window.coordCache[locationStr] = result;
210
+             return result;
211
+         }
212
+     } catch (e) {
213
+         console.error("Geocoding failed:", e);
214
+     }
215
+
216
+     // 3. Fallback
217
+     // returns the center of the Philippines if geocoding fails
218
+     return cityCoords["Philippines"];
219
+ }
alisto_project/backend/models.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from datetime import datetime
3
+
4
+ # initializes the SQLAlchemy object for the application
5
+ db = SQLAlchemy()
6
+
7
+ # defines the main data model for a scraped disaster post
8
+ class DisasterPost(db.Model):
9
+ # sets the name of the database table
10
+ __tablename__ = 'posts'
11
+
12
+ # primary key for the table
13
+ id = db.Column(db.Integer, primary_key=True)
14
+ # unique ID from Reddit to prevent duplicates
15
+ reddit_id = db.Column(db.String(50), unique=True, nullable=False)
16
+ # title of the Reddit post
17
+ title = db.Column(db.String(300), nullable=False)
18
+ # full body content of the post
19
+ content = db.Column(db.Text, nullable=True)
20
+ # stores the contact person's name or Reddit username
21
+ author = db.Column(db.String(50), nullable=True)
22
+ # timestamp when the post was created (UTC)
23
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)
24
+
25
+ # Intelligence Fields
26
+ # extracted location or address of the incident
27
+ location = db.Column(db.String(100), nullable=True)
28
+ # extracted mobile or contact number
29
+ contact_number = db.Column(db.String(50), nullable=True)
30
+ # AI-classified type of disaster (e.g., Flood, Fire)
31
+ disaster_type = db.Column(db.String(50), nullable=True)
32
+ # AI-classified specific assistance needed (e.g., Medical, Rescue)
33
+ assistance_type = db.Column(db.String(50), nullable=True)
34
+ # dynamically assigned severity level (High, Medium, Low)
35
+ urgency_level = db.Column(db.String(20), nullable=True)
36
+ # boolean flag indicating if the post is a help request
37
+ is_help_request = db.Column(db.Boolean, default=False)
38
+
39
+ # Metadata
40
+ # operational status of the post (New, Verified, Resolved)
41
+ status = db.Column(db.String(20), default="New")
42
+
43
+ # converts the model object into a dictionary format for API responses
44
+ def to_dict(self):
45
+ # converts the timestamp to ISO 8601 format
46
+ iso_time = self.timestamp.isoformat()
47
+ # ensures the time string ends with 'Z' for standardized UTC representation
48
+ if not iso_time.endswith("Z"):
49
+ iso_time += "Z"
50
+
51
+ return {
52
+ "id": self.id,
53
+ "reddit_id": self.reddit_id,
54
+ "title": self.title,
55
+ "content": self.content,
56
+ "author": self.author,
57
+ "timestamp": iso_time,
58
+ "location": self.location,
59
+ "contact_number": self.contact_number or "Check Post",
60
+ "disaster_type": self.disaster_type,
61
+ "assistance_type": self.assistance_type,
62
+ "urgency_level": self.urgency_level,
63
+ "status": self.status
64
+ }
alisto_project/backend/models/tfidf_ensemble.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d09e788d8ecc671cef44bcc67d2f0054b9937e68b27751459455fafbba541f8c
3
+ size 405802
alisto_project/backend/my_generator.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ # ---------------------------------------------------------
4
+ # DATA POOLS
5
+ # ---------------------------------------------------------
6
+ # list of locations used for synthetic data generation
7
+ LOCATIONS = [
8
+ "Manila", "Marikina", "Quezon City", "Pasig", "Cainta", "Rizal",
9
+ "Bulacan", "Pampanga", "Cavite", "Batangas", "Laguna", "San Mateo",
10
+ "Montalban", "Antipolo", "Valenzuela", "Malabon", "Navotas", "Las Pinas",
11
+ "Taguig", "Makati", "Caloocan", "Muntinlupa", "Paranaque", "Pasay"
12
+ ]
13
+
14
+ # list of disaster types used for synthetic data generation
15
+ DISASTERS = [
16
+ "baha", "flood", "flooding", "tubig",
17
+ "bagyo", "typhoon", "storm", "hangin", "ulan",
18
+ "sunog", "fire", "smoke", "apoy",
19
+ "lindol", "earthquake", "quake", "aftershock",
20
+ "landslide", "guho", "mudslide"
21
+ ]
22
+
23
+ # list of critical keywords used in both urgent and non-urgent samples
24
+ URGENT_KEYWORDS = [
25
+ "tulong", "help", "saklolo", "rescue", "emergency", "evacuate",
26
+ "need food", "trapped", "stranded", "stuck", "lubog", "buntis"
27
+ ]
28
+
29
+ # ---------------------------------------------------------
30
+ # GENERATORS
31
+ # ---------------------------------------------------------
32
+
33
+ # generates a synthetic urgent post (positive sample)
34
+ def build_positive():
35
+ kw = random.choice(URGENT_KEYWORDS)
36
+ disaster = random.choice(DISASTERS)
37
+ loc = random.choice(LOCATIONS)
38
+
39
+ templates = [
40
+ f"{kw.upper()} please! {disaster} dito sa {loc}!",
41
+ f"{loc} need {kw}, {disaster} is rising!",
42
+ f"Kami po sa {loc} ay need ng {kw}, sobrang taas ng {disaster}!",
43
+ f"{kw.upper()}! {disaster} sa {loc}, di kami makalabas!",
44
+ f"Na-trap kami sa {loc} dahil sa {disaster}, {kw} please!",
45
+ f"{disaster} alert in {loc}, we need {kw} asap!",
46
+ f"Wala na kaming matatakbuhan sa {loc}, {disaster} na! {kw}!"
47
+ ]
48
+ return random.choice(templates)
49
+
50
+ # generates a synthetic non-urgent post (negative sample/trap)
51
+ def build_negative():
52
+ kw = random.choice(URGENT_KEYWORDS) # uses urgent keywords in a safe context
53
+ disaster = random.choice(DISASTERS)
54
+ loc = random.choice(LOCATIONS)
55
+
56
+ category = random.choice(["news", "donation", "past", "question"])
57
+
58
+ # templates that structure the urgent keyword as general news or a report
59
+ if category == "news":
60
+ return f"Update: {kw} operations for {disaster} victims in {loc} are ongoing."
61
+
62
+ # templates that structure the urgent keyword as a donation or offering help
63
+ elif category == "donation":
64
+ return f"We are sending {kw} and relief goods to {loc}. Stay strong!"
65
+
66
+ # templates that structure the urgent keyword using past tense
67
+ elif category == "past":
68
+ return f"Remembering when we were {kw}ed during the last {disaster} in {loc}."
69
+
70
+ # templates that structure the urgent keyword as a general question or advice
71
+ else:
72
+ return f"Guys in {loc}, do you need {kw}? Comment below."
alisto_project/backend/ner_extractor.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline
2
+ import re
3
+
4
+ # Load NER model once
5
+ model_name = "Davlan/xlm-roberta-base-ner-hrl"
6
+ # loads the tokenizer for the multilingual NER model
7
+ tokenizer = AutoTokenizer.from_pretrained(model_name, force_download=True)
8
+ # loads the pre-trained token classification model (the NER engine)
9
+ model = AutoModelForTokenClassification.from_pretrained(model_name, force_download=True)
10
+ # creates a Hugging Face pipeline for easy entity recognition
11
+ nlp = pipeline("ner", model=model, tokenizer=tokenizer, aggregation_strategy="simple")
12
+
13
+ # function to extract the contact person's name using Regex
14
+ def extract_contact_person_name(text):
15
+ # regex pattern to find name following contact/name keywords
16
+ name_pattern = re.compile(
17
+ r'(?:contact\s*person|contact\s*name|contact|name):\s*(.*?)(?=\s*(?:Mobile|Number|Needs|09\d{2}|[A-Z]{3,}:|\n|$))',
18
+ re.IGNORECASE | re.DOTALL
19
+ )
20
+
21
+ match = name_pattern.search(text)
22
+ if match:
23
+ # returns the captured name after removing parenthetical nicknames and periods
24
+ name = match.group(1).strip()
25
+ return re.sub(r'\s*\(.*\)\s*$', '', name).strip().rstrip('.')
26
+ return None
27
+
28
+ # function to extract mobile or landline phone numbers using Regex
29
+ def extract_contact_number(text):
30
+
31
+ # regex for common Philippine mobile and landline patterns
32
+ phone_pattern = re.compile(r'(09\d{2}\s?-?\s?\d{3}\s?-?\s?\d{4}|(?:\+63|63)?\d{10})')
33
+
34
+ match = phone_pattern.search(text)
35
+ if match:
36
+ # returns the captured phone number
37
+ return match.group(0).strip()
38
+ return None
39
+
40
+ # list of keywords often incorrectly classified as locations by the NER model
41
+ NON_LOC_KEYWORDS = ["S.O.S", "PLS", "HELP", "URGENT", "ALERT", "LOCATION", "ADDRESS", "ASAP"]
42
+
43
+ # function to extract the detailed address line by prioritizing explicit labels
44
+ def extract_address_line(text):
45
+ # regex pattern to capture address text following keywords, stopping at noise or new lines
46
+ address_pattern = re.compile(
47
+ r'(?:location|address|loc|near):\s*(.*?)(?=\s*\n|\s*\(|\s*Contact|\s*Mobile|\s*Number|\s*kami|\s*yung|\s*bahay|\s*kmi|\s*near\s|$)',
48
+ re.IGNORECASE
49
+ )
50
+
51
+ match = address_pattern.search(text)
52
+ if match:
53
+ # returns the cleaned address line
54
+ return match.group(1).strip().rstrip('.')
55
+ return None
56
+
57
+ # main function that orchestrates all entity extraction and filtering
58
+ def extract_entities(text):
59
+
60
+ # 1. Attempt to extract high-confidence address line (Regex)
61
+ explicit_address = extract_address_line(text)
62
+
63
+ # 2. Run general NER extraction
64
+ # runs the text through the Hugging Face NER pipeline
65
+ results = nlp(text)
66
+ locations = []
67
+
68
+ # processes NER results and filters out generic noise keywords
69
+ for entity in results:
70
+ # 'LOC' is the label for Locations in XLM-R NER
71
+ if entity['entity_group'] == 'LOC':
72
+ clean_loc = entity['word'].replace("##", "").strip()
73
+
74
+ # --- FILTERING LOGIC (Skip noise) ---
75
+ # skips locations that are too short
76
+ if len(clean_loc) <= 2:
77
+ continue
78
+
79
+ # skips locations matching known non-location keywords (e.g., S.O.S.)
80
+ clean_loc_upper = clean_loc.upper().replace('.', '')
81
+ if clean_loc_upper in NON_LOC_KEYWORDS:
82
+ continue
83
+ # --- END FILTERING LOGIC ---
84
+
85
+ if clean_loc not in locations:
86
+ locations.append(clean_loc)
87
+
88
+ # 3. Prioritize the explicit address if found (inserts at index 0)
89
+ # ensures the regex-found address is always the primary location
90
+ if explicit_address:
91
+ locations.insert(0, explicit_address)
92
+
93
+ # 4. Extract Contact Number
94
+ contact_number = extract_contact_number(text)
95
+
96
+ # 5. Extract Contact Person Name (NEW)
97
+ contact_name = extract_contact_person_name(text) # <--- NEW CALL
98
+
99
+ # returns all extracted data in a dictionary format
100
+ return {
101
+ "locations": locations,
102
+ "contact": contact_number,
103
+ "contact_person_name": contact_name,
104
+ "raw": results
105
+ }
alisto_project/backend/script.js ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // GLOBAL VARIABLES
2
+ let typeChart = null;
3
+ let urgencyChart = null;
4
+ let lastClickedId = null;
5
+ let knownPostIds = new Set();
6
+ let readPostIds = new Set();
7
+ let isLoggedIn = false; // track login state
8
+ let isInitialLoadComplete = false; // flag to prevent false alerts on page load
9
+ let currentUsername = ''; // stores the logged-in user's username
10
+
11
+ document.addEventListener("DOMContentLoaded", function() {
12
+ console.log("ALISTO Dashboard Loaded");
13
+
14
+ // 1. Check Login Status Immediately
15
+ checkLoginStatus();
16
+
17
+ // 2. Init Data
18
+ initCharts();
19
+ fetchPosts();
20
+ fetchStats();
21
+
22
+ setInterval(() => { fetchPosts(); fetchStats(); }, 30000);
23
+
24
+ // 3. Filter Listeners
25
+ document.getElementById('sort-select').addEventListener('change', () => fetchPosts());
26
+ document.getElementById('view-select').addEventListener('change', () => fetchPosts());
27
+ document.getElementById('urgency-select').addEventListener('change', () => fetchPosts());
28
+ document.getElementById('type-select').addEventListener('change', () => fetchPosts());
29
+ document.getElementById('assist-select').addEventListener('change', () => fetchPosts());
30
+
31
+ // 4. Export (Check login first)
32
+ document.getElementById('export-btn').addEventListener('click', () => {
33
+ if(!isLoggedIn) { alert("Responders Only. Please Log In."); return; }
34
+ window.location.href = '/api/export';
35
+ });
36
+
37
+ // 5. Stats Modal
38
+ const statsModal = document.getElementById('stats-modal');
39
+ document.getElementById('show-stats-btn').addEventListener('click', () => {
40
+ statsModal.classList.remove('hidden');
41
+ fetchStats();
42
+ });
43
+ document.getElementById('close-stats-btn').addEventListener('click', () => statsModal.classList.add('hidden'));
44
+
45
+ // 6. Login Modal Logic
46
+ setupLoginLogic();
47
+ setupSearch();
48
+ setupProfileDropdown();
49
+
50
+ // 🚨 The manual button listeners were correctly removed from here in the previous step.
51
+ });
52
+
53
+ // ----------------------------------------------------------------------
54
+ // ACTION BUTTON VISUAL SYNC LOGIC
55
+ // ----------------------------------------------------------------------
56
+
57
+ function updateActionButtons(postStatus) {
58
+ const verifyBtn = document.getElementById('verify-btn');
59
+ const resolveBtn = document.getElementById('resolve-btn');
60
+
61
+ if (!verifyBtn || !resolveBtn) return; // Safety check
62
+
63
+ // 1. Reset all active states first (CRITICAL STEP)
64
+ verifyBtn.classList.remove('is-verified');
65
+ resolveBtn.classList.remove('is-resolved');
66
+
67
+ // Reset button text
68
+ document.querySelector('#verify-btn .btn-text').textContent = 'Verify';
69
+ document.querySelector('#resolve-btn .btn-text').textContent = 'Resolve';
70
+
71
+ // 2. Apply Active State Classes based *only* on the current status
72
+
73
+ if (postStatus === 'Verified') {
74
+ // If Verified, apply the 'is-verified' style
75
+ verifyBtn.classList.add('is-verified');
76
+ document.querySelector('#verify-btn .btn-text').textContent = 'Verified';
77
+
78
+ } else if (postStatus === 'Resolved') {
79
+ // If Resolved, apply the 'is-resolved' style
80
+ resolveBtn.classList.add('is-resolved');
81
+ document.querySelector('#resolve-btn .btn-text').textContent = 'Resolved';
82
+ }
83
+ }
84
+
85
+ // ----------------------------------------------------------------------
86
+ // AUTHENTICATION LOGIC
87
+ // ----------------------------------------------------------------------
88
+
89
+ // checks the user's current login status via API
90
+ function checkLoginStatus() {
91
+ fetch('/api/user_status')
92
+ .then(res => res.json())
93
+ .then(data => {
94
+ isLoggedIn = data.is_logged_in;
95
+ currentUsername = data.username || '';
96
+ updateUIForAuth();
97
+ });
98
+ }
99
+
100
+ // toggles the visibility of login links, profile dropdown, and action buttons
101
+ function updateUIForAuth() {
102
+ const navBtn = document.getElementById('nav-login-btn');
103
+ const profileWrap = document.getElementById('profile-container-wrap');
104
+ const dropdownUsername = document.getElementById('dropdown-username');
105
+ const actionButtonsContainer = document.querySelector('.action-buttons');
106
+
107
+ if (isLoggedIn) {
108
+ // LOGGED IN: Show Profile Icon, Update Name, Hide Login Link
109
+ if (navBtn) navBtn.style.display = 'none';
110
+ if (profileWrap) profileWrap.style.display = 'flex'; // Show the profile icon container
111
+ if (dropdownUsername) dropdownUsername.innerText = currentUsername;
112
+ if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'visible';
113
+ } else {
114
+ // LOGGED OUT: Show Login Link, Hide Profile Icon
115
+ if (navBtn) navBtn.style.display = 'inline-block';
116
+ if (profileWrap) profileWrap.style.display = 'none'; // Hide the profile icon container
117
+ if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'hidden';
118
+ }
119
+ }
120
+
121
+ // handles click events for the new profile icon and logout button inside the dropdown
122
+ function setupProfileDropdown() {
123
+ const profileToggle = document.getElementById('profile-toggle');
124
+ const profileDropdown = document.getElementById('profile-dropdown');
125
+ const logoutBtn = document.getElementById('dropdown-logout-btn');
126
+
127
+ // Get the reference to the existing logout modal element
128
+ const logoutModal = document.getElementById('logout-modal');
129
+
130
+ // 1. Toggle visibility when clicking the icon (Unchanged)
131
+ if (profileToggle) {
132
+ profileToggle.addEventListener('click', (e) => {
133
+ e.stopPropagation();
134
+ profileDropdown.classList.toggle('hidden');
135
+ });
136
+ }
137
+
138
+ // 2. Logout button handler (MODIFIED BLOCK)
139
+ if (logoutBtn) {
140
+ logoutBtn.addEventListener('click', (e) => {
141
+ e.preventDefault();
142
+
143
+ // Action: Show the confirmation modal instead of redirecting
144
+ if (logoutModal) {
145
+ logoutModal.classList.remove('hidden');
146
+ } else {
147
+ // Fallback, should not happen if index.html is correct
148
+ window.location.href = '/api/logout';
149
+ }
150
+ });
151
+ }
152
+
153
+ // 3. Close when clicking outside (Unchanged)
154
+ document.addEventListener('click', (e) => {
155
+ if (profileToggle && profileDropdown && !profileToggle.contains(e.target) && !profileDropdown.contains(e.target)) {
156
+ if (!profileDropdown.classList.contains('hidden')) {
157
+ profileDropdown.classList.add('hidden');
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ // sets up handlers for the login and logout modals
164
+ function setupLoginLogic() {
165
+ const loginModal = document.getElementById('login-modal');
166
+ const logoutModal = document.getElementById('logout-modal');
167
+ const navBtn = document.getElementById('nav-login-btn');
168
+ const closeBtn = document.getElementById('close-login-btn');
169
+ const submitBtn = document.getElementById('login-submit-btn');
170
+
171
+ // 1. NAV BUTTON CLICK HANDLER
172
+ navBtn.addEventListener('click', (e) => {
173
+ e.preventDefault();
174
+ if (isLoggedIn) {
175
+ logoutModal.classList.remove('hidden');
176
+ } else {
177
+ loginModal.classList.remove('hidden');
178
+ }
179
+ });
180
+
181
+ // 2. LOGOUT MODAL HANDLERS
182
+ document.getElementById('confirm-logout-btn').addEventListener('click', () => {
183
+ window.location.href = '/api/logout';
184
+ });
185
+
186
+ document.getElementById('cancel-logout-btn').addEventListener('click', () => {
187
+ logoutModal.classList.add('hidden');
188
+ });
189
+
190
+ // 3. LOGIN MODAL HANDLERS
191
+ closeBtn.addEventListener('click', () => loginModal.classList.add('hidden'));
192
+
193
+ submitBtn.addEventListener('click', () => {
194
+ const u = document.getElementById('username').value;
195
+ const p = document.getElementById('password').value;
196
+
197
+ fetch('/api/login', {
198
+ method: 'POST',
199
+ headers: {'Content-Type': 'application/json'},
200
+ body: JSON.stringify({username: u, password: p})
201
+ })
202
+ .then(res => {
203
+ if(res.ok) return res.json();
204
+ throw new Error('Invalid credentials');
205
+ })
206
+ .then(data => {
207
+ isLoggedIn = true;
208
+ updateUIForAuth();
209
+ loginModal.classList.add('hidden');
210
+ document.getElementById('username').value = '';
211
+ document.getElementById('password').value = '';
212
+ document.getElementById('login-error').innerText = '';
213
+ })
214
+ .catch(err => {
215
+ document.getElementById('login-error').innerText = "Invalid Username or Password";
216
+ });
217
+ });
218
+ }
219
+
220
+ // calculates and formats the time difference for 'X hrs ago'
221
+ function formatRelativeTime(timestamp) {
222
+ const now = new Date();
223
+ const posted = new Date(timestamp);
224
+ const diffMs = now - posted;
225
+ const diffMins = Math.floor(diffMs / 60000);
226
+
227
+ if (isNaN(diffMins)) return "Unknown time";
228
+ if (diffMins < 1) return "Just now";
229
+ if (diffMins < 60) return diffMins + " mins ago";
230
+ if (diffMins < 1440) {
231
+ const hours = Math.floor(diffMins / 60);
232
+ return hours + (hours === 1 ? " hr ago" : " hrs ago"); // Use "hrs ago"
233
+ }
234
+ const days = Math.floor(diffMins / 1440);
235
+ return days + (days === 1 ? " day ago" : " days ago");
236
+ }
237
+
238
+ // ----------------------------------------------------------------------
239
+ // RENDER LOGIC
240
+ // ----------------------------------------------------------------------
241
+
242
+ // renders the list of incident posts in the sidebar feed
243
+ function renderSidebar(data) {
244
+ const sidebar = document.getElementById('incident-feed');
245
+ if (!sidebar) return;
246
+ sidebar.innerHTML = '';
247
+
248
+ const countEl = document.getElementById('dashboard-alert-count');
249
+ if (countEl) {
250
+ countEl.innerText = data.length;
251
+ }
252
+
253
+ if (data.length === 0) {
254
+ sidebar.innerHTML = `<p style="color: #ccc; padding: 20px; text-align: center;">No alerts found.</p>`;
255
+ return;
256
+ }
257
+
258
+ data.forEach(post => {
259
+ const box = document.createElement('div');
260
+ box.className = 'alert-box';
261
+
262
+ if (post.id === lastClickedId) box.classList.add('selected');
263
+ else if (post.status === 'New' && !readPostIds.has(post.id)) box.classList.add('unread');
264
+
265
+ const relativeTimeStr = formatRelativeTime(post.timestamp);
266
+ // const timeStr = new Date(post.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
267
+
268
+ let statusColor = '#ed4801'; /*ed4801*/
269
+ if (post.status === 'Verified') statusColor = '#ffc107'; /*00C851*/
270
+ if (post.status === 'Resolved') statusColor = '#00C851'; /*33b5e5*/
271
+
272
+ let statusText = `(${post.status})`;
273
+ if (post.status === 'New' && readPostIds.has(post.id)) statusText = "";
274
+
275
+ // Determine Badge Color Class
276
+ let assistClass = 'assist-badge'; // Default
277
+ const type = (post.assistance_type || "").toLowerCase();
278
+ if (type.includes('medical')) assistClass += ' assist-medical';
279
+ else if (type.includes('rescue')) assistClass += ' assist-rescue';
280
+ else if (type.includes('food')) assistClass += ' assist-food';
281
+ else if (type.includes('evac')) assistClass += ' assist-evac';
282
+
283
+ box.innerHTML = `
284
+ <div class="alert-icon" style="background-color: ${statusColor}"></div>
285
+ <div class="box-text">
286
+ <div class="box-title">
287
+ ${post.disaster_type}
288
+ <span style="font-size:0.7em; opacity:0.7">${statusText}</span>
289
+ </div>
290
+
291
+ <div style="display: flex; align-items: center; margin-top: 4px; gap: 5px;">
292
+ <span class="${assistClass}">${post.assistance_type || "General"}</span>
293
+ <div class="box-subtitle" style="margin-top:0;">${post.location} • ${relativeTimeStr}</div>
294
+ </div>
295
+ </div>
296
+ `;
297
+
298
+ box.addEventListener('click', () => {
299
+ const detailBox = document.getElementById('postInfo');
300
+ if (lastClickedId === post.id) {
301
+ detailBox.classList.remove('active');
302
+ box.classList.remove('selected');    
303
+ lastClickedId = null;
304
+ renderSidebar(data);
305
+ } else {
306
+ document.querySelectorAll('.alert-box').forEach(el => el.classList.remove('selected'));
307
+ box.classList.add('selected');
308
+ lastClickedId = post.id;
309
+ readPostIds.add(post.id);
310
+
311
+ updateDetailView(post);
312
+ detailBox.classList.add('active');
313
+
314
+ // CRITICAL: Check auth again to show/hide buttons for this specific detail view
315
+ updateUIForAuth();
316
+
317
+ renderSidebar(data);
318
+ }
319
+ });
320
+ sidebar.appendChild(box);
321
+ });
322
+ }
323
+
324
+ // updates the detail panel with the selected post's information
325
+ function updateDetailView(post) {
326
+ const setText = (id, text) => {
327
+ const el = document.getElementById(id);
328
+ if (el) el.innerText = text;
329
+ };
330
+
331
+ setText('detail-title', post.title);
332
+ setText('detail-location', post.location || "Unknown");
333
+ setText('detail-assistance', post.assistance_type || "General");
334
+ setText('detail-contact-name', post.author ? (
335
+ // Check if the name contains a space (suggests a full name)
336
+ // If no space is found, assume it is a username and prepend 'u/'
337
+ post.author.includes(' ') ? post.author : `u/${post.author}`
338
+ ) : "Unknown");
339
+ setText('detail-contact-number', post.contact_number || "Check Post");
340
+ setText('detail-body', post.content);
341
+ setText('detail-status', post.status);
342
+
343
+ const statusBadge = document.getElementById('detail-status');
344
+ if (statusBadge) {
345
+ // Clear all previous status classes
346
+ statusBadge.classList.remove('status-new', 'status-verified', 'status-resolved');
347
+
348
+ // Apply the correct new class
349
+ if (post.status) {
350
+ statusBadge.classList.add(`status-${post.status.toLowerCase()}`);
351
+ }
352
+ }
353
+
354
+ const link = document.getElementById('detail-link');
355
+ if (link) {
356
+ const isSimulated = typeof post.reddit_id === 'string' && (post.reddit_id.startsWith('fake') || post.reddit_id.startsWith('sim'));
357
+ link.href = isSimulated ? '#' : `https://reddit.com/comments/${post.reddit_id.replace('t3_', '')}`;
358
+ }
359
+
360
+ const urgEl = document.getElementById('detail-urgency');
361
+ if (urgEl) {
362
+ urgEl.innerText = post.urgency_level;
363
+ urgEl.style.color = post.urgency_level === 'High' ? '#ff4444' : '#00C851';
364
+ }
365
+
366
+ const timeEl = document.getElementById('detail-time');
367
+ if (timeEl) {
368
+ // 1. Calculate relative and exact time strings
369
+ const posted = new Date(post.timestamp);
370
+ const relativeTime = formatRelativeTime(post.timestamp); // e.g., "2 hrs ago"
371
+
372
+ // 2. Format the exact time (e.g., 10:34 PM)
373
+ const exactTimeStr = posted.toLocaleTimeString([], {
374
+ hour: '2-digit',
375
+ minute: '2-digit',
376
+ hour12: true
377
+ });
378
+
379
+ // 3. Generate multi-line HTML structure with inline CSS
380
+ timeEl.innerHTML = `
381
+ <div style="font-size: 1.1em; font-weight: bold; color: white;">${exactTimeStr}</div>
382
+
383
+ <div style="font-size: 0.85em; color: #ed4801; margin-top: 2px; text-transform: uppercase">${relativeTime}</div>
384
+ `;
385
+
386
+ // The redundant if/else logic block for combining text is now removed.
387
+ }
388
+
389
+ // Synchronize buttons with the loaded post status
390
+ updateActionButtons(post.status);
391
+ }
392
+
393
+ // ----------------------------------------------------------------------
394
+ // STANDARD FUNCTIONS (FINAL WORKING VERSION)
395
+ // ----------------------------------------------------------------------
396
+
397
+ // handles the logic for updating the post status (Verify/Resolve)
398
+ function updateStatus(intendedStatus) {
399
+ if (!lastClickedId) return;
400
+ const badge = document.getElementById('detail-status');
401
+ // FIX: Read status and clean it by converting to lowercase and trimming whitespace
402
+ const currentStatus = badge ? badge.innerText.trim() : 'New';
403
+ let finalStatus;
404
+
405
+ if (intendedStatus === 'Verified') {
406
+ // ... (rest of the logic)
407
+ // Check against the current, clean status
408
+ if (currentStatus.toLowerCase() === 'verified') {
409
+ finalStatus = 'New';
410
+ } else {
411
+ finalStatus = 'Verified';
412
+ }
413
+
414
+ } else if (intendedStatus === 'Resolved') {
415
+ // ... (rest of the logic)
416
+ // Check against the current, clean status
417
+ if (currentStatus.toLowerCase() === 'resolved') {
418
+ finalStatus = 'New';
419
+ } else {
420
+ finalStatus = 'Resolved';
421
+ }
422
+ } else {
423
+ return;
424
+ }
425
+
426
+ // Safety check: ensure we are actually changing the status
427
+ if (finalStatus === currentStatus) {
428
+ return;
429
+ }
430
+
431
+ // --- API CALL AND UI UPDATE ---
432
+ fetch(`/api/posts/${lastClickedId}/status`, {
433
+ method: 'POST',
434
+ headers: { 'Content-Type': 'application/json' },
435
+ body: JSON.stringify({ status: finalStatus })
436
+ })
437
+ .then(res => {
438
+ if(res.status === 401) { alert("Unauthorized. Please log in."); return; }
439
+ // If the API call returns success (200), proceed with UI updates based on the finalStatus.
440
+ return res.json();
441
+ })
442
+ .then(data => {
443
+ if(data) {
444
+ fetchPosts();
445
+ fetchStats();
446
+
447
+ if (badge) {
448
+ // 1. Update text
449
+ badge.innerText = finalStatus;
450
+
451
+ // 2. Apply color class (visual sync fix)
452
+ badge.classList.remove('status-new', 'status-verified', 'status-resolved');
453
+ badge.classList.add(`status-${finalStatus.toLowerCase()}`);
454
+ }
455
+
456
+ // 3. Update buttons' appearance
457
+ updateActionButtons(finalStatus);
458
+ }
459
+ })
460
+ .catch(error => {
461
+ console.error("Status Update Failed:", error);
462
+ alert("Failed to update status due to network error.");
463
+ });
464
+ }
465
+
466
+ // pop up notificaiton for new alerts
467
+ function showNotification(message) {
468
+ const popup = document.getElementById('new-alert-notification');
469
+ const msgEl = document.getElementById('notification-message');
470
+ if (!popup || !msgEl) return;
471
+
472
+ msgEl.innerText = message;
473
+
474
+ // 1. Prepare for animation (ensure it's visible but off-screen)
475
+ popup.classList.remove('hidden');
476
+
477
+ // 2. Force reflow to ensure CSS animation starts correctly
478
+ void popup.offsetWidth;
479
+
480
+ // 3. Trigger slide-in animation
481
+ popup.classList.add('visible');
482
+
483
+ // 4. Set timeout to slide out after 6 seconds
484
+ setTimeout(() => {
485
+ popup.classList.remove('visible');
486
+
487
+ // 5. Hide completely after animation finishes (0.5s transition time in CSS)
488
+ setTimeout(() => {
489
+ popup.classList.add('hidden');
490
+ }, 500);
491
+ }, 6000); // Display for 6 seconds
492
+ }
493
+
494
+ // initializes and draws the chart.js graphs for stats modal
495
+ function initCharts() {
496
+ const ctxType = document.getElementById('typeChart');
497
+ const ctxUrg = document.getElementById('urgencyChart');
498
+ if (!ctxType || !ctxUrg) return;
499
+
500
+ typeChart = new Chart(ctxType.getContext('2d'), {
501
+ type: 'doughnut',
502
+ data: { labels: [], datasets: [{ data: [], backgroundColor: ['#ed4801', '#33b5e5', '#00C851', '#ffbb33', '#aa66cc'], borderWidth: 0 }] },
503
+ options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: 'white' } } } }
504
+ });
505
+
506
+ urgencyChart = new Chart(ctxUrg.getContext('2d'), {
507
+ type: 'bar',
508
+ data: { labels: ['High', 'Low/Med'], datasets: [{ label: 'Count', data: [0, 0], backgroundColor: ['#ff4444', '#00C851'], borderRadius: 5 }] },
509
+ options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: 'white' } }, x: { ticks: { color: 'white' } } }, plugins: { legend: { display: false } } }
510
+ });
511
+ }
512
+
513
+ // fetches statistical data from the API
514
+ function fetchStats() { fetch('/api/stats').then(res=>res.json()).then(data=>updateCharts(data)).catch(err=>console.error(err)); }
515
+
516
+ // updates the chart data and redraws the graphs
517
+ function updateCharts(data) { if(!typeChart || !urgencyChart) return; const types = data.disaster_types || {}; typeChart.data.labels = Object.keys(types); typeChart.data.datasets[0].data = Object.values(types); typeChart.update(); const levels = data.urgency_levels || {}; urgencyChart.data.datasets[0].data = [levels['High']||0, (levels['Low']||0)+(levels['Medium']||0)]; urgencyChart.update(); }
518
+
519
+ // fetches post data from the API based on current filter selections
520
+ function fetchPosts(q='') {
521
+ const sort = document.getElementById('sort-select')?.value||'newest';
522
+ const view = document.getElementById('view-select')?.value||'active';
523
+ const urgency = document.getElementById('urgency-select')?.value||'all';
524
+ const type = document.getElementById('type-select')?.value||'all';
525
+ const assist = document.getElementById('assist-select')?.value||'all';
526
+ const searchVal = document.querySelector('.search-input')?.value||'';
527
+ let url = `/api/posts?sort=${sort}&view=${view}&urgency=${urgency}&type=${type}&assist=${assist}`;
528
+ if(searchVal) url += `&query=${encodeURIComponent(searchVal)}`;
529
+ url += `&_=${new Date().getTime()}`;
530
+ fetch(url).then(r=>r.json()).then(d=>{renderSidebar(d); checkAudioAlert(d);}).catch(e=>console.error(e));
531
+ }
532
+
533
+ // checks for new high-urgency alerts and plays sound + shows notification
534
+ function checkAudioAlert(p){
535
+ const a=document.getElementById('alert-sound');
536
+ if(!a)return;
537
+ a.volume = 0.1;
538
+ let newAlertFound = false;
539
+
540
+ if (!isInitialLoadComplete) {
541
+ // 1. On the very first load after page navigation/refresh:
542
+ // Populate the known set with ALL current IDs and skip the alert.
543
+ p.forEach(x => knownPostIds.add(x.id));
544
+ isInitialLoadComplete = true;
545
+ return;
546
+ }
547
+
548
+ p.forEach(x => {
549
+ if(x.urgency_level === 'High' && !knownPostIds.has(x.id) && x.status !== 'Resolved') {
550
+ newAlertFound = true;
551
+ }
552
+ });
553
+
554
+ if(newAlertFound) {
555
+ a.play().catch(e=>{});
556
+ showNotification("NEW HIGH PRIORITY ALERT: Check Feed");
557
+ }
558
+
559
+ p.forEach(x => {
560
+ knownPostIds.add(x.id);
561
+ });
562
+ }
563
+
564
+ // sets up the search input logic with clear button
565
+ function setupSearch(){ const i=document.querySelector('.search-input'), c=document.querySelector('.clear-icon'); if(!i)return; i.addEventListener('input',()=>{if(i.value)c?.classList.remove('hidden');else{c?.classList.add('hidden');fetchPosts();}}); i.addEventListener('keydown',e=>{if(e.key==='Enter')fetchPosts();}); c?.addEventListener('click',()=>{i.value='';c.classList.add('hidden');fetchPosts();}); }
566
+
567
+ // Attach listeners once DOM is ready
568
+ (function () {
569
+ // Select all elements with data_tooltip attribute
570
+ const tooltipElements = document.querySelectorAll('[data_tooltip]');
571
+
572
+ tooltipElements.forEach(el => {
573
+ // When clicked: hide tooltip immediately by adding class
574
+ el.addEventListener('mousedown', (e) => {
575
+ // Add class so CSS hides tooltip; use mousedown for immediate feedback
576
+ el.classList.add('tooltip-hidden');
577
+
578
+ // Also remove focus so :focus doesn't keep hiding/showing unpredictably
579
+ if (typeof el.blur === 'function') {
580
+ el.blur();
581
+ }
582
+ });
583
+
584
+ // When mouse leaves the element: remove the hiding class so future hovers work
585
+ el.addEventListener('mouseleave', (e) => {
586
+ el.classList.remove('tooltip-hidden');
587
+ });
588
+
589
+ // Also remove hidden class on touchend for touch devices (optional)
590
+ el.addEventListener('touchend', () => {
591
+ el.classList.remove('tooltip-hidden');
592
+ });
593
+ });
594
+ })();
alisto_project/backend/simulate_feed.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import os
3
+ import pickle
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
7
+ from ingest_reddit import is_news_or_irrelevant, get_disaster_type, check_for_philippine_location
8
+ from ner_extractor import extract_entities
9
+
10
+ # ---------------------------------------------------------
11
+ # CONFIG & SETUP
12
+ # ---------------------------------------------------------
13
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ MODEL_DIR = os.path.join(BASE_DIR, 'models')
15
+ ROBERTA_DIR = os.path.join(MODEL_DIR, 'roberta_model')
16
+ TFIDF_PATH = os.path.join(MODEL_DIR, 'tfidf_ensemble.pkl')
17
+
18
+ # ---------------------------------------------------------
19
+ # LOAD BRAINS
20
+ # ---------------------------------------------------------
21
+ print("--- ALISTO: Loading Simulator ---")
22
+
23
+ tokenizer = None
24
+ roberta_model = None
25
+ tfidf_model = None
26
+
27
+ # 1. Load XLM-R (Context Expert)
28
+ try:
29
+ if os.path.exists(ROBERTA_DIR):
30
+ tokenizer = AutoTokenizer.from_pretrained(ROBERTA_DIR)
31
+ roberta_model = AutoModelForSequenceClassification.from_pretrained(ROBERTA_DIR)
32
+ roberta_model.eval()
33
+ print("✅ XLM-R Loaded")
34
+ else:
35
+ print("❌ Failed to load XLM-R (Folder missing)")
36
+ except Exception as e:
37
+ print(f"❌ Error loading XLM-R: {e}")
38
+
39
+ # 2. Load TF-IDF (Gatekeeper)
40
+ try:
41
+ if os.path.exists(TFIDF_PATH):
42
+ with open(TFIDF_PATH, 'rb') as f:
43
+ tfidf_model = pickle.load(f)
44
+ print("✅ TF-IDF Loaded")
45
+ else:
46
+ print("❌ Failed to load TF-IDF (File missing)")
47
+ except Exception as e:
48
+ print(f"❌ Error loading TF-IDF: {e}")
49
+
50
+ # ---------------------------------------------------------
51
+ # PREDICTION LOGIC (Must match ingest_reddit.py)
52
+ # ---------------------------------------------------------
53
+ def predict_urgency(text):
54
+ # 1. Gatekeeper (TF-IDF)
55
+ if tfidf_model:
56
+ probs = tfidf_model.predict_proba([text])[0]
57
+ tfidf_conf = probs[1]
58
+
59
+ if tfidf_conf < 0.20:
60
+ return False, tfidf_conf, "TF-IDF Reject"
61
+
62
+ # 2. Context Expert (RoBERTa)
63
+ if roberta_model and tokenizer:
64
+ inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128)
65
+ with torch.no_grad():
66
+ outputs = roberta_model(**inputs)
67
+ r_probs = F.softmax(outputs.logits, dim=-1)
68
+ roberta_conf = r_probs[0][1].item()
69
+
70
+ return (roberta_conf > 0.5), roberta_conf, "RoBERTa"
71
+
72
+ return False, 0.0, "No Model"
73
+
74
+ # ---------------------------------------------------------
75
+ # TEST DATA
76
+ # ---------------------------------------------------------
77
+ TEST_POSTS = [
78
+ # --- SHOULD BE ACCEPTED ---
79
+ "Tulong po, stuck kami sa bubong ng bahay, tumataas tubig sa Marikina!",
80
+ "Rescue needed at Provident Village, 3 kids trapped inside ceiling.",
81
+ "Wala na kaming matatakbuhan, lampas tao na ang baha sa Cainta.",
82
+ "Emergency! Landslide blocked the road in Baguio, need extraction.",
83
+ "Please help us, flood entering 2nd floor in San Mateo Rizal.",
84
+
85
+ # --- SHOULD BE REJECTED ---
86
+ "Breaking News: Typhoon Signal No 4 raised in Bicol.",
87
+ "Open for donations via GCash for typhoon victims.",
88
+ "Looking for volunteers to repack relief goods at Ateneo.",
89
+ "Stay safe everyone, praying for all affected.",
90
+ "Discussion: Why is the government so slow?",
91
+ "My heart breaks seeing the flood photos."
92
+ ]
93
+
94
+ def run_simulation():
95
+ print("\n--- 🟢 STARTING SIMULATION ---\n")
96
+
97
+ for text in TEST_POSTS:
98
+ print(f"📝 Post: {text[:60]}...")
99
+
100
+ # A. Logic Filter
101
+ is_bad, reason = is_news_or_irrelevant(text)
102
+ if is_bad:
103
+ print(f" ❌ BLOCKED by Logic: {reason}")
104
+ print("-" * 50)
105
+ time.sleep(0.5)
106
+ continue
107
+
108
+ # B. AI Prediction
109
+ is_urgent, score, source = predict_urgency(text)
110
+
111
+ if is_urgent:
112
+ # C. Entity Extraction
113
+ ner = extract_entities(text)
114
+ locs = ner.get('locations', [])
115
+ disaster = get_disaster_type(text)
116
+
117
+ print(f" ✅ ACCEPTED ({source} Conf: {score:.2%})")
118
+ print(f" 📍 Location: {locs}")
119
+ print(f" 🌊 Type: {disaster}")
120
+ else:
121
+ print(f" ❌ REJECTED by AI (Conf: {score:.2%})")
122
+
123
+ print("-" * 50)
124
+ time.sleep(1)
125
+
126
+ if __name__ == "__main__":
127
+ run_simulation()
alisto_project/backend/style.css ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* --- GLOBAL RESET & FONTS --- */
2
+ * { margin: 0; padding: 0; box-sizing: border-box; }
3
+
4
+ body {
5
+ font-family: 'Roboto', sans-serif;
6
+ color: #fff;
7
+ background: #28282B;
8
+ overflow: hidden;
9
+ height: 100vh;
10
+ }
11
+
12
+ h1, h2, h3 { font-family: 'Montserrat', sans-serif; }
13
+
14
+ /* --- HEADER --- */
15
+ /* --- HEADER STYLES --- */
16
+ .main-header {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 60px;
22
+ padding: 0 2vw;
23
+
24
+ background: rgba(0, 0, 0, 0.9);
25
+ backdrop-filter: blur(8px);
26
+ z-index: 999;
27
+
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
32
+
33
+ /* FIX: Prevent layout breaking on very small screens */
34
+ min-width: 600px;
35
+ }
36
+
37
+ /* 1. LOGO (Locked) */
38
+ .logo {
39
+ display: flex;
40
+ flex-direction: column;
41
+ line-height: 1;
42
+ flex-shrink: 0; /* Never shrink */
43
+ margin-right: 20px;
44
+ }
45
+ .logo-main { font-family: 'Montserrat', sans-serif; font-size: 1.8em; font-weight: 900; color: #ed4801; }
46
+ .logo-sub { font-size: 0.65em; font-weight: 400; color: #ccc; letter-spacing: 2px; text-transform: uppercase; margin-top: 2px; }
47
+
48
+ /* 2. NAVIGATION (Locked) */
49
+ .main-nav {
50
+ display: flex;
51
+ align-items: center;
52
+ flex-shrink: 0; /* Never shrink */
53
+ margin-left: 20px;
54
+ }
55
+
56
+ .main-nav .nav-link {
57
+ color: #ccc;
58
+ text-decoration: none;
59
+ font-weight: 500;
60
+ font-size: 0.9em;
61
+ padding: 8px 15px;
62
+ border-radius: 6px;
63
+ transition: all 0.3s ease;
64
+ margin-left: 10px;
65
+
66
+ /* FIX: Keep text on one line */
67
+ white-space: nowrap;
68
+ }
69
+ .main-nav .nav-link {
70
+ color: #ccc;
71
+ text-decoration: none;
72
+ font-weight: 500;
73
+ font-size: 0.9em;
74
+ padding: 8px 15px;
75
+ border-radius: 6px;
76
+ transition: all 0.3s ease;
77
+ margin-left: 10px;
78
+
79
+ /* FIX: Keep text on one line */
80
+ white-space: nowrap;
81
+
82
+ /* FIX: Add invisible border so it doesn't jump when active */
83
+ border: 1px solid transparent;
84
+ }
85
+
86
+ .main-nav .nav-link:hover {
87
+ color: white;
88
+ background-color: rgba(255, 255, 255, 0.1);
89
+ }
90
+
91
+ .main-nav .nav-link.active {
92
+ color: #ed4801;
93
+ border: 1px solid #ed4801;
94
+ }
95
+
96
+ /* --- SEARCH BAR (Stabilized) --- */
97
+ .search-container {
98
+ display: flex;
99
+ align-items: center;
100
+ background: rgba(255, 255, 255, 0.1);
101
+ border-radius: 20px;
102
+ padding: 8px 15px;
103
+
104
+ /* FIX: Remove 'clamp'. Use standard width so it stops jittering. */
105
+ width: 100%;
106
+ max-width: 400px;
107
+
108
+ transition: background 0.3s ease;
109
+ }
110
+
111
+ .search-container:hover {
112
+ background: rgba(255, 255, 255, 0.15);
113
+ }
114
+
115
+ .search-input {
116
+ flex-grow: 1;
117
+ border: none;
118
+ background: transparent;
119
+ color: white;
120
+ font-size: 0.9em;
121
+ padding: 0;
122
+ outline: none;
123
+ }
124
+
125
+ .search-icon {
126
+ color: #ed4801;
127
+ margin-left: 5px;
128
+ cursor: pointer;
129
+ }
130
+
131
+ .clear-icon {
132
+ color: #ccc;
133
+ margin-right: 10px;
134
+ cursor: pointer;
135
+ }
136
+
137
+ .clear-icon.hidden {
138
+ display: none;
139
+ }
140
+
141
+ /* --- HERO --- */
142
+ .hero {
143
+ margin-top: 60px;
144
+ height: calc(100vh - 60px);
145
+ width: 100%;
146
+ background: url('images/bg2-logan.jpg') center/cover no-repeat fixed;
147
+
148
+ /* FLEXBOX MAGIC */
149
+ display: flex;
150
+ align-items: stretch; /* Make both columns full height */
151
+ flex-wrap: nowrap; /* Desktop: Side-by-side. Mobile: We change this via media query */
152
+
153
+ padding: 0;
154
+ gap: 0;
155
+ }
156
+ /* --- SIDEBAR WRAPPER --- */
157
+ .sidebar-wrapper {
158
+ display: flex;
159
+ flex-direction: column;
160
+ flex: 0 0 400px;
161
+ width: 400px;
162
+
163
+ height: 100%;
164
+
165
+ background: rgba(0, 0, 0, 0.6);
166
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
167
+ backdrop-filter: blur(5px);
168
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
169
+ /* overflow: hidden; */
170
+ z-index: 10;
171
+ }
172
+
173
+ /* --- FILTER BAR --- */
174
+ .filter-bar {
175
+ display: flex; gap: 8px; padding: 15px;
176
+ background: rgba(0,0,0,0.3); border-bottom: 1px solid rgba(255,255,255,0.1);
177
+ align-items: center;
178
+ }
179
+
180
+ .filter-bar-c {
181
+ display: flex; gap: 8px; width: 100%;
182
+ justify-content: center; align-items: center; flex-direction: column;
183
+ }
184
+
185
+
186
+ .filter-bar-r {
187
+ display: flex; gap: 8px; width: 100%;
188
+ justify-content: space-between; align-items: center; flex-direction: row;
189
+ }
190
+
191
+ .filter-select { background: rgba(99, 97, 97, 0.1); color: #acabab; border: 1px solid #555; border-radius: 4px; padding: 5px; font-size: 0.8rem; outline: none; cursor: pointer; flex-grow: 1; min-width: 0; }
192
+ .pulse-btn, .icon-btn { flex-shrink: 0; }
193
+ .icon-btn { background: #ed4801; color: white; border: none; border-radius: 8px; width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
194
+ .pulse-btn { background: linear-gradient(135deg, #ed4801, #ff7e42); color: white; border: none; border-radius: 50%; width: 35px; height: 35px; cursor: pointer; box-shadow: 0 0 10px rgba(237, 72, 1, 0.5); display: flex; align-items: center; justify-content: center; animation: pulse-glow 2s infinite; }
195
+ @keyframes pulse-glow { 0% { box-shadow: 0 0 0 0 rgba(237, 72, 1, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(237, 72, 1, 0); } }
196
+ .separator { width: 1px; background: rgba(255,255,255,0.2); margin: 0 5px; height: 20px; }
197
+
198
+ /* --- SIDEBAR LIST --- */
199
+ .sidebar {
200
+ width: 100%; height: 100%;
201
+ background: transparent; border: none; box-shadow: none;
202
+ padding: 10px; overflow-y: auto; overflow-x: hidden;
203
+ }
204
+ .sidebar::-webkit-scrollbar { width: 6px; }
205
+ .sidebar::-webkit-scrollbar-thumb { background: rgba(237, 72, 1, 0.5); border-radius: 10px; }
206
+
207
+ /* --- ALERT BOXES --- */
208
+ .alert-box { display: flex; align-items: center; background: rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 16px; margin-bottom: 10px; cursor: pointer; transition: transform 0.2s ease, background 0.2s; border-left: 4px solid transparent; }
209
+ .alert-box:hover { background: rgba(255, 255, 255, 0.2); transform: translateX(5px); }
210
+ .alert-box.selected { background: rgba(237, 72, 1, 0.3); border-left: 4px solid #ed4801; }
211
+ .alert-box.unread { background: rgba(255, 255, 255, 0.15); border-left: 3px solid #ed4801; opacity: 1; }
212
+ .alert-icon { width: 12px; height: 12px; background-color: #ed4801; border-radius: 50%; margin-right: 15px; flex-shrink: 0; }
213
+ .box-text { display: flex; flex-direction: column; }
214
+ .box-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 4px; color: #fff; text-transform: uppercase; }
215
+ .box-subtitle { font-size: 0.8rem; color: #ccc; }
216
+ .active-alert-counter{ padding: 5px 10px 5px 10px; text-align: left; }
217
+
218
+
219
+ /* --- DETAIL BOX (ANIMATED) --- */
220
+ .detail-box {
221
+ display: flex;
222
+ flex-direction: column;
223
+
224
+ /* FIX: Take up all remaining space */
225
+ flex: 1;
226
+
227
+ /* FIX: Prevents the box from forcing the screen wider if text is long */
228
+ min-width: 0;
229
+
230
+ /* Keep it floating */
231
+ height: calc(100% - 40px); /* Full height minus margins */
232
+ margin: 20px;
233
+
234
+ background: rgba(0,0,0,0.9);
235
+ padding: 30px;
236
+ border-radius: 15px;
237
+ color: #fff;
238
+ border: 1px solid #ed4801;
239
+ z-index: 20;
240
+
241
+ /* Animation: Hidden by default */
242
+ opacity: 0;
243
+ transform-origin: top right;
244
+
245
+ transform: scale(0.9) translateX(0);
246
+ transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
247
+ pointer-events: none;
248
+ backdrop-filter: blur(5px);
249
+ }
250
+
251
+ /* Animation: Visible state */
252
+ .detail-box.active {
253
+ opacity: 1;
254
+ transform: scale(1) translateX(0);
255
+ pointer-events: auto;
256
+ box-shadow: -10px 10px 30px rgba(0,0,0,0.5);
257
+ }
258
+ /* --- DETAIL CONTENT --- */
259
+ .detail-header-r { display: flex; flex-direction: row; justify-content: space-between; margin: 0 0 10px; align-items: center; }
260
+ .detail-title { font-size: 24px; font-weight: 700; margin: 0; width: 70%; }
261
+ .detail-redirect { font-size: 14px; color: #f6510a; text-decoration: none; border: 1px solid #f6510a; padding: 5px 10px; border-radius: 4px; transition: 0.3s; }
262
+ .detail-redirect:hover { background: #f6510a; color: white; }
263
+ .detail-line { border: none; border-top: 1px solid #ed4801; margin-bottom: 20px; }
264
+ .detail-important-r { display: flex; flex-direction: row; justify-content: space-between; margin-top: 10px; }
265
+ .detail-row1-c { display: flex; flex-direction: column; gap: 15px; flex: 1; }
266
+ .detail-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: #888; margin-bottom: 5px; }
267
+ .detail-content { margin-top: 50px; line-height: 1.6; font-size: 0.95rem; max-height: 400px; overflow-y: auto; color: #ddd; }
268
+ .detail-row1-col2-r { display: flex; flex-direction: row; gap: 20px; align-items: flex-start; }
269
+
270
+
271
+ .detail-content::-webkit-scrollbar { width: 6px; }
272
+ .detail-content::-webkit-scrollbar-track { background: transparent; }
273
+ .detail-content::-webkit-scrollbar-thumb { background: rgba(237, 72, 1, 0.5); border-radius: 10px; }
274
+ .detail-content::-webkit-scrollbar-thumb:hover { background: rgba(237, 72, 1, 1); }
275
+
276
+
277
+ /* Buttons */
278
+ .action-buttons { display: flex; gap: 10px; }
279
+ .action-btn { border: none; padding: 5px 12px; border-radius: 4px; color: white; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 5px; }
280
+ .verify-btn { background: #00C851; } .verify-btn:hover { background: #00e25b; }
281
+ .resolve-btn { background: #33b5e5; } .resolve-btn:hover { background: #4ec2ec; }
282
+ .detail-redirect { border: 1px solid #ed4801; color: #ed4801; padding: 5px 12px; border-radius: 4px; text-decoration: none; font-size: 0.8rem; display: flex; align-items: center; justify-content: center; }
283
+ .detail-redirect:hover { background: #ed4801; color: white; }
284
+ /* .status-badge { margin-top: 5px; font-size: 0.8rem; padding: 2px 8px; border-radius: 4px; background: #555; display: inline-block; } */
285
+ .confirm-btn { background: #ed4801; } .resolve-btn:hover { background: #4ec2ec; }
286
+
287
+ /* --- STATUS BADGE COLOR CLASSES --- */
288
+ /* These classes are applied via JavaScript to synchronize with button clicks */
289
+
290
+ .status-badge {
291
+ margin-top: 5px;
292
+ font-size: 0.8rem;
293
+ padding: 2px 8px;
294
+ border-radius: 4px;
295
+ background: #555;
296
+ display: inline-block;
297
+ font-weight: bold; /* Added for clarity */
298
+ text-transform: uppercase;
299
+ transition: background-color 0.2s; /* Added transition */
300
+ color: white; /* Base text color */
301
+ }
302
+
303
+ /* Status: NEW (Default State/Unverified) */
304
+ .status-new {
305
+ background-color: #555; /* Amber/Yellow ffc107 333 */
306
+ color: #ffffff;
307
+ }
308
+
309
+ /* Status: VERIFIED */
310
+ .status-verified {
311
+ background-color: #ffc107; /* Green */
312
+ color: #ffffff;
313
+ }
314
+
315
+ /* Status: RESOLVED */
316
+ .status-resolved {
317
+ background-color: #28a745; /* Gray 28a745*/
318
+ color: #ffffff;
319
+ }
320
+
321
+ /* Base Style for all action buttons (like 'Post' or 'Verify') */
322
+ .action-btn1 {
323
+ /* Basic button styling (adjust padding, font, etc., as needed) */
324
+ padding: 6px 12px;
325
+ border-radius: 4px;
326
+ font-weight: 500;
327
+ cursor: pointer;
328
+ transition: background-color 0.2s, color 0.2s, border-color 0.2s;
329
+
330
+ /* Ensure icon and text are aligned */
331
+ display: inline-flex;
332
+ align-items: center;
333
+ gap: 5px; /* Spacing between icon and text */
334
+ }
335
+
336
+ /* 1. Default (Unverified) State: Green Outline only */
337
+ #verify-btn {
338
+ background-color: transparent;
339
+ color: #ffc107; /* Green text color */
340
+ border: 1px solid #ffc107; /* Green border (outline) */
341
+ }
342
+
343
+ /* 2. Verified State: Entirely Green */
344
+ #verify-btn.is-verified {
345
+ background-color: #ffc107; /* Solid Green background */
346
+ color: #ffffff; /* White text/icon */
347
+ border-color: #ffc107; /* Maintain border color */
348
+ }
349
+
350
+ /* 3. Hover States */
351
+ #verify-btn:hover:not(.is-verified) {
352
+ background-color: rgba(40, 167, 69, 0.1); /* Light green background on hover */
353
+ }
354
+
355
+ #verify-btn.is-verified:hover {
356
+ /* Make the button slightly darker green on hover when it's already verified */
357
+ background-color: #c77f0c;
358
+ }
359
+
360
+ /* --- RESOLVE BUTTON --- */
361
+ /* 1. Default (Unresolved) State: Blue Outline only */
362
+ #resolve-btn {
363
+ background-color: transparent;
364
+ color: #28a745; /* Blue text color */
365
+ border: 1px solid #28a745; /* Blue border (outline) */
366
+ }
367
+
368
+ /* 2. Resolved State: Entirely Blue */
369
+ #resolve-btn.is-resolved {
370
+ background-color: #28a745; /* Solid Blue background */
371
+ color: #ffffff; /* White text/icon */
372
+ border-color: #28a745;
373
+ }
374
+
375
+ /* 3. Hover States (When Unresolved) */
376
+ #resolve-btn:hover:not(.is-resolved) {
377
+ background-color: rgba(0, 123, 255, 0.1); /* Light blue background on hover */
378
+ }
379
+
380
+ /* 4. Hover States (When Already Resolved) */
381
+ #resolve-btn.is-resolved:hover {
382
+ /* Make the button slightly darker blue on hover when it's already resolved */
383
+ background-color: #157349;
384
+ }
385
+
386
+
387
+ /* --- MODAL --- */
388
+ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(5px); z-index: 2000; display: flex; align-items: center; justify-content: center; opacity: 1; transition: opacity 0.3s ease; }
389
+ .modal-overlay.hidden { opacity: 0; pointer-events: none; display: none !important; }
390
+ .modal-content { background: #1a1a1a; border: 1px solid #444; padding: 30px; border-radius: 12px; width: 90%; max-width: 900px; }
391
+ .modal-header { display: flex; justify-content: space-between; margin-bottom: 20px; }
392
+ .charts-container { display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; margin-top: 20px; }
393
+ .chart-wrapper { background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 20px; flex: 1; min-width: 300px; max-height: 400px; display: flex; flex-direction: column; align-items: center; }
394
+ canvas { max-width: 100%; max-height: 300px; }
395
+ .icon-btn-ghost { background: transparent; border: none; color: #888; font-size: 1.5rem; cursor: pointer; }
396
+ .hidden { display: none !important; }
397
+
398
+ /* --- RESPONSIVE ADJUSTMENT --- */
399
+ @media (max-width: 850px) {
400
+ /* Unlock the body scrolling for small screens */
401
+ body {
402
+ overflow: auto;
403
+ height: auto;
404
+ }
405
+
406
+ .hero {
407
+ /* Stack vertically instead of side-by-side */
408
+ flex-direction: column;
409
+ height: auto;
410
+ min-height: 100vh;
411
+ }
412
+
413
+ .sidebar-wrapper {
414
+ /* Sidebar becomes full width banner at the top */
415
+ width: 100%;
416
+ flex: none; /* Turn off the fixed 350px logic */
417
+ height: 300px; /* Fixed height for list */
418
+ }
419
+
420
+ .detail-box {
421
+ /* Box sits below sidebar */
422
+ width: 95%; /* Almost full width */
423
+ margin: 10px auto; /* Center it */
424
+ height: 500px; /* Fixed height for content */
425
+ flex: none;
426
+
427
+ /* Reset transform for mobile so it doesn't fly in weirdly */
428
+ transform: none;
429
+ }
430
+
431
+ /* Ensure active state just shows opacity on mobile */
432
+ .detail-box.active {
433
+ transform: none;
434
+ }
435
+ }
436
+
437
+ /* --- LOGIN MODAL STYLES --- */
438
+ .small-modal { max-width: 400px; }
439
+
440
+ .login-form {
441
+ display: flex; flex-direction: column; gap: 15px;
442
+ }
443
+
444
+ .login-input {
445
+ background: rgba(255,255,255,0.1);
446
+ border: 1px solid #444;
447
+ color: white;
448
+ padding: 12px;
449
+ border-radius: 6px;
450
+ font-size: 1rem;
451
+ outline: none;
452
+ }
453
+ .login-input:focus { border-color: #ed4801; }
454
+
455
+ .full-width {
456
+ width: 100%;
457
+ justify-content: center;
458
+ padding: 12px;
459
+ font-size: 1rem;
460
+ }
461
+
462
+ /* --- SIDEBAR ASSISTANCE BADGE --- */
463
+ .assist-badge {
464
+ display: inline-block;
465
+ font-size: 0.7rem;
466
+ font-weight: 700;
467
+ text-transform: uppercase;
468
+ padding: 2px 6px;
469
+ border-radius: 4px;
470
+ margin-right: 6px;
471
+ background: rgba(255, 255, 255, 0.1);
472
+ color: #ccc;
473
+ border: 1px solid #555;
474
+ }
475
+
476
+ /* Specific Colors */
477
+ .assist-medical { color: #ff4444; border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
478
+ .assist-rescue { color: #ffbb33; border-color: #ffbb33; background: rgba(255, 187, 51, 0.1); }
479
+ .assist-food { color: #00C851; border-color: #00C851; background: rgba(0, 200, 81, 0.1); }
480
+ .assist-evac { color: #33b5e5; border-color: #33b5e5; background: rgba(51, 181, 229, 0.1); }
481
+
482
+
483
+ /* --- CUSTOM TOOLTIP STYLES --- */
484
+
485
+ /* 1. Base style for any element that has a tooltip */
486
+ [data_tooltip] {
487
+ position: relative;
488
+ cursor: pointer;
489
+ }
490
+
491
+ /* 2. Create the tooltip content (::after) ALWAYS, but invisible */
492
+ [data_tooltip]::after {
493
+ content: attr(data_tooltip);
494
+ position: absolute;
495
+ top: calc(100% + 15px);
496
+
497
+ /* DEFAULT POSITIONING: Centered over parent */
498
+ left: 50%;
499
+ right: auto;
500
+ transform: translateX(-50%);
501
+ margin: 0;
502
+
503
+ /* Appearance & Visibility */
504
+ background-color: #ed4801;
505
+ color: #fff;
506
+ white-space: nowrap;
507
+ padding: 6px 10px;
508
+ border-radius: 4px;
509
+ font-size: 0.75rem;
510
+ font-weight: 500;
511
+ opacity: 0;
512
+ pointer-events: none;
513
+ transition: opacity 0.2s;
514
+ transition-delay: 0s; /* Instant disappearance */
515
+ z-index: 9999;
516
+ }
517
+
518
+ /* 4a. LEFT EDGE OVERRIDE */
519
+ [data_tooltip_align="left"]::after {
520
+ left: 0;
521
+ right: auto;
522
+ transform: none;
523
+ margin-left: 10px;
524
+ }
525
+
526
+ /* 4b. RIGHT EDGE OVERRIDE */
527
+ [data_tooltip_align="right"]::after {
528
+ right: 0;
529
+ left: auto;
530
+ transform: none;
531
+ margin-left: 0;
532
+ margin-right: 10px;
533
+ }
534
+
535
+ /* 5. ON HOVER — animate in (Fade only) */
536
+ [data_tooltip]:hover::after {
537
+ opacity: 1;
538
+ transition-delay: 0.5s !important;
539
+ }
540
+
541
+ /* Add this to your existing CSS */
542
+ [data_tooltip].tooltip-hidden:hover::after {
543
+ opacity: 0 !important;
544
+ transition-delay: 0s !important;
545
+ }
546
+
547
+ /* 5. Tooltip visibility on hover */
548
+ /* [data_tooltip]:hover::after,
549
+ [data_tooltip]:hover::before {
550
+ opacity: 1;
551
+ } */
552
+
553
+ /* Arrow — ALSO always created */
554
+ /* [data_tooltip]::before {
555
+ content: '';
556
+ position: absolute;
557
+ top: 100%;
558
+ left: 50%;
559
+
560
+ transform: translateX(-50%);
561
+
562
+ border-left: 6px solid transparent;
563
+ border-right: 6px solid transparent;
564
+ border-bottom: 6px solid #ed4801;
565
+ opacity: 0;
566
+
567
+ transition: opacity 0.2s;
568
+ transition-delay: 0s;
569
+ z-index: 9999;
570
+ } */
571
+
572
+ /* Floating Sidebar for Map Page */
573
+ .map-sidebar {
574
+ position: absolute;
575
+ right: 20px;
576
+ top: 20px;
577
+ width: 300px;
578
+ background: rgba(0, 0, 0, 0.85);
579
+ padding: 20px;
580
+ border-radius: 12px;
581
+ border: 1px solid #ed4801;
582
+ z-index: 1000; /* Sit on top of map */
583
+ color: white;
584
+ backdrop-filter: blur(5px);
585
+ box-shadow: 0 4px 15px rgba(0,0,0,0.5);
586
+ }
587
+
588
+ .back-btn {
589
+ display: inline-block;
590
+ color: #ccc;
591
+ text-decoration: none;
592
+ margin-bottom: 15px;
593
+ font-size: 0.9em;
594
+ transition: color 0.2s;
595
+ }
596
+ .back-btn:hover { color: #ed4801; }
597
+
598
+ .stat-item {
599
+ margin-top: 10px;
600
+ font-size: 0.85rem;
601
+ color: #aaa;
602
+ }
603
+ .stat-highlight {
604
+ color: white;
605
+ font-weight: bold;
606
+ font-size: 1.1rem;
607
+ }
608
+
609
+ .profile-container {
610
+ position: relative;
611
+ margin-left: 10px;
612
+ height: 35px;
613
+ }
614
+ .profile-icon {
615
+ width: 35px;
616
+ height: 35px;
617
+ background: #ed4801; /* Orange profile background */
618
+ border-radius: 50%;
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: center;
622
+ color: white;
623
+ font-size: 1.1em;
624
+ cursor: pointer;
625
+ transition: box-shadow 0.2s;
626
+ }
627
+ .profile-icon:hover {
628
+ box-shadow: 0 0 0 4px rgba(237, 72, 1, 0.3);
629
+ }
630
+
631
+ /* Dropdown Menu Box */
632
+ .profile-dropdown {
633
+ position: absolute;
634
+ top: 45px;
635
+ right: 0;
636
+ width: 250px;
637
+ background: #1a1a1a;
638
+ border: 1px solid #333;
639
+ border-radius: 8px;
640
+ box-shadow: 0 8px 16px rgba(0,0,0,0.5);
641
+ z-index: 1000;
642
+ padding: 15px;
643
+ transform: translateY(10px);
644
+ transition: opacity 0.2s, transform 0.2s;
645
+ }
646
+ .profile-dropdown.hidden {
647
+ display: none;
648
+ }
649
+
650
+ /* Dropdown Header Info */
651
+ .profile-info-header {
652
+ display: flex;
653
+ flex-direction: column;
654
+ align-items: center;
655
+ padding-bottom: 10px;
656
+ }
657
+ .profile-icon-large {
658
+ width: 50px;
659
+ height: 50px;
660
+ background: #ff7e42;
661
+ border-radius: 50%;
662
+ display: flex;
663
+ align-items: center;
664
+ justify-content: center;
665
+ color: white;
666
+ font-size: 1.8em;
667
+ margin-bottom: 8px;
668
+ }
669
+ .dropdown-username {
670
+ font-weight: bold;
671
+ color: white;
672
+ font-size: 1.1em;
673
+ }
674
+
675
+ /* Logout Button */
676
+ .dropdown-separator {
677
+ border: none;
678
+ border-top: 1px solid #333;
679
+ margin: 10px 0;
680
+ }
681
+ .dropdown-logout-btn {
682
+ width: 100%;
683
+ background: #ed4801;
684
+ color: white;
685
+ border: none;
686
+ padding: 8px;
687
+ border-radius: 4px;
688
+ font-weight: 500;
689
+ cursor: pointer;
690
+ display: flex;
691
+ align-items: center;
692
+ justify-content: center;
693
+ gap: 8px;
694
+ }
695
+
696
+ @media (max-width: 1200px) {
697
+
698
+ /* Target the container holding the Verify/Resolve/Post buttons */
699
+ .detail-header-r .action-buttons {
700
+ /* Change the direction to vertical stacking */
701
+ flex-direction: column;
702
+
703
+ /* Ensure buttons stretch to full width of the container */
704
+ align-items: stretch;
705
+
706
+ /* Add gap for visual separation */
707
+ gap: 8px;
708
+ }
709
+
710
+ /* Ensure the detail redirect link (Post button) also behaves like a block button */
711
+ .detail-redirect {
712
+ justify-content: center; /* Center the text/icon in the full-width button */
713
+ }
714
+
715
+ /* Ensure the action buttons themselves stretch */
716
+ .action-btn1 {
717
+ width: 100%; /* Ensure the buttons take full width */
718
+ }
719
+ }
720
+
721
+ /* style.css (Add these new selectors) */
722
+
723
+ .alert-popup {
724
+ /* Position the box: Fixed allows it to float with scrolling */
725
+ position: fixed;
726
+ top: 70px; /* Just below the 60px high header + 10px margin */
727
+ right: 20px;
728
+
729
+ /* Appearance */
730
+ background: #ed4801; /* ALISTO Primary Color */
731
+ color: white;
732
+ padding: 15px 25px;
733
+ border-radius: 10px; /* Rounded corners */
734
+
735
+ /* Layout */
736
+ display: flex;
737
+ align-items: center;
738
+ gap: 12px;
739
+
740
+ /* Shadow and Stacking */
741
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
742
+ z-index: 1000;
743
+
744
+ /* Animation Control */
745
+ opacity: 0;
746
+ transform: translateX(100%); /* Start off-screen to the right */
747
+ transition: opacity 0.4s ease-out, transform 0.4s ease-out;
748
+ }
749
+
750
+ .alert-popup.visible {
751
+ opacity: 1;
752
+ transform: translateX(0); /* Slide into view */
753
+ }
754
+
755
+ /* Ensure the initial hidden state prevents any display issues */
756
+ .hidden {
757
+ display: none !important;
758
+ }
759
+ /* NOTE: You may need to remove 'display: none !important' from your existing
760
+ .hidden CSS class in style.css if you want to rely only on the opacity/transform
761
+ for the initial state, but for robust functionality, we often keep 'display: none'
762
+ for the .hidden state and only remove it for the sliding animation to work.
763
+ We'll use a .visible class to trigger the animation. */
alisto_project/backend/templates.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # templates.py
2
+
3
+ LOCATIONS = [
4
+ "Marikina", "Quezon City", "Manila", "Pasig", "Cainta", "Antipolo",
5
+ "Batangas", "Tagaytay", "Davao", "Cebu", "Bicol", "Albay",
6
+ "Pampanga", "Bulacan", "Laguna", "Cavite", "Tacloban", "Iligan",
7
+ "Navotas", "Malabon", "San Mateo", "Rizal", "Valenzuela", "Muntinlupa",
8
+ "Las Pinas", "Makati", "Mandaluyong", "San Juan", "Catanduanes",
9
+ "Isabela", "Cagayan", "Iloilo", "Samar", "Leyte", "Mindoro",
10
+ "General Trias", "Dasmarinas", "Bacoor", "Imus", "Tondo"
11
+ ]
12
+
13
+ DISASTERS = [
14
+ "baha", "flood", "flooding", "flash flood", "taas ng tubig",
15
+ "lindol", "earthquake", "quake", "aftershock", "yanig", "uga",
16
+ "bagyo", "typhoon", "storm", "winds", "malakas na hangin",
17
+ "sunog", "fire", "nasusunog", "apoy", "fire alert",
18
+ "landslide", "guho", "mudslide", "falling rocks",
19
+ "volcano", "ashfall", "eruption", "asupre", "smog"
20
+ ]
21
+
22
+ NEEDS = [
23
+ "rescue", "tulong", "help", "saklolo", "assist", "save us",
24
+ "food", "pagkain", "water", "tubig", "relief goods", "makakain",
25
+ "medical", "gamot", "medic", "doctor", "ambulance", "oxygen", "first aid",
26
+ "shelter", "evacuation", "matutuluyan", "tents", "bubong", "trapal",
27
+ "boat", "bangka", "firetruck", "bumbero", "rescuers"
28
+ ]
29
+
30
+ # POSITIVE FRAGMENTS
31
+ POS_INTROS = ["EMERGENCY!", "HELP!", "S.O.S.", "URGENT:", "Please help,", "Tulong po,", "Saklolo,", "Rescue needed!"]
32
+ POS_SITUATIONS = ["trapped kami sa loob", "stuck sa bubong", "water is rising fast", "nasusunog ang bahay", "injured ang kasama ko", "stranded kami", "collapsed ang pader", "lubog na ang first floor"]
33
+ POS_REQUESTS = ["we need {need} immediately", "send {need} ASAP", "paki-dala ng {need}", "kailangan namin ng {need}", "need {need} now!"]
34
+
35
+ # NEGATIVE FRAGMENTS
36
+ NEG_NEWS_INTROS = ["BREAKING:", "Just in:", "News Update:", "Report:", "FYI:", "Advisory:", "Headlines:", "According to news,"]
37
+ NEG_NEWS_BODIES = ["{disaster} hits {loc}", "death toll in {loc} rises", "classes suspended in {loc}", "state of calamity declared in {loc}"]
38
+
39
+ NEG_ACT_INTROS = ["I want to donate", "Looking for volunteers", "Donation drive for", "Salute to", "Praying for", "Condolence to"]
40
+ NEG_ACT_BODIES = ["victims of {disaster} in {loc}", "our heroes in {loc}", "the brave rescuers in {loc}", "{disaster} survivors in {loc}"]
41
+
42
+ NEG_DISCUSS_INTROS = ["Why is the mayor", "The corruption in", "Remembering the", "Throwback to", "Discussion:", "Opinion:", "Is it safe in"]
43
+ NEG_DISCUSS_BODIES = ["response to {disaster} in {loc} is slow", "1990 {disaster} in {loc}", "{loc} politicians are useless", "road to {loc} is passable"]
alisto_project/backend/train_ensemble.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import pickle
3
+ import os
4
+ from sklearn.feature_extraction.text import TfidfVectorizer
5
+ from sklearn.svm import LinearSVC
6
+ from sklearn.calibration import CalibratedClassifierCV
7
+ from sklearn.pipeline import make_pipeline
8
+ from sklearn.model_selection import train_test_split
9
+ from sklearn.metrics import accuracy_score, classification_report
10
+
11
+ # Paths
12
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ DATA_PATH = os.path.join(BASE_DIR, '../data/reddit_disaster_posts.csv')
14
+ MODEL_DIR = os.path.join(BASE_DIR, 'models')
15
+
16
+ # --- 1. CUSTOM TAGLISH STOP WORDS ---
17
+ # removing these prevents the model from cheating by memorizing common grammar words
18
+ TAGLISH_STOP_WORDS = [
19
+ 'ang', 'mga', 'ng', 'sa', 'na', 'ko', 'mo', 'ba', 'ka', 'yung',
20
+ 'ni', 'no', 'at', 'o', 'kay', 'to', 'po', 'pa', 'din', 'rin',
21
+ 'naman', 'nyo', 'nila', 'namin', 'kasi', 'kame', 'kami', 'tayo',
22
+ 'sana', 'lang', 'talaga', 'di', 'eh', 'oh', 'ah', 'yun', 'yan',
23
+ 'the', 'is', 'a', 'an', 'and', 'or', 'of', 'to', 'in', 'on', 'for'
24
+ ]
25
+
26
+ def train_tfidf():
27
+ print(f"Loading data from: {DATA_PATH}")
28
+ if not os.path.exists(DATA_PATH):
29
+ print("Error: CSV not found.")
30
+ return
31
+
32
+ df = pd.read_csv(DATA_PATH)
33
+ df['text'] = df['text'].astype(str).fillna('')
34
+ df = df.dropna(subset=['label'])
35
+
36
+ X = df['text']
37
+ y = df['label'].astype(int)
38
+
39
+ print(f"Training TF-IDF + SVM Ensemble on {len(df)} posts...")
40
+
41
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
42
+
43
+ # --- 2. THE UPGRADED PIPELINE ---
44
+ model = make_pipeline(
45
+ TfidfVectorizer(
46
+ stop_words=TAGLISH_STOP_WORDS,
47
+ max_features=5000,
48
+ ngram_range=(1, 3),
49
+ sublinear_tf=True
50
+ ),
51
+ CalibratedClassifierCV(LinearSVC(dual=False, class_weight='balanced'), method='sigmoid')
52
+ )
53
+
54
+ model.fit(X_train, y_train)
55
+
56
+ # Evaluate
57
+ y_pred = model.predict(X_test)
58
+ acc = accuracy_score(y_test, y_pred)
59
+ print(f"✅ TF-IDF Model Accuracy: {acc * 100:.2f}%")
60
+ print("Classification Report:")
61
+ print(classification_report(y_test, y_pred))
62
+
63
+ # Save
64
+ save_path = os.path.join(MODEL_DIR, 'tfidf_ensemble.pkl')
65
+ with open(save_path, 'wb') as f:
66
+ pickle.dump(model, f)
67
+
68
+ print(f"Model saved to: {save_path}")
69
+
70
+ if __name__ == "__main__":
71
+ train_tfidf()
alisto_project/backend/train_model.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import torch
3
+ import numpy as np
4
+ import os
5
+ from sklearn.model_selection import train_test_split
6
+ from sklearn.metrics import accuracy_score, precision_recall_fscore_support
7
+ from transformers import (
8
+ AutoTokenizer,
9
+ AutoModelForSequenceClassification,
10
+ Trainer,
11
+ TrainingArguments,
12
+ EarlyStoppingCallback
13
+ )
14
+ from torch import nn
15
+
16
+ # 1. Config
17
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ DATA_PATH = os.path.join(BASE_DIR, '../data/reddit_disaster_posts.csv')
19
+ MODEL_OUTPUT_DIR = os.path.join(BASE_DIR, 'models/roberta_model')
20
+
21
+ # --- THE UPGRADE: Multilingual Brain (English + Tagalog) ---
22
+ MODEL_NAME = 'xlm-roberta-base'
23
+
24
+ print(f"--- ALISTO: Training Multilingual Brain ({MODEL_NAME}) ---")
25
+
26
+ # 2. Load Data
27
+ if not os.path.exists(DATA_PATH):
28
+ print("❌ Error: CSV file not found. Run augment_data.py first!")
29
+ exit()
30
+
31
+ df = pd.read_csv(DATA_PATH)
32
+ df = df.dropna(subset=['text', 'label'])
33
+ texts = df['text'].tolist()
34
+ labels = df['label'].tolist()
35
+
36
+ print(f"Loaded {len(df)} samples.")
37
+
38
+ # 3. Split (80% Train, 20% Validation)
39
+ train_texts, val_texts, train_labels, val_labels = train_test_split(
40
+ texts, labels, test_size=0.2, random_state=42, stratify=labels
41
+ )
42
+
43
+ # 4. Tokenize
44
+ print(f"Downloading tokenizer for {MODEL_NAME}...")
45
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
46
+
47
+ def tokenize_function(texts):
48
+ return tokenizer(texts, padding=True, truncation=True, max_length=128)
49
+
50
+ train_encodings = tokenize_function(train_texts)
51
+ val_encodings = tokenize_function(val_texts)
52
+
53
+ # 5. Dataset Class
54
+ class DisasterDataset(torch.utils.data.Dataset):
55
+ def __init__(self, encodings, labels):
56
+ self.encodings = encodings
57
+ self.labels = labels
58
+
59
+ def __getitem__(self, idx):
60
+ item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
61
+ item['labels'] = torch.tensor(self.labels[idx])
62
+ return item
63
+
64
+ def __len__(self):
65
+ return len(self.labels)
66
+
67
+ train_dataset = DisasterDataset(train_encodings, train_labels)
68
+ val_dataset = DisasterDataset(val_encodings, val_labels)
69
+
70
+ # --- CUSTOM TRAINER WITH WEIGHTED LOSS ---
71
+ # Punishes the model 3x more if it misses a Rescue Request (False Negative)
72
+ class WeightedTrainer(Trainer):
73
+ def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None, **kwargs):
74
+ labels = inputs.get("labels")
75
+ outputs = model(**inputs)
76
+ logits = outputs.get("logits")
77
+
78
+ # [1.0, 3.0] -> Label 1 is 3x more important than Label 0
79
+ loss_fct = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0]).to(model.device))
80
+ loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1))
81
+
82
+ return (loss, outputs) if return_outputs else loss
83
+
84
+ # Metrics
85
+ def compute_metrics(pred):
86
+ labels = pred.label_ids
87
+ preds = pred.predictions.argmax(-1)
88
+ precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='binary')
89
+ acc = accuracy_score(labels, preds)
90
+ return {
91
+ 'accuracy': acc,
92
+ 'f1': f1,
93
+ 'precision': precision,
94
+ 'recall': recall
95
+ }
96
+
97
+ # 6. Model Initialization
98
+ print(f"Downloading base model {MODEL_NAME}...")
99
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2)
100
+
101
+ # 7. Training Args
102
+ training_args = TrainingArguments(
103
+ output_dir='./results',
104
+ num_train_epochs=15,
105
+ per_device_train_batch_size=8,
106
+ per_device_eval_batch_size=8,
107
+ warmup_steps=500,
108
+ weight_decay=0.01,
109
+ learning_rate=2e-5,
110
+ logging_dir='./logs',
111
+ logging_steps=50,
112
+ eval_strategy="epoch",
113
+ save_strategy="epoch",
114
+ load_best_model_at_end=True,
115
+ metric_for_best_model="f1",
116
+ seed=42
117
+ )
118
+
119
+ # 8. Train
120
+ trainer = WeightedTrainer(
121
+ model=model,
122
+ args=training_args,
123
+ train_dataset=train_dataset,
124
+ eval_dataset=val_dataset,
125
+ compute_metrics=compute_metrics,
126
+ callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
127
+ )
128
+
129
+ print("Starting training (XLM-R + Weighted Loss)...")
130
+ trainer.train()
131
+
132
+ # 9. Save
133
+ print(f"Saving upgraded model to {MODEL_OUTPUT_DIR}...")
134
+ model.save_pretrained(MODEL_OUTPUT_DIR)
135
+ tokenizer.save_pretrained(MODEL_OUTPUT_DIR)
136
+ print("✅ Multilingual Brain Training Complete.")
alisto_project/data/reddit_disaster_posts.csv ADDED
The diff for this file is too large to render. See raw diff
 
alisto_project/data/seed_data.txt ADDED
The diff for this file is too large to render. See raw diff
 
alisto_project/requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-sqlalchemy
3
+ python-dotenv
4
+ pandas
5
+ numpy
6
+ scikit-learn
7
+ asyncpraw
8
+ torch
9
+ transformers
10
+ sentencepiece
11
+ protobuf
12
+ accelerate
13
+ flask_cors
14
+ flask-login
15
+ werkzeuggit rm -r --cached .
16
+ git add .
17
+ gunicorn
alisto_project/start.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 1. Start the AI Scraper in the background (&)
4
+ # It will silently find alerts and save them to alisto.db
5
+ python ingest_reddit.py &
6
+
7
+ # 2. Start the Website in the foreground
8
+ # This keeps the server running
9
+ gunicorn -b 0.0.0.0:7860 app:app
instance/alisto.db ADDED
Binary file (12.3 kB). View file