Breadknife commited on
Commit
e363da7
Β·
1 Parent(s): 4e35284

Stable state: Localhost fixed with Tailwind v3 restoration

Browse files
STABLE_BACKUP/Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE 1
6
+ ENV PYTHONUNBUFFERED 1
7
+
8
+ # Set the working directory in the container
9
+ WORKDIR /app
10
+
11
+ # Install system dependencies
12
+ RUN apt-get update && apt-get install -y \
13
+ build-essential \
14
+ curl \
15
+ software-properties-common \
16
+ git \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Copy requirements.txt and install dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy the rest of the application code
24
+ COPY . .
25
+
26
+ # Expose the port the app runs on
27
+ EXPOSE 7860
28
+
29
+ # Command to run the application
30
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
STABLE_BACKUP/NewsApex/.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
42
+
43
+ # model
44
+ bias_module/models/
45
+ bias_module/__pycache__/
46
+ bias_module/venv/
STABLE_BACKUP/NewsApex/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NewsApex
2
+
3
+ ### Prerequisites
4
+
5
+ - Node.js 18.x or higher
6
+ - A free API key from [NewsAPI.org](https://newsapi.org/)
7
+
8
+ ### Installation
9
+
10
+ 1. Clone the repository or navigate to the project directory:
11
+
12
+ ```bash
13
+ cd news_apex
14
+ ```
15
+
16
+ 2. Install dependencies:
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ 3. Set up your environment variables:
23
+
24
+ - Copy `.env.local` and add your NewsAPI key:
25
+
26
+ ```
27
+ NEXT_PUBLIC_NEWS_API_KEY=your_actual_api_key_here
28
+ ```
29
+
30
+ 4. Get your free API key:
31
+ - Visit [https://newsapi.org/](https://newsapi.org/)
32
+ - Sign up for a free account
33
+ - Copy your API key
34
+ - Paste it in the `.env.local` file
35
+
36
+ 5. Run the development server:
37
+
38
+ ```bash
39
+ npm run dev
40
+ ```
41
+
42
+ 6. Open [http://localhost:3000](http://localhost:3000) in your browser
43
+
44
+ ## Deployment
45
+
46
+ To build for production:
47
+
48
+ ```bash
49
+ npm run build
50
+ npm start
51
+ ```
52
+
53
+ Deploy to Vercel:
54
+
55
+ ```bash
56
+ npx vercel
57
+ ```
58
+
59
+ Make sure to add your `NEXT_PUBLIC_NEWS_API_KEY` environment variable in your Vercel project settings.
60
+
61
+ ## Limitations
62
+
63
+ - NewsAPI free tier has limitations (500 requests/day for development)
64
+ - Some news sources may block image loading from external domains
65
+ - Historical news data is limited on the free tier
66
+
67
+ ## License
68
+
69
+ MIT
70
+
71
+ ## Support
72
+
73
+ For issues or questions, please visit [NewsAPI Documentation](https://newsapi.org/docs)
STABLE_BACKUP/NewsApex/__init__.py ADDED
File without changes
STABLE_BACKUP/NewsApex/app/api/bias/route.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+
5
+ export const maxDuration = 60; // Set max duration to 60 seconds
6
+
7
+ const IS_VERCEL = process.env.VERCEL === '1';
8
+
9
+ export async function POST(request) {
10
+ try {
11
+ const body = await request.json();
12
+ const articleUrl = body.url || body.articleUrl;
13
+ const action = body.action || 'analyze_bias';
14
+ const existingContent = body.content || body.full_content;
15
+
16
+ if (!articleUrl && !existingContent) {
17
+ return NextResponse.json({ error: 'Article URL or content is required' }, { status: 400 });
18
+ }
19
+
20
+ // πŸ”Ή Restore Python Bridge as the Primary Logic (Fixes Local Host)
21
+ const resultData = await new Promise((resolve, reject) => {
22
+ const scriptPath = path.join(process.cwd(), 'bridge_logic.py');
23
+ const pythonCommand = process.platform === 'win32' ? 'python' : 'python3';
24
+
25
+ const args = [scriptPath, action];
26
+ if (articleUrl) args.push('--url', articleUrl);
27
+ if (existingContent) args.push('--content', existingContent);
28
+
29
+ const pythonProcess = spawn(pythonCommand, args);
30
+ let output = '';
31
+ let error = '';
32
+
33
+ // πŸ”Ή Environment-Aware Timeout
34
+ // On Vercel, we must finish in 10s. On Local, we can wait much longer for BERT.
35
+ const timeoutLimit = IS_VERCEL ? 9000 : 120000; // 9s for Vercel, 2 mins for Local
36
+
37
+ const timeout = setTimeout(() => {
38
+ pythonProcess.kill();
39
+ resolve({
40
+ error: IS_VERCEL
41
+ ? 'Analysis timed out on the server. The local BERT model is too heavy for Vercel.'
42
+ : 'Local analysis timed out. Check if your Python environment is responsive.'
43
+ });
44
+ }, timeoutLimit);
45
+
46
+ pythonProcess.stdout.on('data', (data) => { output += data.toString(); });
47
+ pythonProcess.stderr.on('data', (data) => { error += data.toString(); });
48
+
49
+ pythonProcess.on('close', (code) => {
50
+ clearTimeout(timeout);
51
+ if (code !== 0) {
52
+ try {
53
+ const errData = JSON.parse(error);
54
+ resolve(errData);
55
+ } catch (e) {
56
+ resolve({ error: `Python Error (Code ${code}): ${error || 'Unknown error'}` });
57
+ }
58
+ } else {
59
+ try {
60
+ resolve(JSON.parse(output));
61
+ } catch (e) {
62
+ console.error(`Failed to parse bias output. Raw output: ${output}`);
63
+ reject(new Error(`Backend error: ${output.substring(0, 100)}...`));
64
+ }
65
+ }
66
+ });
67
+ });
68
+
69
+ return NextResponse.json(resultData);
70
+ } catch (error) {
71
+ console.error('API Error:', error);
72
+ return NextResponse.json(
73
+ { error: error.message || 'Analysis failed' },
74
+ { status: 500 }
75
+ );
76
+ }
77
+ }
STABLE_BACKUP/NewsApex/app/api/news/route.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+
5
+ export const maxDuration = 60; // Set max duration to 60 seconds
6
+
7
+ export async function GET(request) {
8
+ const { searchParams } = new URL(request.url);
9
+ const query = searchParams.get('query') || '';
10
+ const category = searchParams.get('category') || '';
11
+
12
+ try {
13
+ const newsData = await new Promise((resolve, reject) => {
14
+ const args = ['bridge_logic.py', 'fetch_news'];
15
+ if (query) {
16
+ args.push('--query', query);
17
+ }
18
+ if (category) {
19
+ args.push('--category', category);
20
+ }
21
+
22
+ const scriptPath = path.join(process.cwd(), 'bridge_logic.py');
23
+ // Use python3 for Linux/Vercel environment
24
+ const pythonCommand = process.platform === 'win32' ? 'python' : 'python3';
25
+ const pythonProcess = spawn(pythonCommand, [scriptPath, ...args.slice(1)], {});
26
+
27
+ // Generous timeout for local host
28
+ const timeout = setTimeout(() => {
29
+ pythonProcess.kill();
30
+ reject(new Error('Backend process timed out. The news fetch is taking too long.'));
31
+ }, 60000);
32
+
33
+ let output = '';
34
+ let error = '';
35
+
36
+ pythonProcess.stdout.on('data', (data) => {
37
+ output += data.toString();
38
+ });
39
+
40
+ pythonProcess.stderr.on('data', (data) => {
41
+ error += data.toString();
42
+ });
43
+
44
+ pythonProcess.on('close', (code) => {
45
+ clearTimeout(timeout);
46
+ if (code !== 0) {
47
+ console.error(`Python process exited with code ${code}. Error: ${error}`);
48
+ try {
49
+ const errData = JSON.parse(error);
50
+ resolve(errData);
51
+ } catch (e) {
52
+ resolve({ error: `Python Error (Code ${code}): ${error || 'Unknown error'}` });
53
+ }
54
+ } else {
55
+ try {
56
+ resolve(JSON.parse(output));
57
+ } catch (e) {
58
+ console.error(`Failed to parse news output. Raw output: ${output}`);
59
+ reject(new Error(`Backend error: ${output.substring(0, 100)}...`));
60
+ }
61
+ }
62
+ });
63
+ });
64
+
65
+ return NextResponse.json(newsData);
66
+ } catch (error) {
67
+ console.error('Error fetching news:', error);
68
+ return NextResponse.json(
69
+ { error: error.message || 'Failed to fetch news articles' },
70
+ { status: 500 }
71
+ );
72
+ }
73
+ }
STABLE_BACKUP/NewsApex/app/components/BiasModal.jsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export default function BiasModal({
6
+ isOpen,
7
+ onClose,
8
+ article,
9
+ biasData,
10
+ isLoading,
11
+ loadingStage,
12
+ error,
13
+ onRunBiasAnalysis
14
+ }) {
15
+ if (!isOpen) return null;
16
+
17
+ const handleReadArticle = () => {
18
+ window.open(article.url, '_blank', 'noopener,noreferrer');
19
+ onClose();
20
+ };
21
+
22
+ const getBiasColor = (level) => {
23
+ switch (level) {
24
+ case 'Low': return 'text-green-600';
25
+ case 'Medium': return 'text-yellow-600';
26
+ case 'High': return 'text-red-600';
27
+ default: return 'text-gray-600';
28
+ }
29
+ };
30
+
31
+ const getBiasBgColor = (level) => {
32
+ switch (level) {
33
+ case 'Low': return 'bg-green-50';
34
+ case 'Medium': return 'bg-yellow-50';
35
+ case 'High': return 'bg-red-50';
36
+ default: return 'bg-gray-50';
37
+ }
38
+ };
39
+
40
+ const getBiasBadgeColor = (level) => {
41
+ switch (level) {
42
+ case 'Low': return 'bg-green-100 text-green-800';
43
+ case 'Medium': return 'bg-yellow-100 text-yellow-800';
44
+ case 'High': return 'bg-red-100 text-red-800';
45
+ default: return 'bg-gray-100 text-gray-800';
46
+ }
47
+ };
48
+
49
+ return (
50
+ <div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
51
+ <div className="bg-white rounded-xl shadow-2xl max-w-6xl w-full max-h-[90vh] flex flex-col overflow-hidden">
52
+ {/* Header */}
53
+ <div className="bg-white border-b border-gray-200 p-5 flex justify-between items-center shrink-0">
54
+ <div>
55
+ <h2 className="text-xl font-bold text-gray-900 flex items-center gap-2">
56
+ Media Bias Analysis
57
+ </h2>
58
+ <p className="text-xs text-gray-500 mt-0.5">
59
+ Powered by BERT-BABE Linguistic Analysis
60
+ </p>
61
+ </div>
62
+ <button
63
+ onClick={onClose}
64
+ className="text-gray-400 hover:text-gray-600 transition-colors p-2 hover:bg-gray-100 rounded-full"
65
+ aria-label="Close modal"
66
+ >
67
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
69
+ </svg>
70
+ </button>
71
+ </div>
72
+
73
+ {/* Split Content Area */}
74
+ <div className="flex flex-1 overflow-hidden">
75
+ {/* Left Column: Summary & Analysis Results */}
76
+ <div className="w-1/3 border-r border-gray-100 overflow-y-auto p-6 bg-gray-50/50">
77
+ {/* Article Info */}
78
+ <div className="mb-6">
79
+ <h3 className="font-bold text-gray-900 text-base mb-1 leading-tight">
80
+ {article?.title}
81
+ </h3>
82
+ <p className="text-xs font-medium text-gray-500 uppercase tracking-wider">
83
+ {article?.source?.name || 'Unknown Source'}
84
+ </p>
85
+ </div>
86
+
87
+ {/* Summary Section */}
88
+ <div className="mb-6">
89
+ <h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-3">Executive Summary</h4>
90
+ {biasData?.summary ? (
91
+ <div className="bg-white p-4 rounded-xl border border-blue-100 shadow-sm">
92
+ <p className="text-sm text-gray-700 leading-relaxed italic">
93
+ "{biasData.summary}"
94
+ </p>
95
+ </div>
96
+ ) : (
97
+ <div className="bg-white p-4 rounded-xl border border-gray-200 shadow-sm border-dashed">
98
+ <p className="text-sm text-gray-400 italic">
99
+ Summary unavailable. Analysis can still proceed on the full text.
100
+ </p>
101
+ </div>
102
+ )}
103
+ </div>
104
+
105
+ {/* Overall Analysis Result */}
106
+ {isLoading ? (
107
+ <div className="flex flex-col items-center justify-center py-12 bg-white rounded-xl border border-gray-100 shadow-sm">
108
+ <div className="animate-spin rounded-full h-10 w-10 border-b-2 border-red-500 mb-4"></div>
109
+ <p className="text-gray-500 text-xs font-bold uppercase tracking-widest animate-pulse text-center px-4">
110
+ {loadingStage === 'extracting' ? 'Extracting article...' : 'Analyzing article...'}
111
+ </p>
112
+ </div>
113
+ ) : error ? (
114
+ <div className="bg-red-50 border border-red-200 rounded-xl p-4 text-center">
115
+ <p className="text-red-700 text-sm font-bold mb-1">Analysis Failed</p>
116
+ <p className="text-red-600 text-xs">{error}</p>
117
+ </div>
118
+ ) : biasData?.bias_level ? (
119
+ <div className="space-y-6">
120
+ {/* Result Card */}
121
+ <div className={`${getBiasBgColor(biasData.bias_level)} rounded-xl p-5 border border-opacity-20 shadow-sm text-center`}>
122
+ <div className="text-4xl font-black mb-1">
123
+ <span className={getBiasColor(biasData.bias_level)}>
124
+ {biasData.bias_score}%
125
+ </span>
126
+ </div>
127
+ <span className={`inline-block px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${getBiasBadgeColor(biasData.bias_level)} mb-4`}>
128
+ Bias Score
129
+ </span>
130
+ <p className="text-xs text-gray-700 font-medium leading-relaxed">
131
+ {biasData.explanation}
132
+ </p>
133
+ </div>
134
+
135
+ {/* Top Biased Words */}
136
+ {biasData?.top_words && biasData.top_words.length > 0 && biasData.bias_level !== 'Low' && (
137
+ <div>
138
+ <h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-3">Key Biased Markers</h4>
139
+ <div className="flex flex-wrap gap-1.5">
140
+ {biasData.top_words.map((item, idx) => (
141
+ <div key={idx} className="bg-white border border-red-100 rounded-lg px-2.5 py-1.5 flex items-center gap-2 shadow-sm">
142
+ <span className="text-xs font-bold text-red-700">{item.word}</span>
143
+ <span className="text-[9px] bg-red-50 text-red-500 px-1 rounded font-mono font-bold">
144
+ {item.score}
145
+ </span>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>
152
+ ) : (
153
+ <div className="space-y-6">
154
+ <div className="flex justify-center">
155
+ <button
156
+ onClick={onRunBiasAnalysis}
157
+ className="w-full bg-red-600 hover:bg-red-700 text-white font-black text-xs uppercase tracking-widest py-4 px-6 rounded-xl shadow-lg shadow-red-200 transform transition active:scale-95 flex items-center justify-center gap-2"
158
+ >
159
+ Start Deep Analysis
160
+ </button>
161
+ </div>
162
+
163
+ {/* Grading Threshold Guide */}
164
+ <div className="bg-white border border-gray-100 rounded-xl p-4 shadow-sm">
165
+ <h4 className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-3">Bias Grading Scale</h4>
166
+ <div className="space-y-3">
167
+ <div className="w-full bg-gray-100 h-2 rounded-full overflow-hidden flex shadow-inner">
168
+ <div className="bg-green-400 h-full w-1/2"></div>
169
+ <div className="bg-yellow-400 h-full w-[20%]"></div>
170
+ <div className="bg-red-500 h-full w-[30%]"></div>
171
+ </div>
172
+ <div className="space-y-1.5">
173
+ <div className="flex items-center justify-between text-[10px]">
174
+ <span className="text-green-600 font-bold">0% - 50%</span>
175
+ <span className="text-gray-500 font-medium italic">Likely Factual</span>
176
+ </div>
177
+ <div className="flex items-center justify-between text-[10px]">
178
+ <span className="text-yellow-600 font-bold">51% - 70%</span>
179
+ <span className="text-gray-500 font-medium italic text-right">Likely Biased</span>
180
+ </div>
181
+ <div className="flex items-center justify-between text-[10px]">
182
+ <span className="text-red-600 font-bold">71% - 100%</span>
183
+ <span className="text-gray-500 font-medium italic text-right">Strongly Biased</span>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ )}
190
+ </div>
191
+
192
+ {/* Right Column: Full Article with Highlighting */}
193
+ <div className="flex-1 overflow-y-auto p-8 bg-white">
194
+ <h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-6">Full Article Context</h4>
195
+
196
+ <div className="prose prose-sm max-w-none">
197
+ {!biasData?.sentence_breakdown ? (
198
+ <div className="space-y-4">
199
+ {(biasData?.full_content || article?.description || 'No content available for preview.').split('\n').map((para, i) => (
200
+ <p key={i} className="text-gray-400 leading-relaxed select-none">
201
+ {para}
202
+ </p>
203
+ ))}
204
+ {!biasData?.bias_level && !isLoading && (
205
+ <div className="bg-gray-50 rounded-xl p-8 text-center border-2 border-dashed border-gray-200">
206
+ <p className="text-gray-400 text-sm italic">
207
+ Run the analysis to see bias highlighting in the full text.
208
+ </p>
209
+ </div>
210
+ )}
211
+ </div>
212
+ ) : (
213
+ <div className="space-y-1">
214
+ {biasData.sentence_breakdown.map((s, idx) => {
215
+ const isBiased = s.label === 'Biased';
216
+ const highlightClass = isBiased
217
+ ? 'bg-red-100/80 border-b-2 border-red-300 hover:bg-red-200 transition-colors cursor-help'
218
+ : 'bg-green-100/60 border-b-2 border-green-200 hover:bg-green-200 transition-colors cursor-help';
219
+
220
+ return (
221
+ <span
222
+ key={idx}
223
+ className={`inline p-0.5 rounded-sm text-gray-900 leading-[1.8] text-sm group relative ${highlightClass}`}
224
+ title={s.reasoning || (isBiased ? 'Flagged as potentially biased' : 'Classified as neutral reporting')}
225
+ >
226
+ {s.text}{' '}
227
+ {/* Tooltip on hover */}
228
+ <span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10 w-48 text-center font-medium leading-normal">
229
+ {s.label}: {s.score}%
230
+ {s.reasoning && <div className="mt-1 text-gray-400 italic font-normal">"{s.reasoning}"</div>}
231
+ </span>
232
+ </span>
233
+ );
234
+ })}
235
+ </div>
236
+ )}
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Footer */}
242
+ <div className="sticky bottom-0 bg-white border-t border-gray-100 p-5 flex gap-4 shrink-0">
243
+ <button
244
+ onClick={handleReadArticle}
245
+ disabled={isLoading}
246
+ className="flex-1 bg-gray-900 hover:bg-black disabled:bg-gray-200 text-white font-black text-xs uppercase tracking-widest py-3 rounded-lg transition-all"
247
+ >
248
+ Visit Original Site
249
+ </button>
250
+ <button
251
+ onClick={onClose}
252
+ className="flex-1 border-2 border-gray-100 hover:bg-gray-50 text-gray-500 font-black text-xs uppercase tracking-widest py-3 rounded-lg transition-all"
253
+ >
254
+ Close Analysis
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
STABLE_BACKUP/NewsApex/app/components/CategoryFilter.jsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ export default function CategoryFilter({ activeCategory, onCategoryChange }) {
4
+ const categories = [
5
+ { id: 'general', name: 'General'},
6
+ { id: 'business', name: 'Business',},
7
+ { id: 'technology', name: 'Technology'},
8
+ { id: 'entertainment', name: 'Entertainment'},
9
+ { id: 'health', name: 'Health'},
10
+ { id: 'science', name: 'Science'},
11
+ { id: 'sports', name: 'Sports'},
12
+ ];
13
+
14
+ return (
15
+ <nav className="ml-10 mr-4" aria-label="Categories">
16
+ <ol className="flex items-center justify-center text-sm text-gray-600">
17
+ {categories.map((category, idx) => {
18
+ const isActive = activeCategory === category.id;
19
+ return (
20
+ <li key={category.id} className="flex items-center">
21
+ <button
22
+ onClick={() => onCategoryChange(category.id)}
23
+ className={`px-1 py-1 transition-all focus:outline-none ${
24
+ isActive
25
+ ? 'text-blue-600 border-b-2 border-blue-600 font-semibold'
26
+ : 'hover:text-blue-600 hover:border-b-2 hover:border-blue-600 border-b-2 border-transparent'
27
+ }`}
28
+ >
29
+ <span>{category.name}</span>
30
+ </button>
31
+
32
+ {idx < categories.length - 1 && (
33
+ <span className="mx-3 text-gray-400 select-none">β€Ί</span>
34
+ )}
35
+ </li>
36
+ );
37
+ })}
38
+ </ol>
39
+ </nav>
40
+ );
41
+ }
STABLE_BACKUP/NewsApex/app/components/LoadingSkeleton.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function LoadingSkeleton() {
2
+ return (
3
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
4
+ {[...Array(6)].map((_, index) => (
5
+ <div key={index} className="bg-white rounded-lg shadow-md overflow-hidden animate-pulse">
6
+ <div className="w-full h-48 bg-gray-300"></div>
7
+ <div className="p-5">
8
+ <div className="flex items-center justify-between mb-2">
9
+ <div className="h-4 bg-gray-300 rounded w-24"></div>
10
+ <div className="h-3 bg-gray-300 rounded w-20"></div>
11
+ </div>
12
+ <div className="h-6 bg-gray-300 rounded w-full mb-2"></div>
13
+ <div className="h-6 bg-gray-300 rounded w-3/4 mb-3"></div>
14
+ <div className="h-4 bg-gray-300 rounded w-full mb-2"></div>
15
+ <div className="h-4 bg-gray-300 rounded w-5/6"></div>
16
+ </div>
17
+ </div>
18
+ ))}
19
+ </div>
20
+ );
21
+ }
STABLE_BACKUP/NewsApex/app/components/NewsCard.jsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export default function NewsCard({ article, onArticleClick }) {
6
+ const {
7
+ title,
8
+ description,
9
+ url,
10
+ urlToImage,
11
+ publishedAt,
12
+ source,
13
+ author,
14
+ content
15
+ } = article;
16
+
17
+ const [imgError, setImgError] = useState(false);
18
+
19
+ const formatDate = (dateString) => {
20
+ const date = new Date(dateString);
21
+ return date.toLocaleDateString('en-US', {
22
+ year: 'numeric',
23
+ month: 'short',
24
+ day: 'numeric',
25
+ hour: '2-digit',
26
+ minute: '2-digit'
27
+ });
28
+ };
29
+
30
+ const isValidImage =
31
+ urlToImage &&
32
+ typeof urlToImage === "string" &&
33
+ urlToImage.startsWith("http") &&
34
+ !imgError;
35
+
36
+ // We show the card even without a perfect image, using a placeholder
37
+ // if (!isValidImage) return null;
38
+
39
+ const handleClick = (e) => {
40
+ e.preventDefault();
41
+ if (onArticleClick) {
42
+ onArticleClick(article);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300 cursor-pointer h-full flex flex-col">
48
+ <button
49
+ onClick={handleClick}
50
+ className="block w-full text-left hover:opacity-95 transition-opacity h-full flex flex-col"
51
+ style={{ border: 'none', background: 'none', padding: 0 }}
52
+ >
53
+ <div className="relative w-full h-48 bg-gray-100 shrink-0">
54
+ {isValidImage ? (
55
+ <img
56
+ src={urlToImage}
57
+ alt={title || 'News image'}
58
+ className="w-full h-full object-cover"
59
+ onError={() => {
60
+ setImgError(true);
61
+ }}
62
+ loading="lazy"
63
+ />
64
+ ) : (
65
+ <div className="flex items-center justify-center h-full text-gray-300 bg-gray-50 border-b border-gray-100">
66
+ <span className="text-2xl opacity-50">πŸ“°</span>
67
+ </div>
68
+ )}
69
+ </div>
70
+
71
+ <div className="p-5 flex-1 flex flex-col">
72
+ <div className="flex items-center justify-between mb-2">
73
+ <span className="text-sm font-semibold text-blue-600">
74
+ {source?.name || 'Unknown Source'}
75
+ </span>
76
+ <time className="text-xs text-gray-500">
77
+ {formatDate(publishedAt)}
78
+ </time>
79
+ </div>
80
+
81
+ <h2 className="text-xl font-bold text-gray-900 mb-2 line-clamp-2 hover:text-blue-600 transition-colors">
82
+ {title}
83
+ </h2>
84
+
85
+ {description && (
86
+ <p className="text-gray-600 text-sm line-clamp-3 mb-3">
87
+ {description}
88
+ </p>
89
+ )}
90
+
91
+ {author && (
92
+ <p className="text-xs text-gray-500">
93
+ By {author}
94
+ </p>
95
+ )}
96
+ </div>
97
+ </button>
98
+ </article>
99
+ );
100
+ }
STABLE_BACKUP/NewsApex/app/favicon.ico ADDED
STABLE_BACKUP/NewsApex/app/globals.css ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: #ffffff;
7
+ --foreground: #171717;
8
+ }
9
+
10
+ @theme inline {
11
+ --color-background: var(--background);
12
+ --color-foreground: var(--foreground);
13
+ --font-sans: var(--font-geist-sans);
14
+ --font-mono: var(--font-geist-mono);
15
+ }
16
+
17
+ @media (prefers-color-scheme: dark) {
18
+ :root {
19
+ --background: #0a0a0a;
20
+ --foreground: #ededed;
21
+ }
22
+ }
23
+
24
+ body {
25
+ background: var(--background);
26
+ color: var(--foreground);
27
+ font-family: Arial, Helvetica, sans-serif;
28
+ }
STABLE_BACKUP/NewsApex/app/layout.jsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Geist, Geist_Mono } from "next/font/google";
2
+ import "./globals.css";
3
+
4
+ const geistSans = Geist({
5
+ variable: "--font-geist-sans",
6
+ subsets: ["latin"],
7
+ });
8
+
9
+ const geistMono = Geist_Mono({
10
+ variable: "--font-geist-mono",
11
+ subsets: ["latin"],
12
+ });
13
+
14
+ export const metadata = {
15
+ description: "Stay informed with the latest news from around the world",
16
+ };
17
+
18
+ export default function RootLayout({ children }) {
19
+ return (
20
+ <html lang="en">
21
+ <body
22
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
23
+ >
24
+ {children}
25
+ </body>
26
+ </html>
27
+ );
28
+ }
STABLE_BACKUP/NewsApex/app/page.jsx ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from "react";
4
+ import Link from 'next/link';
5
+ import NewsCard from './components/NewsCard';
6
+ import CategoryFilter from './components/CategoryFilter';
7
+ import LoadingSkeleton from './components/LoadingSkeleton';
8
+ import BiasModal from './components/BiasModal';
9
+
10
+ export default function Home() {
11
+ const [articles, setArticles] = useState([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [error, setError] = useState(null);
14
+ const [activeCategory, setActiveCategory] = useState('general');
15
+ const [searchQuery, setSearchQuery] = useState('');
16
+ const [submittedQuery, setSubmittedQuery] = useState('');
17
+
18
+ // Bias Modal State
19
+ const [isModalOpen, setIsModalOpen] = useState(false);
20
+ const [selectedArticle, setSelectedArticle] = useState(null);
21
+ const [biasData, setBiasData] = useState(null);
22
+ const [biasLoading, setBiasLoading] = useState(false);
23
+ const [biasError, setBiasError] = useState(null);
24
+ const [loadingStage, setLoadingStage] = useState('extracting'); // 'extracting' or 'analyzing'
25
+
26
+ useEffect(() => {
27
+ fetchNews();
28
+ }, [activeCategory]);
29
+
30
+ const fetchNews = async (query = '') => {
31
+ setLoading(true);
32
+ setError(null);
33
+
34
+ try {
35
+ const params = new URLSearchParams();
36
+ if (query) {
37
+ params.append('query', query);
38
+ } else {
39
+ params.append('category', activeCategory);
40
+ }
41
+
42
+ const response = await fetch(`/api/news?${params}`);
43
+
44
+ const text = await response.text();
45
+ let data;
46
+ try {
47
+ data = JSON.parse(text);
48
+ } catch (e) {
49
+ throw new Error(`Server error: ${text.substring(0, 200)}`);
50
+ }
51
+
52
+ if (!response.ok) {
53
+ throw new Error(data.error || 'Failed to fetch news');
54
+ }
55
+
56
+ setArticles(data.articles || []);
57
+ setSearchQuery(query);
58
+ } catch (err) {
59
+ console.error('Fetch error:', err);
60
+ setError(err.message);
61
+ setArticles([]);
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ };
66
+
67
+ const handleSearch = (query) => {
68
+ fetchNews(query);
69
+ };
70
+
71
+ const handleCategoryChange = (category) => {
72
+ setActiveCategory(category);
73
+ setSearchQuery('');
74
+ };
75
+
76
+ const handleArticleClick = async (article) => {
77
+ // Open modal with loading state for summary
78
+ setSelectedArticle(article);
79
+ setIsModalOpen(true);
80
+ setBiasLoading(true);
81
+ setLoadingStage('extracting');
82
+ setBiasError(null);
83
+ setBiasData(null);
84
+
85
+ try {
86
+ const response = await fetch('/api/bias', {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ },
91
+ body: JSON.stringify({
92
+ url: article.url,
93
+ action: 'get_summary'
94
+ }),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorData = await response.json();
99
+ throw new Error(errorData.error || 'Failed to get summary');
100
+ }
101
+
102
+ const result = await response.json();
103
+ setBiasData(result);
104
+ } catch (err) {
105
+ console.error('Summary error:', err);
106
+ setBiasError(err.message);
107
+ } finally {
108
+ setBiasLoading(false);
109
+ }
110
+ };
111
+
112
+ const handleRunBiasAnalysis = async () => {
113
+ if (!selectedArticle) return;
114
+
115
+ setBiasLoading(true);
116
+ setLoadingStage('analyzing');
117
+ setBiasError(null);
118
+
119
+ try {
120
+ const response = await fetch('/api/bias', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ body: JSON.stringify({
126
+ url: selectedArticle.url,
127
+ content: biasData?.full_content, // πŸ”Ή Pass already extracted content to save time
128
+ action: 'analyze_bias'
129
+ }),
130
+ });
131
+
132
+ if (!response.ok) {
133
+ const errorData = await response.json();
134
+ throw new Error(errorData.error || 'Failed to analyze bias');
135
+ }
136
+
137
+ const result = await response.json();
138
+ // Merge bias results with existing summary data
139
+ setBiasData(prev => ({
140
+ ...prev,
141
+ ...result
142
+ }));
143
+ } catch (err) {
144
+ console.error('Bias analysis error:', err);
145
+ setBiasError(err.message);
146
+ } finally {
147
+ setBiasLoading(false);
148
+ }
149
+ };
150
+
151
+ const handleCloseModal = () => {
152
+ setIsModalOpen(false);
153
+ setSelectedArticle(null);
154
+ setBiasData(null);
155
+ setBiasError(null);
156
+ };
157
+
158
+ return (
159
+ <div className="min-h-screen bg-white text-gray-800">
160
+ {/* Header */}
161
+ <header className="bg-white shadow sticky top-0 z-50 border-b">
162
+ <div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between gap-4">
163
+
164
+
165
+ {/* Category Filter */}
166
+ {!searchQuery && (
167
+ <CategoryFilter
168
+ activeCategory={activeCategory}
169
+ onCategoryChange={handleCategoryChange}
170
+ />
171
+ ) || (
172
+ <CategoryFilter
173
+ activeCategory={''}
174
+ onCategoryChange={handleCategoryChange}
175
+ />
176
+ )}
177
+
178
+ {/* Search Bar*/}
179
+ <div className="ml-auto">
180
+ <form onSubmit={(e) => {
181
+ e.preventDefault();
182
+ if (!searchQuery.trim()) return;
183
+ setSubmittedQuery(searchQuery.trim());
184
+ setActiveCategory('');
185
+ handleSearch(searchQuery);
186
+ }} className="w-full max-w-md mb-0">
187
+ <div className="relative">
188
+ <input
189
+ type="text"
190
+ value={searchQuery}
191
+ onChange={(e) => setSearchQuery(e.target.value)}
192
+ placeholder="Search for..."
193
+ className="w-full px-4 py-2 text-gray-800 bg-gray-50 border border-gray-200"
194
+ />
195
+
196
+ <button
197
+ type="submit"
198
+ className="absolute inset-y-0 right-3 flex items-center text-gray-500 hover:text-gray-800"
199
+ >
200
+ <svg
201
+ xmlns="http://www.w3.org/2000/svg"
202
+ className="w-5 h-5"
203
+ fill="none"
204
+ viewBox="0 0 24 24"
205
+ stroke="currentColor"
206
+ >
207
+ <path
208
+ strokeLinecap="round"
209
+ strokeLinejoin="round"
210
+ strokeWidth={2}
211
+ d="M21 21l-4.35-4.35m1.6-5.65a7 7 0 11-14 0 7 7 0 0114 0z"
212
+ />
213
+ </svg>
214
+ </button>
215
+ </div>
216
+ </form>
217
+ </div>
218
+ </div>
219
+ </header>
220
+
221
+ {/* Main Content */}
222
+ <main className="max-w-7xl mx-auto px-4 py-8">
223
+ {/* Search Query Display */}
224
+ {submittedQuery && !loading && activeCategory === '' && (
225
+ <div className="mb-6 text-center">
226
+ <p className="text-gray-700">
227
+ Showing results for <span className="font-bold">{submittedQuery}</span>
228
+ </p>
229
+ </div>
230
+
231
+ )}
232
+
233
+ {/* Error State */}
234
+ {error && (
235
+ <div className="text-center py-12">
236
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 text-red-500 mb-4">
237
+ <svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
238
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
239
+ </svg>
240
+ </div>
241
+ <h3 className="text-lg font-semibold text-gray-900 mb-1">Unable to load news</h3>
242
+ <p className="text-gray-600 mb-6 max-w-md mx-auto">{error}</p>
243
+ <button
244
+ onClick={() => fetchNews(searchQuery)}
245
+ className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
246
+ >
247
+ Try Again
248
+ </button>
249
+ </div>
250
+ )}
251
+
252
+ {/* Loading State */}
253
+ {loading && <LoadingSkeleton />}
254
+
255
+ {/* News Grid */}
256
+ {!loading && articles.length > 0 && (
257
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
258
+ {articles.map((article, index) => (
259
+ <NewsCard
260
+ key={`${article.url}-${index}`}
261
+ article={article}
262
+ onArticleClick={handleArticleClick}
263
+ />
264
+ ))}
265
+ </div>
266
+ )}
267
+
268
+ {/* No Results */}
269
+ {!loading && articles.length === 0 && !error && (
270
+ <div className="text-center py-12">
271
+ <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
272
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
273
+ </svg>
274
+ <h3 className="mt-2 text-sm font-medium text-gray-900">No articles found</h3>
275
+ <p className="mt-1 text-sm text-gray-500">Try searching for something else or choose a different category.</p>
276
+ </div>
277
+ )}
278
+ </main>
279
+
280
+ {/* Bias Modal */}
281
+ <BiasModal
282
+ isOpen={isModalOpen}
283
+ onClose={handleCloseModal}
284
+ article={selectedArticle}
285
+ biasData={biasData}
286
+ isLoading={biasLoading}
287
+ loadingStage={loadingStage}
288
+ error={biasError}
289
+ onRunBiasAnalysis={handleRunBiasAnalysis}
290
+ />
291
+
292
+ {/* Footer */}
293
+ <footer className="bg-[#1b1b1b] mt-16 border-t border-gray-200">
294
+ <div className="max-w-7xl mx-auto px-4 py-6 text-center text-sm text-gray-300">
295
+ <div>
296
+ <span>
297
+ &copy; {new Date().getFullYear()} NewsApex. All rights reserved.
298
+ </span>
299
+ <span className='text-blue-200 font-bold'> NEXTER </span>
300
+ </div>
301
+ </div>
302
+ </footer>
303
+ </div>
304
+ );
305
+ }
STABLE_BACKUP/NewsApex/bridge_logic.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+ import json
5
+ import argparse
6
+
7
+ # Add current directory to sys.path to access our news_service and model locally
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ if current_dir not in sys.path:
10
+ sys.path.insert(0, current_dir)
11
+
12
+ from news_service import NewsService
13
+
14
+ # Helper to print only valid JSON to stdout
15
+ def print_json(data):
16
+ # Ensure stdout is clean by flushing and then printing our JSON
17
+ sys.stdout.flush()
18
+ print(json.dumps(data))
19
+ sys.stdout.flush()
20
+
21
+ def main():
22
+ try:
23
+ # Check for essential modules before starting
24
+ try:
25
+ import requests
26
+ except ImportError:
27
+ print_json({"error": "Essential Python libraries (requests) are not installed on the server. Deployment environment is incomplete."})
28
+ return
29
+
30
+ parser = argparse.ArgumentParser(description='Bridge logic for NewsApex UI')
31
+ parser.add_argument('action', choices=['fetch_news', 'get_summary', 'analyze_bias'], help='Action to perform')
32
+ parser.add_argument('--query', help='Search query for news')
33
+ parser.add_argument('--category', help='News category')
34
+ parser.add_argument('--url', help='Article URL for analysis')
35
+ parser.add_argument('--content', help='Direct article content')
36
+
37
+ args = parser.parse_args()
38
+ service = NewsService()
39
+
40
+ if args.action == 'fetch_news':
41
+ # Use our filtered news fetching logic with images
42
+ articles = service.fetch_all_news(query=args.query, category=args.category)
43
+ transformed = []
44
+ for a in articles:
45
+ transformed.append({
46
+ "title": a.get("title"),
47
+ "url": a.get("link"),
48
+ "source": {"name": a.get("source_id")},
49
+ "publishedAt": a.get("pubDate"),
50
+ "urlToImage": a.get("image_url"),
51
+ "description": a.get("snippet")
52
+ })
53
+ # Ensure only the JSON is printed to stdout
54
+ print(json.dumps({"articles": transformed}))
55
+ return
56
+
57
+ elif args.action == 'get_summary':
58
+ if not args.url and not args.content:
59
+ print_json({"error": "URL or content required"})
60
+ return
61
+
62
+ # 1. Get content
63
+ content = args.content or service.get_full_content(args.url)
64
+ if not content:
65
+ print_json({"error": "Could not retrieve content for this article."})
66
+ return
67
+
68
+ # 2. Summarization (truncated)
69
+ summary = service.summarize_content(content[:2000])
70
+
71
+ print_json({
72
+ "summary": summary,
73
+ "full_content": content # We keep this for bias analysis later
74
+ })
75
+
76
+ elif args.action == 'analyze_bias':
77
+ if not args.url and not args.content:
78
+ print_json({"error": "URL or content required"})
79
+ return
80
+
81
+ # 1. Get content
82
+ content = args.content or service.get_full_content(args.url)
83
+ if not content:
84
+ print_json({"error": "Could not retrieve content for this article."})
85
+ return
86
+
87
+ # 2. Analyze overall bias
88
+ overall = service.rate_bias(content)
89
+
90
+ # 3. Sentence-by-sentence analysis (Analyze a subset of the article for highlighting)
91
+ sentences = service.split_into_sentences(content)
92
+ # Limit to the first 25 sentences to avoid timeout, while still giving a good breakdown
93
+ analysis_sentences = sentences[:25]
94
+
95
+ batch_results = service.rate_bias_batch(analysis_sentences)
96
+
97
+ sentence_results = []
98
+ factual_count = 0
99
+ biased_count = 0
100
+
101
+ for i, res in enumerate(batch_results):
102
+ label = res["label"]
103
+ if label == "Biased":
104
+ biased_count += 1
105
+ else:
106
+ factual_count += 1
107
+
108
+ # Always show the raw bias score from the model
109
+ display_score = res["score"]
110
+
111
+ sentence_results.append({
112
+ "text": analysis_sentences[i],
113
+ "label": label,
114
+ "score": round(display_score * 100, 1),
115
+ "reasoning": res.get("reasoning", "")
116
+ })
117
+
118
+ # Determine overall level
119
+ bias_prob = overall["score"]
120
+ if bias_prob > 0.7:
121
+ level = "High"
122
+ elif bias_prob > 0.5:
123
+ level = "Medium"
124
+ else:
125
+ level = "Low"
126
+
127
+ # Show the raw bias score from the model
128
+ overall_display_score = bias_prob
129
+
130
+ print_json({
131
+ "bias_level": level,
132
+ "bias_score": round(overall_display_score * 100, 1),
133
+ "explanation": overall.get("reasoning", "No specific reasoning provided."),
134
+ "top_words": overall.get("top_words", []),
135
+ "sentence_breakdown": sentence_results,
136
+ "factual_count": factual_count,
137
+ "biased_count": biased_count,
138
+ "total_sentences_analyzed": len(analysis_sentences),
139
+ "full_content": content
140
+ })
141
+ except Exception as e:
142
+ import sys
143
+ print_json({"error": str(e)})
144
+ print(f"Bridge Error: {str(e)}", file=sys.stderr)
145
+
146
+ if __name__ == "__main__":
147
+ main()
STABLE_BACKUP/NewsApex/eslint.config.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+
4
+ const eslintConfig = defineConfig([
5
+ ...nextVitals,
6
+ // Override default ignores of eslint-config-next.
7
+ globalIgnores([
8
+ // Default ignores of eslint-config-next:
9
+ ".next/**",
10
+ "out/**",
11
+ "build/**",
12
+ "next-env.d.ts",
13
+ ]),
14
+ ]);
15
+
16
+ export default eslintConfig;
STABLE_BACKUP/NewsApex/jsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@/*": ["./*"]
5
+ }
6
+ }
7
+ }
STABLE_BACKUP/NewsApex/news_service.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import re
4
+ import requests
5
+ from dotenv import load_dotenv
6
+ from newspaper import Article, Config
7
+ from huggingface_hub import InferenceClient
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ import sys
10
+
11
+ # Move heavy AI imports inside methods to speed up news fetch startup
12
+ # import torch
13
+ # from transformers import BertTokenizer, BertForSequenceClassification
14
+
15
+ # Add current directory to path for imports
16
+ base_dir = os.path.dirname(os.path.abspath(__file__))
17
+ sys.path.append(base_dir)
18
+
19
+ # Load .env from current dir or parent dir
20
+ load_dotenv(os.path.join(base_dir, ".env"))
21
+ load_dotenv(os.path.join(os.path.dirname(base_dir), ".env"))
22
+
23
+ class NewsService:
24
+ STOP_WORDS = {
25
+ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'with',
26
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
27
+ 'do', 'does', 'did', 'but', 'if', 'then', 'else', 'when', 'where', 'why',
28
+ 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some',
29
+ 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
30
+ 's', 't', 'can', 'will', 'just', 'don', "should", "now", "of", "as", "by",
31
+ "it", "its", "they", "them", "their", "this", "that", "these", "those",
32
+ "i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your"
33
+ }
34
+
35
+ def __init__(self):
36
+ self.newsdata_api_key = os.getenv("NEWSDATA_API_KEY", "pub_c319de1ec46240dc912d9b112e01c866")
37
+ self.guardian_api_key = os.getenv("GUARDIAN_API_KEY", "438ab5df-f19b-42b6-9ca9-83b8e971f219")
38
+ self.hf_token = os.getenv("HF_TOKEN")
39
+
40
+ self.session = requests.Session()
41
+
42
+ self.config = Config()
43
+ self.config.browser_user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
44
+ self.config.request_timeout = 20
45
+ self.config.fetch_images = False
46
+ self.config.memoize_articles = False
47
+ self.config.MAX_TEXT = 100000
48
+
49
+ self.hf_client = None
50
+ if self.hf_token:
51
+ try:
52
+ self.hf_client = InferenceClient(token=self.hf_token)
53
+ except:
54
+ self.hf_client = None
55
+
56
+ self.bias_model = None
57
+ self.bias_tokenizer = None
58
+
59
+ def load_local_bias_model(self):
60
+ try:
61
+ import torch
62
+ from transformers import BertTokenizer, BertForSequenceClassification, BertConfig
63
+ from bias_module import config as bias_config
64
+ base_path = os.path.dirname(os.path.abspath(__file__))
65
+ model_path = os.path.join(base_path, "bias_module", "models", "bert_babe.pt")
66
+ model_cache_dir = os.path.join(base_path, "bias_module", "data", "model_cache")
67
+
68
+ if os.path.exists(model_path):
69
+ if os.path.exists(model_cache_dir):
70
+ from transformers import BertConfig
71
+ self.bias_tokenizer = BertTokenizer.from_pretrained(model_cache_dir)
72
+ config = BertConfig.from_pretrained(os.path.join(model_cache_dir, "config.json"))
73
+ self.bias_model = BertForSequenceClassification(config)
74
+ else:
75
+ self.bias_tokenizer = BertTokenizer.from_pretrained(bias_config.MODEL_NAME)
76
+ self.bias_model = BertForSequenceClassification.from_pretrained(
77
+ bias_config.MODEL_NAME,
78
+ num_labels=2
79
+ )
80
+
81
+ self.bias_model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
82
+ self.bias_model.eval()
83
+ print(f"Local bias model loaded successfully.", file=sys.stderr)
84
+ except Exception as e:
85
+ print(f"Error loading local bias model: {e}", file=sys.stderr)
86
+
87
+ def get_top_biased_words_gradient(self, text, top_k=5):
88
+ if not self.bias_model or not self.bias_tokenizer:
89
+ return []
90
+ try:
91
+ import torch
92
+ encoding = self.bias_tokenizer(text, return_tensors="pt", truncation=True, padding="max_length", max_length=128)
93
+ input_ids = encoding["input_ids"]
94
+ attention_mask = encoding["attention_mask"]
95
+ self.bias_model.zero_grad()
96
+ for param in self.bias_model.bert.embeddings.word_embeddings.parameters():
97
+ param.requires_grad = True
98
+ with torch.enable_grad():
99
+ outputs = self.bias_model(input_ids=input_ids, attention_mask=attention_mask)
100
+ logits = outputs.logits
101
+ bias_logit = logits[0, 1]
102
+ bias_logit.backward()
103
+ embedding_grad = self.bias_model.bert.embeddings.word_embeddings.weight.grad
104
+ if embedding_grad is None: return []
105
+ token_ids = input_ids[0]
106
+ token_grads = embedding_grad[token_ids]
107
+ scores = torch.norm(token_grads, dim=1)
108
+ tokens = self.bias_tokenizer.convert_ids_to_tokens(token_ids)
109
+ filtered = []
110
+ for tok, score in zip(tokens, scores):
111
+ if tok in ["[CLS]", "[SEP]", "[PAD]"] or tok.startswith("##"): continue
112
+ if not any(c.isalnum() for c in tok): continue
113
+ if tok.lower() in self.STOP_WORDS: continue
114
+ if len(tok) < 3 and tok.lower() not in ['a', 'i']: continue
115
+ filtered.append((tok, score.item()))
116
+ sorted_tokens = sorted(filtered, key=lambda x: x[1], reverse=True)
117
+ return [{"word": t[0], "score": round(t[1] * 100, 2)} for t in sorted_tokens[:top_k]]
118
+ except Exception as e:
119
+ print(f"Gradient Calculation Error: {e}", file=sys.stderr)
120
+ return []
121
+
122
+ def get_bias_reasoning(self, text, label, bias_score):
123
+ if bias_score > 0.7: return "Interpretation: Strongly biased"
124
+ elif bias_score > 0.5: return "Interpretation: Likely Biased"
125
+ else: return "Interpretation: Likely Factual"
126
+
127
+ def rate_bias_batch(self, sentences):
128
+ if not sentences: return []
129
+ if not self.bias_model: self.load_local_bias_model()
130
+ if not self.bias_model or not self.bias_tokenizer:
131
+ return [{"label": "Offline", "score": 0.0, "reasoning": "Model not available."} for _ in sentences]
132
+ try:
133
+ import torch
134
+ inputs = self.bias_tokenizer(sentences, return_tensors="pt", truncation=True, padding=True, max_length=128)
135
+ with torch.no_grad():
136
+ outputs = self.bias_model(**inputs)
137
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
138
+ results = []
139
+ for i, prob in enumerate(probs):
140
+ bias_score = prob[1].item()
141
+ label = "Biased" if bias_score > 0.5 else "Factual"
142
+ reasoning = self.get_bias_reasoning(sentences[i], label, bias_score)
143
+ results.append({"label": label, "score": bias_score, "reasoning": reasoning})
144
+ return results
145
+ except Exception as e:
146
+ print(f"Batch Analysis Error: {e}", file=sys.stderr)
147
+ return [{"label": "Error", "score": 0.0, "reasoning": str(e)} for _ in sentences]
148
+
149
+ def rate_bias(self, text):
150
+ if not self.bias_model: self.load_local_bias_model()
151
+ if not text or len(text.strip()) < 10:
152
+ return {"label": "Neutral", "score": 0.0, "reasoning": "Text too short for analysis."}
153
+ filtered_sentences = self.split_into_sentences(text)
154
+ if not filtered_sentences: return {"label": "Neutral", "score": 0.0, "reasoning": "No valid content found."}
155
+ filtered_text = " ".join(filtered_sentences)
156
+ if self.bias_model and self.bias_tokenizer:
157
+ try:
158
+ import torch
159
+ inputs = self.bias_tokenizer(filtered_text, return_tensors="pt", truncation=True, padding=True, max_length=128)
160
+ with torch.no_grad():
161
+ outputs = self.bias_model(**inputs)
162
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
163
+ bias_score = probs[0][1].item()
164
+ label = "Biased" if bias_score > 0.5 else "Factual"
165
+ top_words = self.get_top_biased_words_gradient(filtered_text, top_k=8)
166
+ reasoning = self.get_bias_reasoning(filtered_text, label, bias_score)
167
+ return {"label": label, "score": bias_score, "reasoning": reasoning, "top_words": top_words}
168
+ except Exception as e:
169
+ print(f"Local Model Prediction Error: {e}", file=sys.stderr)
170
+ return {"label": "Offline", "score": 0.0, "reasoning": "Model failed or is offline."}
171
+
172
+ def split_into_sentences(self, text):
173
+ if not text: return []
174
+ sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', text.strip())
175
+ ad_keywords = ["sign up for", "subscribe to", "advertisement", "promoted", "sponsored"]
176
+ filtered_sentences = []
177
+ for s in sentences:
178
+ s_clean = s.strip()
179
+ if not s_clean or len(s_clean.split()) < 4: continue
180
+ if any(keyword in s_clean.lower() for keyword in ad_keywords): continue
181
+ filtered_sentences.append(s_clean)
182
+ return filtered_sentences
183
+
184
+ def fetch_newsdata(self, query=None, category=None, language="en"):
185
+ if not self.newsdata_api_key: return []
186
+ url = "https://newsdata.io/api/1/news"
187
+ params = {"apikey": self.newsdata_api_key, "language": "en"}
188
+ if query: params["q"] = query
189
+ if category and category != 'general': params["category"] = category
190
+ try:
191
+ res = self.session.get(url, params=params, timeout=10)
192
+ data = res.json()
193
+ if data.get("status") == "success":
194
+ return [{"title": r.get("title"), "link": r.get("link"), "source_id": r.get("source_id"), "pubDate": r.get("pubDate"), "image_url": r.get("image_url"), "snippet": r.get("description") or r.get("content")} for r in data.get("results", [])]
195
+ return []
196
+ except: return []
197
+
198
+ def fetch_guardian(self, query=None, category=None):
199
+ if not self.guardian_api_key or "your_" in self.guardian_api_key: return []
200
+ url = "https://content.guardianapis.com/search"
201
+ params = {"api-key": self.guardian_api_key, "show-fields": "thumbnail,trailText"}
202
+ if query: params["q"] = query
203
+ category_map = {'business': 'business', 'technology': 'technology', 'entertainment': 'culture', 'health': 'society', 'science': 'science', 'sports': 'sport'}
204
+ if category and category in category_map: params["section"] = category_map[category]
205
+ try:
206
+ res = self.session.get(url, params=params, timeout=10)
207
+ data = res.json()
208
+ results = data.get("response", {}).get("results", [])
209
+ return [{"title": r.get("webTitle"), "link": r.get("webUrl"), "source_id": "The Guardian", "pubDate": r.get("webPublicationDate"), "image_url": r.get("fields", {}).get("thumbnail"), "snippet": r.get("fields", {}).get("trailText")} for r in results]
210
+ except: return []
211
+
212
+ def fetch_all_news(self, query=None, category=None, language="en"):
213
+ # FASTEST POSSIBLE FETCH: Parallelize the two API calls
214
+ all_articles = []
215
+
216
+ with ThreadPoolExecutor(max_workers=2) as executor:
217
+ # Kick off both API calls at the same time
218
+ future_newsdata = executor.submit(self.fetch_newsdata, query, category, language)
219
+ future_guardian = executor.submit(self.fetch_guardian, query, category)
220
+
221
+ # Collect results
222
+ try:
223
+ all_articles.extend(future_newsdata.result())
224
+ except Exception as e:
225
+ print(f"NewsData error: {e}", file=sys.stderr)
226
+
227
+ try:
228
+ all_articles.extend(future_guardian.result())
229
+ except Exception as e:
230
+ print(f"Guardian error: {e}", file=sys.stderr)
231
+
232
+ unique_articles = []
233
+ seen_titles = set()
234
+ for article in all_articles:
235
+ title = article.get("title")
236
+ if title and title.lower() not in seen_titles:
237
+ unique_articles.append(article)
238
+ seen_titles.add(title.lower())
239
+
240
+ try:
241
+ unique_articles.sort(key=lambda x: x.get('pubDate', ''), reverse=True)
242
+ except:
243
+ pass
244
+
245
+ # --- SCRAPABILITY FILTER ---
246
+ # Only show articles that we can actually extract content from.
247
+ filtered_articles = []
248
+ # Limit the number of articles we check to keep it fast
249
+ articles_to_check = unique_articles[:30]
250
+
251
+ def check_article(article):
252
+ url = article.get("link")
253
+ if not url:
254
+ return None
255
+
256
+ # Use a slightly shorter timeout for the background check
257
+ content = self.get_full_content(url)
258
+ if content and len(content) > 400:
259
+ # Basic check for error messages in the text
260
+ error_terms = ["javascript is disabled", "enable cookies", "paywall", "subscribe to read"]
261
+ content_lower = content.lower()
262
+ if any(term in content_lower for term in error_terms):
263
+ return None
264
+ return article
265
+ return None
266
+
267
+ # Check articles concurrently
268
+ with ThreadPoolExecutor(max_workers=10) as executor:
269
+ results = list(executor.map(check_article, articles_to_check))
270
+ filtered_articles = [r for r in results if r is not None]
271
+
272
+ return filtered_articles[:20]
273
+
274
+ def get_full_content(self, url):
275
+ try:
276
+ headers = {
277
+ 'User-Agent': self.config.browser_user_agent,
278
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
279
+ 'Accept-Language': 'en-US,en;q=0.9',
280
+ 'Connection': 'keep-alive',
281
+ }
282
+ response = self.session.get(url, headers=headers, timeout=15)
283
+ if response.status_code != 200: return None
284
+
285
+ # Quick check for JS-only pages or common error indicators in HTML
286
+ html_lower = response.text.lower()
287
+ if "javascript is disabled" in html_lower or "enable javascript" in html_lower:
288
+ return None
289
+
290
+ from newspaper import Article
291
+ article = Article(url, config=self.config)
292
+ article.set_html(response.text)
293
+ article.parse()
294
+
295
+ text = article.text.strip()
296
+
297
+ # Final validation of the extracted text
298
+ if not text or len(text) < 300:
299
+ return None
300
+
301
+ # Filter out pages that extracted only UI elements/errors
302
+ system_errors = ["please enable javascript", "browser not supported", "access denied", "forbidden"]
303
+ text_lower = text.lower()
304
+ if any(err in text_lower for err in system_errors):
305
+ return None
306
+
307
+ return text
308
+ except Exception as e:
309
+ # print(f"Extraction error: {e}", file=sys.stderr)
310
+ return None
311
+
312
+ def summarize_content(self, text):
313
+ if not self.hf_client or not text or len(text.strip()) < 100: return None
314
+ try:
315
+ truncated_text = text[:3000]
316
+ response = self.hf_client.summarization(truncated_text, model="facebook/bart-large-cnn")
317
+ if hasattr(response, 'summary_text'): return response.summary_text
318
+ if isinstance(response, list) and len(response) > 0: return response[0].get('summary_text') if isinstance(response[0], dict) else str(response[0])
319
+ if isinstance(response, dict): return response.get('summary_text')
320
+ return str(response)
321
+ except Exception as e:
322
+ print(f"Summary error: {e}", file=sys.stderr)
323
+ return None
STABLE_BACKUP/NewsApex/next.config.mjs ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ images: {
4
+ remotePatterns: [
5
+ {
6
+ protocol: 'https',
7
+ hostname: '**',
8
+ },
9
+ {
10
+ protocol: 'http',
11
+ hostname: '**',
12
+ },
13
+ ],
14
+ },
15
+ };
16
+
17
+ export default nextConfig;
STABLE_BACKUP/NewsApex/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
STABLE_BACKUP/NewsApex/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "news_apex",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "pip install -r requirements.txt && next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "15.1.6",
13
+ "react": "19.0.0",
14
+ "react-dom": "19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "autoprefixer": "^10.4.20",
18
+ "eslint": "^9",
19
+ "eslint-config-next": "15.1.6",
20
+ "postcss": "^8.4.49",
21
+ "tailwindcss": "^3.4.17"
22
+ }
23
+ }
STABLE_BACKUP/NewsApex/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
STABLE_BACKUP/NewsApex/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
STABLE_BACKUP/NewsApex/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ requests
2
+ python-dotenv
3
+ newspaper3k
4
+ lxml_html_clean
5
+ huggingface_hub
6
+ streamlit
7
+ colorama
8
+ pandas
9
+ torch
10
+ transformers
11
+ datasets
12
+ scikit-learn
STABLE_BACKUP/NewsApex/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
5
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
STABLE_BACKUP/NewsApex/test_category.json ADDED
Binary file (62.5 kB). View file
 
STABLE_BACKUP/NewsApex/test_output.json ADDED
Binary file (59.5 kB). View file
 
STABLE_BACKUP/NewsApex/vercel.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "framework": "nextjs",
3
+ "buildCommand": "next build"
4
+ }
STABLE_BACKUP/app.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from news_service import NewsService
3
+ import pandas as pd
4
+ import html
5
+ import time
6
+
7
+ # Page Configuration
8
+ st.set_page_config(
9
+ page_title="NEXTER",
10
+ page_icon="πŸ“°",
11
+ layout="wide",
12
+ initial_sidebar_state="expanded"
13
+ )
14
+
15
+ # Custom CSS for better UI and to hide Deploy/Stop buttons
16
+ st.markdown("""
17
+ <style>
18
+ /* Hide Deploy button */
19
+ .stAppDeployButton {
20
+ display: none !important;
21
+ }
22
+ /* Hide the 'Stop' button and the running status indicator to clean up UI */
23
+ [data-testid="stStatusWidget"] {
24
+ display: none !important;
25
+ }
26
+ /* Hide the Main Menu (three dots/hamburger menu) */
27
+ #MainMenu {
28
+ visibility: hidden !important;
29
+ }
30
+ /* Hide the footer (Made with Streamlit) */
31
+ footer {
32
+ visibility: hidden !important;
33
+ }
34
+ /* Hide the header bar entirely */
35
+ /* header {
36
+ visibility: hidden !important;
37
+ } */
38
+ .main {
39
+ background-color: #f0f2f6;
40
+ }
41
+ .stCard {
42
+ background-color: white;
43
+ padding: 1rem;
44
+ border-radius: 12px;
45
+ border: 1px solid #e0e0e0;
46
+ margin-bottom: 1.5rem;
47
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
48
+ transition: transform 0.2s ease-in-out;
49
+ height: 480px; /* Fixed height for alignment */
50
+ display: flex;
51
+ flex-direction: column;
52
+ justify-content: space-between;
53
+ }
54
+ .stCard:hover {
55
+ transform: translateY(-5px);
56
+ box-shadow: 0 8px 15px rgba(0,0,0,0.1);
57
+ }
58
+ .card-img {
59
+ width: 100%;
60
+ height: 200px; /* Fixed image height */
61
+ object-fit: cover;
62
+ border-radius: 8px;
63
+ margin-bottom: 0.8rem;
64
+ background-color: #f8f9fa;
65
+ display: block;
66
+ }
67
+ .card-img-placeholder {
68
+ width: 100%;
69
+ height: 200px;
70
+ background-color: #e9ecef;
71
+ border-radius: 8px;
72
+ margin-bottom: 0.8rem;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ color: #adb5bd;
77
+ font-size: 0.9rem;
78
+ }
79
+ .card-title {
80
+ font-size: 1.1rem;
81
+ font-weight: 700;
82
+ color: #1a1a1a;
83
+ margin-bottom: 0.5rem;
84
+ line-height: 1.4;
85
+ height: 4.2rem; /* Fixed height for 3 lines of text */
86
+ display: -webkit-box;
87
+ -webkit-line-clamp: 3;
88
+ -webkit-box-orient: vertical;
89
+ overflow: hidden;
90
+ }
91
+ .card-meta {
92
+ font-size: 0.8rem;
93
+ color: #6c757d;
94
+ margin-bottom: 0.5rem;
95
+ }
96
+ .stArticle {
97
+ background-color: white;
98
+ padding: 2.5rem;
99
+ border-radius: 15px;
100
+ border: 1px solid #e9ecef;
101
+ box-shadow: 0 10px 25px rgba(0,0,0,0.05);
102
+ }
103
+ .full-content {
104
+ color: #212529;
105
+ font-size: 1.1rem;
106
+ line-height: 1.6;
107
+ margin-top: 1rem;
108
+ margin-bottom: 1rem;
109
+ padding: 0;
110
+ background-color: transparent;
111
+ border-left: none;
112
+ white-space: pre-wrap;
113
+ }
114
+ .summary-content {
115
+ background-color: #e7f3ff;
116
+ padding: 1.5rem;
117
+ border-radius: 8px;
118
+ border: 1px solid #b8daff;
119
+ color: #004085;
120
+ font-style: italic;
121
+ }
122
+ .meta-info {
123
+ color: #6c757d;
124
+ font-size: 0.9rem;
125
+ margin-bottom: 1rem;
126
+ }
127
+ </style>
128
+ """, unsafe_allow_html=True)
129
+
130
+ # --- CACHED FUNCTIONS ---
131
+ @st.cache_resource
132
+ def get_news_service():
133
+ return NewsService()
134
+
135
+ @st.cache_data(show_spinner=False, ttl=3600)
136
+ def cached_fetch_all_news(_service, query, language="en"):
137
+ return _service.fetch_all_news(query=query, language=language)
138
+
139
+ @st.cache_data(show_spinner=False, ttl=86400)
140
+ def cached_get_full_content(_service, url):
141
+ return _service.get_full_content(url)
142
+
143
+ @st.cache_data(show_spinner=False, ttl=86400)
144
+ def cached_summarize_content(_service, text):
145
+ return _service.summarize_content(text)
146
+
147
+ @st.cache_data(show_spinner=False)
148
+ def cached_split_into_sentences(_service, text):
149
+ return _service.split_into_sentences(text)
150
+
151
+ @st.cache_data(show_spinner=False)
152
+ def cached_rate_bias(_service, text):
153
+ return _service.rate_bias(text)
154
+
155
+ def display_article_detail(article, news_service):
156
+ """Displays the detailed view of a selected article with bias analysis and summary."""
157
+ if st.button("← Back to Feed"):
158
+ st.session_state.selected_article = None
159
+ st.rerun()
160
+
161
+ url = article.get('link')
162
+ if not url:
163
+ st.error("Invalid article link.")
164
+ return
165
+
166
+ st.markdown('<div class="stArticle">', unsafe_allow_html=True)
167
+ st.title(article.get('title', 'No Title'))
168
+
169
+ # Hero image
170
+ img_url = article.get('image_url')
171
+ if img_url:
172
+ st.image(img_url, use_container_width=True)
173
+
174
+ st.markdown(f"""
175
+ <div class="meta-info">
176
+ <b>Source:</b> {article.get('source_id', 'Unknown')} |
177
+ <b>Date:</b> {article.get('pubDate', 'Unknown')} |
178
+ <a href="{url}" target="_blank">Original Link</a>
179
+ </div>
180
+ """, unsafe_allow_html=True)
181
+
182
+ with st.spinner("🧠 Analyzing article content..."):
183
+ full_content = cached_get_full_content(news_service, url)
184
+
185
+ # --- ROBUST FALLBACK LOGIC ---
186
+ text_to_analyze = full_content
187
+ status_msg = None
188
+
189
+ # 1. Try Full Content
190
+ if not text_to_analyze or len(text_to_analyze.strip()) < 100:
191
+ # 2. Try Snippet/Description
192
+ text_to_analyze = article.get('snippet')
193
+ if text_to_analyze and len(text_to_analyze.strip()) > 20:
194
+ status_msg = "⚠️ Full content extraction limited. Analyzing article snippet."
195
+ else:
196
+ # 3. Last Resort: Use Title
197
+ text_to_analyze = article.get('title', '')
198
+ status_msg = "⚠️ No content found. Analyzing article headline only."
199
+
200
+ if not text_to_analyze:
201
+ st.error("Could not retrieve any text for this article.")
202
+ return
203
+
204
+ if status_msg:
205
+ st.warning(status_msg)
206
+
207
+ # Display the text being analyzed
208
+ with st.expander("πŸ“– View Analyzed Text", expanded=True):
209
+ st.markdown(f'<div style="font-size: 1rem; line-height: 1.5; color: #333;">{html.escape(text_to_analyze)}</div>', unsafe_allow_html=True)
210
+
211
+ # AI Summary
212
+ summary = cached_summarize_content(news_service, text_to_analyze)
213
+ if summary:
214
+ st.info(f"πŸ€– **AI Summary:** {summary}")
215
+
216
+ st.divider()
217
+
218
+ # Bias Analysis
219
+ st.subheader("βš–οΈ Bias Analysis")
220
+ overall_bias = cached_rate_bias(news_service, text_to_analyze)
221
+
222
+ col_b1, col_b2 = st.columns([1, 3])
223
+ with col_b1:
224
+ color = "#dc3545" if overall_bias['label'] == "Biased" else "#28a745"
225
+ st.markdown(f"**Overall Rating:** <span style='color:{color}; font-weight:bold; font-size:1.2rem;'>{overall_bias['label']}</span>", unsafe_allow_html=True)
226
+ with col_b2:
227
+ st.progress(overall_bias['score'], text=f"Confidence: {overall_bias['score']:.1%}")
228
+
229
+ # --- Interpretation Section ---
230
+ if overall_bias['label'] == "Factual":
231
+ st.success("βœ… **Interpretation: Factual Content**\nThis article primarily uses objective language, reports verifiable events, and avoids subjective modifiers or emotional framing. It aims to inform rather than influence.")
232
+ else:
233
+ st.error("⚠️ **Interpretation: Biased Content**\nThis article contains elements that suggest a non-neutral perspective. This could include the use of loaded language, emotional appeals, or selective framing designed to influence the reader's opinion.")
234
+
235
+ o_reasoning = overall_bias.get('reasoning', 'No specific reasoning provided.')
236
+ st.warning(f"πŸ’‘ **Analysis Reasoning:** {o_reasoning}")
237
+
238
+ st.subheader("πŸ“‹ Sentence-by-Sentence Breakdown")
239
+ sentences = cached_split_into_sentences(news_service, text_to_analyze)
240
+
241
+ sentence_html = ""
242
+ for i, sentence in enumerate(sentences, 1):
243
+ s_bias = cached_rate_bias(news_service, sentence)
244
+ s_label = s_bias.get('label', 'Factual')
245
+ s_reasoning = s_bias.get('reasoning', '')
246
+ s_color = "rgba(220, 53, 69, 0.08)" if s_label == "Biased" else "transparent"
247
+
248
+ escaped_sentence = html.escape(sentence)
249
+ escaped_reasoning = html.escape(s_reasoning)
250
+
251
+ reasoning_html = f'<div style="font-size: 0.85rem; color: #721c24; margin-top: 4px; font-style: italic;">Why? {escaped_reasoning}</div>' if s_label == "Biased" else ""
252
+
253
+ border_style = "border-left: 4px solid #dc3545;" if s_label == "Biased" else "border-left: 4px solid #e9ecef;"
254
+ sentence_html += f'<div style="margin-bottom: 12px; padding: 12px; background-color: {s_color}; border-radius: 6px; {border_style}"><b>{i}.</b> {escaped_sentence} <span style="font-size: 0.8rem; color: #6c757d; margin-left: 10px; font-weight: bold;">[{s_label}]</span>{reasoning_html}</div>'
255
+
256
+ st.markdown(f'<div class="full-content" style="border:none; padding:0;">{sentence_html}</div>', unsafe_allow_html=True)
257
+
258
+ st.markdown('</div>', unsafe_allow_html=True)
259
+
260
+ def fetch_and_display_news(query, news_service, title=None):
261
+ """Fetches and displays news in a grid layout."""
262
+ if title:
263
+ st.subheader(title)
264
+
265
+ spinner_text = f"πŸ” Searching for '{query}'..." if query else "πŸ” Fetching latest headlines..."
266
+ with st.spinner(spinner_text):
267
+ try:
268
+ articles = cached_fetch_all_news(news_service, query, language="en")
269
+
270
+ if not articles:
271
+ st.error("No articles found for this topic. Please try another one.")
272
+ return
273
+
274
+ # Grid layout: 3 columns
275
+ articles = articles[:12]
276
+ cols = st.columns(3)
277
+ for idx, article in enumerate(articles):
278
+ with cols[idx % 3]:
279
+ st.markdown('<div class="stCard">', unsafe_allow_html=True)
280
+
281
+ # Thumbnail Image handling
282
+ img_url = article.get('image_url')
283
+ if img_url and img_url.startswith('http'):
284
+ st.markdown(f'<img src="{img_url}" class="card-img" onerror="this.style.display=\'none\'; this.nextSibling.style.display=\'flex\';">', unsafe_allow_html=True)
285
+ st.markdown('<div class="card-img-placeholder" style="display:none;">πŸ–ΌοΈ Image Unavailable</div>', unsafe_allow_html=True)
286
+ else:
287
+ st.markdown('<div class="card-img-placeholder">πŸ–ΌοΈ No Image</div>', unsafe_allow_html=True)
288
+
289
+ # Source and Date
290
+ date_str = article.get('pubDate', 'Unknown')[:10]
291
+ st.markdown(f'<div class="card-meta">{article.get("source_id", "Unknown")} β€’ {date_str}</div>', unsafe_allow_html=True)
292
+
293
+ # Title
294
+ st.markdown(f'<div class="card-title">{article.get("title", "No Title")}</div>', unsafe_allow_html=True)
295
+
296
+ # Analyze Button
297
+ if st.button("Analyze Article", key=f"btn_{idx}", use_container_width=True):
298
+ st.session_state.selected_article = article
299
+ st.rerun()
300
+
301
+ st.markdown('</div>', unsafe_allow_html=True)
302
+
303
+ except Exception as e:
304
+ st.error(f"Error fetching news: {str(e)}")
305
+
306
+ def main():
307
+ st.title("πŸ“° NEXTER")
308
+ st.markdown("Modern AI-Powered News Analysis")
309
+
310
+ # Initialize session states
311
+ if 'search_query' not in st.session_state: st.session_state.search_query = ""
312
+ if 'selected_article' not in st.session_state: st.session_state.selected_article = None
313
+ if 'is_home' not in st.session_state: st.session_state.is_home = True
314
+
315
+ with st.sidebar:
316
+ st.header("Search Settings")
317
+ news_service = get_news_service()
318
+
319
+ query_input = st.text_input("Topic Search", value=st.session_state.search_query, placeholder="e.g. Finance, AI, Sports")
320
+
321
+ if query_input != st.session_state.search_query:
322
+ st.session_state.search_query = query_input
323
+ st.session_state.selected_article = None
324
+ st.session_state.is_home = False
325
+
326
+ if st.button("Fetch News", type="primary"):
327
+ st.session_state.selected_article = None
328
+ st.session_state.is_home = False
329
+ st.rerun()
330
+
331
+ if st.button("🏠 Home Feed"):
332
+ st.session_state.search_query = ""
333
+ st.session_state.selected_article = None
334
+ st.session_state.is_home = True
335
+ st.rerun()
336
+
337
+ st.divider()
338
+ if st.button("🧹 Clear Cache"):
339
+ st.cache_data.clear()
340
+ st.success("Cache cleared!")
341
+ time.sleep(0.5)
342
+ st.rerun()
343
+
344
+ if news_service.bias_model:
345
+ st.success("βœ… AI Bias Model Loaded")
346
+ else:
347
+ st.info("☁️ Cloud Analysis Active")
348
+
349
+ # Display logic
350
+ if st.session_state.selected_article:
351
+ display_article_detail(st.session_state.selected_article, news_service)
352
+ elif st.session_state.is_home:
353
+ fetch_and_display_news(None, news_service, title="Top Headlines")
354
+ else:
355
+ fetch_and_display_news(st.session_state.search_query, news_service, title=f"Results for: {st.session_state.search_query}")
356
+
357
+
358
+ if __name__ == "__main__":
359
+ main()
STABLE_BACKUP/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ requests
2
+ python-dotenv
3
+ newspaper3k
4
+ lxml_html_clean
5
+ huggingface_hub
6
+ streamlit
7
+ colorama
8
+ pandas
9
+ torch
10
+ transformers
11
+ datasets
12
+ scikit-learn