diff --git a/.DS_Store b/.DS_Store
index f32eb597..ec5e5dcc 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/backend/app/routes/__pycache__/personas.cpython-313.pyc b/backend/app/routes/__pycache__/personas.cpython-313.pyc
index c7bd2b98..4e303915 100644
Binary files a/backend/app/routes/__pycache__/personas.cpython-313.pyc and b/backend/app/routes/__pycache__/personas.cpython-313.pyc differ
diff --git a/backend/app/routes/personas.py b/backend/app/routes/personas.py
index 4bbae4a7..8d70b67e 100644
--- a/backend/app/routes/personas.py
+++ b/backend/app/routes/personas.py
@@ -165,6 +165,7 @@ async def modify_persona_with_ai(persona_id):
- llm_model: Model to use (defaults to 'gemini-2.5-pro')
- reasoning_effort: For GPT-5 (minimal, low, medium, high)
- verbosity: For GPT-5 (low, medium, high)
+ - preview_only: If true, returns modified data without saving to database (defaults to false)
"""
try:
# Get request data
@@ -179,8 +180,10 @@ async def modify_persona_with_ai(persona_id):
llm_model = request_data.get('llm_model', 'gemini-2.5-pro')
reasoning_effort = request_data.get('reasoning_effort', 'medium')
verbosity = request_data.get('verbosity', 'medium')
+ preview_only = request_data.get('preview_only', False)
- print(f"🤖 Backend: Modifying persona {persona_id} with {llm_model}")
+ mode_text = "previewing" if preview_only else "modifying"
+ print(f"🤖 Backend: {mode_text.title()} persona {persona_id} with {llm_model}")
print(f"📝 Modification prompt: {modification_prompt[:100]}...")
# Call the modification service
@@ -189,13 +192,16 @@ async def modify_persona_with_ai(persona_id):
modification_prompt=modification_prompt,
llm_model=llm_model,
reasoning_effort=reasoning_effort,
- verbosity=verbosity
+ verbosity=verbosity,
+ preview_only=preview_only
)
+ success_message = "Persona preview generated successfully" if preview_only else "Persona modified successfully"
return jsonify({
"success": True,
- "message": "Persona modified successfully",
- "persona": make_serializable(modified_persona_data)
+ "message": success_message,
+ "persona": make_serializable(modified_persona_data),
+ "preview_only": preview_only
}), 200
except PersonaModificationError as e:
diff --git a/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc b/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc
index 3adfedcf..b377fea4 100644
Binary files a/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc and b/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc differ
diff --git a/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc b/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc
index 03bad893..662000db 100644
Binary files a/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc and b/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc differ
diff --git a/backend/app/services/ai_persona_service.py b/backend/app/services/ai_persona_service.py
index 7211967f..aec3fb4b 100644
--- a/backend/app/services/ai_persona_service.py
+++ b/backend/app/services/ai_persona_service.py
@@ -178,6 +178,20 @@ async def generate_basic_personas(
raise PersonaGenerationError(
f"Persona {i+1} is missing required fields: {', '.join(missing_fields)}"
)
+
+ # Validate that age is a single number, not a range
+ age_value = persona.get("age", "")
+ if isinstance(age_value, str) and "-" in age_value:
+ raise PersonaGenerationError(
+ f"Persona {i+1} has an invalid age range '{age_value}'. Age must be a single specific number (e.g., '35', not '35-42')"
+ )
+
+ # Validate that age is numeric
+ age_str = str(age_value).strip()
+ if not age_str.isdigit():
+ raise PersonaGenerationError(
+ f"Persona {i+1} has an invalid age '{age_value}'. Age must be a numeric value (e.g., '35')"
+ )
return personas_array
@@ -271,6 +285,20 @@ async def generate_persona(
if missing_fields:
raise PersonaGenerationError(f"Generated persona is missing required fields: {', '.join(missing_fields)}")
+ # Validate that age is a single number, not a range
+ age_value = persona_data.get("age", "")
+ if isinstance(age_value, str) and "-" in age_value:
+ raise PersonaGenerationError(
+ f"Generated persona has an invalid age range '{age_value}'. Age must be a single specific number (e.g., '35', not '35-42')"
+ )
+
+ # Validate that age is numeric
+ age_str = str(age_value).strip()
+ if not age_str.isdigit():
+ raise PersonaGenerationError(
+ f"Generated persona has an invalid age '{age_value}'. Age must be a numeric value (e.g., '35')"
+ )
+
# Generate ID if missing
if "id" not in persona_data:
persona_data["id"] = f"generated-{uuid.uuid4()}"
diff --git a/backend/app/services/persona_modification_service.py b/backend/app/services/persona_modification_service.py
index 1a25f7af..bad46854 100644
--- a/backend/app/services/persona_modification_service.py
+++ b/backend/app/services/persona_modification_service.py
@@ -137,7 +137,8 @@ class PersonaModificationService:
llm_model: str = 'gemini-2.5-pro',
reasoning_effort: str = 'medium',
verbosity: str = 'medium',
- max_retries: int = 3
+ max_retries: int = 3,
+ preview_only: bool = False
) -> Dict[str, Any]:
"""
Modify a persona using AI based on natural language instructions.
@@ -149,6 +150,7 @@ class PersonaModificationService:
reasoning_effort: Reasoning effort for GPT-5 (minimal, low, medium, high)
verbosity: Response verbosity for GPT-5 (low, medium, high)
max_retries: Maximum number of retries for invalid responses
+ preview_only: If True, returns modified data without saving to database
Returns:
Dictionary containing the modified persona data
@@ -211,13 +213,16 @@ class PersonaModificationService:
sanitized_persona, modified_persona_data
)
- # Update the persona in the database
- success = await Persona.update(persona_id, modified_persona_data)
- if not success:
- raise PersonaModificationError("Failed to update persona in database")
+ # Update the persona in the database (only if not preview mode)
+ if not preview_only:
+ success = await Persona.update(persona_id, modified_persona_data)
+ if not success:
+ raise PersonaModificationError("Failed to update persona in database")
+ logger.info(f"Successfully modified persona {persona_id}")
+ else:
+ logger.info(f"Generated preview for persona {persona_id} (not saved to database)")
# Return the modified persona data
- logger.info(f"Successfully modified persona {persona_id}")
return modified_persona_data
except LLMServiceError as e:
diff --git a/backend/prompts/persona-basic-generation.md b/backend/prompts/persona-basic-generation.md
index 3aabd1bd..0a30d543 100644
--- a/backend/prompts/persona-basic-generation.md
+++ b/backend/prompts/persona-basic-generation.md
@@ -51,6 +51,8 @@ EXAMPLE_JSON_START
]
EXAMPLE_JSON_END
+CRITICAL AGE REQUIREMENT: The "age" field MUST contain a single, specific number (e.g., "35", "42") representing the persona's exact age. DO NOT use age ranges (e.g., "35-42", "30-35"). These are individual personas and each person has one specific age, not a range.
+
IMPORTANT:
- Return EXACTLY {count} personas in a JSON array format
- Do not include any comments (like "// Second persona") in the JSON
diff --git a/backend/prompts/persona-detailed-generation.md b/backend/prompts/persona-detailed-generation.md
index 6fc86608..184c38fc 100644
--- a/backend/prompts/persona-detailed-generation.md
+++ b/backend/prompts/persona-detailed-generation.md
@@ -35,7 +35,7 @@ EXAMPLE_JSON_START
{
"id": "generated-[unique-id]",
"name": "[Full Name]",
- "age": "[Age Range]",
+ "age": "[Specific Age]",
"gender": "[Gender]",
"occupation": "[Job Title]",
"education": "[Education Level]",
@@ -106,4 +106,6 @@ EXAMPLE_JSON_START
}
EXAMPLE_JSON_END
+CRITICAL AGE REQUIREMENT: The "age" field MUST contain a single, specific number (e.g., "35", "42") as a string, representing the persona's exact age. DO NOT use age ranges (e.g., "35-42", "30-35"). These are individual personas and each person has one specific age, not a range.
+
IMPORTANT: Return ONLY the JSON object with no additional text, explanations, or formatting.
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index 081a98f6..e505d8ce 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/src/components/FocusGroupModerator.tsx b/src/components/FocusGroupModerator.tsx
index 075a1047..b7ee3682 100644
--- a/src/components/FocusGroupModerator.tsx
+++ b/src/components/FocusGroupModerator.tsx
@@ -1556,7 +1556,7 @@ true;
- Choose which AI model to use for generating responses and discussion guides
+ Choose which AI model to use for generating responses, discussion guides, and thematic analysis
diff --git a/src/components/persona/PersonaModificationModal.tsx b/src/components/persona/PersonaModificationModal.tsx
index a61ef578..37065721 100644
--- a/src/components/persona/PersonaModificationModal.tsx
+++ b/src/components/persona/PersonaModificationModal.tsx
@@ -50,14 +50,14 @@ interface PersonaModificationModalProps {
persona: Persona;
isOpen: boolean;
onClose: () => void;
- onPersonaModified: (modifiedPersona: Persona) => void;
+ onPersonaPreview: (modifiedPersona: Persona) => void;
}
export default function PersonaModificationModal({
persona,
isOpen,
onClose,
- onPersonaModified
+ onPersonaPreview
}: PersonaModificationModalProps) {
const [isProcessing, setIsProcessing] = useState(false);
@@ -81,8 +81,8 @@ export default function PersonaModificationModal({
setIsProcessing(true);
try {
- toastService.info("Modifying persona with AI...", {
- description: `Using ${values.llm_model} to process your modification request`
+ toastService.info("Generating persona preview...", {
+ description: `Using ${values.llm_model} to create a preview of your modifications`
});
// Use the persona's MongoDB _id or fallback to id
@@ -92,26 +92,27 @@ export default function PersonaModificationModal({
modification_prompt: values.modificationPrompt,
llm_model: values.llm_model,
reasoning_effort: values.reasoning_effort || 'medium',
- verbosity: values.verbosity || 'medium'
+ verbosity: values.verbosity || 'medium',
+ preview_only: true
});
if (response.data && response.data.persona) {
- toastService.success("Persona modified successfully!", {
- description: `${persona.name} has been updated with AI modifications`
+ toastService.success("Preview generated successfully!", {
+ description: `Ready to review proposed changes to ${persona.name}`
});
- onPersonaModified(response.data.persona);
+ onPersonaPreview(response.data.persona);
handleClose();
} else {
throw new Error("Invalid response from server");
}
} catch (error: any) {
- console.error("Error modifying persona:", error);
+ console.error("Error generating persona preview:", error);
if (error.response) {
const errorMessage = error.response.data?.error || "Server error occurred";
- toastService.error("Failed to modify persona", {
+ toastService.error("Failed to generate preview", {
description: errorMessage
});
} else if (error.request) {
@@ -119,7 +120,7 @@ export default function PersonaModificationModal({
description: "Unable to connect to the server"
});
} else {
- toastService.error("Modification failed", {
+ toastService.error("Preview generation failed", {
description: error.message || "An unexpected error occurred"
});
}
@@ -283,12 +284,12 @@ export default function PersonaModificationModal({
{isProcessing ? (
<>
- Processing...
+ Generating Preview...
>
) : (
<>
- Process Persona Modification
+ Generate Preview
>
)}
diff --git a/src/components/persona/PersonaProfile.tsx b/src/components/persona/PersonaProfile.tsx
index e195cc9f..32ecbae8 100644
--- a/src/components/persona/PersonaProfile.tsx
+++ b/src/components/persona/PersonaProfile.tsx
@@ -1,5 +1,5 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import Navigation from '@/components/Navigation';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -30,12 +30,17 @@ import { usePersonaDetails } from '@/hooks/usePersonaDetails';
export default function PersonaProfile() {
const {
currentPersona,
+ displayPersona,
isEditing,
isFromReview,
isLoading,
+ isReviewMode,
setIsEditing,
handleGoBack,
- handleSaveEdit
+ handleSaveEdit,
+ enterReviewMode,
+ exitReviewMode,
+ saveReviewedPersona
} = usePersonaDetails();
const { navigationState } = useNavigation();
@@ -43,6 +48,25 @@ export default function PersonaProfile() {
const [isExporting, setIsExporting] = useState(false);
const [isModificationModalOpen, setIsModificationModalOpen] = useState(false);
+ // Navigation blocking during review mode
+ useEffect(() => {
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (isReviewMode) {
+ e.preventDefault();
+ e.returnValue = "You have unsaved changes. Your modifications will be lost if you leave.";
+ return e.returnValue;
+ }
+ };
+
+ if (isReviewMode) {
+ window.addEventListener('beforeunload', handleBeforeUnload);
+ }
+
+ return () => {
+ window.removeEventListener('beforeunload', handleBeforeUnload);
+ };
+ }, [isReviewMode]);
+
// Fetch focus group name if coming from focus group session
useEffect(() => {
if (navigationState.focusGroupId && navigationState.previousRoute?.startsWith('/focus-groups/')) {
@@ -196,43 +220,85 @@ export default function PersonaProfile() {
)}
+ {/* Review Mode Banner */}
+ {isReviewMode && (
+
+
+
+
+ 📝 Reviewing proposed changes to {displayPersona?.name} - Save or cancel to continue
+