tomalex04 commited on
Commit
5bda825
Β·
1 Parent(s): b3046d9

Restructured project: separate backend and ui folders

Browse files
.env.example β†’ misinformation_detection_backend/.env.example RENAMED
File without changes
.gitignore β†’ misinformation_detection_backend/.gitignore RENAMED
File without changes
README.md β†’ misinformation_detection_backend/README.md RENAMED
File without changes
README_GEMINI.md β†’ misinformation_detection_backend/README_GEMINI.md RENAMED
File without changes
bias_analyzer.py β†’ misinformation_detection_backend/bias_analyzer.py RENAMED
File without changes
check_models.py β†’ misinformation_detection_backend/check_models.py RENAMED
File without changes
gdelt_api.py β†’ misinformation_detection_backend/gdelt_api.py RENAMED
File without changes
gdelt_query_builder.py β†’ misinformation_detection_backend/gdelt_query_builder.py RENAMED
File without changes
google_search.py β†’ misinformation_detection_backend/google_search.py RENAMED
File without changes
main.py β†’ misinformation_detection_backend/main.py RENAMED
File without changes
ranker.py β†’ misinformation_detection_backend/ranker.py RENAMED
File without changes
requirements.txt β†’ misinformation_detection_backend/requirements.txt RENAMED
File without changes
whitelisted_domains.py β†’ misinformation_detection_backend/whitelisted_domains.py RENAMED
File without changes
misinformationui/lib/chat_screen.dart ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dart:convert';
2
+ import 'package:flutter/material.dart';
3
+ import 'package:google_fonts/google_fonts.dart';
4
+ import 'package:http/http.dart' as http;
5
+ import 'loading_indicator.dart';
6
+ import 'package:flutter_linkify/flutter_linkify.dart';
7
+ import 'package:url_launcher/url_launcher.dart';
8
+
9
+ class Message {
10
+ final String text;
11
+ final bool isUser;
12
+ final bool isPreFormatted;
13
+ final bool isLoading;
14
+ final int? loadingStep;
15
+ final bool? isStepCompleted;
16
+
17
+ Message({
18
+ required this.text,
19
+ required this.isUser,
20
+ this.isPreFormatted = false,
21
+ this.isLoading = false,
22
+ this.loadingStep,
23
+ this.isStepCompleted,
24
+ });
25
+ }
26
+
27
+ class ChatScreen extends StatefulWidget {
28
+ const ChatScreen({super.key});
29
+
30
+ @override
31
+ State<ChatScreen> createState() => _ChatScreenState();
32
+ }
33
+
34
+ class _ChatScreenState extends State<ChatScreen> {
35
+ final List<Message> _messages = [];
36
+ final TextEditingController _controller = TextEditingController();
37
+ bool _isBackgroundBlurred = false;
38
+ final ScrollController _scrollController = ScrollController();
39
+
40
+ Future<void> _sendMessage() async {
41
+ final query = _controller.text.trim();
42
+ if (query.isEmpty) return;
43
+
44
+ setState(() {
45
+ _isBackgroundBlurred = true;
46
+ _messages.add(Message(text: query, isUser: true));
47
+ _messages.add(Message(text: '', isUser: false, isLoading: true));
48
+ _controller.clear();
49
+ });
50
+ _scrollToBottom();
51
+
52
+ final loadingMessageIndex = _messages.length - 1;
53
+
54
+ try {
55
+ final response = await http.post(
56
+ Uri.parse('http://localhost:5000/api/detect'),
57
+ headers: {'Content-Type': 'application/json'},
58
+ body: jsonEncode({'query': query}),
59
+ );
60
+ final data = jsonDecode(response.body);
61
+
62
+ setState(() {
63
+ _messages[loadingMessageIndex] = Message(
64
+ text: '',
65
+ isUser: false,
66
+ isLoading: true,
67
+ isStepCompleted: true,
68
+ );
69
+ });
70
+
71
+ await Future.delayed(const Duration(milliseconds: 600));
72
+
73
+ setState(() {
74
+ if (loadingMessageIndex < _messages.length) {
75
+ _messages.removeAt(loadingMessageIndex);
76
+ }
77
+
78
+ // --- STEP 1: Show raw JSON ---
79
+ // _messages.add(Message(
80
+ // text: jsonEncode(data),
81
+ // isUser: false,
82
+ // isPreFormatted: true,
83
+ // ));
84
+
85
+ // --- STEP 2: Show only values ---
86
+ String valuesText = '';
87
+ if (data is Map) {
88
+ valuesText = data.values.map((v) => v.toString()).join('\n\n');
89
+ } else if (data is List) {
90
+ valuesText = data.map((v) => v.toString()).join('\n\n');
91
+ } else {
92
+ valuesText = data.toString();
93
+ }
94
+
95
+ _messages.add(Message(
96
+ text: valuesText,
97
+ isUser: false,
98
+ isPreFormatted: true,
99
+ ));
100
+ });
101
+ } catch (e) {
102
+ setState(() {
103
+ if (loadingMessageIndex < _messages.length) {
104
+ _messages[loadingMessageIndex] = Message(
105
+ text: '',
106
+ isUser: false,
107
+ isLoading: true,
108
+ isStepCompleted: true,
109
+ );
110
+ }
111
+ });
112
+ await Future.delayed(const Duration(milliseconds: 600));
113
+ setState(() {
114
+ if (loadingMessageIndex < _messages.length) {
115
+ _messages.removeAt(loadingMessageIndex);
116
+ }
117
+ // Do not add any hardcoded error message.
118
+ });
119
+ }
120
+ _scrollToBottom();
121
+ }
122
+
123
+ void _scrollToBottom() {
124
+ Future.delayed(const Duration(milliseconds: 100), () {
125
+ if (_scrollController.hasClients) {
126
+ _scrollController.animateTo(
127
+ _scrollController.position.maxScrollExtent,
128
+ duration: const Duration(milliseconds: 300),
129
+ curve: Curves.easeOut,
130
+ );
131
+ }
132
+ });
133
+ }
134
+
135
+ @override
136
+ Widget build(BuildContext context) {
137
+ return Scaffold(
138
+ body: Column(
139
+ children: [
140
+ Expanded(
141
+ child: Stack(
142
+ children: [
143
+ if (!_isBackgroundBlurred)
144
+ Center(
145
+ child: SizedBox(
146
+ width: MediaQuery.of(context).size.width * 0.8,
147
+ child: Column(
148
+ mainAxisAlignment: MainAxisAlignment.center,
149
+ crossAxisAlignment: CrossAxisAlignment.start,
150
+ children: [
151
+ Center(
152
+ child: Column(
153
+ mainAxisAlignment: MainAxisAlignment.center,
154
+ crossAxisAlignment: CrossAxisAlignment.center,
155
+ children: [
156
+ Text(
157
+ 'Hey,',
158
+ style: GoogleFonts.courierPrime(
159
+ color: Colors.white,
160
+ fontSize: 64,
161
+ fontWeight: FontWeight.bold,
162
+ ),
163
+ textAlign: TextAlign.start,
164
+ ),
165
+ const SizedBox(height: 10), // Add some spacing
166
+ Text(
167
+ 'Discover misinformations around you...',
168
+ style: GoogleFonts.courierPrime(
169
+ color: Colors.white,
170
+ fontSize: 32,
171
+ fontWeight: FontWeight.w500,
172
+ ),
173
+ ),
174
+ ],
175
+ ),
176
+ ),
177
+ ],
178
+ ),
179
+ ),
180
+ ),
181
+ if (_isBackgroundBlurred)
182
+ Expanded(
183
+ child: ListView.builder(
184
+ controller: _scrollController,
185
+ padding: const EdgeInsets.all(15),
186
+ itemCount: _messages.length,
187
+ itemBuilder: (context, index) {
188
+ final message = _messages[index];
189
+ return MessageBubble(message: message);
190
+ },
191
+ ),
192
+ ),
193
+ ],
194
+ ),
195
+ ),
196
+ Center(
197
+ child: Container(
198
+ width: MediaQuery.of(context).size.width * 0.6,
199
+ margin: const EdgeInsets.all(16.0),
200
+ decoration: BoxDecoration(
201
+ color: Colors.grey[800]!.withOpacity(0.7),
202
+ borderRadius: BorderRadius.circular(30),
203
+ border: Border.all(
204
+ color: Colors.grey[600]!,
205
+ width: 1,
206
+ ),
207
+ ),
208
+ padding: const EdgeInsets.symmetric(horizontal: 15),
209
+ child: Row(
210
+ children: [
211
+ Expanded(
212
+ child: TextField(
213
+ controller: _controller,
214
+ cursorColor: Colors.white,
215
+ style: const TextStyle(color: Colors.white),
216
+ decoration: const InputDecoration(
217
+ hintText: 'Type an info to verify...',
218
+ hintStyle: TextStyle(color: Colors.grey),
219
+ border: InputBorder.none,
220
+ ),
221
+ onSubmitted: (_) => _sendMessage(),
222
+ ),
223
+ ),
224
+ IconButton(
225
+ icon: const Icon(Icons.send, color: Colors.white),
226
+ onPressed: _sendMessage,
227
+ ),
228
+ ],
229
+ ),
230
+ ),
231
+ ),
232
+ ],
233
+ ),
234
+ );
235
+ }
236
+ }
237
+
238
+
239
+
240
+ class MessageBubble extends StatelessWidget {
241
+ final Message message;
242
+
243
+ const MessageBubble({super.key, required this.message});
244
+
245
+ Future<void> _onOpen(LinkableElement link) async {
246
+ if (await canLaunchUrl(Uri.parse(link.url))) {
247
+ await launchUrl(Uri.parse(link.url));
248
+ }
249
+ }
250
+
251
+ @override
252
+ Widget build(BuildContext context) {
253
+ return Align(
254
+ alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
255
+ child: Container(
256
+ constraints: BoxConstraints(
257
+ maxWidth: MediaQuery.of(context).size.width * 0.8,
258
+ ),
259
+ margin: const EdgeInsets.only(bottom: 4),
260
+ padding: const EdgeInsets.all(10),
261
+ decoration: message.isUser
262
+ ? BoxDecoration(
263
+ color: const Color(0xFF414141),
264
+ borderRadius: BorderRadius.only(
265
+ topLeft: const Radius.circular(20),
266
+ topRight: const Radius.circular(20),
267
+ bottomLeft: Radius.circular(message.isUser ? 15 : 5),
268
+ bottomRight: Radius.circular(message.isUser ? 5 : 15),
269
+ ),
270
+ )
271
+ : null,
272
+ child: message.isLoading
273
+ ? LoadingIndicator(
274
+ isCompleted: message.isStepCompleted ?? false,
275
+ )
276
+ : message.isPreFormatted
277
+ ? SelectableLinkify(
278
+ text: message.text,
279
+ onOpen: _onOpen,
280
+ linkStyle: const TextStyle(
281
+ color: Colors.blueAccent,
282
+ decoration: TextDecoration.underline
283
+ ),
284
+ style: const TextStyle(
285
+ fontFamily: 'Montserrat',
286
+ fontSize: 16,
287
+ color: Colors.white,
288
+ ),
289
+ )
290
+ : Text(
291
+ message.text,
292
+ style: const TextStyle(color: Colors.white),
293
+ ),
294
+ ),
295
+ );
296
+ }
297
+ }
misinformationui/lib/loading_indicator.dart ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // loading_indicator.dart
2
+ import 'dart:async';
3
+ import 'package:flutter/material.dart';
4
+
5
+ class LoadingIndicator extends StatefulWidget {
6
+ /// When true, the indicator will finish the current step (show its tick)
7
+ /// and then stop looping.
8
+ final bool isCompleted;
9
+
10
+ /// How long the spinner shows for each step.
11
+ final Duration spinnerDuration;
12
+
13
+ /// How long the tick remains visible before moving to next step.
14
+ final Duration tickDuration;
15
+
16
+ const LoadingIndicator({
17
+ super.key,
18
+ required this.isCompleted,
19
+ this.spinnerDuration = const Duration(seconds: 5),
20
+ this.tickDuration = const Duration(milliseconds: 1500),
21
+ });
22
+
23
+ @override
24
+ State<LoadingIndicator> createState() => _LoadingIndicatorState();
25
+ }
26
+
27
+ class _LoadingIndicatorState extends State<LoadingIndicator> {
28
+ final List<String> _steps = const [
29
+ 'Processing...',
30
+ 'Preprocessing the query...',
31
+ 'Geolocating...',
32
+ 'Fetching news...',
33
+ 'Analysing bias in news articles...',
34
+ ];
35
+
36
+ int _currentStep = 0;
37
+ bool _showTick = false;
38
+
39
+ // Control flags for the async loop
40
+ bool _running = false;
41
+ bool _stopRequested = false;
42
+
43
+ @override
44
+ void initState() {
45
+ super.initState();
46
+ _startLoop();
47
+ }
48
+
49
+ void _startLoop() {
50
+ // Ensure we don't spawn multiple loops
51
+ if (_running) return;
52
+ _stopRequested = false;
53
+ _runLoop();
54
+ }
55
+
56
+ Future<void> _runLoop() async {
57
+ _running = true;
58
+ final spinnerDelay = widget.spinnerDuration;
59
+ final tickDelay = widget.tickDuration;
60
+
61
+ while (mounted && !_stopRequested) {
62
+ // Go through all steps sequentially (this completes one full cycle)
63
+ for (int i = 0; i < _steps.length; i++) {
64
+ if (!mounted || _stopRequested) break;
65
+
66
+ // show spinner for this step
67
+ setState(() {
68
+ _currentStep = i;
69
+ _showTick = false;
70
+ });
71
+
72
+ // wait spinner time
73
+ await Future.delayed(spinnerDelay);
74
+ if (!mounted) break;
75
+
76
+ // if parent requested completion while spinner ran, show tick then stop
77
+ if (widget.isCompleted) {
78
+ setState(() => _showTick = true);
79
+ _stopRequested = true;
80
+ break;
81
+ }
82
+
83
+ // show tick for this step
84
+ setState(() => _showTick = true);
85
+
86
+ // wait tick time
87
+ await Future.delayed(tickDelay);
88
+ if (!mounted) break;
89
+
90
+ // if parent requested completion during tick, stop after this tick
91
+ if (widget.isCompleted) {
92
+ _stopRequested = true;
93
+ break;
94
+ }
95
+ }
96
+
97
+ // If stop requested, break out of the outer while
98
+ if (_stopRequested || !mounted) break;
99
+
100
+ // Completed all steps β€” loop again from step 0
101
+ // continue while loop -> next for-loop iteration restarts steps
102
+ }
103
+
104
+ // Ensure final UI shows tick on whatever the currentStep was
105
+ if (mounted) {
106
+ setState(() => _showTick = true);
107
+ }
108
+ _running = false;
109
+ }
110
+
111
+ @override
112
+ void didUpdateWidget(covariant LoadingIndicator oldWidget) {
113
+ super.didUpdateWidget(oldWidget);
114
+
115
+ // If parent toggles completion on, request stop so loop finishes current step
116
+ if (!oldWidget.isCompleted && widget.isCompleted) {
117
+ _stopRequested = true;
118
+ }
119
+
120
+ // If parent re-enables (isCompleted went false) restart loop if not running
121
+ if (oldWidget.isCompleted && !widget.isCompleted) {
122
+ _stopRequested = false;
123
+ if (!_running) _startLoop();
124
+ }
125
+ }
126
+
127
+ @override
128
+ void dispose() {
129
+ _stopRequested = true;
130
+ super.dispose();
131
+ }
132
+
133
+ @override
134
+ Widget build(BuildContext context) {
135
+ final String text = _steps[_currentStep];
136
+
137
+ // Single Row β€” only the text and icon inside AnimatedSwitchers change.
138
+ return Row(
139
+ mainAxisSize: MainAxisSize.min,
140
+ children: [
141
+ // Animated replace of text (one-line only)
142
+ AnimatedSwitcher(
143
+ duration: const Duration(milliseconds: 300),
144
+ transitionBuilder: (child, animation) =>
145
+ FadeTransition(opacity: animation, child: child),
146
+ child: Text(
147
+ text,
148
+ key: ValueKey<int>(_currentStep),
149
+ style: const TextStyle(
150
+ color: Colors.white,
151
+ fontSize: 16,
152
+ ),
153
+ ),
154
+ ),
155
+ const SizedBox(width: 10),
156
+ // Animated replace of spinner <-> tick
157
+ AnimatedSwitcher(
158
+ duration: const Duration(milliseconds: 250),
159
+ transitionBuilder: (child, animation) =>
160
+ ScaleTransition(scale: animation, child: child),
161
+ child: _showTick
162
+ ? const Icon(
163
+ Icons.check,
164
+ key: ValueKey('tick'),
165
+ color: Colors.green,
166
+ size: 20,
167
+ )
168
+ : const SizedBox(
169
+ key: ValueKey('spinner'),
170
+ width: 20,
171
+ height: 20,
172
+ child: CircularProgressIndicator(
173
+ strokeWidth: 2,
174
+ valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
175
+ ),
176
+ ),
177
+ ),
178
+ ],
179
+ );
180
+ }
181
+ }
misinformationui/lib/main.dart ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'package:flutter/material.dart';
2
+ import 'chat_screen.dart';
3
+
4
+ void main() {
5
+ runApp(const MyApp());
6
+ }
7
+
8
+ class MyApp extends StatelessWidget {
9
+ const MyApp({super.key});
10
+
11
+ @override
12
+ Widget build(BuildContext context) {
13
+ return MaterialApp(
14
+ debugShowCheckedModeBanner: false,
15
+ title: 'MisinfoGuard',
16
+ theme: ThemeData(
17
+ brightness: Brightness.dark,
18
+ scaffoldBackgroundColor: const Color(0xFF171717),
19
+ useMaterial3: true,
20
+ ),
21
+ home: const ChatScreen(),
22
+ );
23
+ }
24
+ }
25
+