Skip to content

Conversation

@DeveloperAmrit
Copy link
Contributor

@DeveloperAmrit DeveloperAmrit commented Jan 11, 2026

Changes made

  • Added custom new badge celebration with confettis
  • Added celebration for when user moves in top 10
  • No points or badges on conceding

Videos

533260979-2392df04-2d08-4d5b-8c3f-73c0b56b4fc1.mov
My.Movie.mp4

@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Added "concede" as a new debate outcome in the backend gamification system with 0 points and blocking automatic badge checks. Frontend introduces GamificationOverlay component for real-time badge/score notifications via WebSocket, centralizing gamification UI logic and adding Top10 badge for reaching leaderboard top 10.

Changes

Cohort / File(s) Summary
Backend Gamification
backend/controllers/debatevsbot_controller.go
Added "concede" outcome branch in updateGamificationAfterBotDebate (0 points, "debate_concede" action); guards automatic badge checks to skip concede results; updated ConcedeDebate flow to use "concede" instead of "loss" for transcript status and gamification updates
Frontend App Setup
frontend/src/App.tsx
Mounted GamificationOverlay and Toaster components inside ThemeProvider alongside AppRoutes for real-time notifications
Gamification Notifications
frontend/src/components/GamificationOverlay.tsx
New functional component establishing WebSocket connection for real-time GamificationEvents; tracks leaderboard rank via user token; triggers BadgeUnlocked modal on badge awards and Top10 badge when user enters top 10; handles score updates and errors with WebSocket cleanup on unmount
Badge Definitions
frontend/src/components/BadgeUnlocked.tsx
Added FaCrown icon and Top10 badge entry with corresponding description
Leaderboard Badge Handling
frontend/src/Pages/Leaderboard.tsx
Removed BadgeUnlocked component, badgeUnlocked state, and WebSocket event handling for badge notifications; retained score_updated logic
Result Display & Type Definitions
frontend/src/components/SavedTranscripts.tsx,
frontend/src/services/transcriptService.ts
Extended result handling to treat "concede" same as "loss" in icon/color logic; updated SavedDebateTranscript interface result field to include 'concede' type

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as Frontend App
    participant GOv as GamificationOverlay
    participant WS as WebSocket
    participant Server
    participant Leaderboard as Leaderboard API
    participant BadgeUI as BadgeUnlocked Modal

    User->>App: Load App
    App->>GOv: Mount GamificationOverlay
    GOv->>WS: Connect WebSocket (with user token)
    WS->>Server: Subscribe to gamification events
    
    User->>Server: Complete debate/trigger action
    Server->>WS: Emit GamificationEvent
    WS->>GOv: Receive event
    
    alt Badge Award
        GOv->>BadgeUI: Open BadgeUnlocked modal (badge name)
        BadgeUI->>User: Display badge
        User->>BadgeUI: Close
    else Score Update
        GOv->>Leaderboard: Fetch user rank
        Leaderboard->>GOv: Return new rank
        GOv->>GOv: Compare to previous rank
        alt Entered Top 10
            GOv->>BadgeUI: Trigger Top10 badge modal
            BadgeUI->>User: Display badge
        end
        GOv->>GOv: Update previousRank
    end
    
    User->>App: Unmount
    GOv->>WS: Close connection
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A badge new-born, when top-ten we climb, 🏆
WebSockets whisper at the perfect time,
Overlays dance with gamified cheer,
Conceding gracefully—no loss to fear! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix issue 156 gamification' is related to the changeset but lacks specificity about what aspects of gamification are being fixed, making it somewhat vague. Consider a more specific title that highlights the main changes, such as 'Add badge celebration animations and prevent rewards on concede' or 'Implement gamification overlays and restrict rewards for conceded debates'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

Copy link

@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: 1

🤖 Fix all issues with AI agents
In @frontend/src/components/GamificationOverlay.tsx:
- Around line 27-32: The initial leaderboard fetch
(fetchGamificationLeaderboard) can finish after the WebSocket begins delivering
score_updated events, leaving previousRankRef.current null and preventing the
first top-10 celebration; either await the initial fetch before
creating/connecting the WebSocket so previousRankRef is populated before any
events are handled, or initialize previousRankRef.current to a safe default
(e.g., Infinity) and update the score_updated handler to use that sentinel (and
compare using !== undefined/null as appropriate) so the first rank delta is
evaluated correctly; locate references to fetchGamificationLeaderboard,
previousRankRef, and the score_updated WebSocket handler to implement the chosen
fix.
🧹 Nitpick comments (5)
frontend/src/components/SavedTranscripts.tsx (2)

139-151: Concede result icon mapping looks consistent with loss UI

Routing case 'concede' to the same icon as loss matches the stated UX requirement. Consider tightening the type to prevent future drift.

Proposed refactor (optional)
-  const getResultIcon = (result: string) => {
+  const getResultIcon = (result: SavedDebateTranscript['result']) => {
     switch (result) {
       case 'win':
         return <Trophy className='w-4 h-4 text-yellow-500' />;
       case 'loss':
       case 'concede':
         return <XCircle className='w-4 h-4 text-red-500' />;
       case 'draw':
         return <MinusCircle className='w-4 h-4 text-gray-500' />;
       default:
         return <Clock className='w-4 h-4 text-blue-500' />;
     }
   };

153-165: Concede result color mapping looks consistent with loss UI

Same as the icon mapping: concede inheriting loss styling is consistent with the PR behavior. Same optional typing tightening applies here too.

Proposed refactor (optional)
-  const getResultColor = (result: string) => {
+  const getResultColor = (result: SavedDebateTranscript['result']) => {
     switch (result) {
       case 'win':
         return 'bg-green-100 text-green-800 border-green-200';
       case 'loss':
       case 'concede':
         return 'bg-red-100 text-red-800 border-red-200';
       case 'draw':
         return 'bg-gray-100 text-gray-800 border-gray-200';
       default:
         return 'bg-blue-100 text-blue-800 border-blue-200';
     }
   };
backend/controllers/debatevsbot_controller.go (1)

583-611: Concede transcript result is now “concede” (good), but update the stale “loss” comment

Persisting "concede" (Line 597) and calling updateGamificationAfterBotDebate(..., "concede", ...) (Line 610) aligns backend state with the new frontend union type. The comment on Line 584 (“We treat concession as a loss”) is now misleading and should be updated (even if the UI renders it like a loss).

Proposed comment fix (optional)
-	// Save transcript to history
-	// We treat concession as a loss
+	// Save transcript to history
+	// Concession is stored as a distinct result ("concede"), though the UI may render it like a loss.
frontend/src/services/transcriptService.ts (1)

5-21: Good type update; consider propagating “concede” to other transcript/stat types for consistency

Adding 'concede' to SavedDebateTranscript.result (Line 12) matches the backend changes. Two follow-ups to consider:

  • If /debate-stats can return concede in recentDebates, update DebateStats.recentDebates.result too.
  • Tighten SaveTranscriptRequest.result from string to the same union to prevent bad values.
Proposed typing alignment
 export interface SaveTranscriptRequest {
   debateType: 'user_vs_bot' | 'user_vs_user';
   topic: string;
   opponent: string;
-  result?: string;
+  result?: SavedDebateTranscript['result'];
   messages: Array<{
     sender: string;
     text: string;
     phase?: string;
   }>;
   transcripts?: Record<string, string>;
 }

 export interface DebateStats {
   totalDebates: number;
   wins: number;
   losses: number;
   draws: number;
   winRate: number;
   recentDebates: Array<{
     topic: string;
-    result: 'win' | 'loss' | 'draw' | 'pending';
+    result: SavedDebateTranscript['result'];
     opponent: string;
     debateType: 'user_vs_bot' | 'user_vs_user';
     date: string;
     eloChange?: number; // We'll need to add this to the backend
   }>;
 }
frontend/src/components/GamificationOverlay.tsx (1)

38-71: Consider adding WebSocket error handling and reconnection logic.

The WebSocket is created without onError or onClose callbacks. If the connection fails or drops unexpectedly (e.g., network issues), users won't receive gamification events until they refresh the page or trigger an effect re-run.

Suggested improvement
     const ws = createGamificationWebSocket(
       token,
       async (event: GamificationEvent) => {
         // ... existing handler
-      }
+      },
+      (error) => {
+        console.error("Gamification WebSocket error:", error);
+      },
+      () => {
+        // Optionally implement reconnection logic here
+        console.log("Gamification WebSocket closed");
+      }
     );
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb07eaf and d5e8bd8.

📒 Files selected for processing (7)
  • backend/controllers/debatevsbot_controller.go
  • frontend/src/App.tsx
  • frontend/src/Pages/Leaderboard.tsx
  • frontend/src/components/BadgeUnlocked.tsx
  • frontend/src/components/GamificationOverlay.tsx
  • frontend/src/components/SavedTranscripts.tsx
  • frontend/src/services/transcriptService.ts
💤 Files with no reviewable changes (1)
  • frontend/src/Pages/Leaderboard.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/App.tsx (1)
frontend/src/components/ui/toaster.tsx (1)
  • Toaster (11-33)
frontend/src/components/GamificationOverlay.tsx (2)
frontend/src/hooks/useUser.ts (1)
  • useUser (12-112)
frontend/src/services/gamificationService.ts (2)
  • fetchGamificationLeaderboard (41-55)
  • createGamificationWebSocket (93-129)
🔇 Additional comments (5)
backend/controllers/debatevsbot_controller.go (2)

342-361: Concede branch correctly awards 0 points, but ensure downstream UI/event handling matches “no reward” intent

case "concede" sets pointsToAdd = 0 and a distinct action = "debate_concede" (Line 352-355), which is good. One thing to double-check: the function still inserts a score_updates record and broadcasts a score_updated event later even when points are 0—if product wants “no gamification popup/toast at all on concede”, clients should filter by action === "debate_concede" (or you can skip broadcasting when pointsToAdd == 0).


453-456: Skipping automatic badge checks on concede matches the PR requirement

The if resultStatus != "concede" guard (Line 454-456) prevents side-effect badge awarding on concede, including badges that could otherwise be granted due to existing score/streak thresholds.

frontend/src/components/BadgeUnlocked.tsx (1)

1-29: Top10 badge mapping looks fine; verify badgeName string matches backend exactly

Adding Top10 to badgeIcons/badgeDescriptions is clean. Just ensure the emitted badgeName from backend/websocket is exactly "Top10" (same casing), otherwise the UI will fall back to the default icon/description.

frontend/src/App.tsx (1)

31-33: Good global wiring; ensure GamificationOverlay is auth-gated to avoid public-page websocket churn

Rendering <GamificationOverlay /> at the app root is a good way to centralize gamification UI. Please verify it behaves safely when the user is logged out (e.g., no websocket connect / no errors / no toasts on the public home/auth pages). If it doesn’t already gate internally, consider gating it on AuthContext.isAuthenticated (either inside the component or via a small wrapper).

Also applies to: 110-119

frontend/src/components/GamificationOverlay.tsx (1)

82-88: LGTM!

The render logic correctly passes the required props to BadgeUnlocked. The onClose handler appropriately preserves the badgeName while closing the modal, allowing any closing animation to display the correct badge.

Comment on lines +27 to +32
fetchGamificationLeaderboard(token).then((data) => {
const myEntry = data.debaters.find(d => d.id === user.id);
if (myEntry) {
previousRankRef.current = myEntry.rank;
}
}).catch(console.error);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Race condition may cause missed top 10 celebration.

The initial leaderboard fetch is asynchronous and runs concurrently with WebSocket setup. If a score_updated event arrives before this fetch completes, previousRankRef.current will still be null, causing the check at line 58 (oldRank !== null) to fail. This means the user won't see the top 10 celebration on their first ranking update.

Consider awaiting the initial rank fetch before establishing the WebSocket, or initializing previousRankRef to a safe default (e.g., Infinity) so the comparison works correctly:

Suggested approach
-  const previousRankRef = useRef<number | null>(null);
+  const previousRankRef = useRef<number>(Infinity);

And update the check accordingly:

-                    if (newRank <= 10 && oldRank !== null && oldRank > 10) {
+                    if (newRank <= 10 && oldRank > 10) {
🤖 Prompt for AI Agents
In @frontend/src/components/GamificationOverlay.tsx around lines 27 - 32, The
initial leaderboard fetch (fetchGamificationLeaderboard) can finish after the
WebSocket begins delivering score_updated events, leaving
previousRankRef.current null and preventing the first top-10 celebration; either
await the initial fetch before creating/connecting the WebSocket so
previousRankRef is populated before any events are handled, or initialize
previousRankRef.current to a safe default (e.g., Infinity) and update the
score_updated handler to use that sentinel (and compare using !== undefined/null
as appropriate) so the first rank delta is evaluated correctly; locate
references to fetchGamificationLeaderboard, previousRankRef, and the
score_updated WebSocket handler to implement the chosen fix.

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