diff --git a/backend/app/modules/bias_detection/__init__.py b/backend/app/modules/bias_detection/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/app/modules/bias_detection/check_bias.py b/backend/app/modules/bias_detection/check_bias.py
new file mode 100644
index 00000000..b4b39801
--- /dev/null
+++ b/backend/app/modules/bias_detection/check_bias.py
@@ -0,0 +1,57 @@
+import os
+from groq import Groq
+from dotenv import load_dotenv
+import json
+
+load_dotenv()
+
+client = Groq(api_key=os.getenv("GROQ_API_KEY"))
+
+
+def check_bias(text):
+ try:
+ print(text)
+ print(json.dumps(text))
+
+ if not text:
+ raise ValueError("Missing or empty 'cleaned_text'")
+
+ chat_completion = client.chat.completions.create(
+ messages=[
+ {
+ "role": "system",
+ "content": (
+ "You are an assistant that checks "
+ "if given article is biased and give"
+ "score to each based on biasness where 0 is lowest bias and 100 is highest bias"
+ "Only return a number between 0 to 100 base on bias."
+ "only return Number No Text"
+ ),
+ },
+ {
+ "role": "user",
+ "content": (
+ "Give bias score to the following article "
+ f"\n\n{text}"
+ ),
+ },
+ ],
+ model="gemma2-9b-it",
+ temperature=0.3,
+ max_tokens=512,
+ )
+
+ bias_score = chat_completion.choices[0].message.content.strip()
+
+ return {
+ "bias_score": bias_score,
+ "status": "success",
+ }
+
+ except Exception as e:
+ print(f"Error in bias_detection: {e}")
+ return {
+ "status": "error",
+ "error_from": "bias_detection",
+ "message": str(e),
+ }
diff --git a/backend/app/routes/routes.py b/backend/app/routes/routes.py
index c443e83c..9df55a2a 100644
--- a/backend/app/routes/routes.py
+++ b/backend/app/routes/routes.py
@@ -2,11 +2,12 @@
from pydantic import BaseModel
from app.modules.pipeline import run_scraper_pipeline
from app.modules.pipeline import run_langgraph_workflow
+from app.modules.bias_detection.check_bias import check_bias
+import asyncio
import json
router = APIRouter()
-
class URlRequest(BaseModel):
url: str
@@ -15,10 +16,18 @@ class URlRequest(BaseModel):
async def home():
return {"message": "Perspective API is live!"}
+@router.post("/bias")
+async def bias_detection(request: URlRequest):
+ content = await asyncio.to_thread(run_scraper_pipeline,(request.url))
+ bias_score = await asyncio.to_thread(check_bias,(content))
+ print(bias_score)
+ return bias_score
+
+
@router.post("/process")
async def run_pipelines(request: URlRequest):
- article_text = run_scraper_pipeline(request.url)
+ article_text = await asyncio.to_thread(run_scraper_pipeline,(request.url))
print(json.dumps(article_text, indent=2))
- data = run_langgraph_workflow(article_text)
+ data = await asyncio.to_thread(run_langgraph_workflow,(article_text))
return data
diff --git a/frontend/app/analyze/loading/page.tsx b/frontend/app/analyze/loading/page.tsx
index 055a1a08..1d55ace3 100644
--- a/frontend/app/analyze/loading/page.tsx
+++ b/frontend/app/analyze/loading/page.tsx
@@ -1,12 +1,20 @@
-"use client"
+"use client";
-import { useEffect, useState } from "react"
-import { useRouter } from "next/navigation"
-import { Card } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Globe, Brain, Shield, CheckCircle, Database, Sparkles, Zap } from "lucide-react"
-import ThemeToggle from "@/components/theme-toggle"
-import axios from "axios"
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Card } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import {
+ Globe,
+ Brain,
+ Shield,
+ CheckCircle,
+ Database,
+ Sparkles,
+ Zap,
+} from "lucide-react";
+import ThemeToggle from "@/components/theme-toggle";
+import axios from "axios";
/**
* Displays a multi-step animated loading and progress interface for the article analysis workflow.
@@ -16,10 +24,10 @@ import axios from "axios"
* @remark This component manages its own navigation and redirects based on session state.
*/
export default function LoadingPage() {
- const [currentStep, setCurrentStep] = useState(0)
- const [progress, setProgress] = useState(0)
- const [articleUrl, setArticleUrl] = useState("")
- const router = useRouter()
+ const [currentStep, setCurrentStep] = useState(0);
+ const [progress, setProgress] = useState(0);
+ const [articleUrl, setArticleUrl] = useState("");
+ const router = useRouter();
const steps = [
{
@@ -52,67 +60,88 @@ export default function LoadingPage() {
description: "Creating balanced alternative viewpoints",
color: "from-pink-500 to-rose-500",
},
- ]
+ ];
useEffect(() => {
- const runAnalysis = async () => {
- const storedUrl = sessionStorage.getItem("articleUrl")
- if (storedUrl) {
- setArticleUrl(storedUrl)
+ const runAnalysis = async () => {
+ const storedUrl = sessionStorage.getItem("articleUrl");
+ if (storedUrl) {
+ setArticleUrl(storedUrl);
- try {
- const res = await axios.post("https://Thunder1245-perspective-backend.hf.space/api/process", {
- url: storedUrl,
- })
+ try {
+ const [processRes, biasRes] = await Promise.all([
+ axios.post(
+ "https://Thunder1245-perspective-backend.hf.space/api/process",
+ {
+ url: storedUrl,
+ }
+ ),
+ axios.post(
+ "http://Thunder1245-perspective-backend.hf.space/api/bias",
+ {
+ url: storedUrl,
+ }
+ ),
+ ]);
- // Save response to sessionStorage
- sessionStorage.setItem("analysisResult", JSON.stringify(res.data))
- // optional logging
- console.log("Analysis result saved")
- console.log(res)
- } catch (err) {
- console.error("Failed to process article:", err)
- router.push("/analyze") // fallback in case of error
- return
- }
+ sessionStorage.setItem("BiasScore", JSON.stringify(biasRes.data));
- // Progress and step simulation
- const stepInterval = setInterval(() => {
- setCurrentStep((prev) => {
- if (prev < steps.length - 1) {
- return prev + 1
- } else {
- clearInterval(stepInterval)
- setTimeout(() => {
- router.push("/analyze/results")
- }, 2000)
- return prev
- }
- })
- }, 2000)
+ console.log("Bias score saved");
+ console.log(biasRes);
- const progressInterval = setInterval(() => {
- setProgress((prev) => {
- if (prev < 100) {
- return prev + 1
- }
- return prev
- })
- }, 100)
+ // Save response to sessionStorage
+ sessionStorage.setItem(
+ "analysisResult",
+ JSON.stringify(processRes.data)
+ );
- return () => {
- clearInterval(stepInterval)
- clearInterval(progressInterval)
- }
- } else {
- router.push("/analyze")
- }
- }
+ console.log("Analysis result saved");
+ console.log(processRes);
+
+
+ // optional logging
+ } catch (err) {
+ console.error("Failed to process article:", err);
+ router.push("/analyze"); // fallback in case of error
+ return;
+ }
+
+ // Progress and step simulation
+ const stepInterval = setInterval(() => {
+ setCurrentStep((prev) => {
+ if (prev < steps.length - 1) {
+ return prev + 1;
+ } else {
+ clearInterval(stepInterval);
+ setTimeout(() => {
+ router.push("/analyze/results");
+ }, 2000);
+ return prev;
+ }
+ });
+ }, 2000);
- runAnalysis()
-}, [router])
+ const progressInterval = setInterval(() => {
+ setProgress((prev) => {
+ if (prev < 100) {
+ return prev + 1;
+ }
+ return prev;
+ });
+ }, 100);
+ return () => {
+ clearInterval(stepInterval);
+ clearInterval(progressInterval);
+ };
+ } else {
+ router.push("/analyze");
+ }
+ };
+
+ runAnalysis();
+ }, [router]);
return (
@@ -162,8 +191,12 @@ export default function LoadingPage() {
{/* Article URL Display */}
-
Processing:
-
{articleUrl}
+
+ Processing:
+
+
+ {articleUrl}
+
{/* Progress Bar */}
@@ -171,7 +204,9 @@ export default function LoadingPage() {
@@ -190,8 +225,8 @@ export default function LoadingPage() {
index === currentStep
? "bg-white dark:bg-slate-800 shadow-2xl scale-105 ring-2 ring-blue-500/50"
: index < currentStep
- ? "bg-white/80 dark:bg-slate-800/80 shadow-lg opacity-75"
- : "bg-white/40 dark:bg-slate-800/40 shadow-md opacity-50"
+ ? "bg-white/80 dark:bg-slate-800/80 shadow-lg opacity-75"
+ : "bg-white/40 dark:bg-slate-800/40 shadow-md opacity-50"
}`}
>
@@ -200,8 +235,8 @@ export default function LoadingPage() {
index === currentStep
? `bg-gradient-to-br ${step.color} animate-pulse shadow-lg`
: index < currentStep
- ? "bg-gradient-to-br from-emerald-500 to-teal-500 shadow-md"
- : "bg-slate-200 dark:bg-slate-700"
+ ? "bg-gradient-to-br from-emerald-500 to-teal-500 shadow-md"
+ : "bg-slate-200 dark:bg-slate-700"
}`}
>
{index < currentStep ? (
@@ -221,13 +256,15 @@ export default function LoadingPage() {
index === currentStep
? "text-blue-600 dark:text-blue-400"
: index < currentStep
- ? "text-emerald-600 dark:text-emerald-400"
- : "text-slate-500 dark:text-slate-400"
+ ? "text-emerald-600 dark:text-emerald-400"
+ : "text-slate-500 dark:text-slate-400"
}`}
>
{step.title}
-
{step.description}
+
+ {step.description}
+
{index === currentStep && (
@@ -262,5 +299,5 @@ export default function LoadingPage() {
- )
+ );
}
diff --git a/frontend/app/analyze/page.tsx b/frontend/app/analyze/page.tsx
index 541b0dd2..c86c6c9e 100644
--- a/frontend/app/analyze/page.tsx
+++ b/frontend/app/analyze/page.tsx
@@ -1,15 +1,29 @@
-"use client"
+"use client";
-import type React from "react"
+import type React from "react";
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Globe, ArrowRight, Link, Sparkles, Shield, Brain, CheckCircle } from "lucide-react"
-import { useRouter } from "next/navigation"
-import ThemeToggle from "@/components/theme-toggle"
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import {
+ Globe,
+ ArrowRight,
+ Link,
+ Sparkles,
+ Shield,
+ Brain,
+ CheckCircle,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import ThemeToggle from "@/components/theme-toggle";
/**
* Renders the main page for submitting an article URL to initiate AI-powered analysis.
@@ -17,36 +31,36 @@ import ThemeToggle from "@/components/theme-toggle"
* Provides a user interface for entering and validating an article URL, displays real-time feedback on URL validity, and enables users to trigger analysis. Features include a branded header, a hero section, a URL input card with validation, a grid highlighting analysis capabilities, and example article URLs for quick testing. On valid submission, the URL is stored in sessionStorage and the user is navigated to a loading page for further processing.
*/
export default function AnalyzePage() {
- const [url, setUrl] = useState("")
- const [isValidUrl, setIsValidUrl] = useState(false)
- const router = useRouter()
+ const [url, setUrl] = useState("");
+ const [isValidUrl, setIsValidUrl] = useState(false);
+ const router = useRouter();
const validateUrl = (inputUrl: string) => {
try {
- new URL(inputUrl)
- setIsValidUrl(true)
+ new URL(inputUrl);
+ setIsValidUrl(true);
} catch {
- setIsValidUrl(false)
+ setIsValidUrl(false);
}
- }
+ };
const handleUrlChange = (e: React.ChangeEvent
) => {
- const inputUrl = e.target.value
- setUrl(inputUrl)
+ const inputUrl = e.target.value;
+ setUrl(inputUrl);
if (inputUrl.length > 0) {
- validateUrl(inputUrl)
+ validateUrl(inputUrl);
} else {
- setIsValidUrl(false)
+ setIsValidUrl(false);
}
- }
+ };
const handleAnalyze = () => {
if (isValidUrl && url) {
// Store the URL in sessionStorage to pass to loading page
- sessionStorage.setItem("articleUrl", url)
- router.push("/analyze/loading")
+ sessionStorage.setItem("articleUrl", url);
+ router.push("/analyze/loading");
}
- }
+ };
const features = [
{
@@ -64,7 +78,7 @@ export default function AnalyzePage() {
title: "Fact Verification",
description: "Cross-references claims with reliable sources",
},
- ]
+ ];
return (
@@ -109,8 +123,8 @@ export default function AnalyzePage() {
- Paste the URL of any online article and get AI-powered bias detection, fact-checking, and alternative
- perspectives in seconds.
+ Paste the URL of any online article and get AI-powered bias
+ detection, fact-checking, and alternative perspectives in seconds.
@@ -121,7 +135,8 @@ export default function AnalyzePage() {
Enter Article URL
- Provide the link to the article you want to analyze for bias and alternative perspectives
+ Provide the link to the article you want to analyze for bias and
+ alternative perspectives
@@ -157,7 +172,9 @@ export default function AnalyzePage() {
{url && !isValidUrl && (
- Please enter a valid URL
+
+ Please enter a valid URL
+
)}
@@ -204,8 +221,8 @@ export default function AnalyzePage() {
{
- setUrl(exampleUrl)
- setIsValidUrl(true)
+ setUrl(exampleUrl);
+ setIsValidUrl(true);
}}
className="block w-full text-left p-2 md:p-3 rounded-lg hover:bg-white/50 dark:hover:bg-slate-600/50 transition-colors text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm md:text-base break-all"
>
@@ -218,5 +235,5 @@ export default function AnalyzePage() {
- )
+ );
}
diff --git a/frontend/app/analyze/results/page.tsx b/frontend/app/analyze/results/page.tsx
index efede2d9..bf966ac7 100644
--- a/frontend/app/analyze/results/page.tsx
+++ b/frontend/app/analyze/results/page.tsx
@@ -1,101 +1,117 @@
-"use client"
-
-import type React from "react"
-import { useState, useEffect, useRef } from "react"
-import { useRouter } from "next/navigation"
-import Link from "next/link"
-import { ArrowLeft, Globe, MessageSquare, Send, ThumbsDown, ThumbsUp, Menu, Link as LinkIcon } from "lucide-react"
-import { Button } from "@/components/ui/button"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
-import BiasMeter from "@/components/bias-meter"
+"use client";
+
+import type React from "react";
+import { useState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import {
+ Send,
+ Link as LinkIcon,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import BiasMeter from "@/components/bias-meter";
+
/**
* Renders the article analysis page with summary, perspectives, fact checks, bias meter, AI chat, and sources.
*/
export default function AnalyzePage() {
- const [analysisData, setAnalysisData] = useState(null)
- const router = useRouter()
+ const [analysisData, setAnalysisData] = useState(null);
+ const [biasScore, setBiasScore] = useState(null);
+ const router = useRouter();
const isRedirecting = useRef(false);
- const [activeTab, setActiveTab] = useState("summary")
- const [message, setMessage] = useState("")
- const [isLoading, setIsLoading] = useState(true)
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
- const [messages, setMessages] = useState<{ role: string; content: string }[]>([
- {
- role: "system",
- content:
- "Welcome to the Perspective chat. You can ask me questions about this article or request more information about specific claims.",
- },
- ])
+ const [activeTab, setActiveTab] = useState("summary");
+ const [message, setMessage] = useState("");
+ const [isLoading, setIsLoading] = useState(true);
+ const [messages, setMessages] = useState<{ role: string; content: string }[]>(
+ [
+ {
+ role: "system",
+ content:
+ "Welcome to the Perspective chat. You can ask me questions about this article or request more information about specific claims.",
+ },
+ ]
+ );
useEffect(() => {
- const timer = setTimeout(() => setIsLoading(false), 1500)
- const storedData = sessionStorage.getItem("analysisResult")
- if (storedData) setAnalysisData(JSON.parse(storedData))
- else console.warn("No analysis result found")
- return () => clearTimeout(timer)
- }, [])
+ const storedBiasScore = sessionStorage.getItem("BiasScore");
+ const storedData = sessionStorage.getItem("analysisResult");
+ if (storedBiasScore && storedData){
+ setIsLoading(false);
+ }
+ if (storedBiasScore) setBiasScore(JSON.parse(storedBiasScore).bias_score);
+ else console.warn("No bias score found.");
- useEffect(() => {
+ if (storedData) setAnalysisData(JSON.parse(storedData));
+ else console.warn("No analysis result found");
- if (isRedirecting.current) {
- return;
- }
+ }, []);
- const timer = setTimeout(() => setIsLoading(false), 1500);
- const storedData = sessionStorage.getItem("analysisResult");
-
- if (storedData) {
- const parsedData = JSON.parse(storedData);
- const requiredFields = ['cleaned_text', 'facts', 'sentiment', 'perspective', 'score'];
- const isDataValid = requiredFields.every(field => parsedData[field] !== undefined && parsedData[field] !== null);
-
- if (isDataValid) {
- setAnalysisData(parsedData);
- } else {
- console.warn("Incomplete analysis data. Redirecting...");
-
-
- isRedirecting.current = true;
- router.push("/analyze");
+ useEffect(() => {
+ if (isRedirecting.current) {
+ return;
}
- } else {
- console.warn("No analysis result found. Redirecting...");
+
- isRedirecting.current = true;
- router.push("/analyze");
- }
+ const storedData = sessionStorage.getItem("analysisResult");
+ const storedBiasScore = sessionStorage.getItem("BiasScore");
- return () => clearTimeout(timer);
-}, [router]);
+ if (storedBiasScore && storedData) {
+ // inside here TS knows storedBiasScore and storedData are strings
+ setBiasScore(JSON.parse(storedBiasScore).bias_score);
+ setAnalysisData(JSON.parse(storedData));
+ setIsLoading(false);
+ } else {
+ console.warn("No bias or data found. Redirecting...");
+ router.push("/analyze");
+ }
+ }, [router]);
const handleSendMessage = (e: React.FormEvent) => {
- e.preventDefault()
- if (!message.trim()) return
- const newMessages = [...messages, { role: "user", content: message }]
- setMessages(newMessages)
- setMessage("")
+ e.preventDefault();
+ if (!message.trim()) return;
+ const newMessages = [...messages, { role: "user", content: message }];
+ setMessages(newMessages);
+ setMessage("");
setTimeout(() => {
- setMessages([...newMessages, { role: "system", content: "Based on the article... let me know if you want more details." }])
- }, 1000)
- }
+ setMessages([
+ ...newMessages,
+ {
+ role: "system",
+ content:
+ "Based on the article... let me know if you want more details.",
+ },
+ ]);
+ }, 1000);
+ };
- if (isLoading || !analysisData) {
+ if (isLoading || !analysisData || !biasScore) {
return (
- )
+ );
}
- const { cleaned_text, facts=[], sentiment, perspective, score } = analysisData
+ const {
+ cleaned_text,
+ facts = [],
+ sentiment,
+ perspective,
+ score,
+ } = analysisData;
return (
@@ -103,13 +119,22 @@ export default function AnalyzePage() {
Analysis Results
-
+
Sentiment: {sentiment}
-
-
Bias Score: {score}
+
+
Bias Score: {biasScore}
@@ -123,67 +148,72 @@ export default function AnalyzePage() {
- {cleaned_text.split("\n\n").map((para: string, idx: number) => (
-
{para}
- ))}
+ {cleaned_text
+ .split("\n\n")
+ .map((para: string, idx: number) => (
+
{para}
+ ))}
- {perspective ? (
-
-
Counter-Perspective
-
"{perspective.perspective}"
-
Reasoning:
-
{perspective.reasoning}
-
- ) : (
-
- No counter-perspective was generated for this content.
-
- )}
-
+ {perspective ? (
+
+
+ Counter-Perspective
+
+
"{perspective.perspective}"
+
Reasoning:
+
{perspective.reasoning}
+
+ ) : (
+
+ No counter-perspective was generated for this content.
+
+ )}
+
-
- {facts.length > 0 ? (
- facts.map((fact: any, idx: number) => (
-
-
-
- {fact.original_claim}
-
- {fact.verdict}
-
-
-
-
- {fact.explanation}
-
- Source
-
-
-
- ))
- ) : (
-
- No specific claims were identified for fact-checking in this content.
-
- )}
-
-
+
+ {facts.length > 0 ? (
+ facts.map((fact: any, idx: number) => (
+
+
+
+ {fact.original_claim}
+
+ {fact.verdict}
+
+
+
+
+ {fact.explanation}
+
+ Source
+
+
+
+ ))
+ ) : (
+
+ No specific claims were identified for fact-checking in
+ this content.
+
+ )}
+
+
@@ -191,13 +221,26 @@ export default function AnalyzePage() {
AI Discussion
- Ask questions about this article
+
+ Ask questions about this article
+
{messages.map((msg, i) => (
-
-
+
@@ -220,5 +263,5 @@ export default function AnalyzePage() {
{/* Footer omitted */}
- )
+ );
}