Skip to content

Completed Redesign of the landing page(Futuristic "Matrix" Aesthetic)#149

Open
Prateekiiitg56 wants to merge 2 commits intoAOSSIE-Org:mainfrom
Prateekiiitg56:redesigned-landing-page
Open

Completed Redesign of the landing page(Futuristic "Matrix" Aesthetic)#149
Prateekiiitg56 wants to merge 2 commits intoAOSSIE-Org:mainfrom
Prateekiiitg56:redesigned-landing-page

Conversation

@Prateekiiitg56
Copy link

@Prateekiiitg56 Prateekiiitg56 commented Mar 3, 2026

Description
fix issue no #146
This PR is basically a full visual overhaul of the Perspective-AI frontend

Main focus was purely UI/UX. I didn’t touch any backend endpoints, Axios logic, or core React state behavior. Everything functional stays exactly the same

The vibe I was going for:
dark, premium, slightly hacker-ish… like Bloomberg terminal meets philosophical matrix.

What I Changed

  1. Global Design System
    Switched to a deep midnight navy background (#0B0E14) with a subtle radial grid overlay.
    Added micro-interactions + a custom animated cursor that fits the dark theme.
    Everything now feels consistent and intentional.

  2. Landing Page (/)
    Built a full viewport Hero section.
    Added a 3D tilting interactive demo widget.
    Integrated animated particle background mesh for depth.
    Designed a horizontal CSS grid features section with hover tilt effects.
    Now it feels like an actual product, not just a project page.

  3. Analysis Page (/analyze)
    Converted it into a terminal-style dashboard.
    Added live URL validation with visual feedback (green check / red error).
    Restyled CTA into a glowing “Analyze Article” button with ripple click animation.
    Feels more interactive and dynamic now.

  4. Loading Screen (/analyze/loading)
    Styled it like a hacker terminal logging screen.
    Synced visuals perfectly with the existing 5-step React interval flow:
    No logic changes ,just skinned cleanly over the existing flow.

  5. Results Dashboard (/analyze/results)
    This is where most visual effort went.
    Bias Meter
    Built a large glowing semi-circular CSS gauge.
    Dynamically maps to the 0–100 biasScore.
    Data Tabs
    Cleaned Text, Counter Perspective, and Fact Check are now inside glass-style tabs pulling from sessionStorage.

AI Chat Panel
Integrated the existing /api/chat inside a transparent side terminal widget.
Styled distinct user/system message bubbles.

Technical Checks
sessionStorage + page-to-page state handoffs work properly.
/api/process POST still triggers correctly on valid URL.
/api/chat loop works fine inside new layout.
No .env or config changes.
No backend modifications.

here it the demo video -
https://drive.google.com/file/d/1SXzIQhLPSSiJXuIwweowAW98M4J6oiNq/view?usp=sharing

screenshots -
image
image
image

@Zahnentferner please take a look

Summary by CodeRabbit

  • New Features

    • New compact analyze flow with multi-step loader and progress indicator
    • Results dashboard with bias gauge, sentiment, and an AI chat panel
    • Global Feed for quick URL population and a single-click Analyze action
    • Interactive pipeline nodes with detailed modal views and tilt effects
  • UI/Design Improvements

    • Glass-morphism theme, improved dark mode, expanded fonts, custom cursor & particle background
    • Redesigned loading and results layouts with circular loader and progress percentage
  • Bug Fixes

    • Improved redirect/error handling during analysis flow

Copilot AI review requested due to automatic review settings March 3, 2026 14:38
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Redesigns the frontend UI across analyze flow and landing pages: replaces prior component library usage with bespoke layouts, adds custom cursor visuals and particle background, updates loading/results sessionStorage flow and step/progress timing, introduces multiple static HTML mocks, new fonts, Tailwind updates, and adds three.js/framer-motion dependencies.

Changes

Cohort / File(s) Summary
Analyze Flow Pages
frontend/app/analyze/page.tsx, frontend/app/analyze/loading/page.tsx, frontend/app/analyze/results/page.tsx
Replaces component-based analyze UI with bespoke layouts, adds custom cursor state (cursorPos, ringPos), sessionStorage reads/writes for analysisResult and BiasScore, hardcoded multi-step loading/progress with explicit intervals and cleanup, routing changes (redirects on error/success), and renamed default export for results page.
Static HTML Mock Pages
frontend/app/analyze/stitch-analyze.html, frontend/app/analyze/loading/stitch-loading.html, frontend/app/analyze/results/stitch-results.html, perspective-landing.html
Adds four standalone dark-themed Tailwind HTML mockups for analyze flow and landing page (static markup/styles only) implementing glass panels, bias gauge, terminal mock, and modal/node detail UX examples.
Landing Page & Interactive Assets
frontend/app/page.tsx, perspective-landing.html
Major landing page overhaul: TiltCard, pipeline node data/types (NodeData, NodeTypeWithName), modal node-detail system, typewriter animation, custom cursor and ring, and interactive pipeline visualizer.
Styling & Globals
frontend/app/globals.css, frontend/tailwind.config.ts
Adds custom-cursor CSS, particle-mesh background, glass-card styles, hover/tilt interactions, enables darkMode class, extends theme with new colors/fonts/radii/animations.
Layout & Fonts
frontend/app/layout.tsx
Introduces multiple Google fonts via next/font/google, maps them to CSS variables, injects Material Symbols stylesheet, updates html/body classes for smooth scrolling and font variables.
Package & Tooling
frontend/package.json, frontend/.eslintrc.json
Adds runtime deps: three, @react-three/fiber, @react-three/drei, framer-motion; adds dev deps including @types/three, eslint, eslint-config-next; adds ESLint config extending next/core-web-vitals.
Build / Diagnostics Artifacts
frontend/build_logs.txt, frontend/build_output.txt, frontend/err.html, frontend/tsc_res.txt
Adds build logs, build output, an error HTML page, and a tsc residue file capturing build/runtime diagnostics and an observed PageNotFound/ENOENT for /analyze.
Results Static Mock
frontend/app/analyze/results/stitch-results.html
New static results dashboard mock with bias gauge, tabs (Article/Perspective/Fact Check), AI discussion widget, and responsive glass UI (static only).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoadingPage
    participant SessionStorage as Storage
    participant Router

    User->>LoadingPage: Navigate with URL
    LoadingPage->>Storage: write target URL / init BiasScore
    LoadingPage->>LoadingPage: start stepInterval & progressInterval
    LoadingPage->>LoadingPage: update progress, advance steps
    LoadingPage->>Storage: store analysisResult on completion
    LoadingPage->>Router: navigate to /analyze/results (after delay)
    Router->>AnalyzeResultsPage: load and read analysisResult from Storage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped and tiled each glassy card,
traced rings where cursors danced,
I baked the nodes and stitched the art,
then thumped — the results advanced.
Spin the loader, tiny heart, the rabbit’s done its dance!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: a complete redesign of the landing page with a futuristic 'Matrix' aesthetic. This directly matches the primary focus of the PR, which is a visual overhaul affecting the landing page and related analyze pages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Prateekiiitg56 Prateekiiitg56 changed the title Complete Redesign the landing page(Futuristic "Matrix" Aesthetic) Completed Redesign the landing page(Futuristic "Matrix" Aesthetic) Mar 3, 2026
@Prateekiiitg56 Prateekiiitg56 changed the title Completed Redesign the landing page(Futuristic "Matrix" Aesthetic) Completed Redesign of the landing page(Futuristic "Matrix" Aesthetic) Mar 3, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/app/analyze/loading/page.tsx (1)

57-116: ⚠️ Potential issue | 🔴 Critical

Critical: Interval cleanup is inside async function, not returned to useEffect.

The cleanup function (lines 106-109) is returned from the async runAnalysis function, but useEffect ignores this return value. The intervals stepInterval and progressInterval are never cleaned up on component unmount, causing memory leaks.

🐛 Proposed fix for proper cleanup
   useEffect(() => {
+    let stepInterval: NodeJS.Timeout;
+    let progressInterval: NodeJS.Timeout;
+    let isMounted = true;
+
     const runAnalysis = async () => {
       const storedUrl = sessionStorage.getItem("articleUrl");
       if (storedUrl) {
         setArticleUrl(storedUrl);

         try {
           const [processRes, biasRes] = await Promise.all([
             axios.post("https://thunder1245-perspective-backend.hf.space/api/process", {
               url: storedUrl,
             }),
             axios.post("https://thunder1245-perspective-backend.hf.space/api/bias", {
               url: storedUrl,
             }),
           ]);

+          if (!isMounted) return;
+
           sessionStorage.setItem("BiasScore", JSON.stringify(biasRes.data));
           sessionStorage.setItem("analysisResult", JSON.stringify(processRes.data));

         } catch (err) {
           console.error("Failed to process article:", err);
-          router.push("/analyze"); // fallback in case of error
+          if (isMounted) router.push("/analyze");
           return;
         }

         // Progress and step simulation
-        const stepInterval = setInterval(() => {
+        stepInterval = setInterval(() => {
+          if (!isMounted) return;
           setCurrentStep((prev) => {
             if (prev < steps.length - 1) {
               return prev + 1;
             } else {
               clearInterval(stepInterval);
               setTimeout(() => {
-                router.push("/analyze/results");
+                if (isMounted) router.push("/analyze/results");
               }, 2000);
               return prev;
             }
           });
         }, 2000);

-        const progressInterval = setInterval(() => {
+        progressInterval = setInterval(() => {
+          if (!isMounted) return;
           setProgress((prev) => {
             if (prev < 100) {
               return prev + 1;
             }
             return prev;
           });
         }, 100);

-        return () => {
-          clearInterval(stepInterval);
-          clearInterval(progressInterval);
-        };
       } else {
-        router.push("/analyze");
+        if (isMounted) router.push("/analyze");
       }
     };

     runAnalysis();
-  }, [router, steps.length]);
+
+    return () => {
+      isMounted = false;
+      if (stepInterval) clearInterval(stepInterval);
+      if (progressInterval) clearInterval(progressInterval);
+    };
+  }, [router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` around lines 57 - 116, The cleanup for
the two intervals is currently returned from the async runAnalysis function
(which useEffect ignores); declare stepInterval and progressInterval in the
outer scope of the useEffect, assign them inside runAnalysis, and move the
cleanup into the useEffect return callback so clearInterval(stepInterval) and
clearInterval(progressInterval) run on unmount; ensure you do not return a
cleanup from the async runAnalysis itself and that any early returns in
runAnalysis still leave intervals undefined-safe when the useEffect cleanup
runs.
frontend/app/analyze/results/page.tsx (2)

45-64: ⚠️ Potential issue | 🟠 Major

Missing error handling for JSON.parse on sessionStorage data.

If the stored JSON is malformed, JSON.parse will throw and crash the page. Wrap in try-catch.

🐛 Proposed fix with error handling
   useEffect(() => {
     if (isRedirecting.current) {
       return;
     }

     const storedData = sessionStorage.getItem("analysisResult");
     const storedBiasScore = sessionStorage.getItem("BiasScore");

     if (storedBiasScore && storedData) {
-      setBiasScore(JSON.parse(storedBiasScore).bias_score);
-      setAnalysisData(JSON.parse(storedData));
-      setIsLoading(false);
+      try {
+        setBiasScore(JSON.parse(storedBiasScore).bias_score);
+        setAnalysisData(JSON.parse(storedData));
+        setIsLoading(false);
+      } catch (err) {
+        console.error("Failed to parse stored analysis data:", err);
+        if (!isRedirecting.current) {
+          isRedirecting.current = true;
+          router.push("/analyze");
+        }
+      }
     } else {
       console.warn("No bias or data found. Redirecting...");
       if (!isRedirecting.current) {
         isRedirecting.current = true;
         router.push("/analyze");
       }
     }
   }, [router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 45 - 64, Wrap the
sessionStorage read + JSON.parse logic inside the useEffect's block in a
try-catch to guard against malformed JSON: attempt to parse storedBiasScore and
storedData, call setBiasScore, setAnalysisData and setIsLoading(false) on
success, and on catch log the error, clear the invalid keys ("analysisResult"
and "BiasScore") and trigger the existing redirect via isRedirecting.current and
router.push("/analyze"); keep the existing early-return when
isRedirecting.current is true so you only add try-catch around the parsing and
state updates referenced by useEffect, setBiasScore, setAnalysisData,
setIsLoading, sessionStorage.getItem, and router.push.

66-82: ⚠️ Potential issue | 🟡 Minor

Chat lacks loading state during API call; caught error is not logged.

Users can spam messages while waiting for a response. Add a loading state to disable input during the API call. Also, log the caught error for debugging.

💡 Suggested improvements

Add a loading state:

+  const [isSending, setIsSending] = useState(false);

   async function handleSendMessage(e: React.FormEvent) {
     e.preventDefault();
-    if (!message.trim()) return;
+    if (!message.trim() || isSending) return;
     const newMessages = [...messages, { role: "user", content: message }];
     setMessages(newMessages);
     setMessage("");
+    setIsSending(true);

     try {
       const res = await axios.post("https://thunder1245-perspective-backend.hf.space/api/chat", {
         message: message,
       });
       const data = res.data;
       setMessages([...newMessages, { role: "assistant", content: data.answer }]);
-    } catch (e) {
+    } catch (err) {
+      console.error("Chat API error:", err);
       setMessages([...newMessages, { role: "assistant", content: "Error contacting the server." }]);
+    } finally {
+      setIsSending(false);
     }
   }

Then use isSending to disable the input/button.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 66 - 82,
handleSendMessage lacks a loading flag and doesn't log errors; add a boolean
state (e.g., isSending with setIsSending) and update handleSendMessage to return
early if isSending is true, setIsSending(true) before the axios call and
setIsSending(false) in a finally block, disable the input and submit button
using isSending, and log the caught error in the catch (e.g., console.error(e))
while keeping the current message/error message handling with setMessages.
🧹 Nitpick comments (13)
frontend/package.json (1)

42-42: Move @types/three to devDependencies.

Type definition packages like @types/three are only needed at build/compile time, not at runtime. Placing them in dependencies unnecessarily increases the production bundle footprint.

♻️ Proposed fix
   "@react-three/drei": "^10.7.7",
   "@react-three/fiber": "^9.5.0",
-  "@types/three": "^0.183.1",
   "autoprefixer": "^10.4.20",

Add to devDependencies:

 "devDependencies": {
   "@types/node": "^22",
   "@types/react": "^19",
   "@types/react-dom": "^19",
+  "@types/three": "^0.183.1",
   "postcss": "^8",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` at line 42, The package "@types/three" is currently
listed in dependencies but should be a devDependency; remove the "@types/three"
entry from dependencies in package.json and add the same version string under
devDependencies so it's only installed at build time (reference the dependency
key "@types/three" in package.json).
frontend/app/globals.css (1)

166-169: Add prefers-reduced-motion support for animations.

Users who have enabled reduced motion preferences should not see animated transforms. The .tilt-effect:hover animation should respect this setting.

♻️ Proposed fix
 .tilt-effect:hover {
   transform: perspective(1000px) rotateX(2deg) rotateY(-2deg);
   transition: transform 0.3s ease;
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .tilt-effect:hover {
+    transform: none;
+  }
+  .animate-fade-in,
+  .fade-in,
+  .slide-up,
+  .slide-in-right {
+    animation: none;
+    opacity: 1;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/globals.css` around lines 166 - 169, The .tilt-effect:hover rule
must respect users' reduced-motion preference; update the stylesheet so that
when prefers-reduced-motion: reduce is set the hover transform/transition are
disabled for .tilt-effect (e.g., apply transition: none and avoid changing
transform on hover). Modify the existing .tilt-effect:hover and add a `@media`
(prefers-reduced-motion: reduce) block targeting .tilt-effect and
.tilt-effect:hover to remove the animation effects while keeping the original
behavior for other users.
frontend/tailwind.config.ts (1)

9-9: Overly broad content pattern may cause slow builds or unexpected matches.

The pattern "*.{js,ts,jsx,tsx,mdx}" matches all files at the repository root (e.g., next.config.ts, tailwind.config.ts). This is likely unintended and may slow down Tailwind's class scanning.

♻️ Proposed fix: scope to source directories
 content: [
   "./pages/**/*.{js,ts,jsx,tsx,mdx}",
   "./components/**/*.{js,ts,jsx,tsx,mdx}",
   "./app/**/*.{js,ts,jsx,tsx,mdx}",
-  "*.{js,ts,jsx,tsx,mdx}"
+  "./src/**/*.{js,ts,jsx,tsx,mdx}"
 ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tailwind.config.ts` at line 9, The Tailwind content glob "*.
{js,ts,jsx,tsx,mdx}" is too broad and matches repo root files; replace it with
scoped patterns to only scan source directories (for example use
"src/**/*.{js,ts,jsx,tsx,mdx}" and optionally "app/**/*.{js,ts,jsx,tsx,mdx}",
"pages/**/*.{js,ts,jsx,tsx,mdx}", "components/**/*.{js,ts,jsx,tsx,mdx}") so
Tailwind only processes your app code and not config/root files; update the
content array where the pattern string appears accordingly (replace the existing
"*. {js,ts,jsx,tsx,mdx}" entry with the scoped globs).
frontend/app/layout.tsx (1)

24-26: Avoid manual <head> in App Router layout; use metadata or next/head.

In Next.js 15 App Router, manually inserting a <head> element can lead to unexpected behavior during streaming and hydration. The Material Symbols stylesheet should be loaded using a different approach.

♻️ Recommended approaches

Option 1: Use metadata.icons or a Script component

Move to app/layout.tsx metadata:

export const metadata: Metadata = {
  title: "Perspective - AI-Powered Bias Detection",
  description: "...",
  // Note: stylesheets aren't directly supported in metadata
}

Option 2: Use next/script for external resources

import Script from 'next/script'

// In the body:
<Script
  src="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
  strategy="beforeInteractive"
/>

Option 3: Self-host the icon font (best for performance)

Download and include Material Symbols in your project's public folder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/layout.tsx` around lines 24 - 26, Remove the manual <head>
element from app/layout.tsx and instead load the Material Symbols stylesheet
using Next.js' Script component: import Script from 'next/script' at the top of
layout.tsx and add a <Script src="https://fonts.googleapis.com/...."
strategy="beforeInteractive" /> inside the layout component (near the top of the
returned JSX) so the font loads correctly during streaming/hydration;
alternatively, if you prefer self-hosting, add the font files to public and
reference them via a link tag or CSS instead of keeping a raw <head> tag.
perspective-landing.html (2)

40-85: CSS duplication with globals.css.

The custom cursor, .particle-mesh, .glass-card, and .tilt-effect styles are duplicated here and in frontend/app/globals.css. If this HTML file is intended to be served alongside the React app, consider importing shared styles or extracting to a shared CSS file to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@perspective-landing.html` around lines 40 - 85, The styles for
.custom-cursor, .custom-cursor-ring, .particle-mesh, .glass-card and
.tilt-effect are duplicated; remove them from this HTML and instead reference
the single shared stylesheet used by the React app (or extract these rules into
a new shared CSS file) and include a link to that shared file in the page head
so the page uses the same definitions; ensure the class names (.custom-cursor,
.custom-cursor-ring, .particle-mesh, .glass-card, .tilt-effect) remain unchanged
so existing markup and JS that target them continues to work.

122-122: Hero section may not be responsive on smaller screens.

The hero uses a fixed col-span-6 layout which assumes a 12-column grid is always appropriate. On tablet/mobile viewports, this could result in cramped or broken layouts.

♻️ Suggested responsive classes
-<div class="col-span-6 flex flex-col justify-center gap-8">
+<div class="col-span-12 lg:col-span-6 flex flex-col justify-center gap-8">
...
-<div class="col-span-6 flex justify-end items-center">
+<div class="col-span-12 lg:col-span-6 flex justify-end items-center mt-8 lg:mt-0">

Also applies to: 139-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@perspective-landing.html` at line 122, The hero column is fixed to
"col-span-6" which breaks on smaller viewports; update the class on the element
that currently reads "col-span-6 flex flex-col justify-center gap-8" (and the
other occurrence at the same pattern) to use responsive Tailwind classes like
"col-span-12 md:col-span-6 flex flex-col justify-center gap-8" so the column
becomes full-width on small screens and half-width on medium+ screens.
frontend/app/page.tsx (3)

96-106: Remove unused variable animationFrameId.

The variable animationFrameId on line 97 is declared but never used.

🧹 Remove dead code
   // Custom Cursor
   useEffect(() => {
-    let animationFrameId: number;
     const updateMouse = (e: MouseEvent) => {
       setCursorPos({ x: e.clientX, y: e.clientY });
       setTimeout(() => {
         setRingPos({ x: e.clientX - 11, y: e.clientY - 11 });
       }, 50);
     };
     window.addEventListener("mousemove", updateMouse);
     return () => window.removeEventListener("mousemove", updateMouse);
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/page.tsx` around lines 96 - 106, Remove the dead local variable
animationFrameId declared inside the useEffect; it is unused. Edit the useEffect
block (where updateMouse, setCursorPos and setRingPos are defined) and delete
the line declaring animationFrameId so only the updateMouse handler,
window.addEventListener("mousemove", updateMouse), and the cleanup remain.

89-89: Type the modalData state instead of using any.

Using any loses type safety. Type it with the node interface plus the name field.

💡 Suggested type
+type ModalData = NodeInfo & { name: string } | null;
+
-  const [modalData, setModalData] = useState<any>(null);
+  const [modalData, setModalData] = useState<ModalData>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/page.tsx` at line 89, Replace the useState<any> with a concrete
type for modalData: define or import the node interface used in this file and
extend/union it to include the name field, then change the state declaration
(modalData and setModalData) to use useState<NodeTypeWithName | null> (or the
correct interface name) so modalData is typed instead of any; ensure any related
usages of modalData are updated to match the new type.

7-7: Consider using a typed interface instead of Record<string, any>.

Using any loses type safety. Define a proper interface for the node data structure.

💡 Suggested type definition
+interface NodeInfo {
+  desc: string;
+  icon: string;
+  color: string;
+  cot: string[];
+}
+
-const nodeData: Record<string, any> = {
+const nodeData: Record<string, NodeInfo> = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/page.tsx` at line 7, Replace the untyped Record<string, any>
used for nodeData with a proper interface: define an interface (e.g., interface
NodeData { /* fields used in this file: id?: string; label?: string; children?:
string[]; ... */ }) that lists the actual properties accessed, change the
declaration const nodeData: Record<string, any> to use the new type (e.g., const
nodeData: Record<string, NodeData> or const nodeData: { [key: string]: NodeData
}), and update any code that reads/writes nodeData entries to match the typed
fields; export the interface if other modules use it.
frontend/app/analyze/results/page.tsx (1)

10-10: Consider typing analysisData instead of using any.

Define an interface for the expected analysis response structure to improve type safety and IDE support.

💡 Suggested type definition
+interface AnalysisData {
+  cleaned_text: string;
+  facts: Array<{
+    original_claim: string;
+    verdict: string;
+    explanation: string;
+    source_link?: string;
+  }>;
+  sentiment: string;
+  perspective?: {
+    perspective: string;
+    reasoning: string;
+  };
+}
+
-  const [analysisData, setAnalysisData] = useState<any>(null);
+  const [analysisData, setAnalysisData] = useState<AnalysisData | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` at line 10, Replace the use of any for
analysisData with a proper typed interface: create an interface (e.g.,
AnalysisData) that matches the API response shape used by this page, then change
the state declaration from useState<any>(null) to useState<AnalysisData |
null>(null) and update any handlers/fetchers that call setAnalysisData to
return/accept AnalysisData; reference the state symbols analysisData and
setAnalysisData and the interface name (e.g., AnalysisData) when making these
changes so IDE/type-checking catches mismatches.
frontend/app/analyze/page.tsx (1)

26-33: URL validation accepts potentially dangerous protocols.

The current validation accepts any syntactically valid URL including javascript: and data: URLs. Consider restricting to http: and https: protocols.

🛡️ Suggested protocol validation
   const validateUrl = (inputUrl: string) => {
     try {
-      new URL(inputUrl);
-      setIsValidUrl(true);
+      const parsed = new URL(inputUrl);
+      const allowedProtocols = ['http:', 'https:'];
+      setIsValidUrl(allowedProtocols.includes(parsed.protocol));
     } catch {
       setIsValidUrl(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/page.tsx` around lines 26 - 33, The validateUrl function
currently accepts any syntactically valid URL (including javascript: or data:);
update validateUrl to parse the URL inside the try, then additionally check that
url.protocol is exactly 'http:' or 'https:' and only call setIsValidUrl(true) in
that case—otherwise call setIsValidUrl(false); reference the validateUrl
function and setIsValidUrl state when making this change.
frontend/app/analyze/loading/page.tsx (2)

53-53: Unused field textVerified in steps data.

The textVerified field is defined for the "Generating Perspectives" step but is never rendered in the UI. Either remove it or implement the display logic.

🧹 Options

Option 1: Remove unused field

     {
       title: "Generating Perspectives",
       subtitleVerified: "Analysis Result",
       subtitlePending: "Synthesizing",
-      textVerified: "> Compiling perspectives...\n> Structuring narrative insights..."
     },

Option 2: Render the field when step is completed
Add rendering logic in the step map where isCompleted && step.textVerified displays the text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` at line 53, The steps data includes an
unused textVerified field for the "Generating Perspectives" step; either remove
the textVerified property from the steps array or update the step rendering
logic in page.tsx (where the steps array is mapped and isCompleted is checked)
to conditionally render step.textVerified (e.g., render when isCompleted &&
step.textVerified) so the text appears when the step completes; locate the steps
array and the component that maps over it and apply one of these two fixes
referencing textVerified, steps, and the isCompleted check.

116-116: Dependency on steps.length is unnecessary and could cause issues.

The steps array is defined inside the component but is a constant. Including steps.length in the dependency array is unnecessary since it never changes. If steps were to be moved to state, this could cause infinite re-runs.

💡 Suggested fix

Move steps outside the component or use a ref, and remove from dependencies:

+const steps = [
+  { title: "Fetching Article", subtitleVerified: "Node Verified", subtitlePending: "Awaiting Source" },
+  { title: "AI Analysis", subtitleVerified: "Entropy Mapped", subtitlePending: "Parsing Logic" },
+  { title: "Bias Detection", subtitleVerified: "Cognitive Filter Applied", subtitlePending: "Scanning Narratives" },
+  { title: "Fact Checking", subtitleVerified: "Cross-Reference Complete", subtitlePending: "Querying Database" },
+  { title: "Generating Perspectives", subtitleVerified: "Analysis Result", subtitlePending: "Synthesizing", textVerified: "> Compiling perspectives...\n> Structuring narrative insights..." },
+];
+
 export default function LoadingPage() {
   // ... state declarations ...
-  const steps = [ ... ];
   
   // ... in useEffect ...
-  }, [router, steps.length]);
+  }, [router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` at line 116, The useEffect dependency
includes steps.length even though steps is a constant defined inside the
component; remove steps.length from the dependency array of the useEffect that
references router (leave [router]) and either move the steps array definition
outside the component (so it’s stable) or store it in a ref (e.g., stepsRef) to
ensure it won’t trigger re-runs; update any references to the moved/ref version
accordingly in the effect and component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/analyze/loading/stitch-loading.html`:
- Around line 6-7: Remove the duplicate Material Symbols stylesheet link element
in stitch-loading.html by keeping a single <link> element that references the
href
"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
and deleting the redundant identical one; verify the remaining <link> preserves
the same attributes and, if present elsewhere (e.g., stitch-results.html),
ensure you don’t reintroduce duplicates across templates.
- Line 1: Add the HTML5 doctype declaration to the top of the document by
inserting <!DOCTYPE html> before the existing <html class="dark"> tag in
stitch-loading.html (the same fix applied in stitch-results.html); this ensures
the page renders in standards mode rather than quirks mode.

In `@frontend/app/analyze/page.tsx`:
- Around line 60-62: The logo wrapper currently uses a div with onClick(() =>
router.push("/")) and lacks keyboard accessibility; update the element (in
frontend/app/analyze/page.tsx) to be keyboard-navigable by either replacing the
div with a semantic interactive element (e.g., button or Next.js Link) or adding
role="button" and tabIndex={0} plus an onKeyDown handler that triggers the same
router.push("/") when Enter or Space is pressed; ensure any aria-label or
descriptive text remains and focus/visible outline styles are preserved for
accessibility.
- Around line 160-167: The feed item click handler currently bypasses URL
validation by unconditionally calling setIsValidUrl(true); instead, call the
existing URL validation routine (e.g., validateUrl or the same validation
function used elsewhere when the user types a URL) with item.url,
setUrl(item.url) first, then setIsValidUrl(validationResult) based on that
function's return; update the onClick in the feed mapping (the anonymous
function that calls setUrl and setIsValidUrl) to perform validation and only
mark valid when the validator returns true.

In `@frontend/app/analyze/results/stitch-results.html`:
- Line 1: Add the HTML5 doctype declaration at the very top of the file so the
document does not render in quirks mode; specifically insert <!DOCTYPE html>
immediately before the existing <html class="dark"> element in
stitch-results.html to ensure standards-mode rendering.
- Line 148: Add an accessible label to the chat input so screen readers know its
purpose: either add aria-label="Ask a question" directly on the <input> element
or add a visually-hidden <label for="chat-input">Ask a question</label> and give
the input id="chat-input"; ensure the label text matches the placeholder and
keep existing classes and attributes (target the input element in the diff).
- Around line 6-7: Remove the duplicate Google Fonts stylesheet link for
Material Symbols: locate the two identical <link> tags referencing the
Material+Symbols+Outlined href in stitch-results.html and delete one so only a
single stylesheet inclusion remains; ensure you keep the correctly formed link
(the one with rel="stylesheet" and the Material+Symbols-Outlined href) to avoid
redundant network requests.
- Line 174: Remove the stray Markdown code fence (the triple backticks "```")
present near the end of stitch-results.html so it does not render as literal
text in the browser; open the file (stitch-results.html), locate the extraneous
"```" immediately before or after the closing tags (e.g., around the closing
</body></html>), delete that backtick line and ensure only the proper HTML
closing tags remain.

In `@frontend/app/analyze/stitch-analyze.html`:
- Around line 9-10: Remove the duplicate Material Symbols stylesheet <link> tag
(the repeated link with
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined...") so
the font is only imported once; locate the repeated <link> tag in
stitch-analyze.html (the duplicated tag using the Material Symbols Outlined
href) and delete one occurrence, and scan other static HTML files for the same
duplicated href to ensure only a single import remains across the site.
- Around line 96-105: The label and input lack an accessible programmatic
association: add a unique id (e.g., id="target-url") to the <input> element and
set the <label>'s for attribute to that id (for="target-url"), and also add an
aria-label (e.g., aria-label="Analysis target URL, include scheme like
https://") on the <input> to clarify expected format; keep the existing
placeholder and visual markup unchanged and ensure the input's type="url"
remains.

In `@frontend/app/globals.css`:
- Around line 135-137: Replace the global "body { cursor: none; }" rule with a
scoped rule such as ".custom-cursor-enabled { cursor: none; }" (also update the
similar rules at the other occurrence around lines 174-177) and add a JS gate in
your layout/page component (e.g., inside useEffect) that detects fine pointer
capability (window.matchMedia('(pointer: fine)').matches) and conditionally
adds/removes the "custom-cursor-enabled" class on document.body so the native
cursor remains for users/devices that cannot or should not use a custom cursor.

In `@frontend/app/page.tsx`:
- Around line 346-369: The pipeline node div that calls openNodeModal(node.id)
is not keyboard accessible; update the clickable container (the outer div where
onClick={() => openNodeModal(node.id)}) to include tabIndex={0}, role="button",
and an onKeyDown handler that listens for Enter and Space and calls
openNodeModal(node.id) (prevent default for Space). Also add an appropriate
aria-label (e.g., `aria-label={`Open ${node.id} node`}`) so screen readers
announce the control; ensure the same visual focus styles remain usable.
- Around line 439-478: The modal rendered when modalData is truthy needs
keyboard and ARIA support: add role="dialog" aria-modal="true" and
aria-labelledby="modal-title" to the containing modal element and give the
heading (currently the h3 showing modalData.name) id="modal-title"; make the
close button accessible by adding an aria-label (e.g., "Close modal") and ensure
its click still calls setModalData(null); add an Escape key handler (attach on
mount/unmount when modalData is set) that calls setModalData(null); and
implement simple focus management (move focus into the modal when opened and
restore previous focus when closed) referenced around modalData and setModalData
handling so screen readers and keyboard users can interact correctly.
- Around line 108-131: The typewriter useEffect (typingRef, fullText, type,
setTypewriterText) spawns recursive setTimeouts but has no cleanup; track the
timeout IDs (e.g., store current timer id(s) in a ref), clear them in the
returned cleanup function and stop further updates when unmounted (use typingRef
or an isMounted ref flag) to prevent state updates after unmount and memory
leaks; ensure the recursive type function checks the mounted flag before
scheduling further timeouts and clear any pending timeout IDs in cleanup.

In `@frontend/tailwind.config.ts`:
- Around line 49-65: tailwind.config.ts defines color tokens under chart and
sidebar that rely on CSS variables like --chart-1 and --sidebar-background, but
those variables are not present in the stylesheet imported by layout.tsx; fix
this by ensuring the variable definitions are loaded at runtime—either copy the
variable definitions (e.g., --chart-1...--chart-5 and --sidebar-background,
--sidebar-foreground, --sidebar-primary, etc.) into frontend/app/globals.css or
update layout.tsx to import the existing frontend/styles/globals.css so the
chart and sidebar CSS variables exist when tailwind utilities are used.

In `@perspective-landing.html`:
- Around line 539-575: The modal (`#node-modal`) lacks accessibility: add
role="dialog", aria-modal="true", aria-labelledby on the container and ensure
`#modal-title` has an id; implement keyboard handlers to close on Escape (attach a
keydown listener that calls closeModal/openModal logic) and trap focus within
the modal (capture focusable elements inside `#modal-content` and cycle
Tab/Shift+Tab when open), move focus into the modal when opening and restore the
previously focused element when closing (use the existing `#close-modal` as the
initial focus target), and update the node click handler to stop using the
fragile node.querySelector('span[class*="text-"]').textContent parsing—use a
data attribute like data-node on the clickable element and read
node.dataset.node to populate `#modal-title` and other fields.
- Around line 16-18: Remove the duplicate Material Symbols stylesheet link
element (the <link> with href
"https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined...&display=swap")
from perspective-landing.html (and other static HTML files) so the font is only
included once; locate the redundant <link rel="stylesheet"> entries referencing
that exact href and delete the duplicates, keeping a single canonical include
(preferably in the shared head/template) to avoid repeated requests.

---

Outside diff comments:
In `@frontend/app/analyze/loading/page.tsx`:
- Around line 57-116: The cleanup for the two intervals is currently returned
from the async runAnalysis function (which useEffect ignores); declare
stepInterval and progressInterval in the outer scope of the useEffect, assign
them inside runAnalysis, and move the cleanup into the useEffect return callback
so clearInterval(stepInterval) and clearInterval(progressInterval) run on
unmount; ensure you do not return a cleanup from the async runAnalysis itself
and that any early returns in runAnalysis still leave intervals undefined-safe
when the useEffect cleanup runs.

In `@frontend/app/analyze/results/page.tsx`:
- Around line 45-64: Wrap the sessionStorage read + JSON.parse logic inside the
useEffect's block in a try-catch to guard against malformed JSON: attempt to
parse storedBiasScore and storedData, call setBiasScore, setAnalysisData and
setIsLoading(false) on success, and on catch log the error, clear the invalid
keys ("analysisResult" and "BiasScore") and trigger the existing redirect via
isRedirecting.current and router.push("/analyze"); keep the existing
early-return when isRedirecting.current is true so you only add try-catch around
the parsing and state updates referenced by useEffect, setBiasScore,
setAnalysisData, setIsLoading, sessionStorage.getItem, and router.push.
- Around line 66-82: handleSendMessage lacks a loading flag and doesn't log
errors; add a boolean state (e.g., isSending with setIsSending) and update
handleSendMessage to return early if isSending is true, setIsSending(true)
before the axios call and setIsSending(false) in a finally block, disable the
input and submit button using isSending, and log the caught error in the catch
(e.g., console.error(e)) while keeping the current message/error message
handling with setMessages.

---

Nitpick comments:
In `@frontend/app/analyze/loading/page.tsx`:
- Line 53: The steps data includes an unused textVerified field for the
"Generating Perspectives" step; either remove the textVerified property from the
steps array or update the step rendering logic in page.tsx (where the steps
array is mapped and isCompleted is checked) to conditionally render
step.textVerified (e.g., render when isCompleted && step.textVerified) so the
text appears when the step completes; locate the steps array and the component
that maps over it and apply one of these two fixes referencing textVerified,
steps, and the isCompleted check.
- Line 116: The useEffect dependency includes steps.length even though steps is
a constant defined inside the component; remove steps.length from the dependency
array of the useEffect that references router (leave [router]) and either move
the steps array definition outside the component (so it’s stable) or store it in
a ref (e.g., stepsRef) to ensure it won’t trigger re-runs; update any references
to the moved/ref version accordingly in the effect and component.

In `@frontend/app/analyze/page.tsx`:
- Around line 26-33: The validateUrl function currently accepts any
syntactically valid URL (including javascript: or data:); update validateUrl to
parse the URL inside the try, then additionally check that url.protocol is
exactly 'http:' or 'https:' and only call setIsValidUrl(true) in that
case—otherwise call setIsValidUrl(false); reference the validateUrl function and
setIsValidUrl state when making this change.

In `@frontend/app/analyze/results/page.tsx`:
- Line 10: Replace the use of any for analysisData with a proper typed
interface: create an interface (e.g., AnalysisData) that matches the API
response shape used by this page, then change the state declaration from
useState<any>(null) to useState<AnalysisData | null>(null) and update any
handlers/fetchers that call setAnalysisData to return/accept AnalysisData;
reference the state symbols analysisData and setAnalysisData and the interface
name (e.g., AnalysisData) when making these changes so IDE/type-checking catches
mismatches.

In `@frontend/app/globals.css`:
- Around line 166-169: The .tilt-effect:hover rule must respect users'
reduced-motion preference; update the stylesheet so that when
prefers-reduced-motion: reduce is set the hover transform/transition are
disabled for .tilt-effect (e.g., apply transition: none and avoid changing
transform on hover). Modify the existing .tilt-effect:hover and add a `@media`
(prefers-reduced-motion: reduce) block targeting .tilt-effect and
.tilt-effect:hover to remove the animation effects while keeping the original
behavior for other users.

In `@frontend/app/layout.tsx`:
- Around line 24-26: Remove the manual <head> element from app/layout.tsx and
instead load the Material Symbols stylesheet using Next.js' Script component:
import Script from 'next/script' at the top of layout.tsx and add a <Script
src="https://fonts.googleapis.com/...." strategy="beforeInteractive" /> inside
the layout component (near the top of the returned JSX) so the font loads
correctly during streaming/hydration; alternatively, if you prefer self-hosting,
add the font files to public and reference them via a link tag or CSS instead of
keeping a raw <head> tag.

In `@frontend/app/page.tsx`:
- Around line 96-106: Remove the dead local variable animationFrameId declared
inside the useEffect; it is unused. Edit the useEffect block (where updateMouse,
setCursorPos and setRingPos are defined) and delete the line declaring
animationFrameId so only the updateMouse handler,
window.addEventListener("mousemove", updateMouse), and the cleanup remain.
- Line 89: Replace the useState<any> with a concrete type for modalData: define
or import the node interface used in this file and extend/union it to include
the name field, then change the state declaration (modalData and setModalData)
to use useState<NodeTypeWithName | null> (or the correct interface name) so
modalData is typed instead of any; ensure any related usages of modalData are
updated to match the new type.
- Line 7: Replace the untyped Record<string, any> used for nodeData with a
proper interface: define an interface (e.g., interface NodeData { /* fields used
in this file: id?: string; label?: string; children?: string[]; ... */ }) that
lists the actual properties accessed, change the declaration const nodeData:
Record<string, any> to use the new type (e.g., const nodeData: Record<string,
NodeData> or const nodeData: { [key: string]: NodeData }), and update any code
that reads/writes nodeData entries to match the typed fields; export the
interface if other modules use it.

In `@frontend/package.json`:
- Line 42: The package "@types/three" is currently listed in dependencies but
should be a devDependency; remove the "@types/three" entry from dependencies in
package.json and add the same version string under devDependencies so it's only
installed at build time (reference the dependency key "@types/three" in
package.json).

In `@frontend/tailwind.config.ts`:
- Line 9: The Tailwind content glob "*. {js,ts,jsx,tsx,mdx}" is too broad and
matches repo root files; replace it with scoped patterns to only scan source
directories (for example use "src/**/*.{js,ts,jsx,tsx,mdx}" and optionally
"app/**/*.{js,ts,jsx,tsx,mdx}", "pages/**/*.{js,ts,jsx,tsx,mdx}",
"components/**/*.{js,ts,jsx,tsx,mdx}") so Tailwind only processes your app code
and not config/root files; update the content array where the pattern string
appears accordingly (replace the existing "*. {js,ts,jsx,tsx,mdx}" entry with
the scoped globs).

In `@perspective-landing.html`:
- Around line 40-85: The styles for .custom-cursor, .custom-cursor-ring,
.particle-mesh, .glass-card and .tilt-effect are duplicated; remove them from
this HTML and instead reference the single shared stylesheet used by the React
app (or extract these rules into a new shared CSS file) and include a link to
that shared file in the page head so the page uses the same definitions; ensure
the class names (.custom-cursor, .custom-cursor-ring, .particle-mesh,
.glass-card, .tilt-effect) remain unchanged so existing markup and JS that
target them continues to work.
- Line 122: The hero column is fixed to "col-span-6" which breaks on smaller
viewports; update the class on the element that currently reads "col-span-6 flex
flex-col justify-center gap-8" (and the other occurrence at the same pattern) to
use responsive Tailwind classes like "col-span-12 md:col-span-6 flex flex-col
justify-center gap-8" so the column becomes full-width on small screens and
half-width on medium+ screens.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68a64c3 and f89f37f.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • frontend/app/analyze/loading/page.tsx
  • frontend/app/analyze/loading/stitch-loading.html
  • frontend/app/analyze/page.tsx
  • frontend/app/analyze/results/page.tsx
  • frontend/app/analyze/results/stitch-results.html
  • frontend/app/analyze/stitch-analyze.html
  • frontend/app/globals.css
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/package.json
  • frontend/tailwind.config.ts
  • perspective-landing.html

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR delivers a full UI/UX overhaul of the Perspective-AI frontend, introducing a new “Matrix/terminal” aesthetic across the landing page and the analyze flow while keeping the underlying analysis/chat functionality intact.

Changes:

  • Replaced the existing landing page with a new hero + feature grid + pipeline visual + modal details UI.
  • Restyled /analyze, /analyze/loading, and /analyze/results into a terminal/dashboard experience (custom cursor, glass cards, particle/grid backgrounds, tabs, gauge, chat panel).
  • Updated Tailwind theme configuration (colors + font families) and added new frontend dependencies.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
perspective-landing.html Adds a standalone redesigned landing page prototype (Tailwind CDN + custom cursor + modal).
frontend/tailwind.config.ts Extends theme colors and adds font families for the new design system.
frontend/package.json Adds animation/3D-related dependencies for the new UI direction.
frontend/package-lock.json Lockfile updates for newly added dependencies.
frontend/app/page.tsx Implements the redesigned landing page UI in the Next.js app.
frontend/app/layout.tsx Adds new font variables and includes Material Symbols.
frontend/app/globals.css Adds global cursor override + shared visual utility classes (glass, particle mesh, tilt).
frontend/app/analyze/stitch-analyze.html Adds a static HTML stitch/prototype for the analyze page redesign.
frontend/app/analyze/results/stitch-results.html Adds a static HTML stitch/prototype for the results redesign.
frontend/app/analyze/results/page.tsx Implements the redesigned results dashboard (gauge, tabs, chat).
frontend/app/analyze/page.tsx Implements the redesigned analyze input page with live URL validation UI.
frontend/app/analyze/loading/stitch-loading.html Adds a static HTML stitch/prototype for the loading screen redesign.
frontend/app/analyze/loading/page.tsx Implements the redesigned loading screen around the existing multi-step flow.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

frontend/app/analyze/loading/page.tsx:110

  • The interval cleanup returned inside runAnalysis is never registered as the useEffect cleanup (because it’s returned from the async function, not from the effect callback). As a result, stepInterval/progressInterval can keep running after unmount/navigation. Move interval setup out of the async function (or track IDs in refs) and return a cleanup function directly from useEffect that clears them (and also clears the redirect setTimeout).
  useEffect(() => {
    const runAnalysis = async () => {
      const storedUrl = sessionStorage.getItem("articleUrl");
      if (storedUrl) {
        setArticleUrl(storedUrl);

        try {
          const [processRes, biasRes] = await Promise.all([
            axios.post("https://thunder1245-perspective-backend.hf.space/api/process", {
              url: storedUrl,
            }),
            axios.post("https://thunder1245-perspective-backend.hf.space/api/bias", {
              url: storedUrl,
            }),
          ]);

          sessionStorage.setItem("BiasScore", JSON.stringify(biasRes.data));
          sessionStorage.setItem("analysisResult", JSON.stringify(processRes.data));

        } 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);

        const progressInterval = setInterval(() => {
          setProgress((prev) => {
            if (prev < 100) {
              return prev + 1;
            }
            return prev;
          });
        }, 100);

        return () => {
          clearInterval(stepInterval);
          clearInterval(progressInterval);
        };
      } else {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/package.json (1)

52-53: ⚠️ Potential issue | 🟠 Major

Verify lint stack version alignment with Next.js.

Line 52 (next@15.2.4) appears out-of-sync with Line 73 (eslint-config-next@^16.1.6) and possibly Line 72 (eslint@^10.0.2). This can break lint resolution/peer checks.

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json, re, urllib.request

with open("frontend/package.json") as f:
    pkg = json.load(f)

declared = {
    "next": pkg["dependencies"]["next"],
    "eslint-config-next": pkg["devDependencies"]["eslint-config-next"],
    "eslint": pkg["devDependencies"]["eslint"],
}

def normalize(v: str) -> str:
    return re.sub(r'^[^\d]*', '', v)

print("Declared versions:")
for k, v in declared.items():
    print(f"  {k}: {v}")

print("\nResolved peerDependencies for declared versions:")
for name, raw in declared.items():
    ver = normalize(raw)
    data = json.load(urllib.request.urlopen(f"https://registry.npmjs.org/{name}/{ver}"))
    print(f"\n{name}@{ver}")
    print("  peerDependencies:", data.get("peerDependencies", {}))
PY

Expected result: the eslint-config-next peer requirements should include ranges satisfied by your installed next and eslint versions.

Also applies to: 72-73

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` around lines 52 - 53, The package.json shows
mismatched versions between "next" (next@15.2.4) and the devDependencies
"eslint-config-next" and "eslint" which can break peer dependency resolution;
update the versions so they satisfy eslint-config-next's peerDependencies by
either bumping "next" to a version range required by "eslint-config-next" or
pinning "eslint-config-next"/"eslint" to versions compatible with next@15.2.4,
then re-run the provided npm/registry check (or the included Python snippet) to
confirm the peerDependencies for "eslint-config-next" list ranges that include
your installed "next" and "eslint" values; make the change in package.json
entries for "next", "eslint-config-next", and/or "eslint" accordingly and update
lockfile.
frontend/app/analyze/loading/page.tsx (1)

66-73: ⚠️ Potential issue | 🟠 Major

Add timeout and abort signal configuration to external API requests.

The axios.post calls at lines 67 and 70 lack timeout and signal configuration, allowing a stalled backend to hang the page indefinitely. The cleanup function should also abort any pending requests when the component unmounts.

Implement the fix by:

  • Creating an AbortController in the useEffect
  • Adding timeout: 15000 and signal: controller.signal to both axios requests
  • Calling controller.abort() in the cleanup function

This prevents indefinite hangs and ensures proper cleanup when the component unmounts or the user navigates away.

Also applies to lines 115-118 (cleanup function).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` around lines 66 - 73, The two
axios.post calls inside the useEffect (the Promise.all that assigns processRes
and biasRes) need timeout and abort support: create an AbortController at the
start of the effect, pass { timeout: 15000, signal: controller.signal } as
additional config to both axios.post calls, and ensure the effect's cleanup
calls controller.abort() to cancel any pending requests; update the same pattern
for the other axios calls referenced around lines 115-118 so all external
requests use the AbortController and 15s timeout.
🧹 Nitpick comments (3)
frontend/build_logs.txt (1)

1-20: Avoid committing transient local build logs.

Lines 9-20 are environment-specific diagnostics (including local filesystem paths). Please remove this artifact from source control and keep build logs in CI artifacts/issues instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/build_logs.txt` around lines 1 - 20, Remove the committed transient
build log file (frontend/build_logs.txt) and any other environment-specific logs
from the repo, then prevent future commits by adding the appropriate pattern to
.gitignore (e.g., build logs or *.log) and running git rm --cached on the
tracked log file; if this log must be scrubbed from history, purge it with a
history-rewriting tool (git filter-repo or git filter-branch) and push the
cleaned history. Ensure CI collects build logs as pipeline artifacts instead of
committing them to source control.
frontend/app/analyze/loading/page.tsx (1)

85-88: Hardcoded step boundary will drift if steps change.

Line 87 uses literal 4, coupling control flow to today’s array length.

💡 Suggested fix
   const steps = [
@@
   ];
+  const lastStepIndex = steps.length - 1;
@@
-            if (prev < 4) { // hardcode 4 since steps.length removed from dep array
+            if (prev < lastStepIndex) {
               return prev + 1;
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` around lines 85 - 88, The code
hardcodes the step boundary as 4 inside the setInterval callback (stepInterval /
setCurrentStep), which will break if the steps array changes; capture the steps
length into a local constant (e.g. const maxIndex = steps.length - 1) outside
the interval callback or derive it from a stable ref and use if (prev <
maxIndex) return prev + 1; so replace the literal 4 with that computed maxIndex
to keep setCurrentStep logic in sync with the steps array.
frontend/app/page.tsx (1)

507-513: Prefer rendering the dialog container only when open.

The dialog shell remains mounted with role="dialog" even when closed. Conditionally rendering the wrapper when modalData exists keeps semantics cleaner for assistive tech.

♻️ Proposed refactor
-      <div
-        role="dialog"
-        aria-modal="true"
-        aria-labelledby="modal-title"
-        className={`fixed inset-0 z-[100] flex items-center justify-center p-6 transition-all duration-300 ${modalData ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
-      >
+      {modalData && (
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="modal-title"
+        className="fixed inset-0 z-[100] flex items-center justify-center p-6 transition-all duration-300 opacity-100 pointer-events-auto"
+      >
@@
-        {modalData && (
           <div className="glass-card max-w-2xl w-full rounded-2xl p-6 md:p-8 relative z-10 border border-black/10 dark:border-white/10 shadow-2xl bg-white/90 dark:bg-transparent">
@@
-        )}
       </div>
+      )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/page.tsx` around lines 507 - 513, The dialog wrapper is always
mounted even when closed; update the JSX to render the outer container only when
modalData is truthy instead of toggling visibility classes—i.e., replace the
always-mounted <div role="dialog" ...> block with a conditional render (e.g.,
modalData && (<div role="dialog" aria-modal="true" aria-labelledby="modal-title"
className="fixed inset-0 z-[100] flex items-center justify-center p-6
transition-all duration-300"> ... )) so the element with role="dialog" is not
present when modalData is null/undefined; keep the same aria attributes and
inner modal contents (the existing modal markup) inside that conditional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/analyze/loading/page.tsx`:
- Line 3: The mousemove handler in page.tsx is creating a new setTimeout on
every mouse event which queues stale updates; modify the handler to cancel any
pending timeout before scheduling (store the timeout id in a ref via useRef) or
replace the setTimeout approach with requestAnimationFrame throttling so only
one update runs per frame; update the component that registers the mousemove
(the handler referenced in the useEffect) to clear the ref'd timer on unmount
and before creating a new timer, or implement a simple debounce/throttle wrapper
to limit scheduling.
- Around line 129-132: Replace the non-interactive clickable <div> (the element
containing the logo/title that calls router.push("/")) with a proper interactive
element or make it keyboard-accessible: either render it as a semantic <button>
or <a> (or add role="button" and tabIndex={0}), ensure the click handler that
calls router.push("/") remains on the interactive element (reference:
router.push usage), and add a keyboard handler (onKeyDown) that triggers
router.push("/") for Enter/Space and include an accessible label (aria-label or
descriptive text) so screen readers and keyboard users can activate the home
trigger.

In `@frontend/app/analyze/page.tsx`:
- Around line 173-180: The global feed item is rendered as a clickable div with
onClick calling setUrl and validateUrl but lacks keyboard semantics; change the
interactive element (the div with onClick that uses setUrl(item.url) and
validateUrl(item.url)) to a proper <button> (or add role="button", tabIndex=0
and an onKeyDown handler that triggers the same actions for Enter/Space) and
ensure any cursor/appearance classes are adjusted (remove cursor-none) so
keyboard users can focus and activate the item; keep existing className,
key={idx}, and the setUrl/validateUrl calls intact.
- Around line 15-24: The mousemove handler (updateMouse inside useEffect)
creates an unbounded setTimeout per event causing timer backlog; introduce a
persistent timer id (e.g., let ringTimeout: number | null) captured by the
effect, clearTimeout(ringTimeout) before scheduling a new timeout in
updateMouse, store the new id, and ensure the cleanup (the returned function)
both removes the "mousemove" listener and clears any outstanding timeout via
clearTimeout to avoid leaked timers when unmounting.

In `@frontend/app/analyze/results/page.tsx`:
- Around line 301-304: The Link JSX element in
frontend/app/analyze/results/page.tsx that sets target="_blank" should also
include rel="noopener noreferrer" to prevent tabnabbing; update the Link (the
JSX element rendering fact.source_link) to add rel="noopener noreferrer"
alongside target="_blank" so external new-tab links are protected.
- Around line 99-101: Replace the hardcoded external URL in the axios.post call
that creates `res` with your app route or an env-configured base URL: change the
axios.post invocation that currently posts to
"https://thunder1245-perspective-backend.hf.space/api/chat" (the line that sends
`{ message: message }`) to call "/api/chat" (or prepend a runtime-configured
base URL from an env var like NEXT_PUBLIC_API_BASE) so requests go through your
app routing and honor runtime environments/CORS; keep the payload (`message`)
and response handling identical.
- Around line 158-161: The navbar brand is an interactive element implemented as
a div with onClick (the element with className "flex items-center gap-2
cursor-none" and onClick={() => router.push("/")}) but lacks keyboard
accessibility; replace it with a semantic interactive element (preferably a
<button>) or add role="button", tabIndex={0}, an onKeyDown handler that triggers
router.push("/") on Enter/Space, and an accessible name (aria-label or use the
existing heading text) while removing any styling that disables pointer cursor
so keyboard users can reach and operate the control.

In `@perspective-landing.html`:
- Around line 477-483: The modal markup with id="node-modal" remains in the
accessibility tree when hidden; update the toggle behavior and markup so the
closed state sets aria-hidden="true" and inert (or remove from DOM) on the
`#node-modal` container (and ensure the `#modal-backdrop` is also aria-hidden when
closed), and when opening remove aria-hidden and inert and restore focus; also
give the close button (id="close-modal") a clear accessible name (add
aria-label="Close" or aria-labelledby) and ensure focus is trapped inside the
modal while open; apply the same changes to the other modal instance referenced
(the block at lines ~576-614).

---

Outside diff comments:
In `@frontend/app/analyze/loading/page.tsx`:
- Around line 66-73: The two axios.post calls inside the useEffect (the
Promise.all that assigns processRes and biasRes) need timeout and abort support:
create an AbortController at the start of the effect, pass { timeout: 15000,
signal: controller.signal } as additional config to both axios.post calls, and
ensure the effect's cleanup calls controller.abort() to cancel any pending
requests; update the same pattern for the other axios calls referenced around
lines 115-118 so all external requests use the AbortController and 15s timeout.

In `@frontend/package.json`:
- Around line 52-53: The package.json shows mismatched versions between "next"
(next@15.2.4) and the devDependencies "eslint-config-next" and "eslint" which
can break peer dependency resolution; update the versions so they satisfy
eslint-config-next's peerDependencies by either bumping "next" to a version
range required by "eslint-config-next" or pinning "eslint-config-next"/"eslint"
to versions compatible with next@15.2.4, then re-run the provided npm/registry
check (or the included Python snippet) to confirm the peerDependencies for
"eslint-config-next" list ranges that include your installed "next" and "eslint"
values; make the change in package.json entries for "next",
"eslint-config-next", and/or "eslint" accordingly and update lockfile.

---

Nitpick comments:
In `@frontend/app/analyze/loading/page.tsx`:
- Around line 85-88: The code hardcodes the step boundary as 4 inside the
setInterval callback (stepInterval / setCurrentStep), which will break if the
steps array changes; capture the steps length into a local constant (e.g. const
maxIndex = steps.length - 1) outside the interval callback or derive it from a
stable ref and use if (prev < maxIndex) return prev + 1; so replace the literal
4 with that computed maxIndex to keep setCurrentStep logic in sync with the
steps array.

In `@frontend/app/page.tsx`:
- Around line 507-513: The dialog wrapper is always mounted even when closed;
update the JSX to render the outer container only when modalData is truthy
instead of toggling visibility classes—i.e., replace the always-mounted <div
role="dialog" ...> block with a conditional render (e.g., modalData && (<div
role="dialog" aria-modal="true" aria-labelledby="modal-title" className="fixed
inset-0 z-[100] flex items-center justify-center p-6 transition-all
duration-300"> ... )) so the element with role="dialog" is not present when
modalData is null/undefined; keep the same aria attributes and inner modal
contents (the existing modal markup) inside that conditional.

In `@frontend/build_logs.txt`:
- Around line 1-20: Remove the committed transient build log file
(frontend/build_logs.txt) and any other environment-specific logs from the repo,
then prevent future commits by adding the appropriate pattern to .gitignore
(e.g., build logs or *.log) and running git rm --cached on the tracked log file;
if this log must be scrubbed from history, purge it with a history-rewriting
tool (git filter-repo or git filter-branch) and push the cleaned history. Ensure
CI collects build logs as pipeline artifacts instead of committing them to
source control.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f89f37f and dc12e73.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • frontend/.eslintrc.json
  • frontend/app/analyze/loading/page.tsx
  • frontend/app/analyze/loading/stitch-loading.html
  • frontend/app/analyze/page.tsx
  • frontend/app/analyze/results/page.tsx
  • frontend/app/analyze/results/stitch-results.html
  • frontend/app/analyze/stitch-analyze.html
  • frontend/app/globals.css
  • frontend/app/page.tsx
  • frontend/build_logs.txt
  • frontend/build_output.txt
  • frontend/err.html
  • frontend/index.html
  • frontend/package.json
  • frontend/tailwind.config.ts
  • frontend/tsc_output.txt
  • frontend/tsc_res.txt
  • perspective-landing.html
✅ Files skipped from review due to trivial changes (2)
  • frontend/err.html
  • frontend/tsc_res.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/app/analyze/loading/stitch-loading.html
  • frontend/app/analyze/stitch-analyze.html

@@ -2,71 +2,61 @@

import { useEffect, useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mousemove handler is creating unbounded timer churn.

Line 20 schedules a new setTimeout per mouse event, which can queue stale updates and hurt pointer smoothness.

💡 Suggested fix
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
@@
   const [cursorPos, setCursorPos] = useState({ x: -100, y: -100 });
   const [ringPos, setRingPos] = useState({ x: -100, y: -100 });
+  const ringTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
   useEffect(() => {
     const updateMouse = (e: MouseEvent) => {
       setCursorPos({ x: e.clientX, y: e.clientY });
-      setTimeout(() => {
+      if (ringTimeoutRef.current) clearTimeout(ringTimeoutRef.current);
+      ringTimeoutRef.current = setTimeout(() => {
         setRingPos({ x: e.clientX - 11, y: e.clientY - 11 });
       }, 50);
     };
     window.addEventListener("mousemove", updateMouse);
-    return () => window.removeEventListener("mousemove", updateMouse);
+    return () => {
+      window.removeEventListener("mousemove", updateMouse);
+      if (ringTimeoutRef.current) clearTimeout(ringTimeoutRef.current);
+    };
   }, []);

Also applies to: 17-26

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` at line 3, The mousemove handler in
page.tsx is creating a new setTimeout on every mouse event which queues stale
updates; modify the handler to cancel any pending timeout before scheduling
(store the timeout id in a ref via useRef) or replace the setTimeout approach
with requestAnimationFrame throttling so only one update runs per frame; update
the component that registers the mousemove (the handler referenced in the
useEffect) to clear the ref'd timer on unmount and before creating a new timer,
or implement a simple debounce/throttle wrapper to limit scheduling.

Comment on lines +129 to +132
<div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
<span className="material-symbols-outlined text-primary text-2xl">lens_blur</span>
<span className="font-bold tracking-tight text-lg uppercase">Perspective-AI</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Home trigger should be keyboard-accessible.

Line 129 uses a clickable div; keyboard users cannot activate it by default.

💡 Suggested fix
-        <div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
+        <button
+          type="button"
+          aria-label="Go to home page"
+          className="flex items-center gap-2 cursor-none"
+          onClick={() => router.push("/")}
+        >
           <span className="material-symbols-outlined text-primary text-2xl">lens_blur</span>
           <span className="font-bold tracking-tight text-lg uppercase">Perspective-AI</span>
-        </div>
+        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
<span className="material-symbols-outlined text-primary text-2xl">lens_blur</span>
<span className="font-bold tracking-tight text-lg uppercase">Perspective-AI</span>
</div>
<button
type="button"
aria-label="Go to home page"
className="flex items-center gap-2 cursor-none"
onClick={() => router.push("/")}
>
<span className="material-symbols-outlined text-primary text-2xl">lens_blur</span>
<span className="font-bold tracking-tight text-lg uppercase">Perspective-AI</span>
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/loading/page.tsx` around lines 129 - 132, Replace the
non-interactive clickable <div> (the element containing the logo/title that
calls router.push("/")) with a proper interactive element or make it
keyboard-accessible: either render it as a semantic <button> or <a> (or add
role="button" and tabIndex={0}), ensure the click handler that calls
router.push("/") remains on the interactive element (reference: router.push
usage), and add a keyboard handler (onKeyDown) that triggers router.push("/")
for Enter/Space and include an accessible label (aria-label or descriptive text)
so screen readers and keyboard users can activate the home trigger.

Comment on lines +15 to +24
useEffect(() => {
const updateMouse = (e: MouseEvent) => {
setCursorPos({ x: e.clientX, y: e.clientY });
setTimeout(() => {
setRingPos({ x: e.clientX - 11, y: e.clientY - 11 });
}, 50);
};
window.addEventListener("mousemove", updateMouse);
return () => window.removeEventListener("mousemove", updateMouse);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mousemove logic queues unbounded timers.

A new setTimeout is created for every mouse event, which can backlog updates and degrade responsiveness. Clear the previous timer before scheduling the next one, and clear on cleanup.

⚙️ Proposed fix
   useEffect(() => {
+    let ringTimer: ReturnType<typeof setTimeout> | null = null;
     const updateMouse = (e: MouseEvent) => {
       setCursorPos({ x: e.clientX, y: e.clientY });
-      setTimeout(() => {
+      if (ringTimer) clearTimeout(ringTimer);
+      ringTimer = setTimeout(() => {
         setRingPos({ x: e.clientX - 11, y: e.clientY - 11 });
       }, 50);
     };
     window.addEventListener("mousemove", updateMouse);
-    return () => window.removeEventListener("mousemove", updateMouse);
+    return () => {
+      window.removeEventListener("mousemove", updateMouse);
+      if (ringTimer) clearTimeout(ringTimer);
+    };
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/page.tsx` around lines 15 - 24, The mousemove handler
(updateMouse inside useEffect) creates an unbounded setTimeout per event causing
timer backlog; introduce a persistent timer id (e.g., let ringTimeout: number |
null) captured by the effect, clearTimeout(ringTimeout) before scheduling a new
timeout in updateMouse, store the new id, and ensure the cleanup (the returned
function) both removes the "mousemove" listener and clears any outstanding
timeout via clearTimeout to avoid leaked timers when unmounting.

Comment on lines +173 to +180
<div
key={idx}
className="flex items-center gap-4 p-3 rounded-lg border border-black/5 dark:border-white/5 bg-white/30 dark:bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition-colors group cursor-none"
onClick={() => {
setUrl(item.url);
validateUrl(item.url);
}}
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Global feed items are not keyboard-activatable.

These entries act like buttons but are mouse-only. Add keyboard semantics (or render as <button>).

♿ Proposed fix
               <div
                 key={idx}
                 className="flex items-center gap-4 p-3 rounded-lg border border-black/5 dark:border-white/5 bg-white/30 dark:bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition-colors group cursor-none"
                 onClick={() => {
                   setUrl(item.url);
                   validateUrl(item.url);
                 }}
+                role="button"
+                tabIndex={0}
+                onKeyDown={(e) => {
+                  if (e.key === "Enter" || e.key === " ") {
+                    e.preventDefault();
+                    setUrl(item.url);
+                    validateUrl(item.url);
+                  }
+                }}
               >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
key={idx}
className="flex items-center gap-4 p-3 rounded-lg border border-black/5 dark:border-white/5 bg-white/30 dark:bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition-colors group cursor-none"
onClick={() => {
setUrl(item.url);
validateUrl(item.url);
}}
>
<div
key={idx}
className="flex items-center gap-4 p-3 rounded-lg border border-black/5 dark:border-white/5 bg-white/30 dark:bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition-colors group cursor-none"
onClick={() => {
setUrl(item.url);
validateUrl(item.url);
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setUrl(item.url);
validateUrl(item.url);
}
}}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/page.tsx` around lines 173 - 180, The global feed item
is rendered as a clickable div with onClick calling setUrl and validateUrl but
lacks keyboard semantics; change the interactive element (the div with onClick
that uses setUrl(item.url) and validateUrl(item.url)) to a proper <button> (or
add role="button", tabIndex=0 and an onKeyDown handler that triggers the same
actions for Enter/Space) and ensure any cursor/appearance classes are adjusted
(remove cursor-none) so keyboard users can focus and activate the item; keep
existing className, key={idx}, and the setUrl/validateUrl calls intact.

Comment on lines +67 to +69
if (storedBiasScore && storedData) {
setBiasScore(JSON.parse(storedBiasScore).bias_score);
setAnalysisData(JSON.parse(storedData));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bias score parsing is not validated before math.

If BiasScore is malformed or missing bias_score, biasRotation can become NaN and break the gauge UI.

🛡️ Proposed fix
-      if (storedBiasScore && storedData) {
-        setBiasScore(JSON.parse(storedBiasScore).bias_score);
+      if (storedBiasScore && storedData) {
+        const parsedBias = JSON.parse(storedBiasScore);
+        const parsedScore =
+          typeof parsedBias === "number" ? parsedBias : parsedBias?.bias_score;
+        if (typeof parsedScore !== "number" || Number.isNaN(parsedScore)) {
+          throw new Error("Invalid BiasScore payload");
+        }
+        setBiasScore(parsedScore);
         setAnalysisData(JSON.parse(storedData));
         setIsLoading(false);
       } else {
@@
-  const biasRotation = biasScore !== null ? Math.min(Math.max((biasScore / 100) * 180, 0), 180) : 0;
+  const safeBiasScore =
+    typeof biasScore === "number" && Number.isFinite(biasScore) ? biasScore : 0;
+  const biasRotation = Math.min(Math.max((safeBiasScore / 100) * 180, 0), 180);

Also applies to: 128-128

Comment on lines +99 to +101
const res = await axios.post("https://thunder1245-perspective-backend.hf.space/api/chat", {
message: message,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Chat endpoint is hardcoded to a single external host.

This bypasses app routing and makes environment changes brittle (local/dev/prod/CORS). Use the existing app route (/api/chat) or an env-configured base URL.

🌐 Proposed fix
-      const res = await axios.post("https://thunder1245-perspective-backend.hf.space/api/chat", {
+      const res = await axios.post("/api/chat", {
         message: message,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await axios.post("https://thunder1245-perspective-backend.hf.space/api/chat", {
message: message,
});
const res = await axios.post("/api/chat", {
message: message,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 99 - 101, Replace the
hardcoded external URL in the axios.post call that creates `res` with your app
route or an env-configured base URL: change the axios.post invocation that
currently posts to "https://thunder1245-perspective-backend.hf.space/api/chat"
(the line that sends `{ message: message }`) to call "/api/chat" (or prepend a
runtime-configured base URL from an env var like NEXT_PUBLIC_API_BASE) so
requests go through your app routing and honor runtime environments/CORS; keep
the payload (`message`) and response handling identical.

Comment on lines +158 to +161
<div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
<span className="material-symbols-outlined text-primary">lens_blur</span>
<h2 className="text-slate-100 text-lg font-bold leading-tight tracking-tight uppercase">Perspective-AI</h2>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Navbar brand click target is missing keyboard semantics.

This is an interactive control but currently pointer-only.

♿ Proposed fix
-          <div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
+          <div
+            className="flex items-center gap-2 cursor-none"
+            onClick={() => router.push("/")}
+            role="button"
+            tabIndex={0}
+            onKeyDown={(e) => {
+              if (e.key === "Enter" || e.key === " ") {
+                e.preventDefault();
+                router.push("/");
+              }
+            }}
+            aria-label="Go to home page"
+          >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex items-center gap-2 cursor-none" onClick={() => router.push("/")}>
<span className="material-symbols-outlined text-primary">lens_blur</span>
<h2 className="text-slate-100 text-lg font-bold leading-tight tracking-tight uppercase">Perspective-AI</h2>
</div>
<div
className="flex items-center gap-2 cursor-none"
onClick={() => router.push("/")}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
router.push("/");
}
}}
aria-label="Go to home page"
>
<span className="material-symbols-outlined text-primary">lens_blur</span>
<h2 className="text-slate-100 text-lg font-bold leading-tight tracking-tight uppercase">Perspective-AI</h2>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 158 - 161, The navbar
brand is an interactive element implemented as a div with onClick (the element
with className "flex items-center gap-2 cursor-none" and onClick={() =>
router.push("/")}) but lacks keyboard accessibility; replace it with a semantic
interactive element (preferably a <button>) or add role="button", tabIndex={0},
an onKeyDown handler that triggers router.push("/") on Enter/Space, and an
accessible name (aria-label or use the existing heading text) while removing any
styling that disables pointer cursor so keyboard users can reach and operate the
control.

Comment on lines +301 to +304
<Link
href={fact.source_link}
target="_blank"
className="inline-flex items-center text-xs text-primary/60 hover:text-primary transition-colors font-mono uppercase tracking-widest mt-2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add rel protection for external new-tab links.

target="_blank" should include rel="noopener noreferrer" to prevent tabnabbing.

🔒 Proposed fix
                                   <Link
                                     href={fact.source_link}
                                     target="_blank"
+                                    rel="noopener noreferrer"
                                     className="inline-flex items-center text-xs text-primary/60 hover:text-primary transition-colors font-mono uppercase tracking-widest mt-2"
                                   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Link
href={fact.source_link}
target="_blank"
className="inline-flex items-center text-xs text-primary/60 hover:text-primary transition-colors font-mono uppercase tracking-widest mt-2"
<Link
href={fact.source_link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-primary/60 hover:text-primary transition-colors font-mono uppercase tracking-widest mt-2"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/analyze/results/page.tsx` around lines 301 - 304, The Link JSX
element in frontend/app/analyze/results/page.tsx that sets target="_blank"
should also include rel="noopener noreferrer" to prevent tabnabbing; update the
Link (the JSX element rendering fact.source_link) to add rel="noopener
noreferrer" alongside target="_blank" so external new-tab links are protected.

Comment on lines +477 to +483
<div class="fixed inset-0 z-[100] flex items-center justify-center p-6 opacity-0 pointer-events-none transition-all duration-300"
id="node-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="absolute inset-0 bg-[#03050e]/80 backdrop-blur-[12px]" id="modal-backdrop"></div>
<div class="glass-card max-w-2xl w-full rounded-2xl p-8 relative z-10 border border-white/10 shadow-2xl">
<button class="absolute top-6 right-6 text-slate-400 hover:text-white transition-colors" id="close-modal">
<span class="material-symbols-outlined">close</span>
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hidden dialog remains accessible/focusable when “closed”.

opacity-0 + pointer-events-none doesn’t remove the modal from the accessibility tree or keyboard flow. Keep it inert/aria-hidden while closed, and give the close button an accessible name.

♿ Proposed fix
-    <div class="fixed inset-0 z-[100] flex items-center justify-center p-6 opacity-0 pointer-events-none transition-all duration-300"
-        id="node-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
+    <div class="fixed inset-0 z-[100] flex items-center justify-center p-6 opacity-0 pointer-events-none transition-all duration-300"
+        id="node-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true" inert>
@@
-            <button class="absolute top-6 right-6 text-slate-400 hover:text-white transition-colors" id="close-modal">
+            <button class="absolute top-6 right-6 text-slate-400 hover:text-white transition-colors"
+                id="close-modal" aria-label="Close node details">
@@
         const openModal = (nodeName) => {
@@
             modal.classList.remove('opacity-0', 'pointer-events-none');
             modal.classList.add('opacity-100', 'pointer-events-auto');
+            modal.removeAttribute('inert');
+            modal.setAttribute('aria-hidden', 'false');
@@
         const closeModal = () => {
             modal.classList.add('opacity-0', 'pointer-events-none');
             modal.classList.remove('opacity-100', 'pointer-events-auto');
+            modal.setAttribute('aria-hidden', 'true');
+            modal.setAttribute('inert', '');
             if (previousFocus) {
                 previousFocus.focus();
             }
         };

Also applies to: 576-614

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@perspective-landing.html` around lines 477 - 483, The modal markup with
id="node-modal" remains in the accessibility tree when hidden; update the toggle
behavior and markup so the closed state sets aria-hidden="true" and inert (or
remove from DOM) on the `#node-modal` container (and ensure the `#modal-backdrop` is
also aria-hidden when closed), and when opening remove aria-hidden and inert and
restore focus; also give the close button (id="close-modal") a clear accessible
name (add aria-label="Close" or aria-labelledby) and ensure focus is trapped
inside the modal while open; apply the same changes to the other modal instance
referenced (the block at lines ~576-614).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants