fixed folders again, bug fixes for gpt-5, adjusted response length calculation, cosmetic UI changes, other bug fixes
This commit is contained in:
parent
fbb444037a
commit
da8639aee8
23 changed files with 436 additions and 437 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
BIN
backend/app/.DS_Store
vendored
BIN
backend/app/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
|
|
@ -769,6 +769,25 @@ class FocusGroup:
|
|||
print(traceback.format_exc())
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def clear_uploaded_assets(focus_group_id):
|
||||
"""Clear all uploaded assets for a focus group from database."""
|
||||
db = get_db()
|
||||
try:
|
||||
result = db.focus_groups.update_one(
|
||||
{"_id": ObjectId(focus_group_id)},
|
||||
{
|
||||
"$unset": {"uploaded_assets": ""},
|
||||
"$set": {"updated_at": datetime.utcnow()}
|
||||
}
|
||||
)
|
||||
|
||||
return result.modified_count > 0
|
||||
except Exception as e:
|
||||
print(f"Error clearing uploaded assets for focus group {focus_group_id}: {e}")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _activate_visual_assets(focus_group_id, asset_filenames, message_id):
|
||||
"""Internal method to activate visual assets in conversation context."""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -423,7 +423,8 @@ def update_focus_group(focus_group_id):
|
|||
with open('/tmp/focus_group_debug.log', 'a') as f:
|
||||
f.write(log_msg)
|
||||
f.flush()
|
||||
print(f"🔧 FOCUS GROUP UPDATE DATA: {data}")
|
||||
# Removed verbose data logging to reduce log noise
|
||||
# print(f"🔧 FOCUS GROUP UPDATE DATA: {data}")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
@ -1162,6 +1163,10 @@ def upload_assets(focus_group_id):
|
|||
try:
|
||||
logger.debug(f"=== UPLOAD ASSETS API called for focus group {focus_group_id} ===")
|
||||
|
||||
# Check for replace flag
|
||||
replace_existing = request.form.get('replace', '').lower() == 'true'
|
||||
logger.info(f"Replace existing assets flag: {replace_existing}")
|
||||
|
||||
# Set up temporary directory for file processing (optional)
|
||||
temp_dir = setup_temp_directory()
|
||||
if temp_dir:
|
||||
|
|
@ -1175,6 +1180,55 @@ def upload_assets(focus_group_id):
|
|||
logger.warning(f"Focus group not found: {focus_group_id}")
|
||||
return jsonify({"error": "Focus group not found"}), 404
|
||||
|
||||
# If replace flag is set, clear existing assets first
|
||||
if replace_existing:
|
||||
logger.info("Replace flag set - clearing existing assets")
|
||||
existing_assets = focus_group.get('uploaded_assets', [])
|
||||
if existing_assets:
|
||||
logger.info(f"Deleting {len(existing_assets)} existing assets")
|
||||
|
||||
# Delete existing files from disk
|
||||
for existing_asset in existing_assets:
|
||||
filename = existing_asset.get('filename')
|
||||
if filename:
|
||||
# Try to delete from both possible locations
|
||||
file_deleted = False
|
||||
|
||||
# Try subdirectory location first
|
||||
upload_dir = get_upload_folder(focus_group_id)
|
||||
subdirectory_path = os.path.join(upload_dir, filename)
|
||||
if os.path.exists(subdirectory_path):
|
||||
try:
|
||||
os.remove(subdirectory_path)
|
||||
file_deleted = True
|
||||
logger.info(f"Deleted existing asset from subdirectory: {filename}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete {filename} from subdirectory: {e}")
|
||||
|
||||
# Try flat storage location
|
||||
if not file_deleted:
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
main_upload_dir = os.path.join(base_dir, 'uploads')
|
||||
flat_path = os.path.join(main_upload_dir, filename)
|
||||
if os.path.exists(flat_path):
|
||||
try:
|
||||
os.remove(flat_path)
|
||||
file_deleted = True
|
||||
logger.info(f"Deleted existing asset from main uploads: {filename}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete {filename} from main uploads: {e}")
|
||||
|
||||
if not file_deleted:
|
||||
logger.warning(f"Could not find or delete existing asset file: {filename}")
|
||||
|
||||
# Clear assets from database
|
||||
success = FocusGroup.clear_uploaded_assets(focus_group_id)
|
||||
if success:
|
||||
logger.info("Successfully cleared existing assets from database")
|
||||
else:
|
||||
logger.error("Failed to clear existing assets from database")
|
||||
return jsonify({"error": "Failed to clear existing assets"}), 500
|
||||
|
||||
# Try standard Flask file processing first (since temp directories are now working)
|
||||
files = None
|
||||
flask_processing_failed = False
|
||||
|
|
|
|||
|
|
@ -23,15 +23,10 @@ folders_bp = Blueprint('folders', __name__)
|
|||
@folders_bp.route('/', methods=['GET'])
|
||||
@jwt_required(optional=True) # Make JWT optional for development
|
||||
def get_folders():
|
||||
"""Get all folders for the current user."""
|
||||
"""Get all folders - shared across all users."""
|
||||
try:
|
||||
user_id = get_jwt_identity()
|
||||
if user_id:
|
||||
# If authenticated, get user's folders
|
||||
folders = Folder.find_by_user(user_id)
|
||||
else:
|
||||
# For development, return all folders if not authenticated
|
||||
folders = Folder.get_all()
|
||||
# Always return all folders - this is a shared persona system
|
||||
folders = Folder.get_all()
|
||||
|
||||
# Make folders serializable
|
||||
serializable_folders = make_serializable(folders)
|
||||
|
|
@ -91,10 +86,7 @@ def update_folder(folder_id):
|
|||
if not folder:
|
||||
return jsonify({"message": "Folder not found"}), 404
|
||||
|
||||
# Ensure user owns the folder
|
||||
user_id = get_jwt_identity()
|
||||
if folder.get('created_by') != user_id:
|
||||
return jsonify({"message": "Unauthorized"}), 403
|
||||
# Folder operations are shared across all users in this system
|
||||
|
||||
# Ensure _id is not being modified
|
||||
if '_id' in data:
|
||||
|
|
@ -153,10 +145,7 @@ def add_persona_to_folder(folder_id):
|
|||
if not folder:
|
||||
return jsonify({"message": "Folder not found"}), 404
|
||||
|
||||
# Ensure user owns the folder
|
||||
user_id = get_jwt_identity()
|
||||
if folder.get('created_by') != user_id:
|
||||
return jsonify({"message": "Unauthorized"}), 403
|
||||
# Folder operations are shared across all users in this system
|
||||
|
||||
persona_id = data['persona_id']
|
||||
success = Folder.add_persona(folder_id, persona_id)
|
||||
|
|
@ -178,10 +167,7 @@ def remove_persona_from_folder(folder_id, persona_id):
|
|||
if not folder:
|
||||
return jsonify({"message": "Folder not found"}), 404
|
||||
|
||||
# Ensure user owns the folder
|
||||
user_id = get_jwt_identity()
|
||||
if folder.get('created_by') != user_id:
|
||||
return jsonify({"message": "Unauthorized"}), 403
|
||||
# Folder operations are shared across all users in this system
|
||||
|
||||
success = Folder.remove_persona(folder_id, persona_id)
|
||||
|
||||
|
|
@ -207,10 +193,7 @@ def add_personas_to_folder_batch(folder_id):
|
|||
if not folder:
|
||||
return jsonify({"message": "Folder not found"}), 404
|
||||
|
||||
# Ensure user owns the folder
|
||||
user_id = get_jwt_identity()
|
||||
if folder.get('created_by') != user_id:
|
||||
return jsonify({"message": "Unauthorized"}), 403
|
||||
# Folder operations are shared across all users in this system
|
||||
|
||||
persona_ids = data['persona_ids']
|
||||
if not isinstance(persona_ids, list):
|
||||
|
|
@ -245,10 +228,7 @@ def remove_personas_from_folder_batch(folder_id):
|
|||
if not folder:
|
||||
return jsonify({"message": "Folder not found"}), 404
|
||||
|
||||
# Ensure user owns the folder
|
||||
user_id = get_jwt_identity()
|
||||
if folder.get('created_by') != user_id:
|
||||
return jsonify({"message": "Unauthorized"}), 403
|
||||
# Folder operations are shared across all users in this system
|
||||
|
||||
persona_ids = data['persona_ids']
|
||||
if not isinstance(persona_ids, list):
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -274,9 +274,9 @@ def _determine_response_length_preference(
|
|||
Response length preference: 'short', 'medium', or 'long'
|
||||
"""
|
||||
# Base probabilities for response lengths
|
||||
short_prob = 0.25 # 25% chance of short responses
|
||||
short_prob = 0.10 # 10% chance of short responses
|
||||
medium_prob = 0.50 # 50% chance of medium responses
|
||||
long_prob = 0.25 # 25% chance of long responses
|
||||
long_prob = 0.40 # 40% chance of long responses
|
||||
|
||||
# Adjust based on persona extraversion (if available)
|
||||
ocean_traits = persona.get('oceanTraits', {})
|
||||
|
|
@ -285,25 +285,25 @@ def _determine_response_length_preference(
|
|||
|
||||
# High extraversion = more likely to give longer responses
|
||||
# Low extraversion = more likely to give shorter responses
|
||||
if extraversion > 0.7: # High extraversion
|
||||
short_prob *= 0.6 # Reduce short response probability
|
||||
medium_prob *= 0.9 # Slightly reduce medium
|
||||
long_prob *= 1.8 # Increase long response probability
|
||||
elif extraversion < 0.3: # Low extraversion
|
||||
short_prob *= 1.6 # Increase short response probability
|
||||
medium_prob *= 1.1 # Slightly increase medium
|
||||
long_prob *= 0.5 # Reduce long response probability
|
||||
if extraversion > 0.7: # High extraversion (>70%)
|
||||
short_prob *= 0.6 # Short probability × 0.6
|
||||
medium_prob *= 0.9 # Medium probability × 0.9
|
||||
long_prob *= 1.8 # Long probability × 1.8
|
||||
elif extraversion < 0.3: # Low extraversion (<30%)
|
||||
short_prob *= 1.3 # Short probability × 1.3
|
||||
medium_prob *= 1.1 # Medium probability × 1.1
|
||||
long_prob *= 0.7 # Long probability × 0.7
|
||||
|
||||
# Adjust based on communication preferences
|
||||
comm_prefs = persona.get('communicationPreferences', '').lower()
|
||||
if 'brief' in comm_prefs or 'concise' in comm_prefs or 'direct' in comm_prefs:
|
||||
short_prob *= 1.4
|
||||
medium_prob *= 1.1
|
||||
long_prob *= 0.6
|
||||
short_prob *= 1.2 # Short ×1.2
|
||||
medium_prob *= 1.1 # Medium ×1.1
|
||||
long_prob *= 0.8 # Long ×0.8
|
||||
elif 'detailed' in comm_prefs or 'verbose' in comm_prefs or 'elaborate' in comm_prefs:
|
||||
short_prob *= 0.7
|
||||
medium_prob *= 0.9
|
||||
long_prob *= 1.5
|
||||
short_prob *= 0.7 # Short ×0.7
|
||||
medium_prob *= 0.9 # Medium ×0.9
|
||||
long_prob *= 1.5 # Long ×1.5
|
||||
|
||||
# Analyze recent message context
|
||||
if previous_messages:
|
||||
|
|
@ -318,24 +318,22 @@ def _determine_response_length_preference(
|
|||
if recent_lengths:
|
||||
avg_recent_length = sum(recent_lengths) / len(recent_lengths)
|
||||
|
||||
# If recent messages are short, sometimes match the brevity
|
||||
if avg_recent_length < 10: # Very short recent messages
|
||||
short_prob *= 1.3
|
||||
medium_prob *= 1.0
|
||||
long_prob *= 0.7
|
||||
# If recent messages are long, sometimes provide contrast with shorter response
|
||||
elif avg_recent_length > 50: # Long recent messages
|
||||
short_prob *= 1.2
|
||||
medium_prob *= 1.1
|
||||
long_prob *= 0.8
|
||||
# Very short recent messages (<18 words avg)
|
||||
if avg_recent_length < 18:
|
||||
short_prob *= 1.3 # Short ×1.3
|
||||
long_prob *= 0.7 # Long ×0.7
|
||||
# Long recent messages (>60 words avg)
|
||||
elif avg_recent_length > 60:
|
||||
short_prob *= 1.2 # Short ×1.2
|
||||
medium_prob *= 1.1 # Medium ×1.1
|
||||
long_prob *= 0.8 # Long ×0.8
|
||||
|
||||
# Consider topic complexity (simple heuristic)
|
||||
# Consider topic complexity (>15 words or multiple questions)
|
||||
topic_words = current_topic.split()
|
||||
if len(topic_words) > 15 or current_topic.count('?') > 1:
|
||||
# Complex topics may warrant longer responses
|
||||
short_prob *= 0.8
|
||||
medium_prob *= 1.0
|
||||
long_prob *= 1.3
|
||||
short_prob *= 0.8 # Short ×0.8
|
||||
long_prob *= 1.3 # Long ×1.3
|
||||
|
||||
# Normalize probabilities
|
||||
total_prob = short_prob + medium_prob + long_prob
|
||||
|
|
@ -365,7 +363,7 @@ def _get_length_specific_instructions(length_preference: str) -> str:
|
|||
"""
|
||||
if length_preference == 'short':
|
||||
return """
|
||||
RESPONSE LENGTH: Provide a SHORT response (1-8 words or a brief phrase).
|
||||
RESPONSE LENGTH: Provide a SHORT response (1-18 words or brief phrase).
|
||||
Examples of appropriate short responses:
|
||||
- "Absolutely!"
|
||||
- "I disagree."
|
||||
|
|
@ -374,12 +372,14 @@ Examples of appropriate short responses:
|
|||
- "Exactly my point."
|
||||
- "Makes sense to me."
|
||||
- "I'm not sure about that."
|
||||
- "I love that design approach."
|
||||
- "The colors feel too bright for me."
|
||||
|
||||
Keep it natural and conversational, but brief. Sometimes a simple reaction or acknowledgment is all that's needed.
|
||||
"""
|
||||
elif length_preference == 'medium':
|
||||
return """
|
||||
RESPONSE LENGTH: Provide a MEDIUM response (1-3 sentences).
|
||||
RESPONSE LENGTH: Provide a MEDIUM response (1-5 sentences).
|
||||
This should be conversational but not overly detailed. Share your perspective clearly and concisely.
|
||||
Example length: "I think that's a great point about mobile payments. I've had similar experiences with apps that make checkout too complicated."
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class FocusGroupService:
|
|||
Raises:
|
||||
Exception: If all retry attempts fail
|
||||
"""
|
||||
logger.info(f"Starting discussion guide generation for '{focus_group_name}' (duration: {duration}min, topics: {discussion_topics})")
|
||||
logger.info(f"Generating discussion guide: '{focus_group_name}' ({duration}min)")
|
||||
|
||||
# Calculate approximate section times based on duration
|
||||
total_minutes = int(duration)
|
||||
|
|
@ -94,26 +94,11 @@ class FocusGroupService:
|
|||
uploaded_assets = []
|
||||
if focus_group_id:
|
||||
try:
|
||||
# DEBUG: Check if focus group exists and log its data
|
||||
focus_group_doc = FocusGroup.find_by_id(focus_group_id)
|
||||
if focus_group_doc:
|
||||
logger.info(f"Found focus group document: {focus_group_id}")
|
||||
logger.info(f"Focus group keys: {list(focus_group_doc.keys()) if focus_group_doc else 'None'}")
|
||||
if 'uploaded_assets' in focus_group_doc:
|
||||
logger.info(f"Raw uploaded_assets in document: {focus_group_doc['uploaded_assets']}")
|
||||
else:
|
||||
logger.warning(f"No 'uploaded_assets' key found in focus group document")
|
||||
else:
|
||||
logger.error(f"Focus group document not found: {focus_group_id}")
|
||||
|
||||
uploaded_assets = FocusGroup.get_uploaded_assets(focus_group_id)
|
||||
logger.info(f"Found {len(uploaded_assets)} uploaded assets for focus group {focus_group_id}")
|
||||
if uploaded_assets:
|
||||
logger.info(f"Asset details: {uploaded_assets}")
|
||||
logger.info(f"Retrieved {len(uploaded_assets)} assets for focus group {focus_group_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Could not retrieve assets for focus group {focus_group_id}: {e}")
|
||||
import traceback
|
||||
logger.error(f"Asset retrieval traceback: {traceback.format_exc()}")
|
||||
|
||||
# Load and format the discussion guide prompt
|
||||
try:
|
||||
|
|
@ -136,53 +121,15 @@ class FocusGroupService:
|
|||
'uploaded_assets': uploaded_assets,
|
||||
'has_assets': len(uploaded_assets) > 0,
|
||||
'asset_count': len(uploaded_assets),
|
||||
'asset_requirement_note': ' (will require creative review activities)' if len(uploaded_assets) > 0 else '',
|
||||
# Create a formatted list of asset filenames for the LLM
|
||||
'uploaded_asset_list': '\n'.join([f"- {asset.get('filename', 'unknown')} ({asset.get('original_filename', 'unknown')})" for asset in uploaded_assets]) if uploaded_assets else 'No assets uploaded',
|
||||
# Jinja2-style template variables to avoid conflicts with Python formatting
|
||||
'jinja_if_has_assets': '{% if has_assets %}' if len(uploaded_assets) > 0 else '',
|
||||
'jinja_else': '{% else %}' if len(uploaded_assets) == 0 else '',
|
||||
'jinja_endif': '{% endif %}'
|
||||
'uploaded_asset_list': '\n'.join([f"- {asset.get('filename', 'unknown')} ({asset.get('original_name', asset.get('original_filename', 'unknown'))})" for asset in uploaded_assets]) if uploaded_assets else 'No assets uploaded',
|
||||
# Conditional content for asset sections
|
||||
'assets_section': FocusGroupService._generate_assets_section(uploaded_assets) if uploaded_assets else 'No creative assets have been uploaded for this focus group.'
|
||||
}
|
||||
|
||||
# DEBUG: Log template variables before prompt generation
|
||||
logger.info("=== DEBUG: TEMPLATE VARIABLES ===")
|
||||
logger.info(f"has_assets: {template_vars['has_assets']}")
|
||||
logger.info(f"asset_count: {template_vars['asset_count']}")
|
||||
logger.info(f"uploaded_asset_list: {template_vars['uploaded_asset_list']}")
|
||||
logger.info(f"jinja_if_has_assets: {template_vars['jinja_if_has_assets']}")
|
||||
logger.info(f"jinja_else: {template_vars['jinja_else']}")
|
||||
logger.info(f"jinja_endif: {template_vars['jinja_endif']}")
|
||||
|
||||
prompt = load_prompt('discussion-guide-generation', template_vars)
|
||||
logger.info(f"Successfully loaded discussion guide prompt template")
|
||||
|
||||
# DEBUG: Log the complete prompt to verify asset information is included
|
||||
logger.info("=== DEBUG: COMPLETE PROMPT BEING SENT TO LLM ===")
|
||||
logger.info(f"Prompt length: {len(prompt)} characters")
|
||||
logger.info(f"LLM model being used: {llm_model or 'default (gemini-2.5-pro)'}")
|
||||
logger.info(f"Assets in template variables: {len(uploaded_assets)} assets")
|
||||
if uploaded_assets:
|
||||
logger.info(f"Asset details: {[{'filename': a.get('filename'), 'original': a.get('original_filename')} for a in uploaded_assets]}")
|
||||
|
||||
# Log sections around creative assets to verify template population
|
||||
if "CREATIVE ASSETS REQUIREMENTS" in prompt:
|
||||
creative_section_start = prompt.find("CREATIVE ASSETS REQUIREMENTS")
|
||||
creative_section_end = prompt.find("BEST PRACTICES:", creative_section_start)
|
||||
if creative_section_end == -1:
|
||||
creative_section_end = creative_section_start + 1000
|
||||
creative_section = prompt[creative_section_start:creative_section_end]
|
||||
logger.info("=== CREATIVE ASSETS SECTION IN PROMPT ===")
|
||||
logger.info(creative_section)
|
||||
else:
|
||||
logger.warning("CREATIVE ASSETS REQUIREMENTS section not found in prompt!")
|
||||
|
||||
# ENHANCED DEBUG: Log specific template variables for asset handling
|
||||
logger.info(f"=== ASSET TEMPLATE DEBUG ===")
|
||||
logger.info(f"has_assets: {template_vars.get('has_assets', False)}")
|
||||
logger.info(f"asset_count: {template_vars.get('asset_count', 0)}")
|
||||
logger.info(f"uploaded_asset_list: {template_vars.get('uploaded_asset_list', 'None')}")
|
||||
|
||||
logger.info("=== END DEBUG PROMPT ===")
|
||||
logger.info(f"Starting discussion guide generation: {len(uploaded_assets)} assets, {llm_model or 'default'} model")
|
||||
except PromptLoaderError as e:
|
||||
error_msg = f"Error loading discussion guide prompt: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
|
|
@ -192,8 +139,6 @@ class FocusGroupService:
|
|||
last_error = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
logger.info(f"Discussion guide generation attempt {attempt}/{max_retries}")
|
||||
|
||||
# Special handling for GPT models to ensure creative review compliance
|
||||
enhanced_prompt = prompt
|
||||
if llm_model and llm_model.startswith('gpt'):
|
||||
|
|
@ -207,7 +152,6 @@ class FocusGroupService:
|
|||
asset_emphasis += f"FAILURE TO INCLUDE ALL {len(uploaded_assets)} CREATIVE_REVIEW ACTIVITIES WILL RESULT IN INVALID OUTPUT\n"
|
||||
asset_emphasis += f"🚨🚨🚨 END CRITICAL INSTRUCTIONS 🚨🚨🚨\n\n"
|
||||
enhanced_prompt = asset_emphasis + prompt
|
||||
logger.info(f"Enhanced prompt for GPT model with {len(uploaded_assets)} asset emphasis")
|
||||
|
||||
# Generate content using LLM
|
||||
response = LLMService.generate_content(
|
||||
|
|
@ -217,7 +161,6 @@ class FocusGroupService:
|
|||
model_name=llm_model
|
||||
)
|
||||
|
||||
logger.info(f"Received LLM response (length: {len(response)} chars)")
|
||||
|
||||
# Clean up the response to remove code fences if present
|
||||
clean_response = response.strip()
|
||||
|
|
@ -326,17 +269,55 @@ class FocusGroupService:
|
|||
last_error = Exception(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"LLM service error during generation: {str(e)}"
|
||||
logger.warning(error_msg)
|
||||
logger.warning(f"Generation attempt {attempt} failed: {str(e)}")
|
||||
last_error = e
|
||||
|
||||
# If this wasn't the last attempt, wait before retrying (exponential backoff)
|
||||
if attempt < max_retries:
|
||||
wait_time = 2 ** (attempt - 1) # 1, 2, 4 seconds
|
||||
logger.info(f"Attempt {attempt} failed, waiting {wait_time} seconds before retry...")
|
||||
logger.info(f"Retrying in {wait_time}s (attempt {attempt + 1}/{max_retries})")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# All attempts failed
|
||||
final_error_msg = f"Discussion guide generation failed after {max_retries} attempts. Last error: {str(last_error)}"
|
||||
logger.error(final_error_msg)
|
||||
raise Exception(final_error_msg)
|
||||
|
||||
@staticmethod
|
||||
def _generate_assets_section(uploaded_assets: List[Dict[str, Any]]) -> str:
|
||||
"""Generate the assets section content for the discussion guide prompt."""
|
||||
if not uploaded_assets:
|
||||
return 'No creative assets have been uploaded for this focus group.'
|
||||
|
||||
asset_count = len(uploaded_assets)
|
||||
uploaded_asset_list = '\n'.join([f"- {asset.get('filename', 'unknown')} ({asset.get('original_name', asset.get('original_filename', 'unknown'))})" for asset in uploaded_assets])
|
||||
|
||||
return f"""🚨 CRITICAL REQUIREMENT: This focus group has {asset_count} uploaded creative asset(s) that MUST be included in the discussion guide.
|
||||
|
||||
**MANDATORY CREATIVE REVIEW ACTIVITIES:**
|
||||
YOU MUST CREATE EXACTLY {asset_count} "creative_review" ACTIVITIES - ONE FOR EACH ASSET BELOW:
|
||||
|
||||
**UPLOADED ASSET FILENAMES:**
|
||||
{uploaded_asset_list}
|
||||
|
||||
**CREATIVE REVIEW ACTIVITY REQUIREMENTS:**
|
||||
- CREATE one "creative_review" activity for EACH asset filename listed above
|
||||
- Each activity type MUST be "creative_review" (not "open_question" or any other type)
|
||||
- MANDATORY: Include the exact asset filename in the activity content
|
||||
- Example format: "Please take a look at the creative asset on your screen, titled 'EXACT_FILENAME_HERE'. What is your immediate gut reaction? What words come to mind?"
|
||||
- Distribute these activities throughout different sections (not all in one place)
|
||||
- Allow 3-5 minutes per creative review activity
|
||||
- Add 1-2 probe questions after each creative review
|
||||
|
||||
**VALIDATION CHECKLIST:**
|
||||
Before finalizing your JSON, verify:
|
||||
□ You have created exactly {asset_count} activities with type "creative_review"
|
||||
□ Each creative_review activity includes an exact filename from the asset list above
|
||||
□ Creative review activities are spread across different sections of the guide
|
||||
□ Each creative review activity has adequate time allocation
|
||||
|
||||
**CREATIVE ASSET INTEGRATION:**
|
||||
- Integrate creative review activities naturally into the flow of discussion
|
||||
- Place creative assets strategically within relevant topic sections
|
||||
- Ensure creative reviews don't dominate the discussion - balance with other questions
|
||||
- Use creative assets to support and enhance the main discussion topics"""
|
||||
|
|
|
|||
|
|
@ -231,9 +231,7 @@ class LLMService:
|
|||
text_config["verbosity"] = "medium" # Default
|
||||
kwargs["text"] = text_config
|
||||
|
||||
# Add max_tokens if specified (Responses API uses max_tokens in text config)
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
# Note: GPT-5 Responses API does not support max_tokens parameter
|
||||
|
||||
response = openai_client.responses.create(**kwargs)
|
||||
result = LLMService._extract_responses_api_content(response)
|
||||
|
|
@ -508,8 +506,13 @@ class LLMService:
|
|||
if actual_model == 'gpt-5':
|
||||
# Use Responses API for GPT-5 multimodal
|
||||
# Note: GPT-5 Responses API supports multimodal input
|
||||
input_content = [{"type": "text", "text": prompt}]
|
||||
input_content.extend(image_content)
|
||||
input_content = [{"role": "user", "content": [{"type": "input_text", "text": prompt}]}]
|
||||
# Add images to the content array
|
||||
for img_content in image_content:
|
||||
input_content[0]["content"].append({
|
||||
"type": "input_image",
|
||||
"image_url": img_content["image_url"]["url"]
|
||||
})
|
||||
|
||||
kwargs = {
|
||||
"model": actual_model,
|
||||
|
|
@ -521,8 +524,7 @@ class LLMService:
|
|||
}
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
# Note: GPT-5 Responses API does not support max_tokens parameter
|
||||
|
||||
response = openai_client.responses.create(**kwargs)
|
||||
result = LLMService._extract_responses_api_content(response)
|
||||
|
|
@ -721,8 +723,13 @@ class LLMService:
|
|||
|
||||
if actual_model == 'gpt-5':
|
||||
# Use Responses API for GPT-5 contextual multimodal
|
||||
input_content = [{"type": "text", "text": full_prompt}]
|
||||
input_content.extend(image_content)
|
||||
input_content = [{"role": "user", "content": [{"type": "input_text", "text": full_prompt}]}]
|
||||
# Add images to the content array
|
||||
for img_content in image_content:
|
||||
input_content[0]["content"].append({
|
||||
"type": "input_image",
|
||||
"image_url": img_content["image_url"]["url"]
|
||||
})
|
||||
|
||||
kwargs = {
|
||||
"model": actual_model,
|
||||
|
|
@ -734,8 +741,7 @@ class LLMService:
|
|||
}
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
# Note: GPT-5 Responses API does not support max_tokens parameter
|
||||
|
||||
response = openai_client.responses.create(**kwargs)
|
||||
result = LLMService._extract_responses_api_content(response)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ FOCUS GROUP DETAILS:
|
|||
- Research Brief: {research_brief}
|
||||
- Key Discussion Topics: {discussion_topics}
|
||||
- Total Duration: {duration} minutes
|
||||
- Creative Assets: {asset_count} uploaded asset(s){jinja_if_has_assets} (will require creative review activities){jinja_endif}
|
||||
- Creative Assets: {asset_count} uploaded asset(s){asset_requirement_note}
|
||||
|
||||
TIME ALLOCATION (approximate):
|
||||
- Introduction: {intro_time} minutes
|
||||
|
|
@ -275,39 +275,7 @@ When assets are uploaded, you MUST include activities like this:
|
|||
**FLEXIBILITY NOTE:** Types can be used in either array based on context and flow.
|
||||
|
||||
**CREATIVE ASSETS REQUIREMENTS:**
|
||||
{jinja_if_has_assets}
|
||||
🚨 CRITICAL REQUIREMENT: This focus group has {asset_count} uploaded creative asset(s) that MUST be included in the discussion guide.
|
||||
|
||||
**MANDATORY CREATIVE REVIEW ACTIVITIES:**
|
||||
YOU MUST CREATE EXACTLY {asset_count} "creative_review" ACTIVITIES - ONE FOR EACH ASSET BELOW:
|
||||
|
||||
**UPLOADED ASSET FILENAMES:**
|
||||
{uploaded_asset_list}
|
||||
|
||||
**CREATIVE REVIEW ACTIVITY REQUIREMENTS:**
|
||||
- CREATE one "creative_review" activity for EACH asset filename listed above
|
||||
- Each activity type MUST be "creative_review" (not "open_question" or any other type)
|
||||
- MANDATORY: Include the exact asset filename in the activity content
|
||||
- Example format: "Please take a look at the creative asset on your screen, titled 'EXACT_FILENAME_HERE'. What is your immediate gut reaction? What words come to mind?"
|
||||
- Distribute these activities throughout different sections (not all in one place)
|
||||
- Allow 3-5 minutes per creative review activity
|
||||
- Add 1-2 probe questions after each creative review
|
||||
|
||||
**VALIDATION CHECKLIST:**
|
||||
Before finalizing your JSON, verify:
|
||||
□ You have created exactly {asset_count} activities with type "creative_review"
|
||||
□ Each creative_review activity includes an exact filename from the asset list above
|
||||
□ Creative review activities are spread across different sections of the guide
|
||||
□ Each creative review activity has adequate time allocation
|
||||
|
||||
**CREATIVE ASSET INTEGRATION:**
|
||||
- Integrate creative review activities naturally into the flow of discussion
|
||||
- Place creative assets strategically within relevant topic sections
|
||||
- Ensure creative reviews don't dominate the discussion - balance with other questions
|
||||
- Use creative assets to support and enhance the main discussion topics
|
||||
{jinja_else}
|
||||
No creative assets have been uploaded for this focus group.
|
||||
{jinja_endif}
|
||||
{assets_section}
|
||||
|
||||
BEST PRACTICES:
|
||||
- Use clear, specific questions optimized for flowing conversation
|
||||
|
|
|
|||
1
backend/test_asset.txt
Normal file
1
backend/test_asset.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
test file content
|
||||
BIN
backend/test_image.png
Normal file
BIN
backend/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
|
|
@ -7,7 +7,7 @@
|
|||
<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-ULt1o08x.js"></script>
|
||||
<script type="module" crossorigin src="/semblance/assets/index-DHXCQiw7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-D7sAAnG7.css">
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -737,6 +737,9 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
|
|||
// Allow auto-save after loading is complete
|
||||
setTimeout(() => {
|
||||
isLoadingDraftRef.current = false;
|
||||
// Ensure initial form state is captured after loading
|
||||
const initialFormState = JSON.stringify(form.getValues());
|
||||
prevWatchedFieldsRef.current = initialFormState;
|
||||
}, 1000); // Give it a second to settle
|
||||
}
|
||||
}, [draftToEdit, form]);
|
||||
|
|
@ -768,9 +771,12 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
|
|||
if (!draftToEdit) {
|
||||
setTimeout(() => {
|
||||
isLoadingDraftRef.current = false;
|
||||
// Ensure initial form state is captured for new focus groups
|
||||
const initialFormState = JSON.stringify(form.getValues());
|
||||
prevWatchedFieldsRef.current = initialFormState;
|
||||
}, 500); // Allow initial render to complete
|
||||
}
|
||||
}, [draftToEdit]);
|
||||
}, [draftToEdit, form]);
|
||||
|
||||
|
||||
// Save Status Indicator Component
|
||||
|
|
@ -854,30 +860,8 @@ export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSele
|
|||
});
|
||||
}
|
||||
|
||||
// Fallback to template if API fails
|
||||
const guide = `
|
||||
# Discussion Guide: ${values.focusGroupName}
|
||||
|
||||
## Introduction (5 minutes)
|
||||
${sampleGuide.introduction}
|
||||
|
||||
## Warm-up Questions (10 minutes)
|
||||
${sampleGuide.warmup}
|
||||
|
||||
## ${values.discussionTopics.split(',')[0]} Exploration (15 minutes)
|
||||
${sampleGuide.exploration}
|
||||
|
||||
## Creative Testing (20 minutes)
|
||||
${sampleGuide.creative}
|
||||
${values.creativeAssets && values.creativeAssets.length > 0 ? `We'll be reviewing ${values.creativeAssets.length} creative assets.` : ''}
|
||||
|
||||
## Conclusion (10 minutes)
|
||||
${sampleGuide.conclusion}
|
||||
|
||||
## Research Brief Context
|
||||
${values.researchBrief}
|
||||
`;
|
||||
return guide;
|
||||
// Don't provide fallback template - throw the error to prevent showing dummy guide
|
||||
throw error;
|
||||
}
|
||||
// Note: Don't set isGenerating to false here - let the progress bar handle it
|
||||
};
|
||||
|
|
@ -923,7 +907,7 @@ ${values.researchBrief}
|
|||
formData.append('assets', file);
|
||||
});
|
||||
|
||||
const uploadResponse = await focusGroupsApi.uploadAssets(focusGroupId, formData);
|
||||
const uploadResponse = await focusGroupsApi.uploadAssets(focusGroupId, formData, true);
|
||||
const uploadResult = uploadResponse.data;
|
||||
console.log("Assets uploaded successfully:", uploadResult);
|
||||
|
||||
|
|
@ -989,48 +973,61 @@ ${values.researchBrief}
|
|||
}
|
||||
}
|
||||
|
||||
// Generate discussion guide based on form input (after database is updated)
|
||||
const guide = await generateDiscussionGuide(values, focusGroupId);
|
||||
setDiscussionGuide(guide);
|
||||
|
||||
// Update the focus group with the discussion guide
|
||||
try {
|
||||
const updateData = {
|
||||
name: values.focusGroupName,
|
||||
status: 'draft',
|
||||
participants: selectedParticipants,
|
||||
participants_count: selectedParticipants.length,
|
||||
date: new Date().toISOString(),
|
||||
duration: parseInt(values.duration),
|
||||
topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'),
|
||||
description: values.researchBrief,
|
||||
objective: values.researchBrief,
|
||||
llm_model: values.llm_model,
|
||||
reasoning_effort: values.reasoning_effort,
|
||||
verbosity: values.verbosity,
|
||||
discussionGuide: guide
|
||||
};
|
||||
// Generate discussion guide based on form input (after database is updated)
|
||||
const guide = await generateDiscussionGuide(values, focusGroupId);
|
||||
setDiscussionGuide(guide);
|
||||
|
||||
await focusGroupsApi.update(focusGroupId, updateData);
|
||||
console.log("Focus group updated with discussion guide");
|
||||
// Update the focus group with the discussion guide
|
||||
try {
|
||||
const updateData = {
|
||||
name: values.focusGroupName,
|
||||
status: 'draft',
|
||||
participants: selectedParticipants,
|
||||
participants_count: selectedParticipants.length,
|
||||
date: new Date().toISOString(),
|
||||
duration: parseInt(values.duration),
|
||||
topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'),
|
||||
description: values.researchBrief,
|
||||
objective: values.researchBrief,
|
||||
llm_model: values.llm_model,
|
||||
reasoning_effort: values.reasoning_effort,
|
||||
verbosity: values.verbosity,
|
||||
discussionGuide: guide
|
||||
};
|
||||
|
||||
await focusGroupsApi.update(focusGroupId, updateData);
|
||||
console.log("Focus group updated with discussion guide");
|
||||
|
||||
toast.success("Progress saved as draft", {
|
||||
description: "Your focus group setup has been automatically saved",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update focus group with discussion guide:", error);
|
||||
toast.error("Failed to save draft", {
|
||||
description: "Discussion guide generated, but draft save failed",
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Progress saved as draft", {
|
||||
description: "Your focus group setup has been automatically saved",
|
||||
// Move to review tab after successful generation
|
||||
setActiveTab('review');
|
||||
|
||||
toast.success("Discussion guide generated", {
|
||||
description: "Review and edit before proceeding",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update focus group with discussion guide:", error);
|
||||
toast.error("Failed to save draft", {
|
||||
description: "Discussion guide generated, but draft save failed",
|
||||
|
||||
} catch (guideError) {
|
||||
console.error("Discussion guide generation failed:", guideError);
|
||||
// Don't set discussion guide or move to review tab
|
||||
// Show error message with instruction to go back to setup tab and try again
|
||||
toast.error("Discussion guide generation failed", {
|
||||
description: "Please go back to the setup tab and try generating again. Check your inputs and try a different AI model if the issue persists.",
|
||||
duration: 8000, // Show longer so user can read the instruction
|
||||
});
|
||||
// Stay on current tab (setup) so user can try again
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to review tab after successful generation
|
||||
setActiveTab('review');
|
||||
|
||||
toast.success("Discussion guide generated", {
|
||||
description: "Review and edit before proceeding",
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in focus group creation flow:", error);
|
||||
toast.error("Focus group creation failed", {
|
||||
|
|
@ -1644,7 +1641,14 @@ Controls how much time GPT-5 spends thinking before responding
|
|||
/>
|
||||
) : (
|
||||
<div className="bg-slate-50 p-4 rounded border text-center text-slate-600">
|
||||
No discussion guide generated yet. Complete the setup and click "Generate Discussion Guide" to create one.
|
||||
{guideGenerationError ? (
|
||||
<div>
|
||||
<p className="mb-2">Discussion guide generation failed.</p>
|
||||
<p className="text-sm">Go back to the <strong>Setup</strong> tab and try generating again. Check your inputs and try a different AI model if the issue persists.</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>No discussion guide generated yet. Complete the setup and click "Generate Discussion Guide" to create one.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1268,8 +1268,8 @@ const DiscussionPanel = ({
|
|||
className={`hover-transition ${autoScroll ? 'bg-blue-50 text-blue-600 hover:bg-blue-100' : ''}`}
|
||||
title={autoScroll ? 'Disable auto-scroll' : 'Enable auto-scroll'}
|
||||
>
|
||||
<ArrowDown className={`h-3 w-3 ${autoScroll ? 'mr-1' : ''}`} />
|
||||
{autoScroll && 'Auto-scroll'}
|
||||
<ArrowDown className="h-3 w-3 mr-1" />
|
||||
Auto-scroll
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -459,13 +459,19 @@ export const focusGroupsApi = {
|
|||
api.delete(`/focus-groups/${focusGroupId}/notes/${noteId}`),
|
||||
|
||||
// Asset management endpoints
|
||||
uploadAssets: (focusGroupId: string, formData: FormData) =>
|
||||
api.post(`/focus-groups/${focusGroupId}/assets`, formData, {
|
||||
uploadAssets: (focusGroupId: string, formData: FormData, replace?: boolean) => {
|
||||
// Add replace flag to form data if specified
|
||||
if (replace === true) {
|
||||
formData.append('replace', 'true');
|
||||
}
|
||||
|
||||
return api.post(`/focus-groups/${focusGroupId}/assets`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 120000 // 2 minutes for file upload
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
getAssets: (focusGroupId: string) =>
|
||||
api.get(`/focus-groups/${focusGroupId}/assets`),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue