fixed folders again, bug fixes for gpt-5, adjusted response length calculation, cosmetic UI changes, other bug fixes

This commit is contained in:
michael 2025-08-09 10:08:45 -05:00
parent fbb444037a
commit da8639aee8
23 changed files with 436 additions and 437 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
backend/.DS_Store vendored

Binary file not shown.

BIN
backend/app/.DS_Store vendored

Binary file not shown.

View file

@ -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."""

View file

@ -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

View file

@ -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):

View file

@ -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."
"""

View file

@ -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"""

View file

@ -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)

View file

@ -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
View file

@ -0,0 +1 @@
test file content

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
View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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`),