adnaan05 KhaqanNasir commited on
Commit
e7ce344
·
verified ·
1 Parent(s): 934bf5b

Update app.py (#20)

Browse files

- Update app.py (c65efe8e34efe9e865e0a322c6d61d397ad37000)


Co-authored-by: Muhammad Khaqan Nasir <[email protected]>

Files changed (1) hide show
  1. app.py +430 -158
app.py CHANGED
@@ -1,192 +1,464 @@
1
- import sys
2
- from pathlib import Path
3
- import os
4
- import gdown
5
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- # Set page config - must be first Streamlit command
8
- st.set_page_config(
9
- page_title="TruthCheck - Advanced Fake News Detector",
10
- page_icon="🛡️",
11
- layout="wide",
12
- initial_sidebar_state="expanded"
13
- )
14
 
15
- # Custom CSS for modern styling
 
 
 
 
16
  st.markdown("""
17
  <style>
18
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
19
-
20
- body {
21
- font-family: 'Poppins', sans-serif;
22
- background-color: #FFFFFF;
23
- color: #333333;
24
- }
25
-
26
- .sidebar .sidebar-content {
27
- background-color: #F4F7FA;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  padding: 1rem;
29
  }
30
-
31
- .stButton>button {
32
- background-color: #4B5EAA;
33
- color: white;
34
- border-radius: 8px;
35
- padding: 0.5rem 1rem;
36
- font-weight: 600;
37
- transition: background-color 0.3s;
38
  }
39
-
40
- .stButton>button:hover {
41
- background-color: #3A4A8C;
 
 
 
 
 
42
  }
43
-
44
- .stTextArea textarea {
45
- border: 1px solid #E0E0E0;
46
- border-radius: 8px;
 
 
 
 
 
 
 
47
  padding: 1rem;
48
  }
49
-
50
- .hero-section {
51
- background-color: #F4F7FA;
52
- padding: 2rem;
53
- border-radius: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  margin-bottom: 2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
-
57
- .flash-message {
58
  padding: 1rem;
59
  border-radius: 8px;
 
60
  margin-bottom: 1rem;
61
- font-weight: 600;
62
  }
63
-
64
- .success-message {
65
- background-color: #E6F4EA;
66
- color: #2E7D32;
67
  }
68
-
69
- .error-message {
70
- background-color: #FEE2E2;
71
- color: #B71C1C;
72
  }
73
-
74
- /* Enhanced sidebar styling */
75
- .stRadio > div {
 
 
 
 
76
  gap: 0.5rem;
77
  }
78
-
79
- .stRadio > div > label {
80
- padding: 0.5rem 1rem;
81
- border-radius: 8px;
82
- transition: all 0.3s ease;
83
- cursor: pointer;
84
- font-weight: 500;
85
  }
86
-
87
- .stRadio > div > label:hover {
88
- background-color: #E8F0FE;
89
- transform: translateX(5px);
 
 
90
  }
91
-
92
- .stRadio > div > label[data-checked="true"] {
93
- background-color: #4B5EAA;
94
- color: white;
 
 
 
95
  }
96
  </style>
97
  """, unsafe_allow_html=True)
98
 
99
- MODEL_PATH = "models/saved/final_model.pt"
100
- GOOGLE_DRIVE_FILE_ID = "1xhYKuC5_Yri3mm3Ejt-SpB2WAVUTJvc_"
101
- GOOGLE_DRIVE_URL = f"https://drive.google.com/uc?id={GOOGLE_DRIVE_FILE_ID}"
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  @st.cache_resource
104
- def download_model():
105
- """Download model from Google Drive if not exists."""
106
- if not os.path.exists(MODEL_PATH):
107
- os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
108
- with st.spinner("Downloading model from Google Drive..."):
109
- try:
110
- gdown.download(GOOGLE_DRIVE_URL, MODEL_PATH, quiet=False)
111
- st.markdown('<div class="flash-message success-message">Model downloaded successfully!</div>', unsafe_allow_html=True)
112
- except Exception as e:
113
- st.markdown(f'<div class="flash-message error-message">Failed to download model: {str(e)}</div>', unsafe_allow_html=True)
114
- st.markdown('<div class="flash-message error-message">Please check your Google Drive link and make sure the file is publicly accessible.</div>', unsafe_allow_html=True)
115
- return False
116
- return True
117
-
118
- # Add src directory to Python path
119
- src_path = Path(__file__).parent / "src"
120
- sys.path.append(str(src_path))
121
-
122
- # Enhanced Sidebar navigation with icons
123
- st.sidebar.markdown("""
124
- <div style="text-align: center; margin-bottom: 2rem;">
125
- <div style="font-size: 2.5rem; margin-bottom: 0.5rem;">🛡️</div>
126
- <h1 style="color: #4B5EAA; font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; line-height: 1.2;">
127
- TruthCheck
128
- </h1>
129
- <p style="color: #666; font-size: 0.9rem; margin: 0; font-weight: 300; line-height: 1.3;">
130
- Advanced Fake News Detector
131
- </p>
132
- </div>
133
- """, unsafe_allow_html=True)
134
 
135
- st.sidebar.markdown("---")
136
-
137
- # Navigation with icons
138
- page = st.sidebar.radio(
139
- "Navigate",
140
- [
141
- "🏠 Home",
142
- "ℹ️ About",
143
- "📋 Terms of Use",
144
- "🔒 Privacy Policy",
145
- "👥 Team"
146
- ],
147
- label_visibility="collapsed",
148
- key="navigation"
149
- )
150
-
151
- # Add some spacing and additional info
152
- st.sidebar.markdown("---")
153
- st.sidebar.markdown("""
154
- <div style="text-align: center; padding: 1.5rem; background: linear-gradient(135deg, #F8F9FA 0%, #E8F0FE 100%); border-radius: 12px; margin-top: 1rem; border: 1px solid #E0E7FF;">
155
- <div style="font-size: 2rem; margin-bottom: 0.8rem;">🔍</div>
156
- <p style="font-size: 0.85rem; color: #4B5EAA; margin: 0; font-weight: 500; line-height: 1.4;">
157
- Protecting you from misinformation with AI-powered detection
158
- </p>
159
- </div>
160
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
161
 
162
- # Add footer info
163
- st.sidebar.markdown("""
164
- <div style="position: fixed; bottom: 1rem; left: 1rem; right: 1rem; text-align: center; padding: 0.5rem; background-color: rgba(255,255,255,0.9); border-radius: 8px; border-top: 1px solid #E0E0E0;">
165
- <p style="font-size: 0.7rem; color: #888; margin: 0;">
166
- © 2025 TruthCheck | AI-Powered
167
- </p>
168
- </div>
169
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- # Clean up the page variable to remove icons for the conditional logic
172
- page_clean = page.split(" ", 1)[1] if " " in page else page
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- if __name__ == "__main__":
175
- if page_clean == "Home":
176
- if download_model():
177
- from src.app import main
178
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  else:
180
- st.markdown('<div class="flash-message error-message">Cannot start the application without the model file.</div>', unsafe_allow_html=True)
181
- elif page_clean == "About":
182
- from src.about import main
183
- main()
184
- elif page_clean == "Terms of Use":
185
- from src.terms_of_use import main
186
- main()
187
- elif page_clean == "Privacy Policy":
188
- from src.privacy_policy import main
189
- main()
190
- elif page_clean == "Team":
191
- from src.team import main
192
- main()
 
 
 
 
 
1
  import streamlit as st
2
+ import torch
3
+ import pandas as pd
4
+ import numpy as np
5
+ from pathlib import Path
6
+ import sys
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+ from transformers import BertTokenizer
10
+ import nltk
11
+
12
+ # Download required NLTK data
13
+ try:
14
+ nltk.data.find('tokenizers/punkt')
15
+ except LookupError:
16
+ nltk.download('punkt')
17
+ try:
18
+ nltk.data.find('corpora/stopwords')
19
+ except LookupError:
20
+ nltk.download('stopwords')
21
+ try:
22
+ nltk.data.find('tokenizers/punkt_tab')
23
+ except LookupError:
24
+ nltk.download('punkt_tab')
25
+ try:
26
+ nltk.data.find('corpora/wordnet')
27
+ except LookupError:
28
+ nltk.download('wordnet')
29
 
30
+ # Add project root to Python path
31
+ project_root = Path(__file__).parent.parent
32
+ sys.path.append(str(project_root))
 
 
 
 
33
 
34
+ from src.models.hybrid_model import HybridFakeNewsDetector
35
+ from src.config.config import *
36
+ from src.data.preprocessor import TextPreprocessor
37
+
38
+ # Custom CSS for streamlined styling
39
  st.markdown("""
40
  <style>
41
+ /* Import Google Fonts */
42
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
43
+
44
+ /* Global Styles */
45
+ * {
46
+ margin: 0;
47
+ padding: 0;
48
+ box-sizing: border-box;
49
+ }
50
+
51
+ .stApp {
52
+ font-family: 'Inter', sans-serif;
53
+ background: #f8fafc;
54
+ min-height: 100vh;
55
+ color: #1a202c;
56
+ }
57
+
58
+ /* Hide Streamlit elements */
59
+ #MainMenu {visibility: hidden;}
60
+ footer {visibility: hidden;}
61
+ .stDeployButton {display: none;}
62
+ header {visibility: hidden;}
63
+ .stApp > header {visibility: hidden;}
64
+
65
+ /* Container */
66
+ .container {
67
+ max-width: 1200px;
68
+ margin: 0 auto;
69
  padding: 1rem;
70
  }
71
+
72
+ /* Header */
73
+ .header {
74
+ padding: 1rem 0;
75
+ text-align: center;
 
 
 
76
  }
77
+
78
+ .header-title {
79
+ font-size: 2rem;
80
+ font-weight: 800;
81
+ color: #1a202c;
82
+ display: inline-flex;
83
+ align-items: center;
84
+ gap: 0.5rem;
85
  }
86
+
87
+ /* Hero Section */
88
+ .hero {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 2rem;
92
+ margin-bottom: 2rem;
93
+ }
94
+
95
+ .hero-left {
96
+ flex: 1;
97
  padding: 1rem;
98
  }
99
+
100
+ .hero-right {
101
+ flex: 1;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ }
106
+
107
+ .hero-right img {
108
+ max-width: 100%;
109
+ height: auto;
110
+ border-radius: 8px;
111
+ }
112
+
113
+ .hero-title {
114
+ font-size: 2.5rem;
115
+ font-weight: 700;
116
+ color: #1a202c;
117
+ margin-bottom: 0.5rem;
118
+ }
119
+
120
+ .hero-text {
121
+ font-size: 1rem;
122
+ color: #4a5568;
123
+ line-height: 1.5;
124
+ max-width: 450px;
125
+ }
126
+
127
+ /* About Section */
128
+ .about-section {
129
  margin-bottom: 2rem;
130
+ text-align: center;
131
+ }
132
+
133
+ .about-title {
134
+ font-size: 1.8rem;
135
+ font-weight: 600;
136
+ color: #1a202c;
137
+ margin-bottom: 0.5rem;
138
+ }
139
+
140
+ .about-text {
141
+ font-size: 1rem;
142
+ color: #4a5568;
143
+ line-height: 1.5;
144
+ max-width: 600px;
145
+ margin: 0 auto;
146
+ }
147
+
148
+ /* Input Section */
149
+ .input-container {
150
+ max-width: 800px;
151
+ margin: 0 auto;
152
+ }
153
+
154
+ .stTextArea > div > div > textarea {
155
+ border-radius: 8px !important;
156
+ border: 1px solid #d1d5db !important;
157
+ padding: 1rem !important;
158
+ font-size: 1rem !important;
159
+ font-family: 'Inter', sans-serif !important;
160
+ background: #ffffff !important;
161
+ min-height: 150px !important;
162
+ transition: all 0.2s ease !important;
163
+ }
164
+
165
+ .stTextArea > div > div > textarea:focus {
166
+ border-color: #6366f1 !important;
167
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1) !important;
168
+ outline: none !important;
169
+ }
170
+
171
+ .stTextArea > div > div > textarea::placeholder {
172
+ color: #9ca3af !important;
173
+ }
174
+
175
+ /* Button Styling */
176
+ .stButton > button {
177
+ background: #6366f1 !important;
178
+ color: white !important;
179
+ border-radius: 8px !important;
180
+ padding: 0.75rem 2rem !important;
181
+ font-size: 1rem !important;
182
+ font-weight: 600 !important;
183
+ font-family: 'Inter', sans-serif !important;
184
+ transition: all 0.2s ease !important;
185
+ border: none !important;
186
+ width: 100% !important;
187
+ }
188
+
189
+ .stButton > button:hover {
190
+ background: #4f46e5 !important;
191
+ transform: translateY(-1px) !important;
192
+ }
193
+
194
+ /* Results Section */
195
+ .results-container {
196
+ margin-top: 1rem;
197
+ padding: 1rem;
198
+ border-radius: 8px;
199
  }
200
+
201
+ .result-card {
202
  padding: 1rem;
203
  border-radius: 8px;
204
+ border-left: 4px solid transparent;
205
  margin-bottom: 1rem;
 
206
  }
207
+
208
+ .fake-news {
209
+ background: #fef2f2;
210
+ border-left-color: #ef4444;
211
  }
212
+
213
+ .real-news {
214
+ background: #ecfdf5;
215
+ border-left-color: #10b981;
216
  }
217
+
218
+ .prediction-badge {
219
+ font-weight: 600;
220
+ font-size: 1rem;
221
+ margin-bottom: 0.5rem;
222
+ display: flex;
223
+ align-items: center;
224
  gap: 0.5rem;
225
  }
226
+
227
+ .confidence-score {
228
+ font-weight: 600;
229
+ margin-left: auto;
230
+ font-size: 1rem;
 
 
231
  }
232
+
233
+ /* Chart Containers */
234
+ .chart-container {
235
+ padding: 1rem;
236
+ border-radius: 8px;
237
+ margin: 1rem 0;
238
  }
239
+
240
+ /* Footer */
241
+ .footer {
242
+ margin-top: 2rem;
243
+ padding: 1rem 0;
244
+ text-align: center;
245
+ border-top: 1px solid #e5e7eb;
246
  }
247
  </style>
248
  """, unsafe_allow_html=True)
249
 
250
+ @st.cache_resource
251
+ def load_model_and_tokenizer():
252
+ """Load the model and tokenizer (cached)."""
253
+ model = HybridFakeNewsDetector(
254
+ bert_model_name=BERT_MODEL_NAME,
255
+ lstm_hidden_size=LSTM_HIDDEN_SIZE,
256
+ lstm_num_layers=LSTM_NUM_LAYERS,
257
+ dropout_rate=DROPOUT_RATE
258
+ )
259
+ state_dict = torch.load(SAVED_MODELS_DIR / "final_model.pt", map_location=torch.device('cpu'))
260
+ model_state_dict = model.state_dict()
261
+ filtered_state_dict = {k: v for k, v in state_dict.items() if k in model_state_dict}
262
+ model.load_state_dict(filtered_state_dict, strict=False)
263
+ model.eval()
264
+ tokenizer = BertTokenizer.from_pretrained(BERT_MODEL_NAME)
265
+ return model, tokenizer
266
 
267
  @st.cache_resource
268
+ def get_preprocessor():
269
+ """Get the text preprocessor (cached)."""
270
+ return TextPreprocessor()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ def predict_news(text):
273
+ """Predict if the given news is fake or real."""
274
+ model, tokenizer = load_model_and_tokenizer()
275
+ preprocessor = get_preprocessor()
276
+ processed_text = preprocessor.preprocess_text(text)
277
+ encoding = tokenizer.encode_plus(
278
+ processed_text,
279
+ add_special_tokens=True,
280
+ max_length=MAX_SEQUENCE_LENGTH,
281
+ padding='max_length',
282
+ truncation=True,
283
+ return_attention_mask=True,
284
+ return_tensors='pt'
285
+ )
286
+ with torch.no_grad():
287
+ outputs = model(
288
+ encoding['input_ids'],
289
+ encoding['attention_mask']
290
+ )
291
+ probabilities = torch.softmax(outputs['logits'], dim=1)
292
+ prediction = torch.argmax(outputs['logits'], dim=1)
293
+ attention_weights = outputs['attention_weights']
294
+ attention_weights_np = attention_weights[0].cpu().numpy()
295
+ return {
296
+ 'prediction': prediction.item(),
297
+ 'label': 'FAKE' if prediction.item() == 1 else 'REAL',
298
+ 'confidence': torch.max(probabilities, dim=1)[0].item(),
299
+ 'probabilities': {
300
+ 'REAL': probabilities[0][0].item(),
301
+ 'FAKE': probabilities[0][1].item()
302
+ },
303
+ 'attention_weights': attention_weights_np
304
+ }
305
 
306
+ def plot_confidence(probabilities):
307
+ """Plot prediction confidence with simplified styling."""
308
+ fig = go.Figure(data=[
309
+ go.Bar(
310
+ x=list(probabilities.keys()),
311
+ y=list(probabilities.values()),
312
+ text=[f'{p:.1%}' for p in probabilities.values()],
313
+ textposition='auto',
314
+ marker=dict(
315
+ color=['#10b981', '#ef4444'],
316
+ line=dict(color='#ffffff', width=1),
317
+ ),
318
+ )
319
+ ])
320
+ fig.update_layout(
321
+ title={'text': 'Prediction Confidence', 'x': 0.5, 'xanchor': 'center', 'font': {'size': 18}},
322
+ xaxis=dict(title='Classification', titlefont={'size': 12}, tickfont={'size': 10}),
323
+ yaxis=dict(title='Probability', range=[0, 1], tickformat='.0%', titlefont={'size': 12}, tickfont={'size': 10}),
324
+ template='plotly_white',
325
+ height=300,
326
+ margin=dict(t=60, b=60)
327
+ )
328
+ return fig
329
 
330
+ def plot_attention(text, attention_weights):
331
+ """Plot attention weights with simplified styling."""
332
+ tokens = text.split()[:20]
333
+ attention_weights = attention_weights[:len(tokens)]
334
+ if isinstance(attention_weights, (list, np.ndarray)):
335
+ attention_weights = np.array(attention_weights).flatten()
336
+ normalized_weights = attention_weights / max(attention_weights) if max(attention_weights) > 0 else attention_weights
337
+ colors = [f'rgba(99, 102, 241, {0.4 + 0.6 * float(w)})' for w in normalized_weights]
338
+ fig = go.Figure(data=[
339
+ go.Bar(
340
+ x=tokens,
341
+ y=attention_weights,
342
+ text=[f'{float(w):.3f}' for w in attention_weights],
343
+ textposition='auto',
344
+ marker=dict(color=colors),
345
+ )
346
+ ])
347
+ fig.update_layout(
348
+ title={'text': 'Attention Weights', 'x': 0.5, 'xanchor': 'center', 'font': {'size': 18}},
349
+ xaxis=dict(title='Words', tickangle=45, titlefont={'size': 12}, tickfont={'size': 10}),
350
+ yaxis=dict(title='Attention Score', titlefont={'size': 12}, tickfont={'size': 10}),
351
+ template='plotly_white',
352
+ height=350,
353
+ margin=dict(t=60, b=80)
354
+ )
355
+ return fig
356
 
357
+ def main():
358
+ # Header
359
+ st.markdown("""
360
+ <div class="header">
361
+ <div class="container">
362
+ <h1 class="header-title">🛡️ TruthCheck</h1>
363
+ </div>
364
+ </div>
365
+ """, unsafe_allow_html=True)
366
+
367
+ # Hero Section
368
+ st.markdown("""
369
+ <div class="container">
370
+ <div class="hero">
371
+ <div class="hero-left">
372
+ <h2 class="hero-title">Instant Fake News Detection</h2>
373
+ <p class="hero-text">
374
+ Verify news articles with our AI-powered tool, driven by BERT and BiLSTM for fast and accurate authenticity analysis.
375
+ </p>
376
+ </div>
377
+ <div class="hero-right">
378
+ <img src="/static/hero.png" alt="TruthCheck Illustration">
379
+ </div>
380
+ </div>
381
+ </div>
382
+ """, unsafe_allow_html=True)
383
+
384
+ # About Section
385
+ st.markdown("""
386
+ <div class="container">
387
+ <div class="about-section">
388
+ <h2 class="about-title">About TruthCheck</h2>
389
+ <p class="about-text">
390
+ TruthCheck uses a hybrid BERT-BiLSTM model to detect fake news with high accuracy. Paste an article below for instant analysis.
391
+ </p>
392
+ </div>
393
+ </div>
394
+ """, unsafe_allow_html=True)
395
+
396
+ # Input Section
397
+ st.markdown('<div class="container"><div class="input-container">', unsafe_allow_html=True)
398
+ news_text = st.text_area(
399
+ "Analyze a News Article",
400
+ height=150,
401
+ placeholder="Paste your news article here for instant AI analysis...",
402
+ key="news_input"
403
+ )
404
+ st.markdown('</div>', unsafe_allow_html=True)
405
+
406
+ # Analyze Button
407
+ st.markdown('<div class="container">', unsafe_allow_html=True)
408
+ col1, col2, col3 = st.columns([1, 2, 1])
409
+ with col2:
410
+ analyze_button = st.button("🔍 Analyze Now", key="analyze_button")
411
+ st.markdown('</div>', unsafe_allow_html=True)
412
+
413
+ if analyze_button:
414
+ if news_text and len(news_text.strip()) > 10:
415
+ with st.spinner("Analyzing article..."):
416
+ try:
417
+ result = predict_news(news_text)
418
+ st.markdown('<div class="container"><div class="results-container">', unsafe_allow_html=True)
419
+
420
+ # Prediction Result
421
+ col1, col2 = st.columns([1, 1], gap="medium")
422
+ with col1:
423
+ if result['label'] == 'FAKE':
424
+ st.markdown(f'''
425
+ <div class="result-card fake-news">
426
+ <div class="prediction-badge">🚨 Fake News Detected <span class="confidence-score">{result["confidence"]:.1%}</span></div>
427
+ <p>Our AI has identified this content as likely misinformation based on linguistic patterns and content analysis.</p>
428
+ </div>
429
+ ''', unsafe_allow_html=True)
430
+ else:
431
+ st.markdown(f'''
432
+ <div class="result-card real-news">
433
+ <div class="prediction-badge">✅ Authentic News <span class="confidence-score">{result["confidence"]:.1%}</span></div>
434
+ <p>This content appears to be legitimate based on professional writing style and factual consistency.</p>
435
+ </div>
436
+ ''', unsafe_allow_html=True)
437
+
438
+ with col2:
439
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
440
+ st.plotly_chart(plot_confidence(result['probabilities']), use_container_width=True)
441
+ st.markdown('</div>', unsafe_allow_html=True)
442
+
443
+ # Attention Analysis
444
+ st.markdown('<div class="chart-container">', unsafe_allow_html=True)
445
+ st.plotly_chart(plot_attention(news_text, result['attention_weights']), use_container_width=True)
446
+ st.markdown('</div></div></div>', unsafe_allow_html=True)
447
+ except Exception as e:
448
+ st.markdown('<div class="container">', unsafe_allow_html=True)
449
+ st.error(f"Error: {str(e)}. Please try again or contact support.")
450
+ st.markdown('</div>', unsafe_allow_html=True)
451
  else:
452
+ st.markdown('<div class="container">', unsafe_allow_html=True)
453
+ st.error("Please enter a news article (at least 10 words) for analysis.")
454
+ st.markdown('</div>', unsafe_allow_html=True)
455
+
456
+ # Footer
457
+ st.markdown("""
458
+ <div class="footer">
459
+ <p style="text-align: center; font-weight: 600; font-size: 16px;">💻 Developed with ❤️ using Streamlit | © 2025</p>
460
+ </div>
461
+ """, unsafe_allow_html=True)
462
+
463
+ if __name__ == "__main__":
464
+ main()