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