Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 57 additions & 13 deletions backend/controllers/debatevsbot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package controllers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"

Expand Down Expand Up @@ -119,7 +121,8 @@ func CreateDebate(c *gin.Context) {
c.JSON(200, response)
}

func SendDebateMessage(c *gin.Context) {

func SendDebateMessageStream(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "Authorization token required"})
Expand All @@ -139,10 +142,50 @@ 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)
// Set headers for Server-Sent Events
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Transfer-Encoding", "chunked")
c.Writer.Header().Set("X-Accel-Buffering", "no")
c.Writer.Flush()

flusher, _ := c.Writer.(http.Flusher)

// Update debate history with the bot's response.
sendEvent := func(eventType string, data interface{}) {
payload, err := json.Marshal(data)
if err != nil {
return
}
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", eventType, payload)
if flusher != nil {
flusher.Flush()
}
}

// Stream bot response in real-time
botResponse, err := services.StreamBotResponse(
c.Request.Context(),
req.BotName,
req.BotLevel,
req.Topic,
req.History,
req.Stance,
req.Context,
150,
func(chunk string) error {
sendEvent("chunk", gin.H{"text": chunk})
return nil
},
)

if err != nil {
log.Printf("Stream error: %v", err)
sendEvent("error", gin.H{"error": err.Error()})
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Update debate history with the bot's response
updatedHistory := append(req.History, models.Message{
Sender: "Bot",
Text: botResponse,
Expand All @@ -164,17 +207,18 @@ func SendDebateMessage(c *gin.Context) {
debate.ID = primitive.NewObjectID()
}
if err := db.SaveDebateVsBot(debate); err != nil {
log.Printf("Error saving debate vs bot: %v", err)
}

response := DebateMessageResponse{
DebateId: debate.ID.Hex(),
BotName: req.BotName,
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
Response: botResponse,
}
c.JSON(200, response)
// Send completion event
sendEvent("done", gin.H{
"debateId": debate.ID.Hex(),
"botName": req.BotName,
"botLevel": req.BotLevel,
"topic": req.Topic,
"stance": req.Stance,
"response": botResponse,
})
}

func JudgeDebate(c *gin.Context) {
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/debatevsbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func SetupDebateVsBotRoutes(router *gin.RouterGroup) {
vsbot := router.Group("/vsbot")
{
vsbot.POST("/create", controllers.CreateDebate)
vsbot.POST("/debate", controllers.SendDebateMessage)
vsbot.POST("/debate", controllers.SendDebateMessageStream)
vsbot.POST("/judge", controllers.JudgeDebate)
vsbot.POST("/concede", controllers.ConcedeDebate)
}
Expand Down
42 changes: 27 additions & 15 deletions backend/services/debatevsbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ Your debating style must strictly adhere to the following guidelines:
Your stance is: %s.
%s
%s
%s
Provide an opening statement that embodies your persona and stance.
[Your opening argument]
%s %s`,
Expand Down Expand Up @@ -265,32 +264,45 @@ 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 {

// StreamBotResponse streams the bot's response chunks through onChunk callback in real-time.
// It returns the full accumulated response.
func StreamBotResponse(ctx context.Context, botName, botLevel, topic string, history []models.Message, stance, extraContext string, maxWords int, onChunk func(string) error) (string, error) {
if geminiClient == nil {
return personalityErrorResponse(botName, "My systems are offline, it seems.")
errResp := personalityErrorResponse(botName, "My systems are offline, it seems.")
_ = onChunk(errResp)
return errResp, nil
}

bot := GetBotPersonality(botName)
// Construct prompt with enhanced personality integration
prompt := constructPrompt(bot, topic, history, stance, extraContext, maxWords)

ctx := context.Background()
response, err := generateDefaultModelText(ctx, prompt)
var fullResponse strings.Builder
err := generateDefaultModelStream(ctx, prompt, func(chunk string) error {
fullResponse.WriteString(chunk)
return onChunk(chunk)
})

if err != nil {
log.Printf("❌ Gemini error in GenerateBotResponse: %v", err)
return personalityErrorResponse(botName, "A glitch in my logic, there is.")
}
if response == "" {
return personalityErrorResponse(botName, "Lost in translation, my thoughts are.")
if fullResponse.Len() == 0 {
errResp := personalityErrorResponse(botName, "A glitch in my logic, there is.")
_ = onChunk(errResp)
return errResp, nil
}
return fullResponse.String(), err
}
if strings.Contains(strings.ToLower(response), "clarify") {
return personalityClarificationRequest(botName)

cleaned := cleanModelOutput(fullResponse.String())
if cleaned == "" {
errResp := personalityErrorResponse(botName, "A glitch in my logic, there is.")
_ = onChunk(errResp)
return errResp, nil
}
return response
return cleaned, nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}


// personalityErrorResponse returns a personality-specific error message
func personalityErrorResponse(botName, defaultMsg string) string {
// Dynamically construct error message using bot personality
Expand Down
29 changes: 29 additions & 0 deletions backend/services/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,32 @@ func cleanModelOutput(text string) string {
func generateDefaultModelText(ctx context.Context, prompt string) (string, error) {
return generateModelText(ctx, defaultGeminiModel, prompt)
}

func generateDefaultModelStream(ctx context.Context, prompt string, onChunk func(string) error) error {
if geminiClient == nil {
return errors.New("gemini client not initialized")
}

config := &genai.GenerateContentConfig{
SafetySettings: []*genai.SafetySetting{
{Category: genai.HarmCategoryHarassment, Threshold: genai.HarmBlockThresholdBlockNone},
{Category: genai.HarmCategoryHateSpeech, Threshold: genai.HarmBlockThresholdBlockNone},
{Category: genai.HarmCategorySexuallyExplicit, Threshold: genai.HarmBlockThresholdBlockNone},
{Category: genai.HarmCategoryDangerousContent, Threshold: genai.HarmBlockThresholdBlockNone},
},
}

for resp, err := range geminiClient.Models.GenerateContentStream(ctx, defaultGeminiModel, genai.Text(prompt), config) {
if err != nil {
return err
}
chunkText := resp.Text()
if chunkText != "" {
if err := onChunk(chunkText); err != nil {
return err
}
}
}
return nil
}

76 changes: 62 additions & 14 deletions frontend/src/Pages/DebateRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "../components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { sendDebateMessage, judgeDebate, concedeDebate } from "@/services/vsbot";
import { sendDebateMessageStream, judgeDebate, concedeDebate } from "@/services/vsbot";
import JudgmentPopup from "@/components/JudgementPopup";
import { Mic, MicOff } from "lucide-react";
import { useAtom } from "jotai";
Expand Down Expand Up @@ -250,9 +250,12 @@ const DebateRoom: React.FC = () => {
const [judgmentData, setJudgmentData] = useState<JudgmentData | null>(null);
const [isRecognizing, setIsRecognizing] = useState(false);
const [nextTurnPending, setNextTurnPending] = useState(false);
const [streamingBotText, setStreamingBotText] = useState("");
const [isBotThinking, setIsBotThinking] = useState(false);
const [isBotStreaming, setIsBotStreaming] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const botTurnRef = useRef(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const botMessagesEndRef = useRef<HTMLDivElement>(null);
const recognitionRef = useRef<SpeechRecognition | null>(null);

const bot = allBots.find((b) => b.name === debateData.botName) || allBots[0];
Expand Down Expand Up @@ -417,8 +420,8 @@ const DebateRoom: React.FC = () => {
]);

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [state.messages]);
botMessagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [state.messages, streamingBotText, isBotThinking]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const getPhaseInstructions = (phaseIndex: number) => {
switch (phaseIndex) {
Expand Down Expand Up @@ -480,6 +483,7 @@ const DebateRoom: React.FC = () => {
};

const handleNextTurn = () => {
if (isBotThinking || isBotStreaming) return;
setState((prev) => {
advanceTurn(prev);
return prev;
Expand Down Expand Up @@ -526,14 +530,28 @@ const DebateRoom: React.FC = () => {
: "Provide your answer";
}

const { response } = await sendDebateMessage({
botLevel: debateData.botLevel,
topic: debateData.topic,
history: state.messages,
botName: debateData.botName,
stance: state.botStance,
context,
});
setIsBotThinking(true);
setIsBotStreaming(true);
setStreamingBotText("");

const { response } = await sendDebateMessageStream(
{
botLevel: debateData.botLevel,
topic: debateData.topic,
history: state.messages,
botName: debateData.botName,
stance: state.botStance,
context,
},
(_chunk, accumulated) => {
setIsBotThinking(false);
setStreamingBotText(accumulated);
}
);

setIsBotThinking(false);
setIsBotStreaming(false);
setStreamingBotText("");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const botMessage: Message = {
sender: "Bot",
Expand All @@ -554,6 +572,10 @@ const DebateRoom: React.FC = () => {
});
} catch (error) {
console.error("Bot error:", error);
setIsBotThinking(false);
setIsBotStreaming(false);
setStreamingBotText("");

// Even on error, advance turn to prevent getting stuck
setState((prev) => {
const errorMessage: Message = {
Expand Down Expand Up @@ -676,7 +698,32 @@ setPopup({ show: false, message: "" });
{msg.text}
</div>
))}
<div ref={messagesEndRef} />
{sender === "Bot" && (isBotThinking || isBotStreaming) && (
<div className="p-3 bg-muted rounded-lg shadow-sm text-foreground break-words border border-primary/30 transition-all duration-200">
<span className="text-xs text-muted-foreground block mb-1">
{phases[state.currentPhase]?.name || "In Progress"}
</span>
{isBotThinking && (
<div className="flex items-center gap-2 py-1">
<div className="flex items-center space-x-1.5">
<span className="w-2 h-2 bg-primary rounded-full animate-bounce [animation-delay:-0.3s]"></span>
<span className="w-2 h-2 bg-primary rounded-full animate-bounce [animation-delay:-0.15s]"></span>
<span className="w-2 h-2 bg-primary rounded-full animate-bounce"></span>
</div>
<span className="text-xs text-muted-foreground italic ml-1.5">
{debateData.botName} is typing...
</span>
</div>
)}
{isBotStreaming && streamingBotText && (
<div className="whitespace-pre-wrap leading-relaxed">
<span>{streamingBotText}</span>
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse align-middle" />
</div>
)}
</div>
)}
{sender === "Bot" && <div ref={botMessagesEndRef} />}
</div>
);
};
Expand Down Expand Up @@ -778,9 +825,10 @@ setPopup({ show: false, message: "" });
{bot.rating ? `Rating: ${bot.rating}` : "Ready to argue!"}
</div>
</div>
{nextTurnPending && (
{nextTurnPending && !isBotThinking && !isBotStreaming && (
<Button
onClick={handleNextTurn}
disabled={isBotThinking || isBotStreaming}
className="ml-auto bg-primary hover:bg-primary/90 text-primary-foreground rounded-md px-3 text-sm"
>
Next Turn
Expand Down
Loading