+ {section.title ?? `Key Point ${idx + 1}`} +
++ {paragraph} +
+ ))} +diff --git a/backend/app/logging/logging_config.py b/backend/app/logging/logging_config.py index f326005b..682e3eff 100644 --- a/backend/app/logging/logging_config.py +++ b/backend/app/logging/logging_config.py @@ -24,14 +24,20 @@ def setup_logger(name: str) -> logging.Logger: datefmt="%Y-%m-%d %H:%M:%S" ) - # Console Handler + # Console Handler with UTF-8 encoding for Windows support console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter(formatter) + # Enable UTF-8 encoding to handle emoji and special characters on Windows + if hasattr(console_handler.stream, 'reconfigure'): + try: + console_handler.stream.reconfigure(encoding='utf-8') + except (AttributeError, ValueError): + pass logger.addHandler(console_handler) - # File Handler - file_handler = logging.FileHandler("app.log") + # File Handler with UTF-8 encoding + file_handler = logging.FileHandler("app.log", encoding="utf-8") file_handler.setLevel(logging.DEBUG) # Keep detailed logs in file file_handler.setFormatter(formatter) logger.addHandler(file_handler) diff --git a/backend/app/modules/bias_detection/check_bias.py b/backend/app/modules/bias_detection/check_bias.py index a0644529..555456b7 100644 --- a/backend/app/modules/bias_detection/check_bias.py +++ b/backend/app/modules/bias_detection/check_bias.py @@ -61,7 +61,7 @@ def check_bias(text): "content": (f"Give bias score to the following article \n\n{text}"), }, ], - model="gemma2-9b-it", + model="llama-3.1-8b-instant", temperature=0.3, max_tokens=512, ) diff --git a/backend/app/modules/chat/llm_processing.py b/backend/app/modules/chat/llm_processing.py index 2d5134fa..9ab7fc6b 100644 --- a/backend/app/modules/chat/llm_processing.py +++ b/backend/app/modules/chat/llm_processing.py @@ -39,11 +39,23 @@ def build_context(docs): return "\n".join( f"{m['metadata'].get('explanation') or m['metadata'].get('reasoning', '')}" for m in docs - ) + ).strip() + + +def ask_llm(question, docs, article_context=""): + rag_context = build_context(docs) + article_context = (article_context or "").strip() + + if rag_context and article_context: + context = f"Article:\n{article_context}\n\nRetrieved Insights:\n{rag_context}" + else: + context = rag_context or article_context + if not context: + return ( + "I don't have article context yet. Please analyze an article first and then ask me again." + ) -def ask_llm(question, docs): - context = build_context(docs) logger.debug(f"Generated context for LLM:\n{context}") prompt = f"""You are an assistant that answers based on context. @@ -55,7 +67,7 @@ def ask_llm(question, docs): """ response = client.chat.completions.create( - model="gemma2-9b-it", + model="llama-3.1-8b-instant", messages=[ {"role": "system", "content": "Use only the context to answer."}, {"role": "user", "content": prompt}, @@ -63,3 +75,5 @@ def ask_llm(question, docs): ) logger.info("LLM response retrieved successfully.") return response.choices[0].message.content + + \ No newline at end of file diff --git a/backend/app/modules/facts_check/llm_processing.py b/backend/app/modules/facts_check/llm_processing.py index dc223a85..134116cf 100644 --- a/backend/app/modules/facts_check/llm_processing.py +++ b/backend/app/modules/facts_check/llm_processing.py @@ -63,7 +63,7 @@ def run_claim_extractor_sdk(state): ), }, ], - model="gemma2-9b-it", + model="llama-3.1-8b-instant", temperature=0.3, max_tokens=512, ) @@ -128,7 +128,7 @@ def run_fact_verifier_sdk(search_results): ), }, ], - model="gemma2-9b-it", + model="llama-3.1-8b-instant", temperature=0.3, max_tokens=256, ) diff --git a/backend/app/modules/facts_check/web_search.py b/backend/app/modules/facts_check/web_search.py index 527b3cc9..9e8ce05b 100644 --- a/backend/app/modules/facts_check/web_search.py +++ b/backend/app/modules/facts_check/web_search.py @@ -28,15 +28,24 @@ def search_google(query): - results = requests.get( - f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}" - ) - res = results.json() - first = {} - first["title"] = res["items"][0]["title"] - first["link"] = res["items"][0]["link"] - first["snippet"] = res["items"][0]["snippet"] - - return [ - first, - ] + try: + results = requests.get( + f"https://www.googleapis.com/customsearch/v1?key={GOOGLE_SEARCH}&cx=f637ab77b5d8b4a3c&q={query}" + ) + res = results.json() + + # Check if the response contains 'items' (successful search) + if "items" not in res: + # Handle error responses from Google API + error_msg = res.get("error", {}).get("message", "Unknown error") + raise ValueError(f"Google API Error: {error_msg}") + + first = {} + first["title"] = res["items"][0]["title"] + first["link"] = res["items"][0]["link"] + first["snippet"] = res["items"][0]["snippet"] + + return [first] + except Exception as e: + print(f"Search Google Error: {e}") + raise diff --git a/backend/app/modules/langgraph_nodes/judge.py b/backend/app/modules/langgraph_nodes/judge.py index 57100301..c716dedf 100644 --- a/backend/app/modules/langgraph_nodes/judge.py +++ b/backend/app/modules/langgraph_nodes/judge.py @@ -24,7 +24,7 @@ # Init once groq_llm = ChatGroq( - model="gemma2-9b-it", + model="llama-3.1-8b-instant", temperature=0.0, max_tokens=10, ) diff --git a/backend/app/modules/langgraph_nodes/sentiment.py b/backend/app/modules/langgraph_nodes/sentiment.py index fef1d39d..d0c807b5 100644 --- a/backend/app/modules/langgraph_nodes/sentiment.py +++ b/backend/app/modules/langgraph_nodes/sentiment.py @@ -49,7 +49,7 @@ def run_sentiment_sdk(state): ), }, ], - model="gemma2-9b-it", + model="llama-3.1-8b-instant", temperature=0.2, max_tokens=3, ) diff --git a/backend/app/routes/routes.py b/backend/app/routes/routes.py index 6988f5e8..6ed053fc 100644 --- a/backend/app/routes/routes.py +++ b/backend/app/routes/routes.py @@ -30,7 +30,7 @@ """ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from pydantic import BaseModel from app.modules.pipeline import run_scraper_pipeline from app.modules.pipeline import run_langgraph_workflow @@ -52,6 +52,7 @@ class URlRequest(BaseModel): class ChatQuery(BaseModel): message: str + article_context: str | None = None @router.get("/") @@ -77,9 +78,20 @@ async def run_pipelines(request: URlRequest): @router.post("/chat") async def answer_query(request: ChatQuery): - query = request.message - results = search_pinecone(query) - answer = ask_llm(query, results) - logger.info(f"Chat answer generated: {answer}") - - return {"answer": answer} + try: + query = request.message.strip() + if not query: + raise HTTPException(status_code=400, detail="Message cannot be empty.") + + article_context = (request.article_context or "").strip() + + results = search_pinecone(query) + answer = ask_llm(query, results, article_context) + logger.info("Chat answer generated successfully.") + + return {"answer": answer} + except HTTPException: + raise + except Exception as e: + logger.exception(f"Chat request failed: {e}") + raise HTTPException(status_code=500, detail="Failed to generate chat response.") \ No newline at end of file diff --git a/backend/app/utils/fact_check_utils.py b/backend/app/utils/fact_check_utils.py index e7cc7a62..2f5f2f77 100644 --- a/backend/app/utils/fact_check_utils.py +++ b/backend/app/utils/fact_check_utils.py @@ -45,14 +45,14 @@ def run_fact_check_pipeline(state): result = run_claim_extractor_sdk(state) if state.get("status") != "success": - logger.error("❌ Claim extraction failed.") + logger.error("Claim extraction failed.") return [], "Claim extraction failed." # Step 1: Extract claims raw_output = result.get("verifiable_claims", "") claims = re.findall(r"^[\*\-•]\s+(.*)", raw_output, re.MULTILINE) claims = [claim.strip() for claim in claims if claim.strip()] - logger.info(f"🧠 Extracted claims: {claims}") + logger.info(f"Extracted claims: {claims}") if not claims: return [], "No verifiable claims found." @@ -60,21 +60,24 @@ def run_fact_check_pipeline(state): # Step 2: Search each claim with polite delay search_results = [] for claim in claims: - logger.info(f"\n🔍 Searching for claim: {claim}") + logger.info(f"Searching for claim: {claim}") try: results = search_google(claim) if results: results[0]["claim"] = claim search_results.append(results[0]) - logger.info(f"✅ Found result: {results[0]['title']}") + logger.info(f"Found result: {results[0]['title']}") else: - logger.warning(f"⚠️ No search result for: {claim}") + logger.warning(f"No search result for: {claim}") except Exception as e: - logger.error(f"❌ Search failed for: {claim} -> {e}") + logger.error(f"Search failed for: {claim} -> {e}") if not search_results: + logger.error("All claim searches failed or returned no results.") return [], "All claim searches failed or returned no results." # Step 3: Verify facts using LLM + logger.info(f"Verifying {len(search_results)} claims using LLM...") final = run_fact_verifier_sdk(search_results) + logger.info("Fact-checking pipeline completed successfully.") return final.get("verifications", []), None diff --git a/frontend/app/analyze/loading/page.tsx b/frontend/app/analyze/loading/page.tsx index 05067e9c..cef7bf11 100644 --- a/frontend/app/analyze/loading/page.tsx +++ b/frontend/app/analyze/loading/page.tsx @@ -16,7 +16,7 @@ import { import ThemeToggle from "@/components/theme-toggle"; import axios from "axios"; -// const backend_url = process.env.NEXT_PUBLIC_API_URL; +const backend_url = process.env.NEXT_PUBLIC_API_URL; @@ -74,10 +74,10 @@ export default function LoadingPage() { try { const [processRes, biasRes] = await Promise.all([ - axios.post("https://thunder1245-perspective-backend.hf.space/api/process", { + axios.post(`${backend_url}/api/process`, { url: storedUrl, }), - axios.post("https://thunder1245-perspective-backend.hf.space/api/bias", { + axios.post(`${backend_url}/api/bias`, { url: storedUrl, }), ]); diff --git a/frontend/app/analyze/results/page.tsx b/frontend/app/analyze/results/page.tsx index bd484492..245d3ad8 100644 --- a/frontend/app/analyze/results/page.tsx +++ b/frontend/app/analyze/results/page.tsx @@ -1,7 +1,7 @@ "use client"; import type React from "react"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Send, Link as LinkIcon } from "lucide-react"; @@ -19,7 +19,7 @@ import { Badge } from "@/components/ui/badge"; import BiasMeter from "@/components/bias-meter"; import axios from "axios"; -// const backend_url = process.env.NEXT_PUBLIC_API_URL; +const backendUrl = (process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000").trim(); /** * Renders the article analysis page with summary, perspectives, fact checks, bias meter, AI chat, and sources. @@ -32,6 +32,7 @@ export default function AnalyzePage() { const [activeTab, setActiveTab] = useState("summary"); const [message, setMessage] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [isChatLoading, setIsChatLoading] = useState(false); const [messages, setMessages] = useState<{ role: string; content: string }[]>( [ { @@ -80,21 +81,110 @@ export default function AnalyzePage() { async function handleSendMessage(e: React.FormEvent) { e.preventDefault(); - if (!message.trim()) return; - const newMessages = [...messages, { role: "user", content: message }]; + const userMessage = message.trim(); + if (!userMessage || isChatLoading) return; + + const newMessages = [...messages, { role: "user", content: userMessage }]; setMessages(newMessages); setMessage(""); + setIsChatLoading(true); - const res = await axios.post("https://thunder1245-perspective-backend.hf.space/api/chat", { - message: message, - }); - const data = res.data; - - console.log(data); + try { + const articleContext = analysisData?.cleaned_text?.trim() ?? ""; + const res = await axios.post( + `${backendUrl}/api/chat`, + { + message: userMessage, + article_context: articleContext, + }, + { timeout: 45000 } + ); + const answer = res.data?.answer ?? "I could not generate an answer right now."; - // 🔹 Step 2: Append LLM’s response - setMessages([...newMessages, { role: "assistant", content: data.answer }]); + setMessages([...newMessages, { role: "assistant", content: answer }]); + } catch (error: any) { + const fallback = + error?.response?.data?.detail || + error?.message || + "Chat request failed. Please try again."; + setMessages([ + ...newMessages, + { + role: "assistant", + content: `Sorry, I couldn't fetch a reply: ${fallback}`, + }, + ]); + } finally { + setIsChatLoading(false); + } } + + const cleanedTextForView = analysisData?.cleaned_text ?? ""; + + const articleLayout = useMemo(() => { + if (!cleanedTextForView) { + return { + highlights: [] as string[], + sections: [] as { title: string | null; paragraphs: string[] }[], + }; + } + + const blocks = cleanedTextForView + .split(/\n{2,}/) + .map((block: string) => block.trim()) + .filter(Boolean); + + const isTitleLike = (block: string) => { + const wordCount = block.split(/\s+/).length; + return ( + block.length <= 90 && + wordCount <= 12 && + !/[.]$/.test(block) && + !/^\d+[.)]/.test(block) + ); + }; + + const highlights: string[] = []; + const sections: { title: string | null; paragraphs: string[] }[] = []; + + for (let i = 0; i < blocks.length; i += 1) { + const block = blocks[i]; + const nextBlock = blocks[i + 1]; + + if (isTitleLike(block)) { + if (nextBlock && !isTitleLike(nextBlock)) { + sections.push({ title: block, paragraphs: [nextBlock] }); + i += 1; + } else { + highlights.push(block); + } + continue; + } + + const lastSection = sections[sections.length - 1]; + if (lastSection && lastSection.paragraphs.length < 2) { + lastSection.paragraphs.push(block); + } else { + sections.push({ title: null, paragraphs: [block] }); + } + } + + return { highlights, sections }; + }, [cleanedTextForView]); + + const articleStats = useMemo(() => { + const words = cleanedTextForView.trim() + ? cleanedTextForView.trim().split(/\s+/).length + : 0; + const readTimeMin = words > 0 ? Math.max(1, Math.round(words / 220)) : 0; + + return { + words, + readTimeMin, + sectionCount: articleLayout.sections.length, + }; + }, [cleanedTextForView, articleLayout.sections.length]); + if (isLoading) { return (
{para}
- ))} ++ Article Highlights +
++ {paragraph} +
+ ))} +