added persona modification service - users can adjust individual personas with NLP via LLM
This commit is contained in:
parent
fbef4f42f6
commit
065c368aeb
14 changed files with 808 additions and 152 deletions
20
.env
20
.env
|
|
@ -1,13 +1,13 @@
|
|||
# Production Environment Configuration
|
||||
# Frontend URL (production server)
|
||||
VITE_FRONTEND_BASE_URL=https://ai-sandbox.oliver.solutions/semblance
|
||||
# Development Environment Configuration
|
||||
# Frontend URL (local development)
|
||||
VITE_FRONTEND_BASE_URL=http://localhost:5173
|
||||
|
||||
# Backend API URL (production server)
|
||||
VITE_API_BASE_URL=https://ai-sandbox.oliver.solutions/semblance_back/api
|
||||
# Backend API URL (local development - no base path)
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# WebSocket path (production server)
|
||||
VITE_WEBSOCKET_PATH=/semblance_back/socket.io/
|
||||
# WebSocket path (local development - no base path)
|
||||
VITE_WEBSOCKET_PATH=/socket.io/
|
||||
|
||||
# MSAL Authentication (production server)
|
||||
VITE_MSAL_REDIRECT_URI=https://ai-sandbox.oliver.solutions/semblance
|
||||
VITE_MSAL_POST_LOGOUT_REDIRECT_URI=https://ai-sandbox.oliver.solutions/semblance
|
||||
# MSAL Authentication (local development)
|
||||
VITE_MSAL_REDIRECT_URI=http://localhost:5173/
|
||||
VITE_MSAL_POST_LOGOUT_REDIRECT_URI=http://localhost:5173/
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
venv/
|
||||
*pycache*/
|
||||
Binary file not shown.
|
|
@ -2,6 +2,7 @@ from flask import Blueprint, request, jsonify
|
|||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from app.models.persona import Persona
|
||||
from app.services.persona_export_service import PersonaExportService
|
||||
from app.services.persona_modification_service import PersonaModificationService, PersonaModificationError
|
||||
from bson import ObjectId
|
||||
import datetime
|
||||
|
||||
|
|
@ -153,6 +154,57 @@ def create_multiple_personas():
|
|||
"persona_ids": persona_ids
|
||||
}), 201
|
||||
|
||||
@personas_bp.route('/<persona_id>/modify-with-ai', methods=['POST'])
|
||||
@jwt_required(optional=True) # Make JWT optional for development
|
||||
def modify_persona_with_ai(persona_id):
|
||||
"""
|
||||
Modify a persona using AI based on natural language instructions.
|
||||
|
||||
Request body should include:
|
||||
- modification_prompt: Natural language description of desired changes
|
||||
- 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)
|
||||
"""
|
||||
try:
|
||||
# Get request data
|
||||
request_data = request.get_json()
|
||||
if not request_data:
|
||||
return jsonify({"error": "No request data provided"}), 400
|
||||
|
||||
modification_prompt = request_data.get('modification_prompt')
|
||||
if not modification_prompt:
|
||||
return jsonify({"error": "modification_prompt is required"}), 400
|
||||
|
||||
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')
|
||||
|
||||
print(f"🤖 Backend: Modifying persona {persona_id} with {llm_model}")
|
||||
print(f"📝 Modification prompt: {modification_prompt[:100]}...")
|
||||
|
||||
# Call the modification service
|
||||
modified_persona_data = PersonaModificationService.modify_persona(
|
||||
persona_id=persona_id,
|
||||
modification_prompt=modification_prompt,
|
||||
llm_model=llm_model,
|
||||
reasoning_effort=reasoning_effort,
|
||||
verbosity=verbosity
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Persona modified successfully",
|
||||
"persona": make_serializable(modified_persona_data)
|
||||
}), 200
|
||||
|
||||
except PersonaModificationError as e:
|
||||
print(f"❌ Persona modification error: {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error in persona modification: {e}")
|
||||
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
||||
|
||||
@personas_bp.route('/<persona_id>/export-profile', methods=['POST'])
|
||||
@jwt_required(optional=True) # Make JWT optional for development
|
||||
def export_persona_profile(persona_id):
|
||||
|
|
|
|||
Binary file not shown.
231
backend/app/services/persona_modification_service.py
Normal file
231
backend/app/services/persona_modification_service.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"""
|
||||
Persona Modification Service
|
||||
|
||||
This service handles AI-powered modification of existing personas using natural language instructions.
|
||||
It integrates with the LLM service to process modification requests while maintaining data integrity
|
||||
and internal consistency of persona attributes.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from .llm_service import LLMService, LLMServiceError
|
||||
from app.utils.prompt_loader import load_prompt, PromptLoaderError
|
||||
from app.models.persona import Persona
|
||||
from bson import ObjectId
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PersonaModificationError(Exception):
|
||||
"""Exception raised for errors in the persona modification process."""
|
||||
pass
|
||||
|
||||
class PersonaModificationService:
|
||||
"""Service for modifying personas using AI."""
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_persona_for_json(persona_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Sanitize persona data to make it JSON serializable for the LLM prompt.
|
||||
|
||||
Args:
|
||||
persona_data: The persona data dictionary that may contain non-serializable objects
|
||||
|
||||
Returns:
|
||||
A sanitized dictionary that can be JSON serialized
|
||||
"""
|
||||
sanitized = {}
|
||||
|
||||
for key, value in persona_data.items():
|
||||
if isinstance(value, ObjectId):
|
||||
# Convert ObjectId to string
|
||||
sanitized[key] = str(value)
|
||||
elif isinstance(value, datetime):
|
||||
# Convert datetime to ISO string
|
||||
sanitized[key] = value.isoformat()
|
||||
elif isinstance(value, dict):
|
||||
# Recursively sanitize nested dictionaries
|
||||
sanitized[key] = PersonaModificationService._sanitize_persona_for_json(value)
|
||||
elif isinstance(value, list):
|
||||
# Sanitize list items
|
||||
sanitized_list = []
|
||||
for item in value:
|
||||
if isinstance(item, dict):
|
||||
sanitized_list.append(PersonaModificationService._sanitize_persona_for_json(item))
|
||||
elif isinstance(item, ObjectId):
|
||||
sanitized_list.append(str(item))
|
||||
elif isinstance(item, datetime):
|
||||
sanitized_list.append(item.isoformat())
|
||||
else:
|
||||
sanitized_list.append(item)
|
||||
sanitized[key] = sanitized_list
|
||||
else:
|
||||
# Keep other values as-is
|
||||
sanitized[key] = value
|
||||
|
||||
return sanitized
|
||||
|
||||
@staticmethod
|
||||
def _protect_readonly_fields(original_persona: Dict[str, Any], modified_persona: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Protect readonly fields from being modified by the LLM.
|
||||
|
||||
Args:
|
||||
original_persona: The original persona data
|
||||
modified_persona: The LLM-modified persona data
|
||||
|
||||
Returns:
|
||||
Modified persona with readonly fields restored from original
|
||||
"""
|
||||
# List of fields that should never be modified
|
||||
protected_fields = ['id', '_id', 'created_at', 'created_by']
|
||||
|
||||
for field in protected_fields:
|
||||
if field in original_persona:
|
||||
modified_persona[field] = original_persona[field]
|
||||
|
||||
# Ensure updated_at is set to current time
|
||||
modified_persona['updated_at'] = datetime.utcnow().isoformat()
|
||||
|
||||
return modified_persona
|
||||
|
||||
@staticmethod
|
||||
def _validate_persona_structure(persona_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Validate that the modified persona contains all required fields.
|
||||
|
||||
Args:
|
||||
persona_data: The persona data to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
required_fields = ['name', 'age', 'gender', 'occupation', 'location', 'personality']
|
||||
|
||||
for field in required_fields:
|
||||
if field not in persona_data or persona_data[field] is None:
|
||||
logger.error(f"Missing required field: {field}")
|
||||
return False
|
||||
|
||||
# Validate numeric fields are within expected ranges
|
||||
numeric_fields = {
|
||||
'techSavviness': (0, 100),
|
||||
'brandLoyalty': (0, 100),
|
||||
'priceConsciousness': (0, 100),
|
||||
'environmentalConcern': (0, 100)
|
||||
}
|
||||
|
||||
for field, (min_val, max_val) in numeric_fields.items():
|
||||
if field in persona_data:
|
||||
try:
|
||||
value = int(persona_data[field])
|
||||
if not (min_val <= value <= max_val):
|
||||
logger.error(f"Field {field} value {value} out of range [{min_val}, {max_val}]")
|
||||
return False
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Field {field} is not a valid number")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def modify_persona(
|
||||
persona_id: str,
|
||||
modification_prompt: str,
|
||||
llm_model: str = 'gemini-2.5-pro',
|
||||
reasoning_effort: str = 'medium',
|
||||
verbosity: str = 'medium',
|
||||
max_retries: int = 3
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Modify a persona using AI based on natural language instructions.
|
||||
|
||||
Args:
|
||||
persona_id: The ID of the persona to modify
|
||||
modification_prompt: Natural language description of desired changes
|
||||
llm_model: The LLM model to use for modification
|
||||
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
|
||||
|
||||
Returns:
|
||||
Dictionary containing the modified persona data
|
||||
|
||||
Raises:
|
||||
PersonaModificationError: If modification fails or validation fails
|
||||
"""
|
||||
try:
|
||||
# Fetch the original persona
|
||||
original_persona = Persona.find_by_id(persona_id)
|
||||
if not original_persona:
|
||||
raise PersonaModificationError(f"Persona with ID {persona_id} not found")
|
||||
|
||||
# Convert to dict and sanitize for JSON serialization
|
||||
original_persona_dict = dict(original_persona) if hasattr(original_persona, '_data') else original_persona
|
||||
sanitized_persona = PersonaModificationService._sanitize_persona_for_json(original_persona_dict)
|
||||
|
||||
# Load the modification prompt template
|
||||
try:
|
||||
final_prompt = load_prompt('persona-modification', {
|
||||
'original_persona_json': json.dumps(sanitized_persona, indent=2),
|
||||
'modification_prompt': modification_prompt
|
||||
})
|
||||
except PromptLoaderError as e:
|
||||
logger.error(f"Failed to load persona modification prompt: {e}")
|
||||
raise PersonaModificationError(f"Failed to load modification prompt: {str(e)}")
|
||||
|
||||
# Attempt modification with retries
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"Attempting persona modification (attempt {attempt + 1}/{max_retries})")
|
||||
|
||||
# Call LLM service
|
||||
llm_response = LLMService.generate_content(
|
||||
prompt=final_prompt,
|
||||
temperature=0.3, # Lower temperature for consistent modifications
|
||||
model_name=llm_model,
|
||||
reasoning_effort=reasoning_effort if llm_model == 'gpt-5' else None,
|
||||
verbosity=verbosity if llm_model == 'gpt-5' else None
|
||||
)
|
||||
|
||||
# Parse JSON response
|
||||
try:
|
||||
modified_persona_data = json.loads(llm_response.strip())
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Invalid JSON response on attempt {attempt + 1}: {e}")
|
||||
if attempt == max_retries - 1:
|
||||
raise PersonaModificationError(f"LLM returned invalid JSON after {max_retries} attempts")
|
||||
continue
|
||||
|
||||
# Validate the modified persona structure
|
||||
if not PersonaModificationService._validate_persona_structure(modified_persona_data):
|
||||
logger.warning(f"Invalid persona structure on attempt {attempt + 1}")
|
||||
if attempt == max_retries - 1:
|
||||
raise PersonaModificationError(f"LLM returned invalid persona structure after {max_retries} attempts")
|
||||
continue
|
||||
|
||||
# Protect readonly fields
|
||||
modified_persona_data = PersonaModificationService._protect_readonly_fields(
|
||||
sanitized_persona, modified_persona_data
|
||||
)
|
||||
|
||||
# Update the persona in the database
|
||||
success = Persona.update(persona_id, modified_persona_data)
|
||||
if not success:
|
||||
raise PersonaModificationError("Failed to update persona in database")
|
||||
|
||||
# Return the modified persona data
|
||||
logger.info(f"Successfully modified persona {persona_id}")
|
||||
return modified_persona_data
|
||||
|
||||
except LLMServiceError as e:
|
||||
logger.error(f"LLM service error on attempt {attempt + 1}: {e}")
|
||||
if attempt == max_retries - 1:
|
||||
raise PersonaModificationError(f"LLM service failed after {max_retries} attempts: {str(e)}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during persona modification: {e}")
|
||||
raise PersonaModificationError(f"Persona modification failed: {str(e)}")
|
||||
40
backend/prompts/persona-modification.md
Normal file
40
backend/prompts/persona-modification.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
You are an expert persona modification assistant. Your task is to modify an existing synthetic persona based on natural language instructions while maintaining data integrity, internal consistency, and realistic human characteristics.
|
||||
|
||||
CRITICAL REQUIREMENTS:
|
||||
1. Return ONLY properly formatted JSON with no additional text, explanations, or markdown formatting
|
||||
2. Preserve all original JSON structure and field names exactly as provided
|
||||
3. Maintain internal consistency between personality traits, goals, frustrations, behaviors, and demographics
|
||||
4. Ensure modifications are realistic and authentic for human personas
|
||||
5. Protected fields (id, _id, created_at, created_by) must NEVER be modified
|
||||
|
||||
MODIFICATION GUIDELINES:
|
||||
- Make only the changes requested in the modification prompt
|
||||
- If demographic changes are requested, ensure they align with personality and behavioral traits
|
||||
- If personality changes are requested, update related fields (OCEAN traits, goals, frustrations, scenarios) accordingly
|
||||
- Maintain coherence between all persona elements - demographics, personality, behaviors, and life situations
|
||||
- Keep the persona realistic and avoid stereotypes
|
||||
- If income/economic status changes, adjust related fields like purchasing power, price consciousness, etc.
|
||||
- If tech savviness changes, ensure device usage and communication preferences align
|
||||
- If profession changes, update education, goals, frustrations, and relevant scenarios
|
||||
|
||||
FIELD INTEGRITY RULES:
|
||||
- All numeric fields (techSavviness, brandLoyalty, etc.) must remain within 0-100 range
|
||||
- Boolean fields (hasPurchasingPower, hasChildren) must remain true/false
|
||||
- OCEAN trait scores must sum to realistic human personality patterns
|
||||
- Required fields from the original persona must never be removed
|
||||
- Maintain consistent formatting for dates, arrays, and nested objects
|
||||
|
||||
INTERNAL CONSISTENCY CHECKS:
|
||||
- If age changes significantly, adjust life stage-appropriate goals, scenarios, and tech usage
|
||||
- If location changes, consider cultural and economic implications
|
||||
- If education level changes, ensure occupation and interests are appropriate
|
||||
- If family status changes (hasChildren), update relevant scenarios and priorities
|
||||
- If environmental concern changes, reflect in lifestyle choices and brand preferences
|
||||
|
||||
Original Persona Data:
|
||||
{original_persona_json}
|
||||
|
||||
Modification Instructions:
|
||||
{modification_prompt}
|
||||
|
||||
Return the complete modified persona JSON with all original fields preserved and only the requested changes applied. Ensure the persona remains internally consistent and realistic.
|
||||
1
dist/assets/index-C-lVT2hb.css
vendored
1
dist/assets/index-C-lVT2hb.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/index-QYazl09u.css
vendored
Normal file
1
dist/assets/index-QYazl09u.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -7,8 +7,8 @@
|
|||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<meta property="og:image" content="/og-image.png" />
|
||||
<script type="module" crossorigin src="/semblance/assets/index-NobeZ-BW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-C-lVT2hb.css">
|
||||
<script type="module" crossorigin src="/semblance/assets/index-D1F9eUHr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-QYazl09u.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
301
src/components/persona/PersonaModificationModal.tsx
Normal file
301
src/components/persona/PersonaModificationModal.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import React, { useState } from 'react';
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Loader2, Bot, Wand2 } from 'lucide-react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { personasApi } from '@/lib/api';
|
||||
import { toastService } from '@/lib/toast';
|
||||
import { Persona } from '@/types/persona';
|
||||
|
||||
const modificationFormSchema = z.object({
|
||||
modificationPrompt: z.string().min(10, {
|
||||
message: "Modification prompt must be at least 10 characters long.",
|
||||
}),
|
||||
llm_model: z.string().min(1, {
|
||||
message: "Please select an AI model.",
|
||||
}),
|
||||
reasoning_effort: z.string().optional(),
|
||||
verbosity: z.string().optional(),
|
||||
});
|
||||
|
||||
type ModificationFormData = z.infer<typeof modificationFormSchema>;
|
||||
|
||||
interface PersonaModificationModalProps {
|
||||
persona: Persona;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onPersonaModified: (modifiedPersona: Persona) => void;
|
||||
}
|
||||
|
||||
export default function PersonaModificationModal({
|
||||
persona,
|
||||
isOpen,
|
||||
onClose,
|
||||
onPersonaModified
|
||||
}: PersonaModificationModalProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const form = useForm<ModificationFormData>({
|
||||
resolver: zodResolver(modificationFormSchema),
|
||||
defaultValues: {
|
||||
modificationPrompt: "",
|
||||
llm_model: "gemini-2.5-pro",
|
||||
reasoning_effort: "medium",
|
||||
verbosity: "medium",
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
if (isProcessing) return; // Prevent closing while processing
|
||||
form.reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onSubmit = async (values: ModificationFormData) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
toastService.info("Modifying persona with AI...", {
|
||||
description: `Using ${values.llm_model} to process your modification request`
|
||||
});
|
||||
|
||||
// Use the persona's MongoDB _id or fallback to id
|
||||
const personaId = persona._id || persona.id;
|
||||
|
||||
const response = await personasApi.modifyWithAI(personaId, {
|
||||
modification_prompt: values.modificationPrompt,
|
||||
llm_model: values.llm_model,
|
||||
reasoning_effort: values.reasoning_effort || 'medium',
|
||||
verbosity: values.verbosity || 'medium'
|
||||
});
|
||||
|
||||
if (response.data && response.data.persona) {
|
||||
toastService.success("Persona modified successfully!", {
|
||||
description: `${persona.name} has been updated with AI modifications`
|
||||
});
|
||||
|
||||
onPersonaModified(response.data.persona);
|
||||
handleClose();
|
||||
} else {
|
||||
throw new Error("Invalid response from server");
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error modifying persona:", error);
|
||||
|
||||
if (error.response) {
|
||||
const errorMessage = error.response.data?.error || "Server error occurred";
|
||||
toastService.error("Failed to modify persona", {
|
||||
description: errorMessage
|
||||
});
|
||||
} else if (error.request) {
|
||||
toastService.error("Network error", {
|
||||
description: "Unable to connect to the server"
|
||||
});
|
||||
} else {
|
||||
toastService.error("Modification failed", {
|
||||
description: error.message || "An unexpected error occurred"
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3">
|
||||
<Bot className="h-5 w-5 text-primary" />
|
||||
Modify Persona with Natural Language
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Describe the changes you'd like to make to <strong>{persona.name}</strong>,
|
||||
and AI will modify the persona data accordingly.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Modification Prompt */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="modificationPrompt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Modification Instructions</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="E.g., 'Make this person more tech-savvy and interested in sustainable products' or 'Increase their income level and add marketing experience'"
|
||||
className="min-h-[120px]"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Describe what aspects of the persona you'd like to modify or enhance
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* AI Model Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="llm_model"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AI Model</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select AI model" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="gemini-2.5-pro">Gemini 2.5 Pro</SelectItem>
|
||||
<SelectItem value="gpt-4.1">GPT-4.1</SelectItem>
|
||||
<SelectItem value="gpt-5">GPT-5</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Choose which AI model to use for modifying the persona
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* GPT-5 specific parameters */}
|
||||
{form.watch("llm_model") === "gpt-5" && (
|
||||
<>
|
||||
{/* Reasoning Effort Parameter */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="reasoning_effort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Reasoning Effort</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select reasoning effort" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="minimal">Minimal - Fast responses</SelectItem>
|
||||
<SelectItem value="low">Low - Quick thinking</SelectItem>
|
||||
<SelectItem value="medium">Medium - Balanced (default)</SelectItem>
|
||||
<SelectItem value="high">High - Deep reasoning</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Controls how much the AI thinks through the modification
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Verbosity Parameter */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="verbosity"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Response Detail</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select verbosity level" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Concise</SelectItem>
|
||||
<SelectItem value="medium">Balanced (default)</SelectItem>
|
||||
<SelectItem value="high">Detailed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Controls how detailed the AI's modifications will be
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isProcessing}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="h-4 w-4" />
|
||||
Process Persona Modification
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { ArrowLeft, Edit, Home, Users, User, Download } from 'lucide-react';
|
||||
import { ArrowLeft, Edit, Home, Users, User, Download, Bot } from 'lucide-react';
|
||||
import { useNavigation } from '@/contexts/NavigationContext';
|
||||
import { focusGroupsApi, personasApi } from '@/lib/api';
|
||||
import { toastService } from '@/lib/toast';
|
||||
|
|
@ -23,6 +23,7 @@ import { PersonaScenarios } from './PersonaScenarios';
|
|||
import { PersonaNotFound } from './PersonaNotFound';
|
||||
import { PersonaProfileSkeleton } from './PersonaProfileSkeleton';
|
||||
import PersonaEditor from './PersonaEditor';
|
||||
import PersonaModificationModal from './PersonaModificationModal';
|
||||
import { usePersonaDetails } from '@/hooks/usePersonaDetails';
|
||||
|
||||
export default function PersonaProfile() {
|
||||
|
|
@ -39,6 +40,7 @@ export default function PersonaProfile() {
|
|||
const { navigationState } = useNavigation();
|
||||
const [focusGroupName, setFocusGroupName] = useState<string>('');
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isModificationModalOpen, setIsModificationModalOpen] = useState(false);
|
||||
|
||||
// Fetch focus group name if coming from focus group session
|
||||
useEffect(() => {
|
||||
|
|
@ -212,6 +214,14 @@ export default function PersonaProfile() {
|
|||
<Download className="h-4 w-4 mr-2" />
|
||||
{isExporting ? 'Generating...' : 'Download Profile'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsModificationModalOpen(true)}
|
||||
className="hover-transition"
|
||||
>
|
||||
<Bot className="h-4 w-4 mr-2" />
|
||||
Modify with AI
|
||||
</Button>
|
||||
<Button onClick={() => setIsEditing(true)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit Persona
|
||||
|
|
@ -248,6 +258,20 @@ export default function PersonaProfile() {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Persona Modification Modal */}
|
||||
{currentPersona && (
|
||||
<PersonaModificationModal
|
||||
persona={currentPersona}
|
||||
isOpen={isModificationModalOpen}
|
||||
onClose={() => setIsModificationModalOpen(false)}
|
||||
onPersonaModified={(modifiedPersona) => {
|
||||
// Update the current persona with the modified data
|
||||
// This will refresh the persona detail view
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -143,6 +143,12 @@ export const personasApi = {
|
|||
}
|
||||
return api.put(`/personas/${id}`, personaData);
|
||||
},
|
||||
|
||||
modifyWithAI: (id: string, modificationData: any) => {
|
||||
const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id;
|
||||
console.log(`Modifying persona with AI, ID: ${personaId}`);
|
||||
return api.post(`/personas/${personaId}/modify-with-ai`, modificationData);
|
||||
},
|
||||
|
||||
delete: (id: string) => {
|
||||
// Handle different ID formats
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue