diff --git a/.env.example b/.env.example
deleted file mode 100644
index 8c878f3..0000000
--- a/.env.example
+++ /dev/null
@@ -1,9 +0,0 @@
-# Backend Secrets (place in backend/.env)
-GEMINI_API_KEY=your_gemini_key_here
-JWT_SECRET=your_jwt_secret_here
-GOOGLE_CLIENT_ID=your_google_client_id_here
-# SMTP_PASSWORD=your_smtp_password_here (if needed)
-
-# Frontend Secrets (place in frontend/.env)
-VITE_GOOGLE_CLIENT_ID=your_google_client_id_here
-VITE_BASE_URL=http://localhost:1313
diff --git a/backend/config/config.prod.sample.yml b/backend/config/config.prod.sample.yml
deleted file mode 100644
index 48a187c..0000000
--- a/backend/config/config.prod.sample.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-server:
- port: 1313 # The port number your backend server will run on
-
-database:
- uri: "mongodb+srv://:@/"
- # Replace with your MongoDB Atlas connection string
- # Get this from your MongoDB Atlas dashboard after creating a cluster and database
-
-gemini:
- apiKey: ""
- # API key for OpenAI / Gemini model access
- # Obtain from your OpenRouter.ai or OpenAI account dashboard
-
-jwt:
- secret: ""
- # A secret string used to sign JWT tokens
- # Generate a strong random string (e.g. use `openssl rand -hex 32`)
-
- expiry: 1440
- # Token expiry time in minutes (e.g. 1440 = 24 hours)
-
-smtp:
- host: "smtp.gmail.com"
- # SMTP server host for sending emails (example is Gmail SMTP)
-
- port: 587
- # SMTP server port (587 for TLS)
-
- username: ""
- # Email username (your email address)
-
- password: ""
- # Password for the email or app-specific password if 2FA is enabled
-
- senderEmail: ""
- # The 'from' email address used when sending mails
-
- senderName: "DebateAI Team"
-
-googleOAuth:
- clientID: ""
- # Google OAuth Client ID for OAuth login
- # Obtain from Google Cloud Console (APIs & Services > Credentials > OAuth 2.0 Client IDs)
diff --git a/backend/controllers/debatevsbot_controller.go b/backend/controllers/debatevsbot_controller.go
index 9571099..bdac254 100644
--- a/backend/controllers/debatevsbot_controller.go
+++ b/backend/controllers/debatevsbot_controller.go
@@ -23,6 +23,7 @@ type DebateRequest struct {
BotLevel string `json:"botLevel" binding:"required"`
Topic string `json:"topic" binding:"required"`
Stance string `json:"stance" binding:"required"`
+ Role string `json:"role"`
History []models.Message `json:"history"`
PhaseTimings []PhaseTiming `json:"phaseTimings"`
Context string `json:"context"`
@@ -43,6 +44,7 @@ type DebateResponse struct {
BotLevel string `json:"botLevel"`
Topic string `json:"topic"`
Stance string `json:"stance"`
+ Role string `json:"role"`
PhaseTimings []models.PhaseTiming `json:"phaseTimings,omitempty"` // Backend format
}
@@ -52,6 +54,7 @@ type DebateMessageResponse struct {
BotLevel string `json:"botLevel"`
Topic string `json:"topic"`
Stance string `json:"stance"`
+ Role string `json:"role"`
Response string `json:"response"`
}
@@ -97,6 +100,7 @@ func CreateDebate(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
+ Role: req.Role,
History: req.History,
PhaseTimings: backendPhaseTimings,
CreatedAt: time.Now().Unix(),
@@ -114,6 +118,7 @@ func CreateDebate(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
+ Role: req.Role,
PhaseTimings: backendPhaseTimings,
}
c.JSON(200, response)
@@ -139,8 +144,8 @@ func SendDebateMessage(c *gin.Context) {
return
}
- // Generate bot response with the additional context field.
- botResponse := services.GenerateBotResponse(req.BotName, req.BotLevel, req.Topic, req.History, req.Stance, req.Context, 150)
+ // Generate bot response with the additional context and role field.
+ botResponse := services.GenerateBotResponse(req.BotName, req.BotLevel, req.Topic, req.History, req.Stance, req.Context, req.Role, 150)
// Update debate history with the bot's response.
updatedHistory := append(req.History, models.Message{
@@ -155,6 +160,7 @@ func SendDebateMessage(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
+ Role: req.Role,
History: updatedHistory,
CreatedAt: time.Now().Unix(),
}
@@ -172,6 +178,7 @@ func SendDebateMessage(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
+ Role: req.Role,
Response: botResponse,
}
c.JSON(200, response)
diff --git a/backend/models/debate.go b/backend/models/debate.go
index 233d1bd..20d7c57 100644
--- a/backend/models/debate.go
+++ b/backend/models/debate.go
@@ -21,6 +21,8 @@ type Debate struct {
PreRD float64 `bson:"preRD" json:"preRD"`
PostRating float64 `bson:"postRating" json:"postRating"`
PostRD float64 `bson:"postRD" json:"postRD"`
+ Role string `bson:"role" json:"role"` // User's role
+ OpponentRole string `bson:"opponentRole" json:"opponentRole"` // Opponent's role
Date time.Time `bson:"date" json:"date"`
}
diff --git a/backend/models/debatevsbot.go b/backend/models/debatevsbot.go
index 6411e2c..b5e6d6f 100644
--- a/backend/models/debatevsbot.go
+++ b/backend/models/debatevsbot.go
@@ -27,5 +27,6 @@ type DebateVsBot struct {
History []Message `json:"history" bson:"history"`
PhaseTimings []PhaseTiming `json:"phaseTimings" bson:"phaseTimings"` // Added for custom timings
Outcome string `json:"outcome" bson:"outcome"` // Result of the debate (e.g., "User wins")
+ Role string `json:"role" bson:"role"` // User's role (historian, scientist, lawyer, neutral)
CreatedAt int64 `json:"createdAt" bson:"createdAt"`
}
diff --git a/backend/routes/rooms.go b/backend/routes/rooms.go
index b4df0c7..7a8b19d 100644
--- a/backend/routes/rooms.go
+++ b/backend/routes/rooms.go
@@ -32,6 +32,7 @@ type Participant struct {
Elo int `json:"elo" bson:"elo"`
AvatarURL string `json:"avatarUrl" bson:"avatarUrl,omitempty"`
Email string `json:"email" bson:"email,omitempty"`
+ Role string `json:"role" bson:"role,omitempty"` // historian, scientist, lawyer, neutral
}
// generateRoomID creates a random six-digit room ID as a string.
diff --git a/backend/services/debatevsbot.go b/backend/services/debatevsbot.go
index 0446b66..a365fdd 100644
--- a/backend/services/debatevsbot.go
+++ b/backend/services/debatevsbot.go
@@ -110,7 +110,7 @@ func inferOpponentStyle(message string) string {
// constructPrompt builds a prompt that adjusts based on bot personality, debate topic, history,
// extra context, and uses the provided stance directly. It includes phase-specific instructions
// and leverages InteractionModifiers and PhilosophicalTenets for tailored responses.
-func constructPrompt(bot BotPersonality, topic string, history []models.Message, stance, extraContext string, maxWords int) string {
+func constructPrompt(bot BotPersonality, topic string, history []models.Message, stance, extraContext, role string, maxWords int) string {
// Level-based instructions
levelInstructions := ""
switch strings.ToLower(bot.Level) {
@@ -169,6 +169,17 @@ Your responses must reflect this persona consistently, as if you are the charact
limitInstruction = fmt.Sprintf("Limit your response to %d words.", maxWords)
}
+ // Role-based instructions
+ roleInstruction := ""
+ switch strings.ToLower(role) {
+ case "historian":
+ roleInstruction = "Act as a Historian: use historical examples, timelines, and precedents to support your arguments."
+ case "scientist":
+ roleInstruction = "Act as a Scientist: use empirical evidence, data, and experimental results to support your arguments."
+ case "lawyer":
+ roleInstruction = "Act as a Lawyer: use persuasion, airtight logic, and sharp rebuttals to support your arguments."
+ }
+
// Base instruction for all responses
baseInstruction := "Provide only your own argument without simulating an opponent’s dialogue. " +
"If the user’s input is unclear, off-topic, or empty, respond with a personality-appropriate clarification request, e.g., for Yoda: 'Clouded, your point is, young one. Clarify, you must.'"
@@ -182,17 +193,19 @@ Your debating style must strictly adhere to the following guidelines:
- Level Instructions: %s
- Personality Instructions: %s
- Interaction Modifier: %s
+- Role Instructions: %s
Your stance is: %s.
%s
%s
%s
Provide an opening statement that embodies your persona and stance.
[Your opening argument]
-%s %s`,
+%s`,
bot.Name, bot.Level, stance, topic,
levelInstructions,
personalityInstructions,
modifierInstruction,
+ roleInstruction,
stance,
func() string {
if extraContext != "" {
@@ -237,6 +250,7 @@ Your debating style must strictly adhere to the following guidelines:
- Level Instructions: %s
- Personality Instructions: %s
- Interaction Modifier: %s
+- Role Instructions: %s
Your stance is: %s.
%s
%s
@@ -250,6 +264,7 @@ Please provide your full argument.`,
levelInstructions,
personalityInstructions,
modifierInstruction,
+ roleInstruction,
stance,
func() string {
if extraContext != "" {
@@ -267,14 +282,14 @@ Please provide your full argument.`,
// GenerateBotResponse generates a response from the debate bot using the Gemini client library.
// It uses the bot’s personality to handle errors and responses vividly.
-func GenerateBotResponse(botName, botLevel, topic string, history []models.Message, stance, extraContext string, maxWords int) string {
+func GenerateBotResponse(botName, botLevel, topic string, history []models.Message, stance, extraContext, role string, maxWords int) string {
if geminiClient == nil {
return personalityErrorResponse(botName, "My systems are offline, it seems.")
}
bot := GetBotPersonality(botName)
// Construct prompt with enhanced personality integration
- prompt := constructPrompt(bot, topic, history, stance, extraContext, maxWords)
+ prompt := constructPrompt(bot, topic, history, stance, extraContext, role, maxWords)
ctx := context.Background()
response, err := generateDefaultModelText(ctx, prompt)
diff --git a/backend/services/matchmaking.go b/backend/services/matchmaking.go
index 2d777e5..5413443 100644
--- a/backend/services/matchmaking.go
+++ b/backend/services/matchmaking.go
@@ -94,9 +94,7 @@ func (ms *MatchmakingService) RemoveFromPool(userID string) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
- if _, exists := ms.pool[userID]; exists {
- delete(ms.pool, userID)
- }
+ delete(ms.pool, userID)
}
// UpdateActivity updates the last activity time for a user
diff --git a/backend/structs/auth.go b/backend/structs/auth.go
index cca1262..57be3fd 100644
--- a/backend/structs/auth.go
+++ b/backend/structs/auth.go
@@ -2,7 +2,7 @@ package structs
type SignUpRequest struct {
Email string `json:"email" binding:"required,email"`
- Password string `json:"password" binding:"required,min=8"`
+ Password string `json:"password" binding:"required,min=6"`
}
type VerifyEmailRequest struct {
@@ -12,7 +12,7 @@ type VerifyEmailRequest struct {
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
- Password string `json:"password" binding:"required,min=8"`
+ Password string `json:"password" binding:"required,min=6"`
}
type ForgotPasswordRequest struct {
@@ -22,5 +22,5 @@ type ForgotPasswordRequest struct {
type VerifyForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
- NewPassword string `json:"newPassword" binding:"required,min=8"`
+ NewPassword string `json:"newPassword" binding:"required,min=6"`
}
diff --git a/backend/test_server.go b/backend/test_server.go
index 8151629..8330504 100644
--- a/backend/test_server.go
+++ b/backend/test_server.go
@@ -35,7 +35,7 @@ func main() {
// Check pool - should have 2 users now
pool = ms.GetPool()
- for _, user := range pool {
+ for range pool {
}
// Wait a bit for matching
@@ -44,7 +44,7 @@ func main() {
// Check pool after matching
pool = ms.GetPool()
- for _, user := range pool {
+ for range pool {
}
}
diff --git a/backend/websocket/websocket.go b/backend/websocket/websocket.go
index bae9351..f95bb41 100644
--- a/backend/websocket/websocket.go
+++ b/backend/websocket/websocket.go
@@ -50,6 +50,7 @@ type Client struct {
LastActivity time.Time
IsMuted bool // New field to track mute status
Role string // New field to track debate role (for/against)
+ PersonaRole string // New field to track persona role (historian, etc.)
SpeechText string // New field to store speech text
ConnectionID string
}
@@ -82,10 +83,11 @@ type Message struct {
Timestamp int64 `json:"timestamp,omitempty"`
Mode string `json:"mode,omitempty"` // 'type' or 'speak'
// Debate-specific fields
- Phase string `json:"phase,omitempty"`
- Topic string `json:"topic,omitempty"`
- Role string `json:"role,omitempty"`
- Ready *bool `json:"ready,omitempty"`
+ Phase string `json:"phase,omitempty"`
+ Topic string `json:"topic,omitempty"`
+ Role string `json:"role,omitempty"`
+ Persona string `json:"persona,omitempty"`
+ Ready *bool `json:"ready,omitempty"`
// New fields for automatic muting
IsMuted bool `json:"isMuted,omitempty"`
CurrentTurn string `json:"currentTurn,omitempty"` // "for" or "against"
@@ -173,6 +175,7 @@ func buildParticipantsMessage(room *Room) map[string]interface{} {
"displayName": client.Username,
"email": client.Email,
"role": client.Role,
+ "persona": client.PersonaRole,
"isMuted": client.IsMuted,
})
}
@@ -466,6 +469,8 @@ func WebsocketHandler(c *gin.Context) {
handleTopicChange(room, conn, message, roomID)
case "roleSelection":
handleRoleSelection(room, conn, message, roomID)
+ case "personaSelection":
+ handlePersonaSelection(room, conn, message, roomID)
case "ready":
handleReadyStatus(room, conn, message, roomID)
case "mute":
@@ -684,6 +689,28 @@ func handleRoleSelection(room *Room, conn *websocket.Conn, message Message, room
broadcastParticipants(room)
}
+// handlePersonaSelection handles persona role selection
+func handlePersonaSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
+ // Store the persona in the client
+ room.Mutex.Lock()
+ defer room.Mutex.Unlock()
+ if client, exists := room.Clients[conn]; exists {
+ if client.IsSpectator {
+ return
+ }
+ client.PersonaRole = message.Persona
+ }
+
+ // Broadcast persona selection to other clients
+ for _, r := range snapshotRecipients(room, conn) {
+ if err := r.SafeWriteJSON(message); err != nil {
+ }
+ }
+
+ // Send updated participant snapshot to everyone
+ broadcastParticipants(room)
+}
+
// handleReadyStatus handles ready status
func handleReadyStatus(room *Room, conn *websocket.Conn, message Message, roomID string) {
// Broadcast ready status to other clients
diff --git a/frontend/src/Pages/Authentication/forms.tsx b/frontend/src/Pages/Authentication/forms.tsx
index 221257f..f0120d2 100644
--- a/frontend/src/Pages/Authentication/forms.tsx
+++ b/frontend/src/Pages/Authentication/forms.tsx
@@ -5,6 +5,8 @@ import { useContext, useState, useEffect } from 'react';
import { AuthContext } from '../../context/authContext';
import { useCallback } from "react";
+const MIN_PASSWORD_LENGTH = 6;
+
interface LoginFormProps {
startForgotPassword: () => void;
infoMessage?: string;
@@ -24,28 +26,26 @@ export const LoginForm: React.FC = ({ startForgotPassword, infoM
const [localError, setLocalError] = useState(null);
-
-const MIN_PASSWORD_LENGTH = 8;
-const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (password.length < MIN_PASSWORD_LENGTH) {
- setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
- return;
- }
- setLocalError(null);
- await login(email, password);
-};
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (password.length < MIN_PASSWORD_LENGTH) {
+ setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
+ return;
+ }
+ setLocalError(null);
+ await login(email, password);
+ };
-const handleGoogleLogin = useCallback(
- (response: { credential: string; select_by: string }) => {
- const idToken = response.credential;
- googleLogin(idToken);
- },
- [googleLogin]
-);
+ const handleGoogleLogin = useCallback(
+ (response: { credential: string; select_by: string }) => {
+ const idToken = response.credential;
+ googleLogin(idToken);
+ },
+ [googleLogin]
+ );
useEffect(() => {
const google = window.google;
if (!google?.accounts) {
@@ -93,7 +93,7 @@ const handleGoogleLogin = useCallback(
{localError}
-)}
+ )}
= ({ startOtpVerification })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+ if (password.length < MIN_PASSWORD_LENGTH) {
+ authContext.handleError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
+ return;
+ }
+
if (password !== confirmPassword) {
authContext.handleError('Passwords do not match');
return;
}
-
await signup(email, password);
startOtpVerification(email);
};
- const handleGoogleLogin = useCallback(
- (response: { credential: string; select_by: string }) => {
- const idToken = response.credential;
- googleLogin(idToken);
- },
- [googleLogin]
-);
+ const handleGoogleLogin = useCallback(
+ (response: { credential: string; select_by: string }) => {
+ const idToken = response.credential;
+ googleLogin(idToken);
+ },
+ [googleLogin]
+ );
useEffect(() => {
@@ -345,11 +349,15 @@ export const ResetPasswordForm: React.FC
= ({ email, han
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+ if (newPassword.length < MIN_PASSWORD_LENGTH) {
+ authContext.handleError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
+ return;
+ }
+
if (newPassword !== confirmNewPassword) {
authContext.handleError('Passwords do not match');
return;
}
-
await confirmForgotPassword(email, code, newPassword);
await login(email, newPassword);
handlePasswordReset();
diff --git a/frontend/src/Pages/BotSelection.tsx b/frontend/src/Pages/BotSelection.tsx
index bbe4dc3..6968e42 100644
--- a/frontend/src/Pages/BotSelection.tsx
+++ b/frontend/src/Pages/BotSelection.tsx
@@ -209,6 +209,7 @@ const BotSelection: React.FC = () => {
const [stance, setStance] = useState("random");
const [phaseTimings, setPhaseTimings] =
useState<{ name: string; time: number }[]>(defaultPhaseTimings);
+ const [role, setRole] = useState("neutral");
const [isLoading, setIsLoading] = useState(false);
const [user] = useAtom(userAtom);
const [error, setError] = useState(null);
@@ -280,6 +281,7 @@ const BotSelection: React.FC = () => {
botLevel: bot.level,
topic: effectiveTopic,
stance: finalStance,
+ role,
history: [],
phaseTimings,
};
@@ -291,6 +293,7 @@ const BotSelection: React.FC = () => {
...data,
phaseTimings,
stance: finalStance,
+ role,
userId: user?.email || "guest@example.com",
botName: bot.name,
botLevel: bot.level,
@@ -366,11 +369,10 @@ const BotSelection: React.FC = () => {
{levels.map((level) => (
{
setSelectedBot(bot.name)}
- className={`relative flex flex-col items-center p-2 rounded-md border transition-colors cursor-pointer group ${
- selectedBot === bot.name
+ className={`relative flex flex-col items-center p-2 rounded-md border transition-colors cursor-pointer group ${selectedBot === bot.name
? "border-2 border-primary bg-primary/10"
: "border-border hover:bg-muted"
- }`}
+ }`}
>
![]()
{
+
+ {/* Role Selection */}
+
+
+
+
{/* Responsive Timer Section */}
@@ -531,7 +550,7 @@ const BotSelection: React.FC = () => {
-
+
>
);
};
diff --git a/frontend/src/Pages/DebateRoom.tsx b/frontend/src/Pages/DebateRoom.tsx
index c2ed137..44ba7d8 100644
--- a/frontend/src/Pages/DebateRoom.tsx
+++ b/frontend/src/Pages/DebateRoom.tsx
@@ -145,6 +145,7 @@ type DebateProps = {
stance: string;
phaseTimings: { name: string; time: number }[];
debateId: string;
+ role: string;
};
type DebateState = {
@@ -197,20 +198,20 @@ const turnTypes = [
const extractJSON = (response: string): string => {
if (!response) return "{}";
-
+
// Try to extract JSON from markdown code fences
const fenceRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
const match = fenceRegex.exec(response);
if (match && match[1]) {
return match[1].trim();
}
-
+
// Try to find JSON object in the response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return jsonMatch[0];
}
-
+
// If no JSON found, return empty object
console.warn("No JSON found in response:", response);
return "{}";
@@ -229,15 +230,15 @@ const DebateRoom: React.FC = () => {
return savedState
? JSON.parse(savedState)
: {
- messages: [],
- currentPhase: 0,
- phaseStep: 0,
- isBotTurn: false,
- userStance: "",
- botStance: "",
- timer: phases[0].time,
- isDebateEnded: false,
- };
+ messages: [],
+ currentPhase: 0,
+ phaseStep: 0,
+ isBotTurn: false,
+ userStance: "",
+ botStance: "",
+ timer: phases[0].time,
+ isDebateEnded: false,
+ };
});
const [finalInput, setFinalInput] = useState("");
const [interimInput, setInterimInput] = useState("");
@@ -263,18 +264,18 @@ const DebateRoom: React.FC = () => {
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);
+ await concedeDebate(debateData.debateId, state.messages);
}
-
+
setState(prev => ({ ...prev, isDebateEnded: true }));
setPopup({
- show: true,
- message: "You have conceded the debate.",
- isJudging: false
+ show: true,
+ message: "You have conceded the debate.",
+ isJudging: false
});
-
+
setTimeout(() => {
- navigate("/game");
+ navigate("/game");
}, 2000);
} catch (error) {
@@ -360,7 +361,7 @@ const DebateRoom: React.FC = () => {
if (!state.userStance) {
const stanceNormalized =
debateData.stance.toLowerCase() === "for" ||
- debateData.stance.toLowerCase() === "against"
+ debateData.stance.toLowerCase() === "against"
? debateData.stance.toLowerCase() === "for"
? "For"
: "Against"
@@ -451,9 +452,8 @@ const DebateRoom: React.FC = () => {
const newPhase = currentState.currentPhase + 1;
setPopup({
show: true,
- message: `${phases[currentState.currentPhase].name} completed. Next: ${
- phases[newPhase].name
- } - ${getPhaseInstructions(newPhase)}`,
+ message: `${phases[currentState.currentPhase].name} completed. Next: ${phases[newPhase].name
+ } - ${getPhaseInstructions(newPhase)}`,
});
setTimeout(() => {
setPopup({ show: false, message: "" });
@@ -582,10 +582,10 @@ const DebateRoom: React.FC = () => {
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);
@@ -603,7 +603,7 @@ const DebateRoom: React.FC = () => {
throw new Error(`Failed to parse JSON: ${e}`);
}
}
-
+
console.log("Parsed judgment:", judgment);
setJudgmentData(judgment);
setPopup({ show: false, message: "" });
@@ -616,7 +616,7 @@ const DebateRoom: React.FC = () => {
message: `Judgment error: ${error instanceof Error ? error.message : "Unknown error"}. Showing default results.`,
isJudging: false,
});
-
+
// Set default judgment data
setJudgmentData({
opening_statement: {
@@ -656,9 +656,8 @@ const DebateRoom: React.FC = () => {
.padStart(2, "0")}`;
return (
{timeStr}
@@ -707,8 +706,8 @@ const DebateRoom: React.FC = () => {
{currentTurnType === "statement"
? "make a statement"
: currentTurnType === "question"
- ? "ask a question"
- : "answer"}
+ ? "ask a question"
+ : "answer"}
@@ -754,9 +753,8 @@ const DebateRoom: React.FC = () => {
{/* Bot Section */}
@@ -802,9 +800,8 @@ const DebateRoom: React.FC = () => {
{/* User Section */}
@@ -815,8 +812,13 @@ const DebateRoom: React.FC = () => {
/>
-
+
{user?.displayName || "You"}
+ {debateData.role && debateData.role !== "neutral" && (
+
+ {debateData.role}
+
+ )}
{user?.bio || "Debater"}
@@ -868,8 +870,8 @@ const DebateRoom: React.FC = () => {
currentTurnType === "statement"
? "Make your statement"
: currentTurnType === "question"
- ? "Ask your question"
- : "Provide your answer"
+ ? "Ask your question"
+ : "Provide your answer"
}
className="flex-1 rounded-md text-sm border border-border bg-input text-foreground placeholder:text-muted-foreground focus:border-orange-400"
/>
diff --git a/frontend/src/Pages/OnlineDebateRoom.tsx b/frontend/src/Pages/OnlineDebateRoom.tsx
index 613c5df..18bcd5d 100644
--- a/frontend/src/Pages/OnlineDebateRoom.tsx
+++ b/frontend/src/Pages/OnlineDebateRoom.tsx
@@ -78,6 +78,7 @@ interface UserDetails {
avatarUrl?: string;
displayName?: string;
email?: string;
+ persona?: string;
}
// Define WebSocket message structure
@@ -85,6 +86,7 @@ interface WSMessage {
type: string;
topic?: string;
role?: DebateRole;
+ persona?: string;
ready?: boolean;
phase?: DebatePhase;
offer?: RTCSessionDescriptionInit;
@@ -140,7 +142,7 @@ const BASE_URL = import.meta.env.VITE_BASE_URL || window.location.origin;
const WS_BASE_URL = BASE_URL.replace(
/^https?/,
- (match) => (match === "https" ? "wss" : "ws")
+ (match: string) => (match === "https" ? "wss" : "ws")
);
@@ -205,7 +207,9 @@ const OnlineDebateRoom = (): JSX.Element => {
// State for debate setup and signaling
const [topic, setTopic] = useState("");
const [localRole, setLocalRole] = useState
(null);
+ const [localPersonaRole, setLocalPersonaRole] = useState("neutral");
const [peerRole, setPeerRole] = useState(null);
+ const [peerPersonaRole, setPeerPersonaRole] = useState("neutral");
const [localReady, setLocalReady] = useState(false);
const [peerReady, setPeerReady] = useState(false);
const [debatePhase, setDebatePhase] = useState(
@@ -492,8 +496,8 @@ const OnlineDebateRoom = (): JSX.Element => {
(debatePhase.includes("For")
? "for"
: debatePhase.includes("Against")
- ? "against"
- : null);
+ ? "against"
+ : null);
const startJudgmentPolling = useCallback(
(role: DebateRole) => {
@@ -701,17 +705,17 @@ const OnlineDebateRoom = (): JSX.Element => {
const rolePhases =
role === "for"
? [
- DebatePhase.OpeningFor,
- DebatePhase.CrossForQuestion,
- DebatePhase.CrossForAnswer,
- DebatePhase.ClosingFor,
- ]
+ DebatePhase.OpeningFor,
+ DebatePhase.CrossForQuestion,
+ DebatePhase.CrossForAnswer,
+ DebatePhase.ClosingFor,
+ ]
: [
- DebatePhase.OpeningAgainst,
- DebatePhase.CrossAgainstAnswer,
- DebatePhase.CrossAgainstQuestion,
- DebatePhase.ClosingAgainst,
- ];
+ DebatePhase.OpeningAgainst,
+ DebatePhase.CrossAgainstAnswer,
+ DebatePhase.CrossAgainstQuestion,
+ DebatePhase.ClosingAgainst,
+ ];
return rolePhases.reduce((acc, phase) => {
const storageKey = `${roomId}_${phase}_${role}`;
@@ -940,8 +944,8 @@ const OnlineDebateRoom = (): JSX.Element => {
const participants: UserDetails[] = Array.isArray(data)
? data
: Array.isArray(data?.participants)
- ? data.participants
- : [];
+ ? data.participants
+ : [];
const ownerIdFromServer: string | null = Array.isArray(data)
? null
: data?.ownerId ?? null;
@@ -1040,8 +1044,7 @@ const OnlineDebateRoom = (): JSX.Element => {
// If room not found (404), it might still be being created
if (response.status === 404 && retryCount < 5) {
console.warn(
- `Room not found, might still be creating. Retry ${
- retryCount + 1
+ `Room not found, might still be creating. Retry ${retryCount + 1
}/5 in 2 seconds...`
);
@@ -1111,7 +1114,7 @@ const OnlineDebateRoom = (): JSX.Element => {
const token = getAuthToken();
if (!token || !roomId) return;
- const wsUrl = `${WS_BASE_URL}/ws?room=${roomId}&token=${token}`;
+ const wsUrl = `${WS_BASE_URL}/ws?room=${roomId}&token=${token}`;
const rws = new ReconnectingWebSocket(wsUrl, [], {
connectionTimeout: 4000,
@@ -1144,6 +1147,9 @@ const OnlineDebateRoom = (): JSX.Element => {
case "ready":
if (data.ready !== undefined) setPeerReady(data.ready);
break;
+ case "personaSelection":
+ if (data.persona) setPeerPersonaRole(data.persona);
+ break;
case "phaseChange":
if (data.phase) {
console.debug(
@@ -1254,6 +1260,9 @@ const OnlineDebateRoom = (): JSX.Element => {
opponentParticipant.avatarUrl ||
"https://avatar.iran.liara.run/public/31",
});
+ if (opponentParticipant.persona) {
+ setPeerPersonaRole(opponentParticipant.persona);
+ }
} else {
setOpponentUser(null);
}
@@ -1311,7 +1320,7 @@ const OnlineDebateRoom = (): JSX.Element => {
for (const candidate of pending) {
try {
await spectatorPc.addIceCandidate(candidate);
- } catch (err) {}
+ } catch (err) { }
}
spectatorPendingCandidatesRef.current.delete(
data.connectionId
@@ -1998,6 +2007,12 @@ const OnlineDebateRoom = (): JSX.Element => {
wsRef.current?.send(message);
};
+ const handlePersonaRoleSelection = (persona: string) => {
+ setLocalPersonaRole(persona);
+ const message = JSON.stringify({ type: "personaSelection", persona });
+ wsRef.current?.send(message);
+ };
+
const toggleReady = () => {
const newReadyState = !localReady;
setLocalReady(newReadyState);
@@ -2057,9 +2072,8 @@ const OnlineDebateRoom = (): JSX.Element => {
.padStart(2, "0")}`;
return (
{timeStr}
@@ -2072,10 +2086,10 @@ const OnlineDebateRoom = (): JSX.Element => {
debatePhase !== DebatePhase.Setup &&
debatePhase !== DebatePhase.Finished &&
!isAutoMuted
- ? "Click start when you're ready to speak."
- : isAutoMuted
- ? "Auto-muted while the opponent is speaking."
- : "Waiting for your turn...";
+ ? "Click start when you're ready to speak."
+ : isAutoMuted
+ ? "Auto-muted while the opponent is speaking."
+ : "Waiting for your turn...";
const canStartSpeaking =
isMyTurn &&
@@ -2108,8 +2122,8 @@ const OnlineDebateRoom = (): JSX.Element => {
{debatePhase.includes("Question")
? "ask a question"
: debatePhase.includes("Answer")
- ? "answer"
- : "make a statement"}
+ ? "answer"
+ : "make a statement"}
{isAutoMuted && (
@@ -2189,9 +2203,8 @@ const OnlineDebateRoom = (): JSX.Element => {
className="w-20 h-20 rounded-full object-cover"
/>
{
@@ -2236,6 +2247,17 @@ const OnlineDebateRoom = (): JSX.Element => {
: "Against"
: "Not selected"}
+ {/* Persona Role Selection */}
+
{/* Opponent Avatar */}
@@ -2249,9 +2271,8 @@ const OnlineDebateRoom = (): JSX.Element => {
className="w-20 h-20 rounded-full object-cover"
/>
{
: "Against"
: "Not selected"}
+ {peerPersonaRole !== "neutral" && (
+
+ {peerPersonaRole}
+
+ )}
{/* Ready Button */}
@@ -2375,11 +2400,10 @@ const OnlineDebateRoom = (): JSX.Element => {
{/* Local User Section */}
@@ -2397,9 +2421,14 @@ const OnlineDebateRoom = (): JSX.Element => {
{localUser?.displayName || currentUser?.displayName || "You"}
-
- Role: {localRole || "Not selected"} | Rating:{" "}
- {localUser?.elo || currentUser?.rating || 1500}
+
+ Role: {localRole || "Not selected"}
+ {localPersonaRole !== "neutral" && (
+
+ {localPersonaRole}
+
+ )}
+ | Rating: {localUser?.elo || currentUser?.rating || 1500}
@@ -2440,11 +2469,10 @@ const OnlineDebateRoom = (): JSX.Element => {
{/* Remote User Section */}
@@ -2463,9 +2491,14 @@ const OnlineDebateRoom = (): JSX.Element => {
opponentUser?.username ||
"Opponent"}
-
- Role: {peerRole || "Not selected"} | Rating:{" "}
- {opponentUser?.elo || 1500}
+
+ Role: {peerRole || "Not selected"}
+ {peerPersonaRole !== "neutral" && (
+
+ {peerPersonaRole}
+
+ )}
+ | Rating: {opponentUser?.elo || 1500}
diff --git a/frontend/src/components/JudgementPopup.tsx b/frontend/src/components/JudgementPopup.tsx
index 1640dac..ebc9c02 100644
--- a/frontend/src/components/JudgementPopup.tsx
+++ b/frontend/src/components/JudgementPopup.tsx
@@ -113,6 +113,7 @@ const JudgmentPopup: React.FC
= ({
botName,
userStance,
botStance,
+ botDesc,
forRole,
againstRole,
localRole = null,
@@ -133,64 +134,64 @@ const JudgmentPopup: React.FC = ({
localStorage.getItem('opponentAvatar') ||
'https://avatar.iran.liara.run/public/31';
-const isUserBotFormat = 'user' in judgment.opening_statement;
-
-const defaultForName = forRole || 'For Debater';
-const defaultAgainstName = againstRole || 'Against Debater';
-const resolvedLocalName = localDisplayName || userName;
-const resolvedOpponentName = opponentDisplayName || 'Opponent';
-const derivedLocalAvatar = localAvatarUrl || localAvatar;
-const derivedOpponentAvatar = opponentAvatarUrl || opponentAvatar;
-
-const resolvedForName = isUserBotFormat
- ? defaultForName
- : localRole === 'for'
- ? resolvedLocalName
- : localRole === 'against'
- ? resolvedOpponentName
- : defaultForName;
-
-const resolvedAgainstName = isUserBotFormat
- ? defaultAgainstName
- : localRole === 'against'
- ? resolvedLocalName
- : localRole === 'for'
- ? resolvedOpponentName
- : defaultAgainstName;
-
-const player1Name = isUserBotFormat ? userName : resolvedForName;
-const player2Name = isUserBotFormat ? botName || 'Bot' : resolvedAgainstName;
-const player1Stance = isUserBotFormat ? userStance : 'For';
-const player2Stance = isUserBotFormat ? botStance : 'Against';
-
-const resolvedForAvatar = isUserBotFormat
- ? userAvatar
- : localRole === 'for'
- ? derivedLocalAvatar
- : localRole === 'against'
- ? derivedOpponentAvatar
- : derivedLocalAvatar || derivedOpponentAvatar;
-
-const resolvedAgainstAvatar = isUserBotFormat
- ? botAvatar
- : localRole === 'against'
- ? derivedLocalAvatar
- : localRole === 'for'
- ? derivedOpponentAvatar
- : derivedOpponentAvatar || derivedLocalAvatar;
-
-const player1Avatar = resolvedForAvatar || localAvatar;
-const player2Avatar = resolvedAgainstAvatar || opponentAvatar;
-const player2Desc = isUserBotFormat ? botDesc : resolvedAgainstName || 'Debater';
-
-const formatChange = (value: number) =>
- `${value >= 0 ? '+' : ''}${value.toFixed(2)}`;
-const formatRating = (value: number) => value.toFixed(2);
-
-const player1RatingSummary =
- !isUserBotFormat && ratingSummary ? ratingSummary.for : null;
-const player2RatingSummary =
- !isUserBotFormat && ratingSummary ? ratingSummary.against : null;
+ const isUserBotFormat = 'user' in judgment.opening_statement;
+
+ const defaultForName = forRole || 'For Debater';
+ const defaultAgainstName = againstRole || 'Against Debater';
+ const resolvedLocalName = localDisplayName || userName;
+ const resolvedOpponentName = opponentDisplayName || 'Opponent';
+ const derivedLocalAvatar = localAvatarUrl || localAvatar;
+ const derivedOpponentAvatar = opponentAvatarUrl || opponentAvatar;
+
+ const resolvedForName = isUserBotFormat
+ ? defaultForName
+ : localRole === 'for'
+ ? resolvedLocalName
+ : localRole === 'against'
+ ? resolvedOpponentName
+ : defaultForName;
+
+ const resolvedAgainstName = isUserBotFormat
+ ? defaultAgainstName
+ : localRole === 'against'
+ ? resolvedLocalName
+ : localRole === 'for'
+ ? resolvedOpponentName
+ : defaultAgainstName;
+
+ const player1Name = isUserBotFormat ? userName : resolvedForName;
+ const player2Name = isUserBotFormat ? botName || 'Bot' : resolvedAgainstName;
+ const player1Stance = isUserBotFormat ? userStance : 'For';
+ const player2Stance = isUserBotFormat ? botStance : 'Against';
+
+ const resolvedForAvatar = isUserBotFormat
+ ? userAvatar
+ : localRole === 'for'
+ ? derivedLocalAvatar
+ : localRole === 'against'
+ ? derivedOpponentAvatar
+ : derivedLocalAvatar || derivedOpponentAvatar;
+
+ const resolvedAgainstAvatar = isUserBotFormat
+ ? botAvatar
+ : localRole === 'against'
+ ? derivedLocalAvatar
+ : localRole === 'for'
+ ? derivedOpponentAvatar
+ : derivedOpponentAvatar || derivedLocalAvatar;
+
+ const player1Avatar = resolvedForAvatar || localAvatar;
+ const player2Avatar = resolvedAgainstAvatar || opponentAvatar;
+ const player2Desc = isUserBotFormat ? botDesc : resolvedAgainstName || 'Debater';
+
+ const formatChange = (value: number) =>
+ `${value >= 0 ? '+' : ''}${value.toFixed(2)}`;
+ const formatRating = (value: number) => value.toFixed(2);
+
+ const player1RatingSummary =
+ !isUserBotFormat && ratingSummary ? ratingSummary.for : null;
+ const player2RatingSummary =
+ !isUserBotFormat && ratingSummary ? ratingSummary.against : null;
const handleGoHome = () => {
navigate('/startdebate');
@@ -283,15 +284,15 @@ const player2RatingSummary =
cross_questions: isUserBotFormat
? getScoreAndReason('cross_examination', 'player1').reason.toLowerCase()
: getScoreAndReason(
- 'cross_examination_questions',
- 'player1'
- ).reason.toLowerCase(),
+ 'cross_examination_questions',
+ 'player1'
+ ).reason.toLowerCase(),
cross_answers: isUserBotFormat
? getScoreAndReason('answers', 'player1').reason.toLowerCase()
: getScoreAndReason(
- 'cross_examination_answers',
- 'player1'
- ).reason.toLowerCase(),
+ 'cross_examination_answers',
+ 'player1'
+ ).reason.toLowerCase(),
closing: getScoreAndReason('closing', 'player1').reason.toLowerCase(),
};
@@ -650,11 +651,10 @@ const player2RatingSummary =
= 0
- ? 'text-green-600'
- : 'text-red-600'
- }`}
+ className={`text-sm font-semibold ${player1RatingSummary.change >= 0
+ ? 'text-green-600'
+ : 'text-red-600'
+ }`}
>
Change: {formatChange(player1RatingSummary.change)}
@@ -670,11 +670,10 @@ const player2RatingSummary =
= 0
- ? 'text-green-600'
- : 'text-red-600'
- }`}
+ className={`text-sm font-semibold ${player2RatingSummary.change >= 0
+ ? 'text-green-600'
+ : 'text-red-600'
+ }`}
>
Change: {formatChange(player2RatingSummary.change)}
diff --git a/frontend/src/services/vsbot.ts b/frontend/src/services/vsbot.ts
index 0e48f82..bece269 100644
--- a/frontend/src/services/vsbot.ts
+++ b/frontend/src/services/vsbot.ts
@@ -21,6 +21,7 @@ export type DebateRequest = {
stance: string;
phaseTimings?: PhaseTiming[]; // For createDebate
context?: string; // Added optional context field
+ role?: string; // Added role field
};
export type DebateResponse = {
@@ -29,6 +30,7 @@ export type DebateResponse = {
botLevel: string;
topic: string;
stance: string;
+ role?: string;
phaseTimings?: PhaseTiming[]; // Included in response for consistency
};