Skip to content

Conversation

@DeveloperAmrit
Copy link
Contributor

@DeveloperAmrit DeveloperAmrit commented Dec 30, 2025

Fixes #154

Also added config.prod.yml to .gitignore so that it devs could not push it accidentally

Video

Screen.Recording.2026-01-08.at.2.11.59.PM.mov

Summary by CodeRabbit

  • New Features

    • Added concede functionality allowing users to forfeit debates (both bot and multiplayer).
    • Concede button now appears in debate interface during active matches.
    • Confirmation prompt ensures accidental clicks are prevented.
    • Ratings and gamification automatically update upon concession.
    • Users are redirected after conceding.
  • Chores

    • Updated backend configuration file exclusions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

This change introduces a "concede" feature allowing users to admit defeat in ongoing debates. Implementation spans backend endpoints (HTTP for bot debates, WebSocket for online debates), state management, database updates to record losses, gamification triggers, and frontend UI with confirmation dialogs and post-concession navigation.

Changes

Cohort / File(s) Summary
Configuration & Routing
backend/.gitignore, backend/routes/debatevsbot.go
Added production config to gitignore; registered new /vsbot/concede POST endpoint for bot debate concession.
Backend Bot Debate Handler
backend/controllers/debatevsbot_controller.go
Implemented ConcedeDebate HTTP handler with request validation, debate outcome updates, transcript recording (marking loss), and panic-safe gamification trigger via updateGamificationAfterBotDebate.
Backend Real-time Communication
backend/websocket/websocket.go
Added "concede" message switch handler that broadcasts concede event to all participants, identifies opponent, and invokes services.UpdateRatings to record loss for conceder and win for opponent.
Frontend Bot Debate UI
frontend/src/Pages/DebateRoom.tsx
Imported navigation utility; added handleConcede function with confirmation, API call, state update, and post-concession navigation to /game.
Frontend Online Debate UI
frontend/src/Pages/OnlineDebateRoom.tsx
Added handleConcede function (useCallback) with confirmation and WebSocket emission; added WebSocket listener for "concede" message to transition debate state; added Concede button UI. ⚠️ Note: possible duplication of handleConcede definition and Concede button rendering.
Frontend API Service
frontend/src/services/vsbot.ts
Added concedeDebate() function to POST concession request to /vsbot/concede endpoint with optional history and authorization header.

Sequence Diagrams

sequenceDiagram
    participant User as User (Browser)
    participant UI as DebateRoom UI
    participant Service as VS Bot Service
    participant Backend as Backend Controller
    participant DB as MongoDB
    participant Gamification as Gamification Service

    User->>UI: Clicks Concede Button
    UI->>UI: Show Confirmation Dialog
    User->>UI: Confirms Concession
    UI->>Service: concedeDebate(debateId, messages)
    Service->>Backend: POST /vsbot/concede
    Backend->>DB: Update DebateVsBot outcome to "User conceded"
    Backend->>DB: Retrieve user by email
    Backend->>DB: Save transcript with outcome = "loss"
    Backend->>Gamification: updateGamificationAfterBotDebate(userId, loss)
    Note over Backend: Wrapped in recover() to catch panics
    Gamification-->>Backend: Rating/badge updates (or error logged)
    Backend-->>Service: 200 OK
    Service-->>UI: Response received
    UI->>UI: Mark debate as ended
    UI->>UI: Show popup "Debate Conceded"
    UI->>UI: Navigate to /game after 2 seconds
    UI-->>User: Redirect complete
Loading
sequenceDiagram
    participant User as User (Browser)
    participant UI as OnlineDebateRoom UI
    participant WS as WebSocket
    participant Backend as Backend WebSocket Handler
    participant Ratings as Rating Service
    participant Opponent as Opponent (Browser)

    User->>UI: Clicks Concede Button
    UI->>UI: Show Confirmation Dialog
    User->>UI: Confirms Concession
    UI->>WS: Send concede message
    WS->>Backend: "concede" message received
    Backend->>Ratings: UpdateRatings(concederId, opponentId, loss, win)
    Ratings-->>Backend: Ratings updated
    Backend->>WS: Broadcast concede event to all
    WS->>UI: Receive concede message
    UI->>UI: Transition debate to Finished
    UI->>UI: Show popup "You Conceded"
    WS->>Opponent: Broadcast concede event
    Opponent->>Opponent: Transition to Finished
    Opponent->>Opponent: Show popup "You Won"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit's ode to knowing when to yield:

When words grow weary, points concede,
A graceful exit, no shame indeed—
The concede button, shiny and bright,
Lets debaters bow out with grace and might!
Now gamified losses find their place,
And every defeat earns wisdom's grace. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding concede functionality for both vs bot and vs player modes.
Linked Issues check ✅ Passed The PR implements the CONCEDE feature requested in issue #154 by adding concede buttons and handlers for both bot and player debates across frontend and backend.
Out of Scope Changes check ✅ Passed The .gitignore update for config.prod.yml is a minor supporting change; all other changes directly implement the concede feature requested in #154.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
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: 4

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f8db72 and afc9e81.

📒 Files selected for processing (7)
  • backend/.gitignore
  • backend/controllers/debatevsbot_controller.go
  • backend/routes/debatevsbot.go
  • backend/websocket/websocket.go
  • frontend/src/Pages/DebateRoom.tsx
  • frontend/src/Pages/OnlineDebateRoom.tsx
  • frontend/src/services/vsbot.ts
🧰 Additional context used
🧬 Code graph analysis (4)
backend/routes/debatevsbot.go (1)
backend/controllers/debatevsbot_controller.go (1)
  • ConcedeDebate (523-609)
frontend/src/Pages/DebateRoom.tsx (2)
frontend/src/services/vsbot.ts (1)
  • concedeDebate (103-118)
frontend/src/components/ui/button.tsx (1)
  • Button (57-57)
backend/controllers/debatevsbot_controller.go (5)
backend/utils/auth.go (1)
  • ValidateTokenAndFetchEmail (133-146)
backend/models/debatevsbot.go (1)
  • DebateVsBot (20-31)
backend/db/db.go (2)
  • DebateVsBotCollection (19-19)
  • GetCollection (23-25)
backend/models/user.go (1)
  • User (10-35)
backend/services/transcriptservice.go (1)
  • SaveDebateTranscript (662-726)
backend/websocket/websocket.go (5)
backend/routes/rooms.go (1)
  • Room (21-26)
backend/structs/websocket.go (2)
  • Room (30-37)
  • Message (10-13)
backend/services/ai.go (1)
  • Message (21-24)
backend/models/debatevsbot.go (1)
  • Message (6-10)
backend/services/rating_service.go (1)
  • UpdateRatings (28-131)
🔇 Additional comments (10)
backend/.gitignore (1)

2-2: LGTM! Good security practice.

Adding config.prod.yml to .gitignore prevents developers from accidentally committing production configuration files containing sensitive data.

backend/routes/debatevsbot.go (1)

16-16: LGTM! Route follows existing patterns.

The new /vsbot/concede endpoint is properly registered and follows the same pattern as other routes in this file.

frontend/src/services/vsbot.ts (1)

103-118: LGTM! API function follows existing patterns.

The concedeDebate function is properly implemented and follows the same pattern as other API calls in this file (auth header, credentials, error handling).

frontend/src/Pages/DebateRoom.tsx (1)

828-835: LGTM! Concede button placement is appropriate.

The Concede button is properly placed in the user panel and correctly hidden when the debate has ended.

frontend/src/Pages/OnlineDebateRoom.tsx (2)

1263-1270: LGTM! WebSocket concede handler is correct.

The handler properly updates the UI when receiving a concede message from the opponent and provides appropriate feedback.


2120-2129: LGTM! Concede button visibility logic is correct.

The button is appropriately shown only during active debate phases (excluding Setup and Finished).

backend/websocket/websocket.go (2)

475-476: LGTM! Concede case properly added to message switch.


14-16: LGTM! Import addition is appropriate.

The arguehub/services import is needed for the UpdateRatings call in the concede handler.

backend/controllers/debatevsbot_controller.go (2)

518-521: LGTM! ConcedeRequest type is properly defined.

The struct correctly requires debateId and accepts optional history.


388-389: Minor formatting improvements noted.

The log statement formatting changes improve readability.

Also applies to: 421-421, 425-425, 463-464

Comment on lines +523 to +609
func ConcedeDebate(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "Authorization token required"})
return
}

token = strings.TrimPrefix(token, "Bearer ")
valid, email, err := utils.ValidateTokenAndFetchEmail("./config/config.prod.yml", token, c)
if err != nil || !valid {
c.JSON(401, gin.H{"error": "Invalid or expired token"})
return
}

var req ConcedeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid request payload: " + err.Error()})
return
}

objID, err := primitive.ObjectIDFromHex(req.DebateId)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid debate ID"})
return
}

// Fetch the debate to get details
var debate models.DebateVsBot
err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": objID}).Decode(&debate)
if err != nil {
c.JSON(404, gin.H{"error": "Debate not found"})
return
}

// Update debate outcome
filter := bson.M{"_id": objID}
update := bson.M{"$set": bson.M{"outcome": "User conceded"}}

_, err = db.DebateVsBotCollection.UpdateOne(context.Background(), filter, update)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to update debate: " + err.Error()})
return
}

// Get user ID from email
userCollection := db.GetCollection("users")
var user models.User
err = userCollection.FindOne(context.Background(), bson.M{"email": email}).Decode(&user)
if err != nil {
// Log error but don't fail the request as the main action succeeded
log.Printf("Error finding user for concede updates: %v", err)
c.JSON(200, gin.H{"message": "Debate conceded successfully"})
return
}

// Save transcript to history
// We treat concession as a loss
// Use history from request if available, otherwise fall back to debate record history
historyToSave := debate.History
if len(req.History) > 0 {
historyToSave = req.History
}

_ = services.SaveDebateTranscript(
user.ID,
email,
"user_vs_bot",
debate.Topic,
debate.BotName,
"loss",
historyToSave,
nil,
)

// Update gamification (score, badges, streaks)
// Call synchronously but with recover to prevent panics
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in updateGamificationAfterBotDebate (concede): %v", r)
}
}()
updateGamificationAfterBotDebate(user.ID, "loss", debate.Topic)
}()

c.JSON(200, gin.H{"message": "Debate conceded successfully"})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add context timeouts and reconsider early return behavior.

Several concerns with the ConcedeDebate handler:

  1. Missing context timeouts (Lines 551, 561, 570): All database operations use context.Background() without timeouts, which could cause requests to hang indefinitely if the database is slow or unavailable.

  2. Inconsistent success response (Lines 571-576): When user lookup fails, the function returns HTTP 200 with "Debate conceded successfully", but transcript saving and gamification updates are skipped. This creates an inconsistent state where the debate outcome is updated but user history is not. Consider returning an error status or at least a different message indicating partial success.

  3. Silent failures in gamification (Lines 599-606): If updateGamificationAfterBotDebate panics or errors, it's logged but the user receives a success response, potentially misleading them about their points/badges.

🔎 Suggested improvements
 func ConcedeDebate(c *gin.Context) {
 	token := c.GetHeader("Authorization")
 	if token == "" {
 		c.JSON(401, gin.H{"error": "Authorization token required"})
 		return
 	}
 
 	token = strings.TrimPrefix(token, "Bearer ")
 	valid, email, err := utils.ValidateTokenAndFetchEmail("./config/config.prod.yml", token, c)
 	if err != nil || !valid {
 		c.JSON(401, gin.H{"error": "Invalid or expired token"})
 		return
 	}
 
 	var req ConcedeRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(400, gin.H{"error": "Invalid request payload: " + err.Error()})
 		return
 	}
 
 	objID, err := primitive.ObjectIDFromHex(req.DebateId)
 	if err != nil {
 		c.JSON(400, gin.H{"error": "Invalid debate ID"})
 		return
 	}
 
 	// Fetch the debate to get details
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	
 	var debate models.DebateVsBot
-	err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": objID}).Decode(&debate)
+	err = db.DebateVsBotCollection.FindOne(ctx, bson.M{"_id": objID}).Decode(&debate)
 	if err != nil {
 		c.JSON(404, gin.H{"error": "Debate not found"})
 		return
 	}
 
 	// Update debate outcome
 	filter := bson.M{"_id": objID}
 	update := bson.M{"$set": bson.M{"outcome": "User conceded"}}
 
-	_, err = db.DebateVsBotCollection.UpdateOne(context.Background(), filter, update)
+	_, err = db.DebateVsBotCollection.UpdateOne(ctx, filter, update)
 	if err != nil {
 		c.JSON(500, gin.H{"error": "Failed to update debate: " + err.Error()})
 		return
 	}
 
 	// Get user ID from email
 	userCollection := db.GetCollection("users")
 	var user models.User
-	err = userCollection.FindOne(context.Background(), bson.M{"email": email}).Decode(&user)
+	err = userCollection.FindOne(ctx, bson.M{"email": email}).Decode(&user)
 	if err != nil {
-		// Log error but don't fail the request as the main action succeeded
 		log.Printf("Error finding user for concede updates: %v", err)
-		c.JSON(200, gin.H{"message": "Debate conceded successfully"})
+		// Return error since we couldn't complete the full concession flow
+		c.JSON(500, gin.H{
+			"error": "Debate outcome updated but failed to save transcript",
+			"message": "Please contact support if your history is not updated",
+		})
 		return
 	}
 
 	// Save transcript to history
 	// We treat concession as a loss
 	// Use history from request if available, otherwise fall back to debate record history
 	historyToSave := debate.History
 	if len(req.History) > 0 {
 		historyToSave = req.History
 	}
 
 	_ = services.SaveDebateTranscript(
 		user.ID,
 		email,
 		"user_vs_bot",
 		debate.Topic,
 		debate.BotName,
 		"loss",
 		historyToSave,
 		nil,
 	)
 
 	// Update gamification (score, badges, streaks)
 	// Call synchronously but with recover to prevent panics
 	func() {
 		defer func() {
 			if r := recover(); r != nil {
 				log.Printf("Panic in updateGamificationAfterBotDebate (concede): %v", r)
 			}
 		}()
 		updateGamificationAfterBotDebate(user.ID, "loss", debate.Topic)
 	}()
 
 	c.JSON(200, gin.H{"message": "Debate conceded successfully"})
 }
🤖 Prompt for AI Agents
In backend/controllers/debatevsbot_controller.go (around lines 551–606) the
handler uses context.Background() for DB calls (FindOne/UpdateOne/FindOne on
users), returns 200 when user lookup fails (skipping transcript/gamification),
and swallows gamification failures; fix by using context.WithTimeout (e.g., 5s)
for each DB operation and pass that ctx to FindOne/UpdateOne/FindOne calls,
change the user lookup error path to return a 500 (or a clear partial-success
response) so caller knows the user-related work failed instead of silently
succeeding, ensure the transcript is saved only after a successful user fetch
(or save with best-effort and clearly indicate partial success), and run
updateGamificationAfterBotDebate in a separate goroutine with defer-recover and
proper logging but do not treat gamification panics as full success—capture
errors (via channel or logs) and if gamification fails return or surface a
partial-success response to the client.

Comment on lines +758 to +796
// handleConcede handles concede requests
func handleConcede(room *Room, conn *websocket.Conn, message Message, client *Client, roomID string) {
// Broadcast concede message to all clients (including spectators)
broadcastMessage := Message{
Type: "concede",
Room: roomID,
Username: client.Username,
UserID: client.UserID,
Content: "User conceded the debate",
}

// Send to all clients
for _, r := range snapshotRecipients(room, nil) {
r.SafeWriteJSON(broadcastMessage)
}

// Find opponent
var opponent *Client
room.Mutex.Lock()
for _, c := range room.Clients {
if !c.IsSpectator && c.UserID != client.UserID {
opponent = c
break
}
}
room.Mutex.Unlock()

if opponent != nil {
// Update ratings
// User lost (0.0), Opponent won (1.0)
userID, _ := primitive.ObjectIDFromHex(client.UserID)
opponentID, _ := primitive.ObjectIDFromHex(opponent.UserID)

_, _, err := services.UpdateRatings(userID, opponentID, 0.0, time.Now())
if err != nil {
log.Printf("Error updating ratings after concede: %v", err)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation and improve error handling.

Several issues with the handleConcede function:

  1. Missing ObjectID validation (Lines 788-789): If client.UserID or opponent.UserID are not valid hex strings, ObjectIDFromHex will fail silently (errors ignored with _), and the rating update on line 791 will proceed with zero-value ObjectIDs.

  2. Rating update errors not reported (Lines 792-794): When UpdateRatings fails, only a log is written. Clients (including the conceding user) are not notified that ratings weren't updated.

  3. Broadcast logic (Line 770): Using snapshotRecipients(room, nil) sends the concede message back to the sender. While not technically wrong, it's redundant since the sender already knows they conceded.

🔎 Suggested improvements
 func handleConcede(room *Room, conn *websocket.Conn, message Message, client *Client, roomID string) {
 	// Broadcast concede message to all clients (including spectators)
 	broadcastMessage := Message{
 		Type:     "concede",
 		Room:     roomID,
 		Username: client.Username,
 		UserID:   client.UserID,
 		Content:  "User conceded the debate",
 	}
 
 	// Send to all clients
-	for _, r := range snapshotRecipients(room, nil) {
+	for _, r := range snapshotRecipients(room, conn) {
 		r.SafeWriteJSON(broadcastMessage)
 	}
 
 	// Find opponent
 	var opponent *Client
 	room.Mutex.Lock()
 	for _, c := range room.Clients {
 		if !c.IsSpectator && c.UserID != client.UserID {
 			opponent = c
 			break
 		}
 	}
 	room.Mutex.Unlock()
 
 	if opponent != nil {
 		// Update ratings
 		// User lost (0.0), Opponent won (1.0)
-		userID, _ := primitive.ObjectIDFromHex(client.UserID)
-		opponentID, _ := primitive.ObjectIDFromHex(opponent.UserID)
+		userID, err := primitive.ObjectIDFromHex(client.UserID)
+		if err != nil {
+			log.Printf("Error parsing conceding user ID: %v", err)
+			return
+		}
+		opponentID, err := primitive.ObjectIDFromHex(opponent.UserID)
+		if err != nil {
+			log.Printf("Error parsing opponent user ID: %v", err)
+			return
+		}
 
 		_, _, err := services.UpdateRatings(userID, opponentID, 0.0, time.Now())
 		if err != nil {
 			log.Printf("Error updating ratings after concede: %v", err)
+			// Notify clients of the rating update failure
+			errorMsg := Message{
+				Type:    "error",
+				Content: "Rating update failed",
+			}
+			client.SafeWriteJSON(errorMsg)
+			opponent.SafeWriteJSON(errorMsg)
 		}
 	}
 }
📝 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
// handleConcede handles concede requests
func handleConcede(room *Room, conn *websocket.Conn, message Message, client *Client, roomID string) {
// Broadcast concede message to all clients (including spectators)
broadcastMessage := Message{
Type: "concede",
Room: roomID,
Username: client.Username,
UserID: client.UserID,
Content: "User conceded the debate",
}
// Send to all clients
for _, r := range snapshotRecipients(room, nil) {
r.SafeWriteJSON(broadcastMessage)
}
// Find opponent
var opponent *Client
room.Mutex.Lock()
for _, c := range room.Clients {
if !c.IsSpectator && c.UserID != client.UserID {
opponent = c
break
}
}
room.Mutex.Unlock()
if opponent != nil {
// Update ratings
// User lost (0.0), Opponent won (1.0)
userID, _ := primitive.ObjectIDFromHex(client.UserID)
opponentID, _ := primitive.ObjectIDFromHex(opponent.UserID)
_, _, err := services.UpdateRatings(userID, opponentID, 0.0, time.Now())
if err != nil {
log.Printf("Error updating ratings after concede: %v", err)
}
}
}
// handleConcede handles concede requests
func handleConcede(room *Room, conn *websocket.Conn, message Message, client *Client, roomID string) {
// Broadcast concede message to all clients (including spectators)
broadcastMessage := Message{
Type: "concede",
Room: roomID,
Username: client.Username,
UserID: client.UserID,
Content: "User conceded the debate",
}
// Send to all clients
for _, r := range snapshotRecipients(room, conn) {
r.SafeWriteJSON(broadcastMessage)
}
// Find opponent
var opponent *Client
room.Mutex.Lock()
for _, c := range room.Clients {
if !c.IsSpectator && c.UserID != client.UserID {
opponent = c
break
}
}
room.Mutex.Unlock()
if opponent != nil {
// Update ratings
// User lost (0.0), Opponent won (1.0)
userID, err := primitive.ObjectIDFromHex(client.UserID)
if err != nil {
log.Printf("Error parsing conceding user ID: %v", err)
return
}
opponentID, err := primitive.ObjectIDFromHex(opponent.UserID)
if err != nil {
log.Printf("Error parsing opponent user ID: %v", err)
return
}
_, _, err := services.UpdateRatings(userID, opponentID, 0.0, time.Now())
if err != nil {
log.Printf("Error updating ratings after concede: %v", err)
// Notify clients of the rating update failure
errorMsg := Message{
Type: "error",
Content: "Rating update failed",
}
client.SafeWriteJSON(errorMsg)
opponent.SafeWriteJSON(errorMsg)
}
}
}

Comment on lines +262 to +284
const handleConcede = async () => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
try {
if (debateData.debateId) {
await concedeDebate(debateData.debateId, state.messages);
}

setState(prev => ({ ...prev, isDebateEnded: true }));
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false
});

setTimeout(() => {
navigate("/game");
}, 2000);

} catch (error) {
console.error("Error conceding:", 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 | 🟠 Major

Improve error handling and state consistency.

The concede flow has a few concerns:

  1. State inconsistency risk: Line 269 sets isDebateEnded: true before the API call completes. If concedeDebate fails (line 266), the local state will show the debate as ended, but the backend won't reflect this change.
  2. Silent error handling: Lines 280-282 catch errors but only log them to console. Users won't know their concede action failed.
  3. Navigation timing: The 2-second delay (line 276) before navigating away might be too short for users to read the confirmation message.
🔎 Suggested improvements
 const handleConcede = async () => {
   if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
     try {
       if (debateData.debateId) {
           await concedeDebate(debateData.debateId, state.messages);
       }
       
+      // Only update state after successful API call
       setState(prev => ({ ...prev, isDebateEnded: true }));
       setPopup({
           show: true,
           message: "You have conceded the debate.",
           isJudging: false
       });
       
+      // Increase delay to give users more time to read the message
       setTimeout(() => {
           navigate("/game");
-      }, 2000);
+      }, 3000);

     } catch (error) {
       console.error("Error conceding:", error);
+      // Inform user of the error
+      setPopup({
+        show: true,
+        message: "Failed to concede the debate. Please try again.",
+        isJudging: false
+      });
     }
   }
 };
📝 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 handleConcede = async () => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
try {
if (debateData.debateId) {
await concedeDebate(debateData.debateId, state.messages);
}
setState(prev => ({ ...prev, isDebateEnded: true }));
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false
});
setTimeout(() => {
navigate("/game");
}, 2000);
} catch (error) {
console.error("Error conceding:", error);
}
}
};
const handleConcede = async () => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
try {
if (debateData.debateId) {
await concedeDebate(debateData.debateId, state.messages);
}
// Only update state after successful API call
setState(prev => ({ ...prev, isDebateEnded: true }});
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false
});
// Increase delay to give users more time to read the message
setTimeout(() => {
navigate("/game");
}, 3000);
} catch (error) {
console.error("Error conceding:", error);
// Inform user of the error
setPopup({
show: true,
message: "Failed to concede the debate. Please try again.",
isJudging: false
});
}
}
};
🤖 Prompt for AI Agents
In frontend/src/Pages/DebateRoom.tsx around lines 262 to 284, the concede flow
updates local state and navigates before confirming the backend succeeded and
only logs errors; change the flow so the concede API is awaited first and only
on success setState(prev=>({...prev, isDebateEnded:true})), show a success
popup, then navigate (either after a longer timeout like 4s or after user
acknowledgment). In the catch block, replace the console.error with setting a
visible error popup (e.g., setPopup({show:true,message:"Failed to concede.
Please try again.",isError:true})) and do not change isDebateEnded or navigate
when the API fails; ensure all branches handle missing debateId gracefully by
showing an error popup instead of proceeding.

Comment on lines +800 to +817
const handleConcede = useCallback(() => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
if (wsRef.current) {
wsRef.current.send(JSON.stringify({
type: "concede",
room: roomId,
userId: currentUserId,
username: currentUser?.displayName || "User"
}));
}
setDebatePhase(DebatePhase.Finished);
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false,
});
}
}, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for WebSocket failures.

The concede flow updates local state without confirming the WebSocket message was sent successfully:

  1. Line 810 sets debatePhase to Finished immediately, but if wsRef.current is null or the WebSocket is disconnected, the concede message won't be sent to the opponent.
  2. No verification that the WebSocket send succeeded before updating UI state.
🔎 Suggested improvements
 const handleConcede = useCallback(() => {
   if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
-    if (wsRef.current) {
+    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
       wsRef.current.send(JSON.stringify({
         type: "concede",
         room: roomId,
         userId: currentUserId,
         username: currentUser?.displayName || "User"
       }));
+      setDebatePhase(DebatePhase.Finished);
+      setPopup({
+        show: true,
+        message: "You have conceded the debate.",
+        isJudging: false,
+      });
+    } else {
+      // WebSocket not connected
+      setPopup({
+        show: true,
+        message: "Connection lost. Unable to concede. Please try again.",
+        isJudging: false,
+      });
+      return;
     }
-    setDebatePhase(DebatePhase.Finished);
-    setPopup({
-      show: true,
-      message: "You have conceded the debate.",
-      isJudging: false,
-    });
   }
 }, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
📝 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 handleConcede = useCallback(() => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
if (wsRef.current) {
wsRef.current.send(JSON.stringify({
type: "concede",
room: roomId,
userId: currentUserId,
username: currentUser?.displayName || "User"
}));
}
setDebatePhase(DebatePhase.Finished);
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false,
});
}
}, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
const handleConcede = useCallback(() => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
type: "concede",
room: roomId,
userId: currentUserId,
username: currentUser?.displayName || "User"
}));
setDebatePhase(DebatePhase.Finished);
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false,
});
} else {
// WebSocket not connected
setPopup({
show: true,
message: "Connection lost. Unable to concede. Please try again.",
isJudging: false,
});
return;
}
}
}, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
🤖 Prompt for AI Agents
In frontend/src/Pages/OnlineDebateRoom.tsx around lines 800 to 817, the concede
handler updates local state immediately without ensuring the WebSocket message
was actually sent; change the flow to (1) verify wsRef.current exists and
wsRef.current.readyState === WebSocket.OPEN before attempting to send, (2) wrap
the send in a try/catch and only call setDebatePhase(DebatePhase.Finished) and
the success popup after send completes without throwing, and (3) on failure (no
socket, not open, or send error) show an error popup and do not mark the debate
finished (optionally offer a retry). Ensure you include meaningful error text in
the popup/log to aid debugging.

@bhavik-mangla
Copy link
Contributor

looks good

@bhavik-mangla bhavik-mangla merged commit f00b839 into AOSSIE-Org:main Jan 8, 2026
1 check passed
@bhavik-mangla
Copy link
Contributor

this will end the debae for other user too right?

@DeveloperAmrit
Copy link
Contributor Author

Yes

@coderabbitai coderabbitai bot mentioned this pull request Jan 8, 2026
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.

Add feature: Adding a CONCEDE option on debate window

2 participants