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/cmd/server/main.go b/backend/cmd/server/main.go index a4a346e..f523b9d 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -132,6 +132,9 @@ func setupRouter(cfg *config.Config) *gin.Engine { // Set up transcript routes routes.SetupTranscriptRoutes(auth) + // Set up assumption extraction routes + routes.SetupAssumptionRoutes(auth) + auth.GET("/coach/strengthen-argument/weak-statement", routes.GetWeakStatement) auth.POST("/coach/strengthen-argument/evaluate", routes.EvaluateStrengthenedArgument) 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/assumption_controller.go b/backend/controllers/assumption_controller.go new file mode 100644 index 0000000..3d8fca2 --- /dev/null +++ b/backend/controllers/assumption_controller.go @@ -0,0 +1,49 @@ +package controllers + +import ( + "net/http" + + "arguehub/services" + + "github.com/gin-gonic/gin" +) + +// GetDebateAssumptions retrieves or generates assumptions for a debate +func GetDebateAssumptions(c *gin.Context) { + debateID := c.Param("debateId") + + if debateID == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "debateId is required", + }) + return + } + + // Extract assumptions (will use cache if available) + assumptions, err := services.ExtractAssumptions(debateID) + if err != nil { + // Determine appropriate error code + statusCode := http.StatusInternalServerError + errorMessage := err.Error() + + // Provide user-friendly messages for common errors + if err.Error() == "AI service not available - Gemini client not initialized" { + statusCode = http.StatusServiceUnavailable + errorMessage = "AI analysis service is currently unavailable. Please ensure the Gemini API is configured." + } else if err.Error() == "no transcript found for this debate" { + statusCode = http.StatusNotFound + errorMessage = "No debate transcript found for this debate ID." + } + + c.JSON(statusCode, gin.H{ + "error": errorMessage, + }) + return + } + + // Return assumptions (even if empty) + c.JSON(http.StatusOK, gin.H{ + "assumptions": assumptions, + "count": len(assumptions), + }) +} diff --git a/backend/controllers/debatevsbot_controller.go b/backend/controllers/debatevsbot_controller.go index 9571099..f72893d 100644 --- a/backend/controllers/debatevsbot_controller.go +++ b/backend/controllers/debatevsbot_controller.go @@ -34,7 +34,8 @@ type PhaseTiming struct { } type JudgeRequest struct { - History []models.Message `json:"history" binding:"required"` + DebateId string `json:"debateId" binding:"required"` + History []models.Message `json:"history" binding:"required"` } type DebateResponse struct { @@ -207,8 +208,10 @@ func JudgeDebate(c *gin.Context) { // Judge the debate result := services.JudgeDebate(req.History) - // Update debate outcome - if err := db.UpdateDebateVsBotOutcome(email, result); err != nil { + // Update debate outcome using the provided debateId + if err := db.UpdateDebateVsBotOutcome(req.DebateId, result); err != nil { + log.Printf("Warning: Failed to update debate outcome for %s: %v", req.DebateId, err) + // We don't return here because the judging itself succeeded } // Get the latest debate information to extract proper details diff --git a/backend/db/db.go b/backend/db/db.go index fb81431..efb868c 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -10,6 +10,7 @@ import ( "github.com/redis/go-redis/v9" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -69,14 +70,25 @@ func SaveDebateVsBot(debate models.DebateVsBot) error { return nil } -// UpdateDebateVsBotOutcome updates the outcome of the most recent bot debate for a user -func UpdateDebateVsBotOutcome(userId, outcome string) error { - filter := bson.M{"userId": userId} +// UpdateDebateVsBotOutcome updates the outcome of a specific bot debate by its ID +func UpdateDebateVsBotOutcome(debateId, outcome string) error { + objID, err := primitive.ObjectIDFromHex(debateId) + if err != nil { + return fmt.Errorf("invalid debate ID: %w", err) + } + + filter := bson.M{"_id": objID} update := bson.M{"$set": bson.M{"outcome": outcome}} - _, err := DebateVsBotCollection.UpdateOne(context.Background(), filter, update, nil) + + result, err := DebateVsBotCollection.UpdateOne(context.Background(), filter, update) if err != nil { - return err + return fmt.Errorf("failed to update debate outcome: %w", err) } + + if result.MatchedCount == 0 { + return fmt.Errorf("no debate found with ID: %s", debateId) + } + return nil } @@ -115,4 +127,4 @@ func ConnectRedis(addr, password string, db int) error { log.Println("Connected to Redis") return nil -} \ No newline at end of file +} diff --git a/backend/main b/backend/main index e93e6fa..045912c 100755 Binary files a/backend/main and b/backend/main differ diff --git a/backend/models/assumption.go b/backend/models/assumption.go new file mode 100644 index 0000000..4c3d40e --- /dev/null +++ b/backend/models/assumption.go @@ -0,0 +1,33 @@ +package models + +import ( + "encoding/json" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// DebateAssumption represents implicit assumptions extracted from a debate +type DebateAssumption struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + DebateID string `bson:"debateId" json:"debateId"` + ParticipantID string `bson:"participantId,omitempty" json:"participantId,omitempty"` + ParticipantEmail string `bson:"participantEmail,omitempty" json:"participantEmail,omitempty"` + Side string `bson:"side" json:"side"` // "for" or "against" + Assumptions []string `bson:"assumptions" json:"assumptions"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` +} + +// MarshalJSON customizes the JSON serialization to convert ObjectID to string +func (d DebateAssumption) MarshalJSON() ([]byte, error) { + type Alias DebateAssumption + a := Alias(d) + a.ID = primitive.NilObjectID + return json.Marshal(&struct { + ID string `json:"id"` + Alias + }{ + ID: d.ID.Hex(), + Alias: a, + }) +} diff --git a/backend/routes/assumption_routes.go b/backend/routes/assumption_routes.go new file mode 100644 index 0000000..b189ed5 --- /dev/null +++ b/backend/routes/assumption_routes.go @@ -0,0 +1,13 @@ +package routes + +import ( + "arguehub/controllers" + + "github.com/gin-gonic/gin" +) + +// SetupAssumptionRoutes sets up routes for debate assumption extraction +func SetupAssumptionRoutes(router *gin.RouterGroup) { + // Get assumptions for a specific debate + router.GET("/debates/:debateId/assumptions", controllers.GetDebateAssumptions) +} diff --git a/backend/services/assumption_service.go b/backend/services/assumption_service.go new file mode 100644 index 0000000..1fa8733 --- /dev/null +++ b/backend/services/assumption_service.go @@ -0,0 +1,302 @@ +package services + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "time" + + "arguehub/db" + "arguehub/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// AssumptionResponse represents the AI's response structure for assumptions +type AssumptionResponse struct { + Side string `json:"side"` + ParticipantEmail string `json:"participantEmail"` + ParticipantID string `json:"participantId,omitempty"` + Assumptions []string `json:"assumptions"` +} + +// ExtractAssumptions extracts implicit assumptions from a debate transcript using AI +func ExtractAssumptions(roomID string) ([]models.DebateAssumption, error) { + ctx := context.Background() + + // Check if Gemini client is available + if geminiClient == nil { + return nil, errors.New("AI service not available - Gemini client not initialized") + } + + // Check if assumptions already exist for this debate + collection := db.GetCollection("debateAssumptions") + var existingAssumptions []models.DebateAssumption + cursor, err := collection.Find(ctx, bson.M{"debateId": roomID}) + if err == nil { + defer cursor.Close(ctx) + if err := cursor.All(ctx, &existingAssumptions); err == nil && len(existingAssumptions) > 0 { + log.Printf("✅ Returning cached assumptions for debate %s", roomID) + return existingAssumptions, nil + } + } + + // Aggregate debate transcript + transcript, err := aggregateDebateTranscript(ctx, roomID) + if err != nil { + return nil, fmt.Errorf("failed to aggregate transcript: %w", err) + } + + if transcript == "" { + return nil, errors.New("no transcript found for this debate") + } + + // Construct AI prompt for assumption extraction + prompt := buildAssumptionPrompt(transcript) + + // Call Gemini API + log.Printf("🤖 Extracting assumptions for debate %s using Gemini AI", roomID) + response, err := generateDefaultModelText(ctx, prompt) + if err != nil { + return nil, fmt.Errorf("AI extraction failed: %w", err) + } + + // Parse AI response + assumptions, err := parseAssumptionResponse(response, roomID) + if err != nil { + return nil, fmt.Errorf("failed to parse AI response: %w", err) + } + + // Save to database + if len(assumptions) > 0 { + err = saveAssumptions(ctx, collection, assumptions) + if err != nil { + log.Printf("⚠️ Warning: Failed to save assumptions to database: %v", err) + // Don't fail the request, just log the warning + } + } + + log.Printf("✅ Successfully extracted %d assumption groups for debate %s", len(assumptions), roomID) + return assumptions, nil +} + +// aggregateDebateTranscript retrieves and combines all messages from a debate +func aggregateDebateTranscript(ctx context.Context, debateID string) (string, error) { + log.Printf("📋 Attempting to aggregate transcript for ID: %s", debateID) + + // Try to convert debateID to ObjectID - it might be a SavedDebateTranscript ID + savedCollection := db.GetCollection("savedDebateTranscripts") + objID, err := primitive.ObjectIDFromHex(debateID) + + if err == nil { + log.Printf("🔍 Valid ObjectID detected, searching SavedDebateTranscript by ID") + // It's a valid ObjectID, try to find SavedDebateTranscript by ID + var savedTranscript models.SavedDebateTranscript + err := savedCollection.FindOne(ctx, bson.M{"_id": objID}).Decode(&savedTranscript) + if err == nil { + log.Printf("✅ Found SavedDebateTranscript - Messages: %d, Transcripts: %d", + len(savedTranscript.Messages), len(savedTranscript.Transcripts)) + + // Check if it has transcript sections (user vs user debates) + if len(savedTranscript.Transcripts) > 0 { + log.Printf("✅ Using Transcripts field (user vs user format)") + return buildTranscriptFromTranscriptMap(savedTranscript.Transcripts), nil + } + + // Check if it has messages (bot debates or other formats) + if len(savedTranscript.Messages) > 0 { + log.Printf("✅ Using Messages field") + return buildTranscriptFromMessages(savedTranscript.Messages), nil + } + + // If SavedDebateTranscript exists but has no content, create a fallback + log.Printf("⚠️ SavedDebateTranscript found but has no content - using metadata as fallback") + fallbackTranscript := fmt.Sprintf(` +=== DEBATE SUMMARY === + +Topic: %s +Debate Type: %s +Opponent: %s +Result: %s + +[This debate record exists but detailed transcript content is not available. +The system will analyze based on the available metadata.] +`, savedTranscript.Topic, savedTranscript.DebateType, savedTranscript.Opponent, savedTranscript.Result) + + return fallbackTranscript, nil + } else { + log.Printf("⚠️ SavedDebateTranscript not found by ID: %v", err) + } + } else { + log.Printf("🔍 Not a valid ObjectID, will try as roomID") + } + + // Not an ObjectID or not found - try as roomID in DebateTranscript collection + log.Printf("🔍 Searching DebateTranscript collection by roomId") + transcriptCollection := db.GetCollection("debateTranscripts") + cursor, err := transcriptCollection.Find(ctx, bson.M{"roomId": debateID}) + if err != nil && err != mongo.ErrNoDocuments { + return "", err + } + + var transcripts []models.DebateTranscript + if cursor != nil { + defer cursor.Close(ctx) + if err := cursor.All(ctx, &transcripts); err == nil && len(transcripts) > 0 { + log.Printf("✅ Found %d DebateTranscript records by roomId", len(transcripts)) + return buildTranscriptFromDebateTranscripts(transcripts), nil + } + } + + log.Printf("❌ No transcript found for ID: %s", debateID) + return "", errors.New("no transcript found") +} + +// buildTranscriptFromDebateTranscripts builds a formatted transcript from DebateTranscript records +func buildTranscriptFromDebateTranscripts(transcripts []models.DebateTranscript) string { + var builder strings.Builder + + sections := []string{"opening", "constructive argument", "rebuttal", "closing"} + + for _, section := range sections { + builder.WriteString(fmt.Sprintf("\n=== %s ===\n", strings.ToUpper(section))) + + for _, t := range transcripts { + if content, ok := t.Transcripts[section]; ok && content != "" { + builder.WriteString(fmt.Sprintf("\n[%s - %s]:\n%s\n", t.Role, t.Email, content)) + } + } + } + + return builder.String() +} + +// buildTranscriptFromTranscriptMap builds a formatted transcript from a transcript map (SavedDebateTranscript.Transcripts) +func buildTranscriptFromTranscriptMap(transcripts map[string]string) string { + var builder strings.Builder + + sections := []string{"opening", "constructive argument", "rebuttal", "closing"} + + for _, section := range sections { + if content, ok := transcripts[section]; ok && content != "" { + builder.WriteString(fmt.Sprintf("\n=== %s ===\n", strings.ToUpper(section))) + builder.WriteString(fmt.Sprintf("%s\n", content)) + } + } + + return builder.String() +} + +// buildTranscriptFromMessages builds a formatted transcript from Message records +func buildTranscriptFromMessages(messages []models.Message) string { + var builder strings.Builder + + builder.WriteString("\n=== DEBATE TRANSCRIPT ===\n") + + for _, msg := range messages { + // Skip system messages or judge messages + if msg.Sender == "system" || msg.Sender == "judge" || msg.Sender == "Judge" { + continue + } + + builder.WriteString(fmt.Sprintf("\n[%s]:\n%s\n", msg.Sender, msg.Text)) + } + + return builder.String() +} + +// buildAssumptionPrompt creates the AI prompt for extracting assumptions +func buildAssumptionPrompt(transcript string) string { + return fmt.Sprintf(`You are an expert debate analyst. Analyze the following debate transcript and identify implicit assumptions made by each participant. + +Implicit assumptions are unstated premises such as: +- Value judgments (e.g., "freedom is more important than security") +- Causal beliefs (e.g., "this policy will lead to that outcome") +- Generalizations (e.g., "people always behave this way") +- Underlying worldviews or ideological positions + +For each participant, list their assumptions clearly and neutrally without evaluating their truth or validity. + +IMPORTANT: Return your response as a valid JSON array with this exact structure: +[ + { + "side": "for", + "participantEmail": "user@example.com", + "assumptions": ["assumption 1", "assumption 2", "assumption 3"] + }, + { + "side": "against", + "participantEmail": "opponent@example.com", + "assumptions": ["assumption 1", "assumption 2"] + } +] + +Use "for" and "against" as the side values. List 3-5 key assumptions per participant if possible. + +Debate Transcript: +%s`, transcript) +} + +// parseAssumptionResponse parses the AI response into structured data +func parseAssumptionResponse(response string, roomID string) ([]models.DebateAssumption, error) { + // Clean the response + response = cleanModelOutput(response) + + // Parse JSON + var aiResponses []AssumptionResponse + err := json.Unmarshal([]byte(response), &aiResponses) + if err != nil { + log.Printf("⚠️ Failed to parse AI response as JSON: %v", err) + log.Printf("Raw response: %s", response) + return nil, fmt.Errorf("invalid AI response format: %w", err) + } + + // Convert to DebateAssumption models + var assumptions []models.DebateAssumption + now := time.Now() + + for _, aiResp := range aiResponses { + if len(aiResp.Assumptions) == 0 { + continue // Skip empty assumption lists + } + + assumption := models.DebateAssumption{ + ID: primitive.NewObjectID(), + DebateID: roomID, + ParticipantID: aiResp.ParticipantID, + ParticipantEmail: aiResp.ParticipantEmail, + Side: aiResp.Side, + Assumptions: aiResp.Assumptions, + CreatedAt: now, + } + assumptions = append(assumptions, assumption) + } + + return assumptions, nil +} + +// saveAssumptions saves extracted assumptions to the database +func saveAssumptions(ctx context.Context, collection *mongo.Collection, assumptions []models.DebateAssumption) error { + if len(assumptions) == 0 { + return nil + } + + // Convert to interface slice for bulk insert + docs := make([]interface{}, len(assumptions)) + for i, a := range assumptions { + docs[i] = a + } + + _, err := collection.InsertMany(ctx, docs) + if err != nil { + return fmt.Errorf("failed to insert assumptions: %w", err) + } + + log.Printf("💾 Saved %d assumption groups to database", len(assumptions)) + return nil +} 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/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/components/Assumptions.tsx b/frontend/src/components/Assumptions.tsx new file mode 100644 index 0000000..fbf9441 --- /dev/null +++ b/frontend/src/components/Assumptions.tsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Brain, Lightbulb, AlertCircle, Loader2 } from 'lucide-react'; +import { + assumptionService, + DebateAssumption, +} from '@/services/assumptionService'; + +interface AssumptionsProps { + debateId: string; + className?: string; +} + +const Assumptions: React.FC = ({ debateId, className }) => { + const [assumptions, setAssumptions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchAssumptions(); + }, [debateId]); + + const fetchAssumptions = async () => { + try { + setLoading(true); + setError(null); + const data = await assumptionService.getDebateAssumptions(debateId); + setAssumptions(data.assumptions || []); + } catch (err) { + setError( + err instanceof Error + ? err.message + : 'Failed to fetch assumptions' + ); + } finally { + setLoading(false); + } + }; + + // Group assumptions by side + const forSideAssumptions = assumptions.find((a) => a.side === 'for'); + const againstSideAssumptions = assumptions.find((a) => a.side === 'against'); + + if (loading) { + return ( + + + + + Implicit Assumptions + + + +
+
+ +

+ Analyzing debate for implicit assumptions... +

+
+
+
+
+ ); + } + + if (error) { + return ( + + + + + Implicit Assumptions + + + + + + + {error} + + + + + + ); + } + + return ( + + +
+ + + Implicit Assumptions + + + + AI-Generated Insights + +
+
+ + {assumptions.length === 0 ? ( +
+ +

+ No assumptions identified +

+

+ The AI analysis did not identify any clear implicit assumptions in + this debate. +

+
+ ) : ( +
+ {/* For Side Assumptions */} + {forSideAssumptions && forSideAssumptions.assumptions.length > 0 && ( +
+
+ + For Side + + {forSideAssumptions.participantEmail && ( + + {forSideAssumptions.participantEmail} + + )} +
+
    + {forSideAssumptions.assumptions.map((assumption, index) => ( +
  • + + {assumption} +
  • + ))} +
+
+ )} + + {/* Against Side Assumptions */} + {againstSideAssumptions && + againstSideAssumptions.assumptions.length > 0 && ( +
+
+ + Against Side + + {againstSideAssumptions.participantEmail && ( + + {againstSideAssumptions.participantEmail} + + )} +
+
    + {againstSideAssumptions.assumptions.map( + (assumption, index) => ( +
  • + + + {assumption} + +
  • + ) + )} +
+
+ )} + + {/* Info message */} + + + + These assumptions are identified by AI and represent unstated + premises that may underlie the arguments. They are not + judgments on the validity of the arguments. + + +
+ )} +
+
+ ); +}; + +export default Assumptions; diff --git a/frontend/src/components/SavedTranscripts.tsx b/frontend/src/components/SavedTranscripts.tsx index 41bcd6f..4b9d52e 100644 --- a/frontend/src/components/SavedTranscripts.tsx +++ b/frontend/src/components/SavedTranscripts.tsx @@ -29,6 +29,7 @@ import { } from '@/services/transcriptService'; import { format } from 'date-fns'; import CommentTree from './CommentTree'; +import Assumptions from './Assumptions'; import { Share2 } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; @@ -388,18 +389,16 @@ const SavedTranscripts: React.FC = ({ className }) => { {selectedTranscript.messages.map((message, index) => (
@@ -470,6 +469,13 @@ const SavedTranscripts: React.FC = ({ className }) => { + {/* Assumptions Section */} +
+ +
+ + +
{ + const token = getAuthToken(); + if (!token) { + throw new Error('Authentication token not found'); + } + + const response = await fetch( + `${API_BASE_URL}/debates/${debateId}/assumptions`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + let errorMessage = 'Failed to fetch assumptions'; + try { + const errorData = await response.json(); + errorMessage = errorData.error || errorMessage; + } catch { + errorMessage = response.statusText || errorMessage; + } + throw new Error(errorMessage); + } + + return response.json(); + }, +};