tomalex04 commited on
Commit
5906cc6
·
1 Parent(s): 32244bd

Sync latest code from Hugging Face

Browse files
misinformationui/static/front.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Fake News Detection Chatbot</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+
10
+ <body>
11
+ <div class="chat-container" id="chat"></div>
12
+
13
+ <div class="input-area">
14
+ <input type="text" id="query" placeholder="Type news to verify..." />
15
+ <button id="sendBtn">Send</button>
16
+ </div>
17
+
18
+ <div class="chat-background" id="chat-background">
19
+ <p id="p1">Hey,</p>
20
+ <p id="p2">Discover misinformations around you,</p>
21
+ </div>
22
+
23
+ <script src="script.js"></script>
24
+ </body>
25
+ </html>
misinformationui/static/main.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/tom/miniconda3/envs/fake_news_detection/bin/python
2
+ """
3
+ main.py - Server for the Fake News Detection system
4
+
5
+ This script creates a Flask server that exposes API endpoints to:
6
+ 1. Take user input (news query) from the UI
7
+ 2. Process the request through the fake news detection pipeline
8
+ 3. Return the results to the UI for display
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import time
14
+ from dotenv import load_dotenv
15
+ from flask import Flask, request, jsonify
16
+ from flask_cors import CORS
17
+
18
+ # Import required functions from modules
19
+ from gdelt_api import (
20
+ fetch_articles_from_gdelt,
21
+ filter_by_whitelisted_domains,
22
+ normalize_gdelt_articles
23
+ )
24
+ from ranker import ArticleRanker
25
+ from gdelt_query_builder import generate_query, GEMINI_MODEL
26
+ import bias_analyzer
27
+
28
+ # Global variable for embedding model caching across requests
29
+ print("Preloading embedding model for faster request processing...")
30
+ # Preload the embedding model at server startup
31
+ global_ranker = ArticleRanker()
32
+
33
+
34
+ # The function has been removed since bias category descriptions are provided directly by the Gemini model
35
+ # and stored in the bias_analysis["descriptions"] dictionary
36
+
37
+
38
+ def format_results(query, ranked_articles):
39
+ """
40
+ Format the ranked results in a structured way for the UI.
41
+
42
+ Args:
43
+ query (str): The original query
44
+ ranked_articles (list): List of ranked article dictionaries
45
+
46
+ Returns:
47
+ dict: Dictionary with formatted results
48
+ """
49
+ result = {}
50
+
51
+ if not ranked_articles:
52
+ result = {
53
+ "status": "no_results",
54
+ "message": "⚠️ No news found. Possibly Fake.",
55
+ "details": "No reliable sources could verify this information.",
56
+ "articles": []
57
+ }
58
+ else:
59
+ # Get display configuration from environment variables
60
+ show_scores = os.getenv('SHOW_SIMILARITY_SCORES', 'true').lower() == 'true'
61
+ show_date = os.getenv('SHOW_PUBLISH_DATE', 'true').lower() == 'true'
62
+ show_url = os.getenv('SHOW_URL', 'true').lower() == 'true'
63
+
64
+ formatted_articles = []
65
+ for article in ranked_articles:
66
+ formatted_article = {
67
+ "rank": article['rank'],
68
+ "title": article['title'],
69
+ "source": article['source']
70
+ }
71
+
72
+ if show_scores:
73
+ formatted_article["similarity_score"] = round(article['similarity_score'], 4)
74
+
75
+ if show_url:
76
+ formatted_article["url"] = article['url']
77
+
78
+ if show_date:
79
+ formatted_article["published_at"] = article['published_at']
80
+
81
+ formatted_articles.append(formatted_article)
82
+
83
+ result = {
84
+ "status": "success",
85
+ "message": f"✅ Found {len(ranked_articles)} relevant articles for: '{query}'",
86
+ "articles": formatted_articles,
87
+ "footer": "If the news matches these reliable sources, it's likely true. If it contradicts them or no sources are found, it might be fake."
88
+ }
89
+
90
+ return result
91
+
92
+
93
+ def remove_duplicates(articles):
94
+ """
95
+ Remove duplicate articles based on URL.
96
+
97
+ Args:
98
+ articles (list): List of article dictionaries
99
+
100
+ Returns:
101
+ list: List with duplicate articles removed
102
+ """
103
+ unique_urls = set()
104
+ unique_articles = []
105
+
106
+ for article in articles:
107
+ if article['url'] not in unique_urls:
108
+ unique_urls.add(article['url'])
109
+ unique_articles.append(article)
110
+
111
+ return unique_articles
112
+
113
+
114
+ # This function has been removed since Gemini is a cloud API service
115
+ # that does not require local caching - models are instantiated as needed
116
+
117
+
118
+ def main():
119
+ """Main function to run the fake news detection pipeline as a server."""
120
+ # Load environment variables
121
+ load_dotenv()
122
+
123
+ # Create Flask app
124
+ app = Flask(__name__, static_folder='static')
125
+ CORS(app) # Enable CORS for all routes
126
+
127
+ @app.route('/static/')
128
+ def index():
129
+ """Serve the main page."""
130
+ return app.send_static_file('front.html')
131
+
132
+
133
+ @app.route('/api/detect', methods=['POST'])
134
+ def detect_fake_news():
135
+ """API endpoint to check if news is potentially fake."""
136
+ # Start timing the request processing
137
+ start_time = time.time()
138
+
139
+ data = request.json
140
+ query = data.get('query', '')
141
+
142
+ if not query:
143
+ return jsonify({
144
+ "status": "error",
145
+ "message": "Please provide a news statement to verify."
146
+ })
147
+
148
+ # =====================================================
149
+ # 1. Input Handling
150
+ # =====================================================
151
+ # Generate three variations of the query using Gemini
152
+ query_variations = generate_query(query)
153
+
154
+ # Check if the query was flagged as inappropriate
155
+ if query_variations == ["INAPPROPRIATE_QUERY"]:
156
+ return jsonify({
157
+ "status": "error",
158
+ "message": "I cannot provide information on this topic as it appears to contain sensitive or inappropriate content."
159
+ })
160
+
161
+ # =====================================================
162
+ # 2. Data Fetching
163
+ # =====================================================
164
+ # Fetch articles from GDELT API for each query variation
165
+ all_articles = []
166
+ for query_var in query_variations:
167
+ articles = fetch_articles_from_gdelt(query_var)
168
+ if articles:
169
+ all_articles.extend(articles)
170
+
171
+ # Store unique articles in a set to ensure uniqueness
172
+ unique_articles = remove_duplicates(all_articles)
173
+
174
+ # Apply domain whitelist filtering if enabled in .env
175
+ use_whitelist_only = os.getenv('USE_WHITELIST_ONLY', 'false').lower() == 'true'
176
+ if use_whitelist_only:
177
+ print(f"Filtering articles to only include whitelisted domains...")
178
+ unique_articles = filter_by_whitelisted_domains(unique_articles)
179
+ print(f"After whitelist filtering: {len(unique_articles)} articles remain")
180
+
181
+ # Normalize the articles to a standard format
182
+ normalized_articles = normalize_gdelt_articles(unique_articles)
183
+
184
+ if not normalized_articles:
185
+ return jsonify(format_results(query, []))
186
+
187
+ # =====================================================
188
+ # 3. Embedding & Ranking
189
+ # =====================================================
190
+ # Initialize the ranker with model from environment variable
191
+ model_name = os.getenv('SIMILARITY_MODEL', 'intfloat/multilingual-e5-base')
192
+
193
+ # Use global ranker if it matches the requested model, otherwise create a new instance
194
+ if global_ranker.model_name == model_name:
195
+ ranker = global_ranker
196
+ else:
197
+ ranker = ArticleRanker(model_name)
198
+
199
+ # Get TOP_K_ARTICLES from .env file
200
+ TOP_K_ARTICLES = int(os.getenv('TOP_K_ARTICLES', 250))
201
+ min_threshold = float(os.getenv('MIN_SIMILARITY_THRESHOLD', 0.1))
202
+
203
+ # Prepare article texts for embedding
204
+ article_texts = [f"{article['title']} {article['description'] or ''}" for article in normalized_articles]
205
+
206
+ # Create embeddings and calculate similarities
207
+ query_embedding, article_embeddings = ranker.create_embeddings(query, article_texts)
208
+ similarities = ranker.calculate_similarities(query_embedding, article_embeddings)
209
+
210
+ # Get top articles based on similarity
211
+ top_indices = ranker.get_top_articles(similarities, normalized_articles, TOP_K_ARTICLES, min_threshold)
212
+ top_articles = ranker.format_results(top_indices, similarities, normalized_articles)
213
+
214
+ # =====================================================
215
+ # 4. Bias Categorization
216
+ # =====================================================
217
+ # Extract outlet names from the TOP_K_ARTICLES
218
+ # In top_articles, the source is already extracted as a string
219
+ outlet_names = [article['source'] for article in top_articles]
220
+ unique_outlets = list(set(outlet_names))
221
+ print(f"Analyzing {len(unique_outlets)} unique news outlets for bias...")
222
+
223
+ # Analyze bias using Gemini - send just the outlet names, not the whole articles
224
+ bias_analysis = bias_analyzer.analyze_bias(query, unique_outlets, GEMINI_MODEL)
225
+
226
+ # =====================================================
227
+ # 5. Category Embeddings
228
+ # =====================================================
229
+ print("\n" + "=" * 80)
230
+ print("EMBEDDING VECTORS BY BIAS CATEGORY")
231
+ print("=" * 80)
232
+
233
+ # Create embedding vectors for each bias category
234
+ # 1. Group articles based on their outlet's bias category
235
+ # 2. Create an embedding vector for each category using ONLY article titles
236
+ # 3. Rank articles within each category by similarity to query
237
+ category_rankings = bias_analyzer.categorize_and_rank_by_bias(
238
+ query, normalized_articles, bias_analysis, ranker, min_threshold
239
+ )
240
+
241
+ # =====================================================
242
+ # 6. Top N Selection per Category
243
+ # =====================================================
244
+ # Get TOP_N_PER_CATEGORY from .env file (default: 5)
245
+ TOP_N_PER_CATEGORY = int(os.getenv('TOP_N_PER_CATEGORY', 5))
246
+
247
+ # Get total counts of articles per category before filtering
248
+ category_article_counts = {
249
+ category: len(articles)
250
+ for category, articles in category_rankings.items()
251
+ if category not in ["descriptions", "reasoning"]
252
+ }
253
+
254
+ # For each bias category, select the top N articles
255
+ # These are the most relevant articles within each bias perspective
256
+ filtered_category_rankings = {}
257
+ for category, articles in category_rankings.items():
258
+ # Skip non-category keys like "descriptions" or "reasoning"
259
+ if category in ["descriptions", "reasoning"]:
260
+ continue
261
+
262
+ filtered_category_rankings[category] = articles[:TOP_N_PER_CATEGORY]
263
+
264
+ # Only print if there are articles in this category
265
+ if len(filtered_category_rankings[category]) > 0:
266
+ print(f"\n===== Top {len(filtered_category_rankings[category])} articles from {category} category =====")
267
+
268
+ # Print detailed information about each selected article
269
+ for i, article in enumerate(filtered_category_rankings[category], 1):
270
+ print(f"Article #{i}:")
271
+ print(f" Title: {article['title']}")
272
+ print(f" Source: {article['source']}")
273
+ print(f" Similarity Score: {article['similarity_score']:.4f}")
274
+ print(f" Rank: {article['rank']}")
275
+ print(f" URL: {article['url']}")
276
+ print(f" Published: {article['published_at']}")
277
+ print("-" * 50)
278
+
279
+ # =====================================================
280
+ # 7. Summarization
281
+ # =====================================================
282
+ # Generate summary from articles in all categories
283
+ print("\nGenerating factual summary using top articles from all categories...")
284
+
285
+ # Pass the original bias_analysis to include the reasoning in the summary
286
+ # We need to add the reasoning to filtered_category_rankings since that's what gets passed to generate_summary
287
+ filtered_category_rankings["reasoning"] = bias_analysis.get("reasoning", "No reasoning provided")
288
+
289
+ # Call the bias_analyzer's generate_summary function with articles from all categories
290
+ summary = bias_analyzer.generate_summary(
291
+ query,
292
+ normalized_articles,
293
+ filtered_category_rankings,
294
+ GEMINI_MODEL
295
+ )
296
+
297
+ # Print the summary to terminal (already includes its own formatting)
298
+ print(summary)
299
+
300
+ # Prepare response with only the summary and reasoning
301
+ result = {
302
+ "query": query,
303
+ "summary": summary,
304
+ "reasoning": bias_analysis.get("reasoning", "No reasoning provided")
305
+ }
306
+
307
+ return jsonify(result)
308
+
309
+ @app.route('/api/health', methods=['GET'])
310
+ def health_check():
311
+ """API endpoint to check if the server is running."""
312
+ return jsonify({
313
+ "status": "ok",
314
+ "message": "Fake News Detection API is running"
315
+ })
316
+
317
+ # Get port from environment variable or use default 5000
318
+ port = int(os.getenv('PORT', 5000))
319
+ debug = os.getenv('DEBUG', 'false').lower() == 'true'
320
+
321
+ print(f"Starting Fake News Detection API server on port {port}...")
322
+ # Start the Flask server
323
+ app.run(host='0.0.0.0', port=port, debug=debug)
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()
misinformationui/static/script.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const chat = document.getElementById('chat');
2
+ const input = document.getElementById('query');
3
+ const sendBtn = document.getElementById('sendBtn');
4
+
5
+ function addMessage(text, sender, isPre = false) {
6
+ const msg = document.createElement('div');
7
+ msg.classList.add('message', sender);
8
+
9
+ // Add special class for pre-formatted messages to style them properly
10
+ if (isPre) {
11
+ msg.classList.add('pre-formatted');
12
+
13
+ // For pre-formatted text (terminal output)
14
+ const pre = document.createElement('pre');
15
+ pre.textContent = text;
16
+
17
+ // No inline styles - all styling comes from CSS
18
+ msg.appendChild(pre);
19
+ } else {
20
+ msg.textContent = text;
21
+ }
22
+
23
+ chat.appendChild(msg);
24
+
25
+ // Smooth scroll to new message
26
+ setTimeout(() => {
27
+ msg.scrollIntoView({ behavior: 'smooth', block: 'end' });
28
+ }, 100);
29
+
30
+ return msg;
31
+ }
32
+
33
+ async function sendMessage() {
34
+ const query = input.value.trim();
35
+ if (!query) return;
36
+
37
+ const bg = document.getElementById('chat-background');
38
+ if (bg && !bg.classList.contains('blurred')) {
39
+ bg.classList.add('blurred');
40
+ }
41
+
42
+ addMessage(query, 'user');
43
+ input.value = '';
44
+
45
+ const loader = addMessage('Processing...', 'bot');
46
+
47
+ try {
48
+ const response = await fetch('/api/detect', {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({ query: query })
52
+ });
53
+
54
+ const data = await response.json();
55
+ loader.remove();
56
+
57
+ if (data && data.summary) {
58
+ // Display summary exactly as it comes from the backend
59
+ addMessage(data.summary, 'bot', true); // scrollable <pre> block
60
+ } else {
61
+ addMessage("Could not generate a summary.", 'bot');
62
+ }
63
+ } catch (e) {
64
+ loader.remove();
65
+ addMessage("Error checking news.", 'bot');
66
+ }
67
+ }
68
+
69
+ function formatBackendData(data) {
70
+ // If we have a summary, only display that
71
+ if (data && data.summary) {
72
+ if (typeof data.summary === 'string') {
73
+ return data.summary;
74
+ } else if (typeof data.summary === 'object' && data.summary.text) {
75
+ return data.summary.text;
76
+ } else {
77
+ return JSON.stringify(data.summary, null, 2);
78
+ }
79
+ }
80
+
81
+ // If no summary is available, return null so we can fall back to showing basic results
82
+ return null;
83
+ }
84
+
85
+ sendBtn.addEventListener('click', sendMessage);
86
+ input.addEventListener('keypress', (e) => {
87
+ if (e.key === 'Enter') sendMessage();
88
+ });
misinformationui/static/style.css ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ margin: 0;
4
+ padding: 0;
5
+ height: 100vh;
6
+ display: flex;
7
+ flex-direction: column;
8
+ overflow: hidden;
9
+ }
10
+
11
+ /* Blurred background image */
12
+ body::before {
13
+ content: "";
14
+ position: fixed;
15
+ top: 0;
16
+ left: 0;
17
+ width: 100vw;
18
+ height: 100vh;
19
+ z-index: -1;
20
+ background: rgb(23, 23, 23);
21
+ }
22
+
23
+ .chat-container {
24
+ flex: 1;
25
+ display: flex;
26
+ flex-direction: column;
27
+ padding: 15px;
28
+ overflow-y: auto;
29
+ overflow-x: hidden; /* prevent horizontal scrolling */
30
+ margin-bottom: 70px;
31
+ background: transparent;
32
+ width: 100%;
33
+ max-width: 95%; /* wider to accommodate terminal output */
34
+ margin-left: auto; /* center align */
35
+ margin-right: auto; /* center align */
36
+ scroll-behavior: smooth; /* Smooth scrolling */
37
+ height: calc(100vh - 70px); /* Full height minus input area */
38
+ }
39
+
40
+ .message {
41
+ width: fit-content; /* shrink to text */
42
+ max-width: 100%; /* allow full width for terminal output */
43
+ margin-bottom: 12px;
44
+ padding: 12px 15px;
45
+ border-radius: 15px;
46
+ line-height: 1.4;
47
+ }
48
+
49
+ /* Special styling for bot messages with pre-formatted text */
50
+ .message.bot.pre-formatted {
51
+ width: 100%; /* full width for terminal output */
52
+ max-width: 100%; /* no width restriction */
53
+ white-space: pre-wrap; /* wrap text to prevent horizontal scroll */
54
+ overflow-wrap: break-word; /* break long words if needed */
55
+ }
56
+
57
+ .user {
58
+ align-self: flex-end; /* right side */
59
+ background: #414141;
60
+ color: #fff;
61
+ border-bottom-right-radius: 5px;
62
+ }
63
+
64
+ .bot {
65
+ align-self: flex-start; /* left side */
66
+ background: transparent;
67
+ color: #ffffff;
68
+ border-bottom-left-radius: 5px;
69
+ }
70
+
71
+ /* Terminal output style - matching exactly what appears in the terminal */
72
+ .message.bot pre {
73
+ font-family: monospace;
74
+ background-color: transparent; /* No background color */
75
+ color: inherit; /* Use the same text color as the parent */
76
+ padding: 0;
77
+ border: none;
78
+ width: 100%;
79
+ max-height: none; /* No height limit */
80
+ overflow-x: visible; /* No horizontal scrolling */
81
+ white-space: pre-wrap; /* Wrap text to prevent horizontal scrolling */
82
+ font-size: inherit;
83
+ line-height: 1.4;
84
+ }
85
+
86
+
87
+
88
+ .input-area {
89
+ position: fixed;
90
+ bottom: 1rem;
91
+ left: 50%;
92
+ transform: translateX(-50%);
93
+ width: 100%;
94
+ max-width: 95%; /* Match the width of chat container */
95
+ display: flex;
96
+ padding: 10px;
97
+ background: rgba(54, 54, 54, 0.7); /* make input area semi-transparent */
98
+ box-shadow: 0 -2px 5px rgba(0,0,0,0.05);
99
+ border-radius: 30px;
100
+ z-index: 10; /* Ensure input stays on top */
101
+ }
102
+
103
+ .input-area input {
104
+ flex: 1;
105
+ padding: 14px 18px;
106
+ border: 1px solid #1b1b1b;
107
+ box-shadow: #414141;
108
+ border-radius: 25px;
109
+ outline: none;
110
+ font-size: 1rem;
111
+ }
112
+
113
+ .input-area button {
114
+ margin-left: 10px;
115
+ padding: 0 20px;
116
+ border: none;
117
+ background: #1f1f1f;
118
+ color: white;
119
+ border-radius: 25px;
120
+ cursor: pointer;
121
+ font-size: 1rem;
122
+ transition: background 0.2s ease;
123
+ }
124
+
125
+ .input-area button:hover {
126
+ background: #6a6a6a;
127
+ }
128
+
129
+ .loader {
130
+ font-size: 0.9rem;
131
+ color: gray;
132
+ margin: 5px 0;
133
+ }
134
+ .chat-background{
135
+ position: fixed;
136
+ font-family: 'Courier New', Courier, monospace;
137
+ top: 40%;
138
+ left: 50%;
139
+ transform: translate(-50%, -50%);
140
+ font-weight: bold;
141
+ color: rgba(255, 255, 255, 0.8); /* semi-transparent */
142
+ text-align: center;
143
+ z-index: 0; /* below chat messages */
144
+ pointer-events: none; /* makes it "untouchable" */
145
+ transition: all 0.4s ease;
146
+ }
147
+ .chat-background{
148
+ display: inline-block;
149
+ text-align: left;
150
+ }
151
+ .chat-background #p1 {
152
+ font-size: 4rem;
153
+ }
154
+
155
+ .chat-background #p2 {
156
+ font-size: 3rem;
157
+ }
158
+
159
+ /* When blurred */
160
+ .chat-background.blurred {
161
+ filter: blur(12px); /* strong blur */
162
+ opacity: 0.4; /* fade slightly for readability */
163
+ transition: all 0.4s ease;
164
+ }
165
+
166
+ @media (max-width: 600px) {
167
+ .message {
168
+ max-width: 85%;
169
+ }
170
+
171
+ .chat-container {
172
+ padding: 10px;
173
+ max-width: 100%;
174
+ }
175
+
176
+ .input-area {
177
+ max-width: 95%;
178
+ bottom: 0.5rem;
179
+ }
180
+
181
+ .chat-background #p1 {
182
+ font-size: 3rem;
183
+ }
184
+
185
+ .chat-background #p2 {
186
+ font-size: 2rem;
187
+ }
188
+ }
misinformationui/static/test_server.py ADDED
File without changes