Skip to content

Fix/debate room theme sync#328

Open
compiler041 wants to merge 2 commits into
AOSSIE-Org:mainfrom
compiler041:fix/debate-room-theme-sync
Open

Fix/debate room theme sync#328
compiler041 wants to merge 2 commits into
AOSSIE-Org:mainfrom
compiler041:fix/debate-room-theme-sync

Conversation

@compiler041

@compiler041 compiler041 commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Before:

  • Online Debate Room (Bot mode) did not respect the selected Light/Dark theme from the Login page.
    After:
  • Online Debate Room now correctly follows the globally selected theme.
  • Theme updates dynamically and remains consistent across navigation.

Additional Notes:
The Online Debate Room was not properly reacting to the global theme state.
This update ensures that the selected mode (Light/Dark) from the Login page is consistently applied in the Online Debate Room.

Changes made:

  • Synced debate room UI with global theme state.
  • Ensured theme consistency across components.
  • Verified no regression in other pages.

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

AI Usage Disclosure

  • This PR contains AI-generated code. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools:
Claude code for guidance and implementation assistance.

Issue #326

Summary by CodeRabbit

  • New Features

    • Email autocomplete suggestions during sign-in and registration
    • Theme system with multiple selectable palettes for debates
    • Keyboard navigation support for email suggestions (arrow keys, enter, escape)
  • Improvements

    • Real-time email format and password validation with clearer error messaging
    • Responsive card-based redesign of the debate interface
    • Better organization and display of debate messages
    • Enhanced speech recognition integration and stability

@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces email autocomplete suggestions and client-side validation in authentication forms. Refactors DebateRoom component with a theme system, explicit type definitions, typed speech recognition, responsive card-based UI layout, and consolidated phase progression logic.

Changes

Cohort / File(s) Summary
Authentication Forms
frontend/src/Pages/Authentication/forms.tsx
Adds EmailInputWithSuggestions component with keyboard navigation (Arrow Up/Down, Enter, Escape), client-side validation for email format and password length, non-empty and confirmation checks, and improved error message spacing. Replaces onChange handlers to pass values directly.
Debate Room Refactor
frontend/src/Pages/DebateRoom.tsx
Introduces theme system with detectTheme function and Palettes map with dynamic state management. Adds explicit types for DebateProps, DebateState, Message, JudgmentData, and speech recognition interfaces. Implements typed advanceTurn helper for phase progression, refactors UI to responsive card-based layout with palette-driven styling, integrates typed speech recognition lifecycle with start/stop controls, and normalizes message extraction with extractJSON parser.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SpeechRec as Speech Recognition
    participant DebateRoom
    participant BotService
    participant ThemeSystem
    participant UI

    User->>DebateRoom: Start debate / advance turn
    DebateRoom->>ThemeSystem: Fetch active palette
    ThemeSystem-->>UI: Apply theme colors
    
    alt User's Turn
        User->>SpeechRec: Start recording
        SpeechRec-->>DebateRoom: Transcript received
        DebateRoom->>DebateRoom: Validate & store message
        DebateRoom->>UI: Render message with theme
    end
    
    DebateRoom->>DebateRoom: Check phase completion
    alt Phase Complete
        DebateRoom->>BotService: Request bot response
        BotService-->>DebateRoom: Bot message + judgment data
        DebateRoom->>DebateRoom: advanceTurn() - progress phase
        DebateRoom->>UI: Update with bot message & next phase
    end
    
    alt Debate End
        DebateRoom->>UI: Show end-of-phase popup
        UI-->>User: Display final judgment
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #175: Modifies DebateRoom theming and input styling for dark theme compatibility, directly related to the main PR's new palette-driven theme system.
  • PR #160: Updates DebateRoom input rendering and classes, overlaps with the main PR's responsive card-based UI refactoring and input structure changes.

Suggested reviewers

  • bhavik-mangla

Poem

🐰 With email hints that flutter fast,
And themes that make the dark sky vast,
Speech flows where phases dance and turn,
Each message bright, each choice we learn,
A rabbit's delight—debates shine bright! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/debate room theme sync' directly addresses the primary objective of syncing the debate room UI with the global theme state, which is the main focus of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
frontend/src/Pages/Authentication/forms.tsx (1)

445-465: ⚠️ Potential issue | 🟡 Minor

ForgotPasswordForm makes an API call without client-side email validation.

LoginForm and SignUpForm both validate email format before submitting, but ForgotPasswordForm.handleSubmit sends the request even for an empty or malformed email string. This is an inconsistency introduced in the same PR.

🛡️ Proposed fix — add email validation before the fetch
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     setError('');
+
+    if (!email.trim()) {
+      setError('Please enter your email address');
+      return;
+    }
+    if (!validateEmail(email)) {
+      setError('Please enter a valid email address');
+      return;
+    }

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

In `@frontend/src/Pages/Authentication/forms.tsx` around lines 445 - 465,
ForgotPasswordForm.handleSubmit currently sends the POST to
${baseURL}/forgotPassword without validating the email; add the same client-side
validation used in LoginForm/SignUpForm (check non-empty and RFC-like format)
before calling fetch, and if validation fails call setError with a descriptive
message and return early; ensure successful path still calls
startResetPassword(email) and keep the try/catch around the network call so only
valid emails trigger the fetch.
frontend/src/Pages/DebateRoom.tsx (1)

128-132: ⚠️ Potential issue | 🟠 Major

Missing null guard for location.state — direct navigation will crash.

location.state is null when users navigate directly to this URL (bookmark, refresh, deep link). The unchecked cast on line 131 followed by immediate property access on line 132 will throw a TypeError.

🛡️ Proposed fix: guard and redirect
 const DebateRoom: React.FC = () => {
   const location = useLocation();
   const navigate = useNavigate();
-  const debateData = location.state as DebateProps;
+  const debateData = location.state as DebateProps | null;
+
+  useEffect(() => {
+    if (!debateData) navigate("/game", { replace: true });
+  }, [debateData, navigate]);
+
+  if (!debateData) return null;
+
   const phases = debateData.phaseTimings;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/DebateRoom.tsx` around lines 128 - 132, DebateRoom
currently casts location.state to DebateProps and immediately accesses
phaseTimings (debateData and phases) which will crash when location.state is
null; update the component (DebateRoom) to check if location.state is present
(e.g., if (!location.state) or const debateData = location.state as DebateProps
| null and if (!debateData)), and if missing use navigate(...) to redirect to a
safe route (or render a fallback) before accessing debateData.phaseTimings so
phases is only derived when debateData exists.
🧹 Nitpick comments (8)
frontend/src/Pages/Authentication/forms.tsx (3)

130-135: validateEmail and MIN_PASSWORD_LENGTH are duplicated verbatim in both LoginForm and SignUpForm.

Both are defined inside the component body (triggering recreation on every render) and are identical copies. Extract them once at module level.

♻️ Proposed refactor — module-level constants

Add above all component declarations:

+const MIN_PASSWORD_LENGTH = 8;
+
+const validateEmail = (email: string): boolean => {
+  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+  return emailRegex.test(email);
+};
+

Then remove the in-component copies at lines 130–135 (LoginForm) and lines 266–271 (SignUpForm).

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

In `@frontend/src/Pages/Authentication/forms.tsx` around lines 130 - 135,
MIN_PASSWORD_LENGTH and validateEmail are duplicated inside both LoginForm and
SignUpForm and recreated on each render; extract them to module-level constants
instead. Define MIN_PASSWORD_LENGTH and validateEmail once at the top of the
file (above the component declarations) and remove the in-component declarations
from LoginForm and SignUpForm so both components import/use the single
module-level symbols.

30-35: emailDomains and getEmailSuggestions are re-created on every render.

Both are defined inside the component body. As static data and a pure derivation, they should live at module scope (or getEmailSuggestions should be useCallback-memoized if kept inside).

♻️ Proposed refactor — hoist to module level
+const EMAIL_DOMAINS = ['@gmail.com', '@yahoo.com', '@outlook.com', '@hotmail.com'];
+
+const getEmailSuggestions = (email: string): string[] => {
+  if (!email || email.includes('@')) return [];
+  return EMAIL_DOMAINS.map(domain => email + domain);
+};
+
 const EmailInputWithSuggestions: React.FC<EmailInputWithSuggestionsProps> = ({ ... }) => {
-  const emailDomains = ['@gmail.com', '@yahoo.com', '@outlook.com', '@hotmail.com'];
-
-  const getEmailSuggestions = (email: string) => {
-    if (!email || email.includes('@')) return [];
-    return emailDomains.map(domain => email + domain);
-  };
-
   const suggestions = getEmailSuggestions(value);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Authentication/forms.tsx` around lines 30 - 35, The
emailDomains array and getEmailSuggestions function are recreated on every
render because they live inside the component; hoist emailDomains (and
preferably getEmailSuggestions) to module scope so they are defined once, or if
you must keep getEmailSuggestions inside the component, wrap it with useCallback
to memoize it; update references to use the moved
getEmailSuggestions/emailDomains and ensure types remain correct for
getEmailSuggestions.

86-94: type="text" on the email input sacrifices mobile keyboard and browser autofill.

Using type="email" would give mobile users the email-optimized keyboard layout (with @ and .com shortcuts) and let browsers activate their email-address autofill heuristics. The custom suggestion dropdown remains fully functional alongside type="email".

♻️ Proposed change
-        type="text"
+        type="email"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Authentication/forms.tsx` around lines 86 - 94, The email
input currently uses type="text", which prevents mobile email keyboards and
browser autofill; update the <Input ... /> component to use type="email" instead
of "text" while leaving props and handlers intact (value,
onChange/handleInputChange, onKeyDown/handleKeyDown, onFocus that toggles
setShowSuggestions, placeholder, className) so the suggestion dropdown and
behavior remain unchanged but mobile keyboard and autofill are enabled.
frontend/src/Pages/DebateRoom.tsx (5)

200-201: The eslint-disable for exhaustive-deps is tolerable here but worth a note.

The suppressed dependency is judgeDebateResult, which isn't wrapped in useCallback. Its closure is effectively stable (only uses debateData.userId from location.state and stable setters), so the stale closure is safe in practice. If this component grows further, consider wrapping judgeDebateResult in useCallback to remove the suppression.

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 200 - 201, The eslint-disable
bypass hides a missing dependency: wrap the judgeDebateResult function in
useCallback (referencing any external values it uses, e.g., debateData.userId
and stable setters) so its identity is stable, then remove the "//
eslint-disable-next-line react-hooks/exhaustive-deps" comment and include
judgeDebateResult in the useEffect dependency array (the effect currently
depends on phases). Ensure the useCallback dependencies are minimal and correct
to avoid stale closures.

494-496: Abusing setState updater to read current state — confusing and fragile.

setState(pr => { advanceTurn(pr); return pr; }) uses the updater solely to access current state, returns the same reference (so this setState itself is a no-op), and relies on advanceTurn internally calling setState for actual updates. This also triggers side effects inside an updater (same StrictMode concern as above).

A cleaner approach: store state in a ref that's always current, or use useReducer to co-locate side-effect triggers with state transitions.

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 494 - 496, The onClick
currently abuses setState by calling setState(pr => { advanceTurn(pr); return
pr; }) which merely returns the same state reference and performs side effects
inside the updater; replace this pattern by making the current state available
via a stable ref or switching to useReducer so you can call advanceTurn with the
real current state (or dispatch an action) instead of using the updater for side
effects — update the button handler to either call
advanceTurn(currentStateRef.current) or dispatch an action to the reducer; stop
returning the unchanged state from setState and ensure advanceTurn itself
performs pure state transitions or is invoked outside of state updaters.

398-408: Fixed card height of 540px may clip on smaller viewports.

height: "540px" (line 403) combined with overflowY: "auto" on the content area works for desktop, but on mobile or smaller screens this may consume most of the viewport. Consider using max-height with a viewport-relative unit (e.g., max-height: "70vh") or a min-height / max-height range for better responsiveness.

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 398 - 408, The fixed 540px
height in cardStyle causes clipping on smaller viewports; update the cardStyle
function (used to render the debate cards) to remove height: "540px" and instead
use responsive constraints such as maxHeight: "70vh" with an optional minHeight
like "320px" (or a range using minHeight/maxHeight) so the card respects
viewport height on mobile while keeping desktop sizing, and ensure the inner
scrollable content (overflowY) still works with these max/min height rules.

139-153: 300ms polling interval for theme detection is aggressive for a rarely-changed setting.

The setInterval at 300ms continuously runs for the lifetime of the component to detect same-tab theme changes. Since theme changes are infrequent user actions, consider either:

  • Increasing the interval to ~1000ms, or
  • Dispatching a CustomEvent from the theme setter and listening for it here, which eliminates polling entirely.

291-319: handleBotTurn closes over stale state values for the API call, then uses a setState updater for the update — mixed approach risks inconsistency.

Lines 293–301 read state.currentPhase, state.phaseStep, state.messages, and state.botStance directly from the closure (stale by the time the await resolves), while line 304 uses an updater (pr =>) to get fresh state for the message append. If a timer or other effect mutates state between the await call and its resolution, the API request is sent with stale context but the response is appended to current state. This is unlikely to cause visible bugs in practice since bot turns block user interaction, but be aware of the inconsistency.

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 291 - 319, handleBotTurn
currently reads state fields from the closure before awaiting sendDebateMessage
(state.currentPhase, state.phaseStep, state.messages, state.botStance) which can
be stale; fix by reading a fresh snapshot of state immediately before the API
call (e.g. add a stateRef that you update on each state change via useEffect and
then inside handleBotTurn do const snap = stateRef.current and use
snap.currentPhase/snap.phaseStep/snap.messages/snap.botStance when calling
sendDebateMessage), keep the existing setState(pr => ...) updater to append the
bot message so updates remain based on the latest state; reference
handleBotTurn, sendDebateMessage, and setState(pr => ...) when making the
change.
🤖 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/src/Pages/DebateRoom.tsx`:
- Around line 269-284: The setState updater in DebateRoom (the anonymous updater
that reads pr.timer) contains side effects (stopRec, setPopup, advanceTurn,
setNextTurnPending and setTimeout) which must be moved out of the updater;
change the updater to only compute and return the new state (e.g., reduce timer
or set timer to 0 and mark a flag like nextTurnRequested) and then, immediately
after calling setState, trigger the side effects based on the new state or use a
useEffect that watches the relevant state fields (timer,
nextTurnRequested/nextTurnPending) to call stopRec(), setPopup(), advanceTurn(),
setNextTurnPending(), clearInterval(timerRef.current) and schedule
setTimeouts—ensure the updater never invokes external functions and that
useEffect guards avoid double-running in StrictMode.
- Around line 354-372: The judgeDebateResult handler currently does
JSON.parse(extractJSON(result)) as JudgmentData which can silently produce
incomplete objects; add runtime structural validation before calling
setJudgmentData: parse the string from extractJSON(result), verify required
fields (e.g., verdict.winner, verdict.reason and other expected sections like
opening_statement, total) exist and have the correct types, and only then call
setJudgmentData with the parsed object; if validation fails, populate a safe
fallback JudgmentData (similar to the existing error branch) and log the
validation error; implement the validation as a small helper (e.g.,
validateJudgmentData(parsed): parsed is JudgmentData | null) and use it inside
judgeDebateResult to decide whether to set the parsed data or the fallback.
- Around line 117-118: phaseSequences and turnTypes are hardcoded to 3 phases
but debateData.phaseTimings from the backend may have a different length, which
can cause TypeError when indexing phaseSequences[currentPhase] or
turnTypes[state.currentPhase]; validate or derive instead. Fix by checking
debateData.phaseTimings.length against the hardcoded arrays early (e.g., in the
component init where debateData is consumed) and either (a) throw/log an
explicit error and fall back to a safe default if lengths differ, or (b)
refactor to compute phaseSequences and turnTypes dynamically from
debateData.phaseTimings (use the phase configuration to build arrays rather than
hardcoding) so access to phaseSequences[currentPhase] and
turnTypes[state.currentPhase] is always safe; reference the symbols
phaseSequences, turnTypes, debateData.phaseTimings,
currentPhase/state.currentPhase and the backend response producer in vsbot.ts
when implementing the validation or refactor.

---

Outside diff comments:
In `@frontend/src/Pages/Authentication/forms.tsx`:
- Around line 445-465: ForgotPasswordForm.handleSubmit currently sends the POST
to ${baseURL}/forgotPassword without validating the email; add the same
client-side validation used in LoginForm/SignUpForm (check non-empty and
RFC-like format) before calling fetch, and if validation fails call setError
with a descriptive message and return early; ensure successful path still calls
startResetPassword(email) and keep the try/catch around the network call so only
valid emails trigger the fetch.

In `@frontend/src/Pages/DebateRoom.tsx`:
- Around line 128-132: DebateRoom currently casts location.state to DebateProps
and immediately accesses phaseTimings (debateData and phases) which will crash
when location.state is null; update the component (DebateRoom) to check if
location.state is present (e.g., if (!location.state) or const debateData =
location.state as DebateProps | null and if (!debateData)), and if missing use
navigate(...) to redirect to a safe route (or render a fallback) before
accessing debateData.phaseTimings so phases is only derived when debateData
exists.

---

Nitpick comments:
In `@frontend/src/Pages/Authentication/forms.tsx`:
- Around line 130-135: MIN_PASSWORD_LENGTH and validateEmail are duplicated
inside both LoginForm and SignUpForm and recreated on each render; extract them
to module-level constants instead. Define MIN_PASSWORD_LENGTH and validateEmail
once at the top of the file (above the component declarations) and remove the
in-component declarations from LoginForm and SignUpForm so both components
import/use the single module-level symbols.
- Around line 30-35: The emailDomains array and getEmailSuggestions function are
recreated on every render because they live inside the component; hoist
emailDomains (and preferably getEmailSuggestions) to module scope so they are
defined once, or if you must keep getEmailSuggestions inside the component, wrap
it with useCallback to memoize it; update references to use the moved
getEmailSuggestions/emailDomains and ensure types remain correct for
getEmailSuggestions.
- Around line 86-94: The email input currently uses type="text", which prevents
mobile email keyboards and browser autofill; update the <Input ... /> component
to use type="email" instead of "text" while leaving props and handlers intact
(value, onChange/handleInputChange, onKeyDown/handleKeyDown, onFocus that
toggles setShowSuggestions, placeholder, className) so the suggestion dropdown
and behavior remain unchanged but mobile keyboard and autofill are enabled.

In `@frontend/src/Pages/DebateRoom.tsx`:
- Around line 200-201: The eslint-disable bypass hides a missing dependency:
wrap the judgeDebateResult function in useCallback (referencing any external
values it uses, e.g., debateData.userId and stable setters) so its identity is
stable, then remove the "// eslint-disable-next-line
react-hooks/exhaustive-deps" comment and include judgeDebateResult in the
useEffect dependency array (the effect currently depends on phases). Ensure the
useCallback dependencies are minimal and correct to avoid stale closures.
- Around line 494-496: The onClick currently abuses setState by calling
setState(pr => { advanceTurn(pr); return pr; }) which merely returns the same
state reference and performs side effects inside the updater; replace this
pattern by making the current state available via a stable ref or switching to
useReducer so you can call advanceTurn with the real current state (or dispatch
an action) instead of using the updater for side effects — update the button
handler to either call advanceTurn(currentStateRef.current) or dispatch an
action to the reducer; stop returning the unchanged state from setState and
ensure advanceTurn itself performs pure state transitions or is invoked outside
of state updaters.
- Around line 398-408: The fixed 540px height in cardStyle causes clipping on
smaller viewports; update the cardStyle function (used to render the debate
cards) to remove height: "540px" and instead use responsive constraints such as
maxHeight: "70vh" with an optional minHeight like "320px" (or a range using
minHeight/maxHeight) so the card respects viewport height on mobile while
keeping desktop sizing, and ensure the inner scrollable content (overflowY)
still works with these max/min height rules.
- Around line 291-319: handleBotTurn currently reads state fields from the
closure before awaiting sendDebateMessage (state.currentPhase, state.phaseStep,
state.messages, state.botStance) which can be stale; fix by reading a fresh
snapshot of state immediately before the API call (e.g. add a stateRef that you
update on each state change via useEffect and then inside handleBotTurn do const
snap = stateRef.current and use
snap.currentPhase/snap.phaseStep/snap.messages/snap.botStance when calling
sendDebateMessage), keep the existing setState(pr => ...) updater to append the
bot message so updates remain based on the latest state; reference
handleBotTurn, sendDebateMessage, and setState(pr => ...) when making the
change.

Comment on lines +117 to +118
const phaseSequences = [["For","Against"],["For","Against","Against","For"],["For","Against"]];
const turnTypes = [["statement","statement"],["question","answer","question","answer"],["statement","statement"]];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "phaseTimings" --type=ts --type=tsx -C3

Repository: AOSSIE-Org/DebateAI

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Examine the DebateRoom.tsx file around the flagged lines
find . -name "DebateRoom.tsx" -type f | head -1 | xargs wc -l

Repository: AOSSIE-Org/DebateAI

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Get context around lines 117-118 and check the implementation
find . -name "DebateRoom.tsx" -type f | head -1 | xargs sed -n '100,200p'

Repository: AOSSIE-Org/DebateAI

Length of output: 4737


🏁 Script executed:

#!/bin/bash
# Search for how currentPhase is assigned/used
find . -name "DebateRoom.tsx" -type f | head -1 | xargs rg "currentPhase" -n

Repository: AOSSIE-Org/DebateAI

Length of output: 2233


🏁 Script executed:

rg -n "phaseTimings" --type=ts -A 2 -B 2

Repository: AOSSIE-Org/DebateAI

Length of output: 5041


🏁 Script executed:

rg -n "DebateRoom" --type=ts --type=jsx -B 3 -A 3

Repository: AOSSIE-Org/DebateAI

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Look for navigate calls to DebateRoom or state assignments with phaseTimings
rg -n "navigate.*DebateRoom|phaseTimings.*:" --type=ts --type=jsx -B 2 -A 2

Repository: AOSSIE-Org/DebateAI

Length of output: 90


🏁 Script executed:

rg -n "defaultPhaseTimings" --type=ts -B 5 -A 5

Repository: AOSSIE-Org/DebateAI

Length of output: 1916


🏁 Script executed:

#!/bin/bash
# Check what defaultPhaseTimings is set to
find . -name "*.ts" -type f | xargs grep -l "defaultPhaseTimings" | head -5

Repository: AOSSIE-Org/DebateAI

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for where BotSelection navigates to DebateRoom
find . -name "BotSelection.tsx" -type f | head -1 | xargs sed -n '280,310p'

Repository: AOSSIE-Org/DebateAI

Length of output: 834


🏁 Script executed:

#!/bin/bash
# Check if phaseTimings can be created/modified with variable length elsewhere
find . -name "*.ts" -type f | xargs grep -l "phaseTimings" | xargs grep -n "\.push\|\.slice\|\.concat\|\.filter" | grep -i phase

Repository: AOSSIE-Org/DebateAI

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Verify the bound check logic at line 186
find . -name "DebateRoom.tsx" -type f | head -1 | xargs sed -n '180,195p'

Repository: AOSSIE-Org/DebateAI

Length of output: 993


🏁 Script executed:

#!/bin/bash
# Check if the API response can return variable-length phaseTimings
find . -name "vsbot.ts" -type f | head -1 | xargs sed -n '45,85p'

Repository: AOSSIE-Org/DebateAI

Length of output: 1361


Hardcoded 3-phase arrays lack validation against backend response.

phaseSequences and turnTypes are hardcoded for exactly 3 phases, but debateData.phaseTimings comes from a backend response that is never validated. While the frontend UI cannot modify the array length and initialization defaults to 3 phases, the API response (line 45-80 in vsbot.ts) could return a different phase count without triggering an error. This would cause TypeError when accessing phaseSequences[currentPhase] or turnTypes[state.currentPhase] if currentPhase >= 3.

Add validation that backend phaseTimings length matches the hardcoded sequences, or refactor to derive phaseSequences and turnTypes from the phase configuration instead of hardcoding.

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 117 - 118, phaseSequences and
turnTypes are hardcoded to 3 phases but debateData.phaseTimings from the backend
may have a different length, which can cause TypeError when indexing
phaseSequences[currentPhase] or turnTypes[state.currentPhase]; validate or
derive instead. Fix by checking debateData.phaseTimings.length against the
hardcoded arrays early (e.g., in the component init where debateData is
consumed) and either (a) throw/log an explicit error and fall back to a safe
default if lengths differ, or (b) refactor to compute phaseSequences and
turnTypes dynamically from debateData.phaseTimings (use the phase configuration
to build arrays rather than hardcoding) so access to
phaseSequences[currentPhase] and turnTypes[state.currentPhase] is always safe;
reference the symbols phaseSequences, turnTypes, debateData.phaseTimings,
currentPhase/state.currentPhase and the backend response producer in vsbot.ts
when implementing the validation or refactor.

Comment on lines +269 to 284
setState(pr => {
if (pr.timer <= 1) {
if (timerRef.current) clearInterval(timerRef.current);
if (!pr.isBotTurn) {
stopRec();
setPopup({ show: true, message: "Time's up! Moving to next turn." });
setTimeout(() => setPopup({ show: false, message: "" }), 2000);
const updatedState = { ...prev, timer: 0 };
advanceTurn(updatedState);
return updatedState;
} else {
setNextTurnPending(true);
return { ...prev, timer: 0 };
const u = { ...pr, timer: 0 };
advanceTurn(u);
return u;
}
setNextTurnPending(true);
return { ...pr, timer: 0 };
}
return { ...prev, timer: prev.timer - 1 };
return { ...pr, timer: pr.timer - 1 };
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Side effects inside setState updater functions — anti-pattern that breaks in StrictMode.

The updater passed to setState (lines 269–284) calls stopRec(), setPopup(), advanceTurn(), and setNextTurnPending() — all side effects. React 18 StrictMode double-invokes updater functions in development to surface impure updaters, which would fire these side effects twice.

This same pattern recurs at lines 304–308 (setTimeout inside updater), 311–314, 343–347, and 495. Consider moving side effects out of updaters — use useEffect reacting to state changes, or compute the new state in the updater and trigger side effects after setState returns.

🔧 Sketch: refactor timer to separate state update from side effects
   useEffect(() => {
     if (state.timer > 0 && !state.isDebateEnded) {
       timerRef.current = setInterval(() => {
-        setState(pr => {
-          if (pr.timer <= 1) {
-            if (timerRef.current) clearInterval(timerRef.current);
-            if (!pr.isBotTurn) {
-              stopRec();
-              setPopup({ show: true, message: "Time's up! Moving to next turn." });
-              setTimeout(() => setPopup({ show: false, message: "" }), 2000);
-              const u = { ...pr, timer: 0 };
-              advanceTurn(u);
-              return u;
-            }
-            setNextTurnPending(true);
-            return { ...pr, timer: 0 };
-          }
-          return { ...pr, timer: pr.timer - 1 };
-        });
+        setState(pr => ({ ...pr, timer: Math.max(pr.timer - 1, 0) }));
       }, 1000);
     }
     return () => { if (timerRef.current) clearInterval(timerRef.current); };
-  }, [state.timer, state.isDebateEnded, state.isBotTurn, advanceTurn, stopRec]);
+  }, [state.timer, state.isDebateEnded]);

+  // React to timer expiry with a separate effect
+  useEffect(() => {
+    if (state.timer === 0 && !state.isDebateEnded) {
+      if (timerRef.current) clearInterval(timerRef.current);
+      if (!state.isBotTurn) {
+        stopRec();
+        setPopup({ show: true, message: "Time's up! Moving to next turn." });
+        setTimeout(() => setPopup({ show: false, message: "" }), 2000);
+        advanceTurn(state);
+      } else {
+        setNextTurnPending(true);
+      }
+    }
+  }, [state.timer]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/DebateRoom.tsx` around lines 269 - 284, The setState
updater in DebateRoom (the anonymous updater that reads pr.timer) contains side
effects (stopRec, setPopup, advanceTurn, setNextTurnPending and setTimeout)
which must be moved out of the updater; change the updater to only compute and
return the new state (e.g., reduce timer or set timer to 0 and mark a flag like
nextTurnRequested) and then, immediately after calling setState, trigger the
side effects based on the new state or use a useEffect that watches the relevant
state fields (timer, nextTurnRequested/nextTurnPending) to call stopRec(),
setPopup(), advanceTurn(), setNextTurnPending(), clearInterval(timerRef.current)
and schedule setTimeouts—ensure the updater never invokes external functions and
that useEffect guards avoid double-running in StrictMode.

Comment on lines 354 to 372
const judgeDebateResult = async (messages: Message[]) => {
try {
console.log("Starting judgment with messages:", messages);
const { result } = await judgeDebate({
history: messages,
userId: debateData.userId,
});
console.log("Raw judge result:", result);

const jsonString = extractJSON(result);
console.log("Extracted JSON string:", jsonString);

let judgment: JudgmentData;
try {
judgment = JSON.parse(jsonString);
} catch (parseError) {
console.error("JSON parse error:", parseError, "Trying to fix JSON...");
// Try to fix common JSON issues
const fixedJson = jsonString
.replace(/'/g, '"') // Replace single quotes with double quotes
.replace(/(\w+):/g, '"$1":') // Add quotes to keys
.replace(/,\s*}/g, '}') // Remove trailing commas
.replace(/,\s*]/g, ']'); // Remove trailing commas in arrays
try {
judgment = JSON.parse(fixedJson);
} catch (e) {
throw new Error(`Failed to parse JSON: ${e}`);
}
}

console.log("Parsed judgment:", judgment);
setJudgmentData(judgment);
const { result } = await judgeDebate({ history: messages, userId: debateData.userId });
setJudgmentData(JSON.parse(extractJSON(result)) as JudgmentData);
setPopup({ show: false, message: "" });
setShowJudgment(true);
} catch (error) {
console.error("Judging error:", error);
// Show error to user
setPopup({
show: true,
message: `Judgment error: ${error instanceof Error ? error.message : "Unknown error"}. Showing default results.`,
isJudging: false,
});

// Set default judgment data
} catch (e) {
console.error(e);
setJudgmentData({
opening_statement: {
user: { score: 0, reason: "Error occurred during judgment" },
bot: { score: 0, reason: "Error occurred during judgment" },
},
cross_examination: {
user: { score: 0, reason: "Error occurred during judgment" },
bot: { score: 0, reason: "Error occurred during judgment" },
},
answers: {
user: { score: 0, reason: "Error occurred during judgment" },
bot: { score: 0, reason: "Error occurred during judgment" },
},
closing: {
user: { score: 0, reason: "Error occurred during judgment" },
bot: { score: 0, reason: "Error occurred during judgment" },
},
total: { user: 0, bot: 0 },
verdict: {
winner: "None",
reason: error instanceof Error ? error.message : "Judgment failed",
congratulations: "",
opponent_analysis: "",
},
opening_statement: { user: { score:0, reason:"Error" }, bot: { score:0, reason:"Error" } },
cross_examination: { user: { score:0, reason:"Error" }, bot: { score:0, reason:"Error" } },
answers: { user: { score:0, reason:"Error" }, bot: { score:0, reason:"Error" } },
closing: { user: { score:0, reason:"Error" }, bot: { score:0, reason:"Error" } },
total: { user:0, bot:0 },
verdict: { winner:"None", reason: String(e), congratulations:"", opponent_analysis:"" },
});
setTimeout(() => {
setPopup({ show: false, message: "" });
setShowJudgment(true);
}, 3000);
setTimeout(() => { setPopup({ show:false, message:"" }); setShowJudgment(true); }, 3000);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "JudgmentPopup|JudgmentPopup" --type f

Repository: AOSSIE-Org/DebateAI

Length of output: 45


🏁 Script executed:

rg "interface JudgmentData|type JudgmentData" -A 15

Repository: AOSSIE-Org/DebateAI

Length of output: 7084


🏁 Script executed:

cd frontend && find . -name "*.tsx" -o -name "*.ts" | head -20

Repository: AOSSIE-Org/DebateAI

Length of output: 626


🏁 Script executed:

cat -n frontend/src/components/JudgementPopup.tsx | head -100

Repository: AOSSIE-Org/DebateAI

Length of output: 3351


🏁 Script executed:

rg "judgment\." frontend/src/components/JudgementPopup.tsx | head -30

Repository: AOSSIE-Org/DebateAI

Length of output: 294


🏁 Script executed:

cat -n frontend/src/Pages/DebateRoom.tsx | sed -n '354,372p'

Repository: AOSSIE-Org/DebateAI

Length of output: 1201


🏁 Script executed:

rg "function extractJSON|const extractJSON|export.*extractJSON" -A 5

Repository: AOSSIE-Org/DebateAI

Length of output: 1408


🏁 Script executed:

rg "judgment\.verdict" frontend/src/components/JudgementPopup.tsx -B 2 -A 2

Repository: AOSSIE-Org/DebateAI

Length of output: 440


Unsafe cast of parsed JSON to JudgmentData — missing structural validation.

Line 357 does JSON.parse(extractJSON(result)) as JudgmentData. The extractJSON() function can return "{}" on empty/falsy input, and JSON.parse will succeed silently, producing an empty object. The cast succeeds at compile time but provides no runtime validation. If the API returns incomplete JSON, properties like judgment.verdict.winner will be undefined, causing the JudgementPopup to render undefined text.

The error fallback (lines 362–369) catches API failures and parse exceptions, but not the scenario where JSON.parse succeeds on incomplete data. Add a validation check before setting judgment data to ensure required fields exist (e.g., verdict?.winner and verdict?.reason).

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

In `@frontend/src/Pages/DebateRoom.tsx` around lines 354 - 372, The
judgeDebateResult handler currently does JSON.parse(extractJSON(result)) as
JudgmentData which can silently produce incomplete objects; add runtime
structural validation before calling setJudgmentData: parse the string from
extractJSON(result), verify required fields (e.g., verdict.winner,
verdict.reason and other expected sections like opening_statement, total) exist
and have the correct types, and only then call setJudgmentData with the parsed
object; if validation fails, populate a safe fallback JudgmentData (similar to
the existing error branch) and log the validation error; implement the
validation as a small helper (e.g., validateJudgmentData(parsed): parsed is
JudgmentData | null) and use it inside judgeDebateResult to decide whether to
set the parsed data or the fallback.

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.

1 participant