-
Notifications
You must be signed in to change notification settings - Fork 160
Feat/add debate personality profiles for human and ai participants #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/add debate personality profiles for human and ai participants #188
Conversation
📝 WalkthroughWalkthroughThis PR adds personality analysis capability for debate transcripts, introducing backend services and frontend components to generate and display personality profiles from participant arguments. It also enriches transcript metadata with email and role information, removes sensitive configuration examples, and centralizes password validation in authentication forms. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Frontend Client
participant Service as Personality Service
participant Mongo as MongoDB
participant GPT as GPT-4 API
Client->>Service: GeneratePersonalityProfiles(debateID)
Service->>Mongo: Load SavedDebateTranscript by debateID
Mongo-->>Service: Debate transcript data
Service->>Service: Validate debate status & aggregate<br/>transcripts by participant
loop For each participant
Service->>GPT: Analyze participant arguments<br/>(structured JSON request)
GPT-->>Service: Personality profile JSON<br/>(tone, style, evidence, etc.)
Service->>Service: Parse & enrich profile<br/>(add DebateID, ParticipantID, CreatedAt)
Service->>Mongo: Save PersonalityProfile
Mongo-->>Service: Confirm saved
end
Service-->>Client: Return PersonalityProfile[] array
Client->>Client: Display personality analysis UI
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes The changes introduce substantial new functionality (personality analysis service with GPT integration and complex transcript aggregation logic), propagate structural changes across multiple backend layers (model, service, controller), and integrate the feature across diverse frontend components with state management and API handling. The heterogeneous nature—spanning new service logic, data model changes, and multi-component frontend integration—combined with logic density in transcript processing and error handling requires careful review across multiple areas. Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @backend/controllers/auth.go:
- Line 191: Remove the insecure logging of verification codes: delete the
log.Printf("DEMO MODE: Verification code for %s is %s", request.Email,
verificationCode) statement (or ensure it is only ever executed in a hermetic
test environment behind a strict dev-only feature flag), so that neither
request.Email nor verificationCode are written to logs; if demo support is
required, route codes to a mock email service or a secure, ephemeral test store
accessible only via authenticated admin tooling instead.
In @backend/services/personality_service.go:
- Around line 56-70: The participant-mapping for user_vs_user in the block
iterating debate.Transcripts is incorrect: instead of defaulting to "Opponent"
and guessing by phase substring, use explicit participant identifiers from the
transcript data or the debate object; update the logic in the function handling
SavedDebateTranscript (the loop over debate.Transcripts and the participants
map) to check each transcript entry for a Role, Email, or UserID field (e.g.,
Transcript.Role or Transcript.Email) and assign text to "User" when it matches
debate.Email (or the current user's identifier) and to "Opponent" otherwise; if
the SavedDebateTranscript model currently only contains phase keys, first extend
the model to include participant metadata for each phase (or maintain a separate
phase->participant mapping derived from debate setup) and then use that metadata
here to correctly populate participants["User"] and participants["Opponent"].
- Around line 90-96: The participant mapping sets profile.ParticipantID from
message sender but only handles "User" and "Bot"/"Opponent", leaving "Judge" and
other allowed Message sender values unmapped; update the mapping in the
personality analysis code that assigns profile.DebateID and
profile.ParticipantID so it either (a) explicitly handles "Judge" (e.g., map
"Judge" to a canonical debate participant value or debate.Judge/other field if
your domain model has one) or (b) filters out non-debate-participant senders
(like "Judge") before creating profiles/analysis; modify the block that assigns
profile.ParticipantID (where name is checked against "User" and "Bot") to cover
"Judge" and any other allowed sender values consistently.
In @backend/structs/auth.go:
- Line 5: The Password field's validation tag was loosened to min=6 on the auth
struct (Password), which weakens backend password policy; revert or strengthen
it by updating the binding tag on the Password field(s) to a more secure rule
(e.g., min=8 or a regex requiring mixed case/digits/symbols) and apply the same
change to the other Password validation tags present in this diff; if you need
shorter minimums for UX, instead enforce additional entropy rules (regex) or
keep min-length higher and implement compensating controls (rate
limiting/account lockouts) elsewhere.
🧹 Nitpick comments (9)
frontend/src/Pages/ViewDebate.tsx (1)
80-90: Redundant error handling pattern.The
.catch(() => [])on line 83 swallows errors and returns an empty array, so the outertry-catchblock (lines 85-87) will never execute. Either remove the inner.catch()to let the outer block handle errors, or remove the outertry-catchsince the inner handler already provides a fallback.♻️ Suggested simplification
const fetchPersonalityProfiles = async (id: string) => { - try { - setLoadingPersonality(true); - const profiles = await transcriptService.getPersonalityProfiles(id).catch(() => []); - setPersonalityProfiles(profiles); - } catch (err) { - console.error("Error fetching personality profiles:", err); - } finally { - setLoadingPersonality(false); - } + setLoadingPersonality(true); + try { + const profiles = await transcriptService.getPersonalityProfiles(id); + setPersonalityProfiles(profiles); + } catch (err) { + console.error("Error fetching personality profiles:", err); + setPersonalityProfiles([]); + } finally { + setLoadingPersonality(false); + } };backend/controllers/personality_controller.go (1)
29-34: Consider sanitizing error messages returned to clients.Line 32 returns
err.Error()directly to the client. Depending on the error source (e.g., database connection issues, AI service failures), this could expose internal implementation details. Consider returning a generic error message while logging the detailed error server-side.♻️ Suggested improvement
// Generate new profiles profiles, err := services.GeneratePersonalityProfiles(debateID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + // Log detailed error for debugging + log.Printf("Failed to generate personality profiles for debate %s: %v", debateID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate personality profiles"}) return }frontend/src/components/JudgementPopup.tsx (1)
145-159: Cascading GET→POST logic with redundant error handling.The fetch pattern (GET first, then POST if empty) is sound for auto-generating profiles when none exist. However, the error handling has the same issue as in
ViewDebate.tsx: the inner.catch(() => [])handlers prevent the outertry-catchfrom ever triggering.Additionally, this fetch logic is duplicated across
JudgementPopup.tsxandSavedTranscripts.tsx. Consider extracting this into a shared utility or hook.♻️ Suggested refactor
const fetchPersonalityProfiles = async (id: string) => { + setLoadingPersonality(true); try { - setLoadingPersonality(true); - // Try to GET first, then POST if none found - let profiles = await transcriptService.getPersonalityProfiles(id).catch(() => []); + let profiles = await transcriptService.getPersonalityProfiles(id); if (profiles.length === 0) { - profiles = await transcriptService.generatePersonalityProfiles(id).catch(() => []); + profiles = await transcriptService.generatePersonalityProfiles(id); } setPersonalityProfiles(profiles); } catch (err) { console.error('Error fetching personality profiles:', err); + setPersonalityProfiles([]); } finally { setLoadingPersonality(false); } };frontend/src/components/SavedTranscripts.tsx (1)
102-120: Consistent fetch pattern but duplicated across components.This implementation properly propagates errors to the outer
try-catchblock (unlike the similar code inViewDebate.tsxandJudgementPopup.tsx). The GET→POST cascade is appropriate for auto-generating profiles on view.Consider extracting this shared logic into a custom hook (e.g.,
usePersonalityProfiles) to reduce duplication and ensure consistent behavior acrossViewDebate,JudgementPopup, andSavedTranscripts.frontend/src/components/PersonalityProfile.tsx (1)
13-25: Type definition could be aligned with service interface.The
PersonalityTraitinterface is similar to but not identical toPersonalityProfileDatafromtranscriptService.ts(which includesidanddebateIdfields). Since this component receives data from the service, consider reusingPersonalityProfileDatadirectly or usingPick/Omitto derive the type, ensuring type consistency.♻️ Suggested alignment
+import { PersonalityProfileData } from '@/services/transcriptService'; + -interface PersonalityTrait { - participantId: string; - argument_style: string; - tone: string; - evidence_usage: number; - clarity: number; - responsiveness: number; - summary: string; -} - interface PersonalityProfileProps { - profiles: PersonalityTrait[]; + profiles: PersonalityProfileData[]; }frontend/src/services/transcriptService.ts (1)
23-33: Consider using camelCase for consistency.The field names in
PersonalityProfileDatause snake_case (argument_style,evidence_usage,clarity,responsiveness), which is inconsistent with TypeScript conventions and the existing interfaces in this file (e.g.,debateType,createdAt,updatedAtinSavedDebateTranscript).♻️ Proposed refactor to align with TypeScript conventions
export interface PersonalityProfileData { id: string; debateId: string; participantId: string; - argument_style: string; + argumentStyle: string; tone: string; - evidence_usage: number; + evidenceUsage: number; clarity: number; responsiveness: number; summary: string; }Note: This change would require corresponding updates in the backend response serialization or a mapping layer in the frontend.
backend/services/personality_service.go (3)
85-88: Silent error handling may hide analysis failures.When
analyzeParticipantPersonalityfails for a participant, the error is silently skipped withcontinue. This means:
- Partial results may be returned without indication that some participants were not analyzed
- Debugging failures becomes difficult as errors are not logged or surfaced
- Callers cannot distinguish between "no participants found" and "analysis failed for all participants"
♻️ Proposed improvement
Consider at minimum logging the error:
profile, err := analyzeParticipantPersonality(name, transcript) if err != nil { + fmt.Printf("Warning: Failed to analyze participant %s: %v\n", name, err) continue // Skip failed analysis for one participant }Or collect errors and return them alongside successful profiles for better observability.
109-157: Unused parameter in function signature.The
nameparameter at line 109 is never used within the function body. While this might have been intended for logging or inclusion in the prompt, it currently serves no purpose.♻️ Proposed fix
Either remove the unused parameter:
-func analyzeParticipantPersonality(name, transcript string) (*models.PersonalityProfile, error) { +func analyzeParticipantPersonality(transcript string) (*models.PersonalityProfile, error) {And update the caller at line 85:
- profile, err := analyzeParticipantPersonality(name, transcript) + profile, err := analyzeParticipantPersonality(transcript)Or use it meaningfully (e.g., for logging or in the prompt).
138-138: Consider migrating to a newer GPT-4 mini variant.The model
gpt-4o-mini-2024-07-18is currently available but OpenAI has scheduled it for retirement no earlier than September 15, 2025. OpenAI recommends users migrate to newer variants such asGPT-4.1orGPT-4.1-mini. If this service is expected to remain active beyond that date, evaluate whether to upgrade to a recommended newer model version.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
.env.examplebackend/cmd/server/main.gobackend/config/config.prod.sample.ymlbackend/controllers/auth.gobackend/controllers/personality_controller.gobackend/models/personality.gobackend/routes/personality.gobackend/services/personality_service.gobackend/structs/auth.gofrontend/src/Pages/Authentication/forms.tsxfrontend/src/Pages/DebateRoom.tsxfrontend/src/Pages/ViewDebate.tsxfrontend/src/components/JudgementPopup.tsxfrontend/src/components/PersonalityProfile.tsxfrontend/src/components/SavedTranscripts.tsxfrontend/src/services/transcriptService.ts
💤 Files with no reviewable changes (2)
- backend/config/config.prod.sample.yml
- .env.example
🧰 Additional context used
🧬 Code graph analysis (7)
backend/cmd/server/main.go (2)
backend/services/personality_service.go (1)
InitPersonalityService(21-23)backend/routes/personality.go (1)
SetupPersonalityRoutes(10-13)
backend/routes/personality.go (1)
backend/controllers/personality_controller.go (2)
GeneratePersonalityProfilesHandler(12-40)GetPersonalityProfilesHandler(43-62)
backend/controllers/personality_controller.go (1)
backend/services/personality_service.go (2)
GetPersonalityProfiles(159-181)GeneratePersonalityProfiles(25-107)
frontend/src/components/JudgementPopup.tsx (3)
frontend/src/services/transcriptService.ts (2)
PersonalityProfileData(23-33)transcriptService(64-290)backend/models/personality.go (1)
PersonalityProfile(10-21)frontend/src/components/PersonalityProfile.tsx (1)
PersonalityProfile(27-84)
frontend/src/services/transcriptService.ts (1)
frontend/src/utils/auth.ts (1)
getAuthToken(5-7)
backend/services/personality_service.go (3)
backend/services/ai.go (2)
ChatGPT(26-29)NewChatGPT(31-36)backend/models/personality.go (1)
PersonalityProfile(10-21)backend/db/db.go (1)
MongoDatabase(18-18)
frontend/src/Pages/DebateRoom.tsx (1)
frontend/src/services/vsbot.ts (1)
concedeDebate(103-118)
🔇 Additional comments (19)
frontend/src/Pages/Authentication/forms.tsx (2)
31-34: Password validation correctly uses the MIN_PASSWORD_LENGTH constant.The implementation consistently applies the minimum password length across all authentication forms (login, signup, reset). The constant-based approach ensures maintainability.
However, please address the security concern raised about the 6-character minimum length.
Also applies to: 143-146, 352-355
8-8: Reconsider the security impact of reducing the password minimum length to 6 characters.The password minimum length affects security, but the justification in the original concern contains factual inaccuracies:
- NIST SP 800-63B recommends 15 characters for single-factor passwords, or 8 characters when used as part of multi-factor authentication—not a universal 8-character minimum.
- Industry practices are mixed: Google defaults to 6 characters; AWS allows 6–128 (default 8); GitHub enforces 8+; Microsoft recommends 12–14. Not all enforce 8+ as claimed.
- A 6-character minimum is weaker than 8, but verify whether this system's password usage context (single-factor vs. multi-factor, deployment environment, overall authentication architecture) justifies this trade-off.
If moving to 6 is intentional for usability or backwards compatibility, document the decision and consider compensating controls (e.g., rate limiting, MFA enforcement).
Likely an incorrect or invalid review comment.
backend/cmd/server/main.go (2)
31-31: Personality service initialization properly placed.The
InitPersonalityService(cfg)call is correctly positioned after rating service initialization and before database operations, following the established service initialization pattern in this codebase.
163-165: Personality routes correctly registered in authenticated router group.The
SetupPersonalityRoutes(auth)call properly registers personality endpoints within the authenticated router group, consistent with other protected features like team routes and community routes. This ensures personality analysis endpoints are protected by JWT authentication middleware.backend/routes/personality.go (1)
1-13: Personality routes follow RESTful conventions and established patterns.The route setup is well-structured:
- POST endpoint for generating personality profiles (write operation)
- GET endpoint for retrieving existing profiles (read operation)
- Consistent with other route files in the codebase (e.g., transcript routes, team routes)
- Uses standard Gin RouterGroup pattern for modular route registration
- Path parameter
:idappropriately captures debate IDThe handlers referenced (
GeneratePersonalityProfilesHandler,GetPersonalityProfilesHandler) are confirmed to exist in the controllers package.backend/models/personality.go (1)
9-21: Well-structured model definition.The
PersonalityProfilestruct is appropriately designed with proper BSON/JSON tags for MongoDB serialization. The use ofprimitive.ObjectIDforDebateIDmaintains referential integrity with the debate collection.Consider adding validation at the service layer to ensure
EvidenceUsage,Clarity, andResponsivenessremain within the expected 0-10 range, sinceintallows values outside this bound.frontend/src/Pages/DebateRoom.tsx (1)
739-751: Good integration of debateId for personality profiles.Passing
debateId={debateData.debateId}toJudgmentPopupcorrectly enables the personality profile feature to fetch and display analysis for the completed debate.frontend/src/Pages/ViewDebate.tsx (1)
767-783: Clean conditional rendering for personality analysis.The UI section appropriately guards rendering with
loadingPersonality || personalityProfiles.length > 0, displays a loading spinner during fetch, and renders thePersonalityProfilecomponent when data is available.backend/controllers/personality_controller.go (1)
12-40: Good idempotent design for profile generation.The handler appropriately checks for existing profiles before generating new ones, preventing duplicate entries and making the POST endpoint safe to call multiple times. Returning the existing profiles with a "Profiles already exist" message provides clear feedback.
frontend/src/components/JudgementPopup.tsx (1)
728-744: Appropriate UI rendering for personality analysis within judgment popup.The personality analysis section is correctly placed after the verdict and conditionally rendered based on loading state and profile availability. The loading spinner with contextual message ("AI is analyzing your debate style...") provides good UX feedback.
frontend/src/components/SavedTranscripts.tsx (1)
496-514: Complete UI state handling for personality profiles.The section appropriately handles all three states: loading spinner, profiles display via
PersonalityProfilecomponent, and a fallback message when no profiles are available. This provides better UX than the other components which hide the section entirely when no profiles exist.frontend/src/components/PersonalityProfile.tsx (2)
32-37: Radar chart with only 3 data points forms a triangle.Three metrics create a triangular radar chart, which works but may not fully leverage the radar visualization format. If additional metrics become available in the future (e.g., logical rigor, persuasiveness), the chart will become more informative. Current implementation is functional.
39-80: Well-implemented profile card with radar visualization.The component properly handles:
- Responsive chart container
- Dark mode support with appropriate color classes
- Summary section with visual distinction
- AI-generated disclaimer for transparency
Using
indexas the React key is acceptable here since the profiles array is static and not subject to reordering during the component's lifecycle.frontend/src/services/transcriptService.ts (2)
233-259: LGTM!The method follows established patterns in this file for authentication, error handling, and data fetching. The fallback to an empty array when no profiles exist is appropriate.
261-289: LGTM!The method correctly triggers personality profile generation via POST and handles errors consistently with the existing codebase patterns.
backend/services/personality_service.go (4)
21-23: LGTM!The singleton initialization pattern is straightforward and appropriate for this use case.
26-43: LGTM!The debate validation logic correctly checks for completion status and handles ID conversion errors appropriately. The 2-minute timeout is reasonable for AI processing.
48-55: LGTM!The user_vs_bot transcript aggregation correctly iterates through messages and groups by sender, with appropriate handling for empty sender values.
159-181: LGTM!The retrieval logic is straightforward and correctly queries MongoDB for personality profiles associated with the debate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/controllers/debatevsbot_controller.go (1)
270-279: Prefernilovermake(map[...])to preserveomitemptysemantics (unless you need{}specifically). Empty maps can get persisted as empty objects and may change client behavior vs “absent”.Proposed change
- make(map[string]models.TranscriptEntry), + nil,Also applies to: 586-595
🤖 Fix all issues with AI agents
In @backend/controllers/post_controller.go:
- Around line 22-25: The transcriptPreview type currently exposes
models.TranscriptEntry (which includes email) via the Transcripts map; to avoid
leaking PII, replace or map that field to a sanitized DTO when returning public
posts—either change transcriptPreview.Transcripts to
map[string]TranscriptEntryPreview (a new struct without email or with a
displayName only) or, if you build the map in the handler, explicitly
strip/replace the email on each models.TranscriptEntry before adding it for
non-owner or public views (use the post/ownership check already in the handler
to decide), ensuring only owners see the raw email.
In @backend/services/personality_service.go:
- Around line 145-150: The JSON cleaning currently uses sequential TrimPrefix
calls that miss cases like "```json\n{...}\n```"; update the logic around the
cleanResponse variable so you first TrimPrefix "```json\n" (or check HasPrefix
"```json") then TrimPrefix "```" and TrimSuffix "```", or implement the robust
approach: if cleanResponse starts with "```" split on "\n" and join
lines[1:len(lines)-1] to extract the inner JSON, then TrimSpace; apply this
change where response is cleaned (cleanResponse variable in
personality_service.go).
- Around line 101-106: The loop currently calls profileCollection.InsertOne and
returns immediately on the first insert error, causing partial inserts with no
rollback; either make the loop atomic by running all inserts inside a MongoDB
transaction (use client.StartSession and session.WithTransaction, perform
profileCollection.InsertOne calls inside the transaction and only append to
profiles on success), or switch to a partial-result pattern: keep inserting,
append successful inserts to the profiles slice, collect insert errors into an
errs slice and at the end return the profiles plus a combined error (or nil if
no errors) instead of returning immediately; update callers accordingly to
handle the chosen behavior.
- Around line 21-23: InitPersonalityService currently assumes cfg and nested
fields exist and that the API key is non-empty; add defensive validation: check
that cfg is not nil, cfg.Openai (or equivalent struct) is not nil, and
cfg.Openai.GptApiKey is not empty before calling NewChatGPT; if validation
fails, return an error from InitPersonalityService (change its signature to
return error) or log a clear fatal/error message and avoid setting
personalityGPT so GeneratePersonalityProfiles won't panic, and include
references to personalityGPT, NewChatGPT, InitPersonalityService and
GeneratePersonalityProfiles when making the change.
- Around line 74-82: The loop over participants currently swallows errors from
analyzeParticipantPersonality(name, transcript); instead of silently continuing,
log the error (including the participant name and error message, and optionally
transcript length or snippet) before continuing so failures are visible; update
the error branch in the participants loop inside personality_service.go to call
the service/global logger (e.g., svc.logger or log.Printf) with a clear message
and the err, then continue, keeping the existing behavior for DB insert errors
unchanged.
- Around line 25-109: GeneratePersonalityProfiles is not idempotent because it
uses profileCollection.InsertOne which will create duplicates; change the logic
to either clear existing profiles for the debate before regenerating (call
profileCollection.DeleteMany with filter bson.M{"debate_id": objID}) or replace
each profile with an upsert (use profileCollection.ReplaceOne with filter
bson.M{"debate_id": objID, "participant_id": profile.ParticipantID} and
options.Replace().SetUpsert(true)) in place of InsertOne; keep the rest of the
loop and ensure profile.DebateID and profile.ParticipantID are set before the
upsert/delete operations so the operation is atomic and safe for direct service
calls.
In @frontend/src/components/SavedTranscripts.tsx:
- Around line 96-120: When opening a transcript in handleViewTranscript, clear
stale profiles immediately (call setPersonalityProfiles([])) and only
fetch/generate profiles for debates that are completed: in
fetchPersonalityProfiles (called with debateId and/or the transcript object),
call transcriptService.getPersonalityProfiles(debateId) but only call
transcriptService.generatePersonalityProfiles(debateId) if the transcript.status
=== 'completed' (do not POST for 'pending'); ensure
setLoadingPersonality(true/false) and setPersonalityProfiles([]) are used to
avoid showing prior transcript data while loading or when generation is skipped.
In @frontend/src/services/transcriptService.ts:
- Around line 269-297: In generatePersonalityProfiles, make the POST explicit by
adding Content-Type: 'application/json' (and optionally Accept:
'application/json') to the headers and, to avoid ambiguity with empty POSTs,
include an explicit empty JSON body (e.g., JSON.stringify({})); update the
headers object in the fetch call inside generatePersonalityProfiles to include
these keys so the request is consistent with other API calls.
- Around line 241-267: The getPersonalityProfiles function currently throws on
any non-OK response; change its error handling so a 404 returns an empty array
instead of throwing (so the UI can fall back to generation): after the fetch in
getPersonalityProfiles check if response.status === 404 and return []
immediately; otherwise keep the existing logic (parse JSON and throw on other
non-OK statuses), and ensure you only call response.json() once and handle
missing data by returning data.profiles || [].
🧹 Nitpick comments (11)
backend/controllers/transcript_controller.go (1)
409-417: Avoid fetching email if it’s intentionally unused. The_ = emailworkaround suggestsUpdatePendingTranscriptsHandleronly needs token validity; consider adding a “validate token only” helper to avoid unused outputs.frontend/src/components/SavedTranscripts.tsx (2)
463-474: Owner-only PII display is fine, but avoid assumingentry.email/roleare always present. If older transcripts exist, guard with fallbacks to prevent odd UI output.
499-516: Consider exposing errors to the user (not justconsole.error). Right now failures render as “No personality profiles available”, which is misleading when the backend returns “debate not completed” / AI errors.backend/services/transcriptservice.go (2)
180-221: TranscriptEntry map construction looks correct; consider deduplicating the phase lists. The phase arrays are repeated twice; a small helper/const slice would reduce drift risk.
705-705: Signature change is fine; ensure nil/empty map semantics are intentional.omitempty+$setcan still storenull/{}depending on caller input; make sure callers don’t accidentally overwrite existing transcripts with empty maps.backend/models/transcript.go (1)
46-60:MarshalJSONrelies on field shadowing forid/userId; consider making it unambiguous. Prefer a dedicated response struct (or an alias type that omitsID/UserID) to avoid duplicate json tag collisions.backend/services/personality_service.go (5)
41-43: Strengthen debate completion validation.The check only excludes "pending" status but doesn't validate that the debate has a recognized completion state. An empty string or invalid Result value would pass this check.
✅ Proposed fix to use positive validation
- if debate.Result == "pending" { - return nil, errors.New("debate is not yet completed") + if debate.Result != "win" && debate.Result != "loss" && debate.Result != "draw" { + return nil, fmt.Errorf("debate is not completed (current status: %s)", debate.Result) }
111-111: Unused parameter:nameis never used in the function.The
nameparameter is passed toanalyzeParticipantPersonalitybut never referenced in the function body. This could indicate incomplete implementation or unnecessary parameter passing.Consider either:
- Removing the parameter if not needed, or
- Including the participant name in the analysis prompt for more contextual analysis
152-156: Missing validation of AI response fields.After unmarshaling the JSON response, there's no validation that:
- Numeric scores (EvidenceUsage, Clarity, Responsiveness) are within the expected 0-10 range
- Required string fields (ArgumentStyle, Tone, Summary) are non-empty
Invalid AI responses could result in storing malformed profiles.
✅ Add validation after unmarshal
var profile models.PersonalityProfile err = json.Unmarshal([]byte(cleanResponse), &profile) if err != nil { return nil, fmt.Errorf("failed to parse AI response: %v", err) } + + // Validate ranges and required fields + if profile.EvidenceUsage < 0 || profile.EvidenceUsage > 10 || + profile.Clarity < 0 || profile.Clarity > 10 || + profile.Responsiveness < 0 || profile.Responsiveness > 10 { + return nil, fmt.Errorf("AI returned scores outside valid range (0-10)") + } + if profile.ArgumentStyle == "" || profile.Tone == "" || profile.Summary == "" { + return nil, fmt.Errorf("AI returned incomplete profile") + } return &profile, nil
161-183: LGTM with minor suggestion.The retrieval logic is straightforward and correct. The function appropriately returns an empty slice (not an error) when no profiles exist.
Minor improvement: consider adding more context to the error on line 167 for better debugging:
💡 Optional improvement
objID, err := primitive.ObjectIDFromHex(debateID) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid debate ID: %v", err) }
140-140: Consider making the GPT model version configurable.The hardcoded model version "gpt-4o-mini-2024-07-18" works today but has a scheduled retirement on March 31, 2026. Extracting this to a configuration parameter will simplify future model upgrades and avoid code changes when switching to gpt-4.1-mini or other variants.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
backend/controllers/debatevsbot_controller.gobackend/controllers/post_controller.gobackend/controllers/transcript_controller.gobackend/models/transcript.gobackend/services/personality_service.gobackend/services/transcriptservice.gofrontend/src/components/SavedTranscripts.tsxfrontend/src/services/transcriptService.ts
🧰 Additional context used
🧬 Code graph analysis (8)
backend/controllers/debatevsbot_controller.go (1)
backend/models/transcript.go (1)
TranscriptEntry(10-14)
frontend/src/components/SavedTranscripts.tsx (3)
frontend/src/services/transcriptService.ts (2)
PersonalityProfileData(27-37)transcriptService(72-298)backend/models/personality.go (1)
PersonalityProfile(10-21)frontend/src/components/PersonalityProfile.tsx (1)
PersonalityProfile(27-84)
backend/controllers/transcript_controller.go (1)
backend/models/transcript.go (1)
TranscriptEntry(10-14)
backend/controllers/post_controller.go (1)
backend/models/transcript.go (1)
TranscriptEntry(10-14)
frontend/src/services/transcriptService.ts (1)
frontend/src/utils/auth.ts (1)
getAuthToken(5-7)
backend/services/personality_service.go (4)
backend/services/ai.go (2)
ChatGPT(26-29)NewChatGPT(31-36)backend/models/personality.go (1)
PersonalityProfile(10-21)backend/db/db.go (1)
MongoDatabase(18-18)backend/models/transcript.go (1)
SavedDebateTranscript(32-44)
backend/models/transcript.go (3)
backend/websocket/websocket.go (1)
Message(71-94)backend/services/ai.go (1)
Message(21-24)backend/models/debatevsbot.go (1)
Message(6-10)
backend/services/transcriptservice.go (1)
backend/models/transcript.go (1)
TranscriptEntry(10-14)
🔇 Additional comments (5)
backend/controllers/transcript_controller.go (2)
14-15: Prod gating for test endpoints is a good safeguard; keep it strict.Also applies to: 260-263, 313-316
52-55: Good: reject tokens that validate but don’t yield an email. Prevents creating “anonymous” submissions tied to empty identifiers.backend/models/transcript.go (1)
10-14: Model change is coherent and enables richer transcripts.Also applies to: 33-43
backend/services/transcriptservice.go (1)
223-246: The review comment identifies a non-existent functional gap. The personality service already handles this correctly:GeneratePersonalityProfilesexplicitly checksdebate.DebateTypeand usesdebate.Transcriptsfor user-vs-user debates (line 59 in personality_service.go), notdebate.Messages. The design is intentional—Messagesis used only for user-vs-bot debates, whileTranscripts(populated byforTranscriptsMapandagainstTranscriptsMapin the reviewed code) is the source for user-vs-user personality analysis. No changes are required.Likely an incorrect or invalid review comment.
backend/services/personality_service.go (1)
45-69: LGTM!The participant grouping logic correctly handles both debate types (user_vs_bot and user_vs_user) with appropriate field mappings and defensive empty checks.
| type transcriptPreview struct { | ||
| Messages []models.Message `json:"messages"` | ||
| Transcripts map[string]string `json:"transcripts,omitempty"` | ||
| Messages []models.Message `json:"messages"` | ||
| Transcripts map[string]models.TranscriptEntry `json:"transcripts,omitempty"` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PII risk: public feed now includes transcript-entry email values. If posts are publicly visible, avoid returning raw emails in transcript.transcripts (sanitize, replace with display name, or omit email entirely for non-owners).
🤖 Prompt for AI Agents
In @backend/controllers/post_controller.go around lines 22 - 25, The
transcriptPreview type currently exposes models.TranscriptEntry (which includes
email) via the Transcripts map; to avoid leaking PII, replace or map that field
to a sanitized DTO when returning public posts—either change
transcriptPreview.Transcripts to map[string]TranscriptEntryPreview (a new struct
without email or with a displayName only) or, if you build the map in the
handler, explicitly strip/replace the email on each models.TranscriptEntry
before adding it for non-owner or public views (use the post/ownership check
already in the handler to decide), ensuring only owners see the raw email.
| DebateType string `json:"debateType" binding:"required"` | ||
| Topic string `json:"topic" binding:"required"` | ||
| Opponent string `json:"opponent" binding:"required"` | ||
| Result string `json:"result"` | ||
| Messages []models.Message `json:"messages"` | ||
| Transcripts map[string]models.TranscriptEntry `json:"transcripts,omitempty"` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Breaking API change: transcripts shape changed from map[string]string to map[string]TranscriptEntry. If any non-frontend clients exist, consider backward-compatible unmarshalling (accept string or {text,email,role}) or version the endpoint/field.
| func InitPersonalityService(cfg *config.Config) { | ||
| personalityGPT = NewChatGPT(cfg.Openai.GptApiKey) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for configuration and API key.
The initialization function lacks nil checks for the config object and its nested fields, and doesn't validate that the API key is non-empty. If the API key is missing or invalid, subsequent calls to GeneratePersonalityProfiles will fail with cryptic errors or panics.
🛡️ Proposed fix to add validation
func InitPersonalityService(cfg *config.Config) {
+ if cfg == nil || cfg.Openai.GptApiKey == "" {
+ panic("personality service requires a valid OpenAI API key in config")
+ }
personalityGPT = NewChatGPT(cfg.Openai.GptApiKey)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func InitPersonalityService(cfg *config.Config) { | |
| personalityGPT = NewChatGPT(cfg.Openai.GptApiKey) | |
| } | |
| func InitPersonalityService(cfg *config.Config) { | |
| if cfg == nil || cfg.Openai.GptApiKey == "" { | |
| panic("personality service requires a valid OpenAI API key in config") | |
| } | |
| personalityGPT = NewChatGPT(cfg.Openai.GptApiKey) | |
| } |
🤖 Prompt for AI Agents
In @backend/services/personality_service.go around lines 21 - 23,
InitPersonalityService currently assumes cfg and nested fields exist and that
the API key is non-empty; add defensive validation: check that cfg is not nil,
cfg.Openai (or equivalent struct) is not nil, and cfg.Openai.GptApiKey is not
empty before calling NewChatGPT; if validation fails, return an error from
InitPersonalityService (change its signature to return error) or log a clear
fatal/error message and avoid setting personalityGPT so
GeneratePersonalityProfiles won't panic, and include references to
personalityGPT, NewChatGPT, InitPersonalityService and
GeneratePersonalityProfiles when making the change.
| func GeneratePersonalityProfiles(debateID string) ([]models.PersonalityProfile, error) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) | ||
| defer cancel() | ||
|
|
||
| objID, err := primitive.ObjectIDFromHex(debateID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid debate ID: %v", err) | ||
| } | ||
|
|
||
| collection := db.MongoDatabase.Collection("saved_debate_transcripts") | ||
| var debate models.SavedDebateTranscript | ||
| err = collection.FindOne(ctx, bson.M{"_id": objID}).Decode(&debate) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("debate not found: %v", err) | ||
| } | ||
|
|
||
| if debate.Result == "pending" { | ||
| return nil, errors.New("debate is not yet completed") | ||
| } | ||
|
|
||
| // Group messages by participant | ||
| participants := make(map[string]string) | ||
|
|
||
| if debate.DebateType == "user_vs_bot" { | ||
| for _, msg := range debate.Messages { | ||
| sender := msg.Sender | ||
| if sender == "" { | ||
| continue | ||
| } | ||
| participants[sender] += msg.Text + "\n" | ||
| } | ||
| } else { | ||
| // User vs User | ||
| // Map phases to participants using explicit metadata | ||
| for _, entry := range debate.Transcripts { | ||
| if entry.Email == "" { | ||
| continue | ||
| } | ||
| participants[entry.Email] += entry.Text + "\n" | ||
| } | ||
| } | ||
|
|
||
| if len(participants) == 0 { | ||
| return nil, errors.New("no transcript found for participants") | ||
| } | ||
|
|
||
| var profiles []models.PersonalityProfile | ||
| profileCollection := db.MongoDatabase.Collection("personality_profiles") | ||
|
|
||
| for name, transcript := range participants { | ||
| if strings.TrimSpace(transcript) == "" { | ||
| continue | ||
| } | ||
|
|
||
| profile, err := analyzeParticipantPersonality(name, transcript) | ||
| if err != nil { | ||
| continue // Skip failed analysis for one participant | ||
| } | ||
|
|
||
| profile.DebateID = objID | ||
| profile.ParticipantID = name | ||
|
|
||
| // Map "User" and "Bot" keys to actual identifiers for user_vs_bot | ||
| if debate.DebateType == "user_vs_bot" { | ||
| if name == "User" { | ||
| profile.ParticipantID = debate.Email | ||
| } else if name == "Bot" { | ||
| profile.ParticipantID = debate.Opponent | ||
| } else if name == "Judge" { | ||
| continue // Skip analyzing the judge | ||
| } | ||
| } else { | ||
| // For user_vs_user, ParticipantID is already the email | ||
| } | ||
| profile.CreatedAt = time.Now() | ||
|
|
||
| _, err = profileCollection.InsertOne(ctx, profile) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to save profile: %v", err) | ||
| } | ||
| profiles = append(profiles, *profile) | ||
| } | ||
|
|
||
| return profiles, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's search for how GeneratePersonalityProfiles is called
rg -n "GeneratePersonalityProfiles" --type=goRepository: AOSSIE-Org/DebateAI
Length of output: 692
🏁 Script executed:
# Look for MongoDB index definitions or unique constraints on personality_profiles
rg -n -A5 "personality_profiles" --type=go | head -100Repository: AOSSIE-Org/DebateAI
Length of output: 972
🏁 Script executed:
# Check for upsert patterns or duplicate prevention in the codebase
rg -n -B2 -A5 "ReplaceOne|Upsert|UpdateOne.*upsert" --type=go backend/services/ backend/controllers/Repository: AOSSIE-Org/DebateAI
Length of output: 602
🏁 Script executed:
# Look for any migration files or schema definitions
fd -e js -e go -e json . | grep -i "migrat\|schema\|index" | head -20Repository: AOSSIE-Org/DebateAI
Length of output: 45
🏁 Script executed:
# Check the personality controller for any safeguards
cat backend/controllers/personality_controller.goRepository: AOSSIE-Org/DebateAI
Length of output: 1710
🏁 Script executed:
# Check the personality profile model to see if there are any constraints defined
rg -n "type PersonalityProfile struct" -A 15 --type=goRepository: AOSSIE-Org/DebateAI
Length of output: 1255
🏁 Script executed:
# Check if there's any index creation code in the codebase
rg -n "CreateIndex\|EnsureIndex" --type=go backend/Repository: AOSSIE-Org/DebateAI
Length of output: 45
🏁 Script executed:
# Find GetPersonalityProfiles implementation
rg -n "func GetPersonalityProfiles" -A 20 --type=goRepository: AOSSIE-Org/DebateAI
Length of output: 3121
🏁 Script executed:
# Check if GeneratePersonalityProfiles is called from anywhere else besides the controller
rg -n "GeneratePersonalityProfiles\(" --type=go -B 2 -A 2Repository: AOSSIE-Org/DebateAI
Length of output: 894
Move duplicate prevention to service layer for robust design.
While the controller prevents duplicate calls via GetPersonalityProfiles, the service function GeneratePersonalityProfiles is not idempotent. It uses InsertOne without any safeguard, so direct calls to the service would create duplicates. Additionally, no database-level unique constraint on (debateID, participantID) exists.
Implement idempotency in the service function by adding a check before insertion or using upsert logic:
- Query existing profiles by
debateIDand delete before regenerating, or - Use
ReplaceOnewith upsert instead ofInsertOneto replace existing profiles atomically
This ensures the function is safe regardless of how it's called.
🤖 Prompt for AI Agents
In @backend/services/personality_service.go around lines 25 - 109,
GeneratePersonalityProfiles is not idempotent because it uses
profileCollection.InsertOne which will create duplicates; change the logic to
either clear existing profiles for the debate before regenerating (call
profileCollection.DeleteMany with filter bson.M{"debate_id": objID}) or replace
each profile with an upsert (use profileCollection.ReplaceOne with filter
bson.M{"debate_id": objID, "participant_id": profile.ParticipantID} and
options.Replace().SetUpsert(true)) in place of InsertOne; keep the rest of the
loop and ensure profile.DebateID and profile.ParticipantID are set before the
upsert/delete operations so the operation is atomic and safe for direct service
calls.
| for name, transcript := range participants { | ||
| if strings.TrimSpace(transcript) == "" { | ||
| continue | ||
| } | ||
|
|
||
| profile, err := analyzeParticipantPersonality(name, transcript) | ||
| if err != nil { | ||
| continue // Skip failed analysis for one participant | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent error handling: analysis failures are silently skipped.
When analyzeParticipantPersonality fails (line 81), the error is silently ignored without logging, making it difficult to diagnose AI analysis issues. This is inconsistent with the DB insert error handling below (lines 102-104) which aborts the entire operation.
Consider at least logging the error:
📝 Suggested improvement
profile, err := analyzeParticipantPersonality(name, transcript)
if err != nil {
+ fmt.Printf("Warning: failed to analyze personality for %s: %v\n", name, err)
continue // Skip failed analysis for one participant
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for name, transcript := range participants { | |
| if strings.TrimSpace(transcript) == "" { | |
| continue | |
| } | |
| profile, err := analyzeParticipantPersonality(name, transcript) | |
| if err != nil { | |
| continue // Skip failed analysis for one participant | |
| } | |
| for name, transcript := range participants { | |
| if strings.TrimSpace(transcript) == "" { | |
| continue | |
| } | |
| profile, err := analyzeParticipantPersonality(name, transcript) | |
| if err != nil { | |
| fmt.Printf("Warning: failed to analyze personality for %s: %v\n", name, err) | |
| continue // Skip failed analysis for one participant | |
| } |
🤖 Prompt for AI Agents
In @backend/services/personality_service.go around lines 74 - 82, The loop over
participants currently swallows errors from analyzeParticipantPersonality(name,
transcript); instead of silently continuing, log the error (including the
participant name and error message, and optionally transcript length or snippet)
before continuing so failures are visible; update the error branch in the
participants loop inside personality_service.go to call the service/global
logger (e.g., svc.logger or log.Printf) with a clear message and the err, then
continue, keeping the existing behavior for DB insert errors unchanged.
| _, err = profileCollection.InsertOne(ctx, profile) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to save profile: %v", err) | ||
| } | ||
| profiles = append(profiles, *profile) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent error handling and missing rollback.
If InsertOne fails for one profile after successfully inserting others, the function returns an error but doesn't roll back previously inserted profiles. This creates partial data and is inconsistent with line 81 where analysis failures are silently skipped.
Consider either:
- Making all insertions atomic (transaction), or
- Continuing on insert failures like analysis failures, or
- Collecting errors and returning them with partial results
🤖 Prompt for AI Agents
In @backend/services/personality_service.go around lines 101 - 106, The loop
currently calls profileCollection.InsertOne and returns immediately on the first
insert error, causing partial inserts with no rollback; either make the loop
atomic by running all inserts inside a MongoDB transaction (use
client.StartSession and session.WithTransaction, perform
profileCollection.InsertOne calls inside the transaction and only append to
profiles on success), or switch to a partial-result pattern: keep inserting,
append successful inserts to the profiles slice, collect insert errors into an
errs slice and at the end return the profiles plus a combined error (or nil if
no errors) instead of returning immediately; update callers accordingly to
handle the chosen behavior.
| // Clean JSON response (strip markdown fences if any) | ||
| cleanResponse := strings.TrimSpace(response) | ||
| cleanResponse = strings.TrimPrefix(cleanResponse, "```json") | ||
| cleanResponse = strings.TrimPrefix(cleanResponse, "```") | ||
| cleanResponse = strings.TrimSuffix(cleanResponse, "```") | ||
| cleanResponse = strings.TrimSpace(cleanResponse) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect JSON cleaning logic with sequential TrimPrefix.
The sequential TrimPrefix calls won't work correctly. After removing "json" on line 147, the string will no longer start with "", so line 148 has no effect. If the response is wrapped as ```json\n{...}\n```, line 148 never executes.
🔧 Proposed fix
// Clean JSON response (strip markdown fences if any)
cleanResponse := strings.TrimSpace(response)
- cleanResponse = strings.TrimPrefix(cleanResponse, "```json")
- cleanResponse = strings.TrimPrefix(cleanResponse, "```")
+ cleanResponse = strings.TrimPrefix(cleanResponse, "```json\n")
+ cleanResponse = strings.TrimPrefix(cleanResponse, "```")
cleanResponse = strings.TrimSuffix(cleanResponse, "```")
cleanResponse = strings.TrimSpace(cleanResponse)Or use a more robust approach:
// Clean JSON response (strip markdown fences if any)
cleanResponse := strings.TrimSpace(response)
- cleanResponse = strings.TrimPrefix(cleanResponse, "```json")
- cleanResponse = strings.TrimPrefix(cleanResponse, "```")
- cleanResponse = strings.TrimSuffix(cleanResponse, "```")
- cleanResponse = strings.TrimSpace(cleanResponse)
+ if strings.HasPrefix(cleanResponse, "```") {
+ lines := strings.Split(cleanResponse, "\n")
+ if len(lines) > 2 {
+ cleanResponse = strings.Join(lines[1:len(lines)-1], "\n")
+ }
+ }
+ cleanResponse = strings.TrimSpace(cleanResponse)🤖 Prompt for AI Agents
In @backend/services/personality_service.go around lines 145 - 150, The JSON
cleaning currently uses sequential TrimPrefix calls that miss cases like
"```json\n{...}\n```"; update the logic around the cleanResponse variable so you
first TrimPrefix "```json\n" (or check HasPrefix "```json") then TrimPrefix
"```" and TrimSuffix "```", or implement the robust approach: if cleanResponse
starts with "```" split on "\n" and join lines[1:len(lines)-1] to extract the
inner JSON, then TrimSpace; apply this change where response is cleaned
(cleanResponse variable in personality_service.go).
| const handleViewTranscript = (transcript: SavedDebateTranscript) => { | ||
| setSelectedTranscript(transcript); | ||
| setIsViewDialogOpen(true); | ||
| fetchPersonalityProfiles(transcript.id); | ||
| }; | ||
|
|
||
| const fetchPersonalityProfiles = async (debateId: string) => { | ||
| try { | ||
| setLoadingPersonality(true); | ||
| // Try to GET first | ||
| let profiles = await transcriptService.getPersonalityProfiles(debateId); | ||
|
|
||
| // If none found and debate is completed, trigger POST | ||
| if (profiles.length === 0) { | ||
| profiles = await transcriptService.generatePersonalityProfiles(debateId); | ||
| } | ||
|
|
||
| setPersonalityProfiles(profiles); | ||
| } catch (err) { | ||
| console.error('Error fetching personality profiles:', err); | ||
| setPersonalityProfiles([]); | ||
| } finally { | ||
| setLoadingPersonality(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t auto-generate personality profiles for pending debates; also clear stale profiles on open. Current flow will POST for pending debates (wasted calls + confusing “No profiles” UI) and can momentarily show prior transcript’s profiles.
Proposed change
const handleViewTranscript = (transcript: SavedDebateTranscript) => {
setSelectedTranscript(transcript);
setIsViewDialogOpen(true);
- fetchPersonalityProfiles(transcript.id);
+ setPersonalityProfiles([]);
+ if (transcript.result !== 'pending') {
+ fetchPersonalityProfiles(transcript.id);
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleViewTranscript = (transcript: SavedDebateTranscript) => { | |
| setSelectedTranscript(transcript); | |
| setIsViewDialogOpen(true); | |
| fetchPersonalityProfiles(transcript.id); | |
| }; | |
| const fetchPersonalityProfiles = async (debateId: string) => { | |
| try { | |
| setLoadingPersonality(true); | |
| // Try to GET first | |
| let profiles = await transcriptService.getPersonalityProfiles(debateId); | |
| // If none found and debate is completed, trigger POST | |
| if (profiles.length === 0) { | |
| profiles = await transcriptService.generatePersonalityProfiles(debateId); | |
| } | |
| setPersonalityProfiles(profiles); | |
| } catch (err) { | |
| console.error('Error fetching personality profiles:', err); | |
| setPersonalityProfiles([]); | |
| } finally { | |
| setLoadingPersonality(false); | |
| } | |
| }; | |
| const handleViewTranscript = (transcript: SavedDebateTranscript) => { | |
| setSelectedTranscript(transcript); | |
| setIsViewDialogOpen(true); | |
| setPersonalityProfiles([]); | |
| if (transcript.result !== 'pending') { | |
| fetchPersonalityProfiles(transcript.id); | |
| } | |
| }; | |
| const fetchPersonalityProfiles = async (debateId: string) => { | |
| try { | |
| setLoadingPersonality(true); | |
| // Try to GET first | |
| let profiles = await transcriptService.getPersonalityProfiles(debateId); | |
| // If none found and debate is completed, trigger POST | |
| if (profiles.length === 0) { | |
| profiles = await transcriptService.generatePersonalityProfiles(debateId); | |
| } | |
| setPersonalityProfiles(profiles); | |
| } catch (err) { | |
| console.error('Error fetching personality profiles:', err); | |
| setPersonalityProfiles([]); | |
| } finally { | |
| setLoadingPersonality(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In @frontend/src/components/SavedTranscripts.tsx around lines 96 - 120, When
opening a transcript in handleViewTranscript, clear stale profiles immediately
(call setPersonalityProfiles([])) and only fetch/generate profiles for debates
that are completed: in fetchPersonalityProfiles (called with debateId and/or the
transcript object), call transcriptService.getPersonalityProfiles(debateId) but
only call transcriptService.generatePersonalityProfiles(debateId) if the
transcript.status === 'completed' (do not POST for 'pending'); ensure
setLoadingPersonality(true/false) and setPersonalityProfiles([]) are used to
avoid showing prior transcript data while loading or when generation is skipped.
| // Get personality profiles for a debate | ||
| async getPersonalityProfiles( | ||
| debateId: string | ||
| ): Promise<PersonalityProfileData[]> { | ||
| const token = getAuthToken(); | ||
| if (!token) { | ||
| throw new Error('Authentication token not found'); | ||
| } | ||
|
|
||
| const response = await fetch( | ||
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | ||
| { | ||
| method: 'GET', | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json(); | ||
| throw new Error(errorData.error || 'Failed to fetch personality profiles'); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| return data.profiles || []; | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle “no profiles yet” without throwing (e.g., treat 404 as []). This enables the UI to fall back to generation when appropriate.
Proposed change
const response = await fetch(
`${API_BASE_URL}/api/debates/${debateId}/personality`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
}
);
- if (!response.ok) {
+ if (response.status === 404) {
+ return [];
+ }
+ if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch personality profiles');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Get personality profiles for a debate | |
| async getPersonalityProfiles( | |
| debateId: string | |
| ): Promise<PersonalityProfileData[]> { | |
| const token = getAuthToken(); | |
| if (!token) { | |
| throw new Error('Authentication token not found'); | |
| } | |
| const response = await fetch( | |
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | |
| { | |
| method: 'GET', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| } | |
| ); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to fetch personality profiles'); | |
| } | |
| const data = await response.json(); | |
| return data.profiles || []; | |
| }, | |
| // Get personality profiles for a debate | |
| async getPersonalityProfiles( | |
| debateId: string | |
| ): Promise<PersonalityProfileData[]> { | |
| const token = getAuthToken(); | |
| if (!token) { | |
| throw new Error('Authentication token not found'); | |
| } | |
| const response = await fetch( | |
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | |
| { | |
| method: 'GET', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| } | |
| ); | |
| if (response.status === 404) { | |
| return []; | |
| } | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to fetch personality profiles'); | |
| } | |
| const data = await response.json(); | |
| return data.profiles || []; | |
| }, |
🤖 Prompt for AI Agents
In @frontend/src/services/transcriptService.ts around lines 241 - 267, The
getPersonalityProfiles function currently throws on any non-OK response; change
its error handling so a 404 returns an empty array instead of throwing (so the
UI can fall back to generation): after the fetch in getPersonalityProfiles check
if response.status === 404 and return [] immediately; otherwise keep the
existing logic (parse JSON and throw on other non-OK statuses), and ensure you
only call response.json() once and handle missing data by returning
data.profiles || [].
| // Generate personality profiles for a debate | ||
| async generatePersonalityProfiles( | ||
| debateId: string | ||
| ): Promise<PersonalityProfileData[]> { | ||
| const token = getAuthToken(); | ||
| if (!token) { | ||
| throw new Error('Authentication token not found'); | ||
| } | ||
|
|
||
| const response = await fetch( | ||
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json(); | ||
| throw new Error( | ||
| errorData.error || 'Failed to generate personality profiles' | ||
| ); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| return data.profiles || []; | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POST should be explicit about headers (and be consistent). Even with an empty body, adding Content-Type: application/json reduces proxy/framework ambiguity.
Proposed change
{
method: 'POST',
headers: {
+ 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Generate personality profiles for a debate | |
| async generatePersonalityProfiles( | |
| debateId: string | |
| ): Promise<PersonalityProfileData[]> { | |
| const token = getAuthToken(); | |
| if (!token) { | |
| throw new Error('Authentication token not found'); | |
| } | |
| const response = await fetch( | |
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| } | |
| ); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error( | |
| errorData.error || 'Failed to generate personality profiles' | |
| ); | |
| } | |
| const data = await response.json(); | |
| return data.profiles || []; | |
| }, | |
| // Generate personality profiles for a debate | |
| async generatePersonalityProfiles( | |
| debateId: string | |
| ): Promise<PersonalityProfileData[]> { | |
| const token = getAuthToken(); | |
| if (!token) { | |
| throw new Error('Authentication token not found'); | |
| } | |
| const response = await fetch( | |
| `${API_BASE_URL}/api/debates/${debateId}/personality`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| } | |
| ); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error( | |
| errorData.error || 'Failed to generate personality profiles' | |
| ); | |
| } | |
| const data = await response.json(); | |
| return data.profiles || []; | |
| }, |
🤖 Prompt for AI Agents
In @frontend/src/services/transcriptService.ts around lines 269 - 297, In
generatePersonalityProfiles, make the POST explicit by adding Content-Type:
'application/json' (and optionally Accept: 'application/json') to the headers
and, to avoid ambiguity with empty POSTs, include an explicit empty JSON body
(e.g., JSON.stringify({})); update the headers object in the fetch call inside
generatePersonalityProfiles to include these keys so the request is consistent
with other API calls.
📝 Description
This PR introduces post-debate personality analysis for debate participants (human and AI), providing structured, AI-generated insights into how participants argue rather than what they argue.
The feature analyzes completed debate transcripts, generates personality profiles per participant, and displays the results in the UI to support reflection and learning.
🎯 Motivation
DebateAI focuses on improving debating and critical-thinking skills. While current debates capture arguments and outcomes, users lack post-debate feedback on their argumentation style, tone, clarity, and responsiveness.
This change adds a reflection layer by generating Debate Personality Profiles after debate completion, aligned with DebateAI’s educational and AI-assisted goals.
⚙️ Proposed Changes
Backend
➕ New Files
personality.go
Defines PersonalityProfile model:
ID: primitive.ObjectID
DebateID: primitive.ObjectID (linked to saved_debate_transcripts)
ParticipantID: string (user email or bot name)
ArgumentStyle: string
Tone: string
EvidenceUsage: int (0–10)
Clarity: int (0–10)
Responsiveness: int (0–10)
Summary: string
CreatedAt: time.Time
personality_service.go
Implements GeneratePersonalityProfiles(debateID string):
Fetches debate transcript from saved_debate_transcripts
Ensures debate Result is not "pending"
Groups messages by sender (User / Bot / Opponent)
Constructs participant-specific transcript
Calls AI with a structured prompt
Parses JSON response into PersonalityProfile
Persists results to personality_profiles collection
personality_controller.go
Handles:
✏️ Modified Files
debate.go
Adds route:
POST /api/debates/:id/personality
Frontend
➕ New Components
PersonalityProfile.tsx
Renders participant personality traits
Uses radar chart or bar indicators
Displays AI-generated disclaimer clearly
✏️ Modified Components
ViewDebate.tsx
🧪 Verification Plan
Automated Testing
Manual Testing
Complete a debate (User vs User or User vs Bot)
Trigger personality analysis
Open View Debate
Verify:
📦 Scope & Design Notes
Screenshot:
🚀 Future Extensions (Not Included)
🤝 Contribution Note
This PR is intended as a minimal, reviewable MVP. I’m happy to iterate on:
Feedback is very welcome.
Summary by CodeRabbit
Release Notes
closes #184
✏️ Tip: You can customize this high-level summary in your review settings.