import random from typing import List, Dict from breed_health_info import breed_health_info, default_health_note from breed_noise_info import breed_noise_info from dog_database import get_dog_description from scoring_calculation_system import UserPreferences from recommendation_formatter import ( get_breed_recommendations, _format_dimension_scores, calculate_breed_bonus_factors, generate_breed_characteristics_data, parse_noise_information, parse_health_information, generate_dimension_scores_for_display ) from recommendation_html_formatter import RecommendationHTMLFormatter def format_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str: """統一推薦結果HTML格式化,確保視覺與數值邏輯一致""" # 創建HTML格式器實例 formatter = RecommendationHTMLFormatter() # 獲取對應的CSS樣式 html_content = formatter.get_css_styles(is_description_search) + "
" for rec in recommendations: breed = rec['breed'] rank = rec.get('rank', 0) breed_name_for_db = breed.replace(' ', '_') breed_info_from_db = get_dog_description(breed_name_for_db) if is_description_search: # Handle semantic search results structure - use scores directly from semantic recommender overall_score = rec.get('overall_score', 0.7) final_score = rec.get('final_score', overall_score) # Use final_score if available semantic_score = rec.get('semantic_score', 0.7) comparative_bonus = rec.get('comparative_bonus', 0.0) lifestyle_bonus = rec.get('lifestyle_bonus', 0.0) # Use the actual calculated scores without re-computation base_score = final_score # Generate dimension scores using the formatter helper scores = generate_dimension_scores_for_display( base_score, rank, breed, semantic_score, comparative_bonus, lifestyle_bonus, is_description_search ) bonus_score = max(0.0, comparative_bonus + random.uniform(-0.02, 0.02)) info = generate_breed_characteristics_data(breed_info_from_db or {}) info = dict(info) # Convert to dict for compatibility # Add any missing fields from rec if not breed_info_from_db: for key in ['Size', 'Exercise Needs', 'Grooming Needs', 'Good with Children', 'Temperament', 'Lifespan', 'Description']: if key not in info: info[key] = rec.get(key.lower().replace(' ', '_'), 'Unknown' if key != 'Description' else '') # Display scores as percentages with one decimal place display_scores = { 'space': round(scores['space'] * 100, 1), 'exercise': round(scores['exercise'] * 100, 1), 'grooming': round(scores['grooming'] * 100, 1), 'experience': round(scores['experience'] * 100, 1), 'noise': round(scores['noise'] * 100, 1), } else: # Handle traditional search results structure scores = rec['scores'] info = rec['info'] final_score = rec.get('final_score', scores['overall']) bonus_score = rec.get('bonus_score', 0) # Convert traditional scores to percentage display format with one decimal display_scores = { 'space': round(scores.get('space', 0) * 100, 1), 'exercise': round(scores.get('exercise', 0) * 100, 1), 'grooming': round(scores.get('grooming', 0) * 100, 1), 'experience': round(scores.get('experience', 0) * 100, 1), 'noise': round(scores.get('noise', 0) * 100, 1), } progress_bars = {} for metric in ['space', 'exercise', 'grooming', 'experience', 'noise']: if metric in scores: # 使用顯示分數(百分比)來計算進度條 display_score = display_scores[metric] bar_data = formatter.generate_progress_bar(display_score, metric, is_percentage_display=True, is_description_search=is_description_search) progress_bars[metric] = { 'style': f"width: {bar_data['width']}%; background-color: {bar_data['color']};" } # bonus if bonus_score > 0: # bonus_score 通常是 0-1 範圍,需要轉換為百分比顯示 bonus_percentage = bonus_score * 100 bonus_data = formatter.generate_progress_bar(bonus_percentage, 'bonus', is_percentage_display=True, is_description_search=is_description_search) progress_bars['bonus'] = { 'style': f"width: {bonus_data['width']}%; background-color: {bonus_data['color']};" } health_info = breed_health_info.get(breed, {"health_notes": default_health_note}) noise_info = breed_noise_info.get(breed, { "noise_notes": "Noise information not available", "noise_level": "Unknown", "source": "N/A" }) # 解析噪音和健康資訊 noise_characteristics, barking_triggers, noise_level = parse_noise_information(noise_info) health_considerations, health_screenings = parse_health_information(health_info) # 計算獎勵因素 _, bonus_reasons = calculate_breed_bonus_factors(info, None) # User prefs not needed for display # 生成品種卡片標題 html_content += formatter.generate_breed_card_header(breed, rank, final_score, is_description_search) # 品種詳細資訊區域 - 使用格式器方法簡化 tooltip_html = formatter.generate_tooltips_section() html_content += f"""
Space Compatibility:{tooltip_html}
{display_scores['space']:.1f}%
Exercise Match: Exercise Match Score:
• Based on your daily exercise time and type
• Compares your activity level to the breed's exercise needs
• Higher score means your routine aligns well with the breed's energy requirements.
{display_scores['exercise']:.1f}%
Grooming Match: Grooming Match Score:
• Evaluates breed's grooming needs (coat care, trimming, brushing)
• Compares these requirements with your grooming commitment level
• Higher score means the breed's grooming needs fit your willingness and capability.
{display_scores['grooming']:.1f}%
Experience Match: Experience Match Score:
• Based on your dog-owning experience level
• Considers breed's training complexity, temperament, and handling difficulty
• Higher score means the breed is more suitable for your experience level.
{display_scores['experience']:.1f}%
Noise Compatibility: Noise Compatibility Score:
• Based on your noise tolerance preference
• Considers breed's typical noise level and barking tendencies
• Accounts for living environment and sensitivity to noise.
{display_scores['noise']:.1f}%
{f'''
Breed Bonus: Breed Bonus Points:
• {('
• '.join(bonus_reasons) if bonus_reasons else 'No additional bonus points')}

Bonus Factors Include:
• Friendly temperament
• Child compatibility
• Longer lifespan
• Living space adaptability
{bonus_score*100:.1f}%
''' if bonus_score > 0 else ''}
""" # 使用格式器生成詳細區段 html_content += formatter.generate_detailed_sections_html( breed, info, noise_characteristics, barking_triggers, noise_level, health_considerations, health_screenings ) html_content += """
""" # 結束 HTML 內容 html_content += "" return html_content def format_unified_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str: """統一推薦HTML格式化主函數,確保視覺呈現與數值計算完全一致""" # 創建HTML格式器實例 formatter = RecommendationHTMLFormatter() if not recommendations: return '''
🐕

No Recommendations Available

Please try adjusting your preferences or description, and we'll help you find the most suitable breeds.

''' # 使用格式器的統一CSS樣式 html_content = formatter.unified_css + "
" for rec in recommendations: breed = rec['breed'] rank = rec.get('rank', 0) # 統一分數處理 overall_score = rec.get('overall_score', rec.get('final_score', 0.7)) scores = rec.get('scores', {}) # 如果沒有維度分數,基於總分生成一致的維度分數 if not scores: scores = generate_dimension_scores_for_display( overall_score, rank, breed, is_description_search=is_description_search ) # 獲取品種資訊 breed_name_for_db = breed.replace(' ', '_') breed_info = get_dog_description(breed_name_for_db) or {} # 維度標籤 dimension_labels = { 'space': '🏠 Space Compatibility', 'exercise': '🏃 Exercise Requirements', 'grooming': '✂️ Grooming Needs', 'experience': '🎓 Experience Level', 'noise': '🔊 Noise Control', 'family': '👨‍👩‍👧‍👦 Family Compatibility' } # 維度提示氣泡內容 tooltip_content = { 'space': 'Space Compatibility Score:
• Evaluates how well the breed adapts to your living environment
• Considers if your home (apartment/house) and yard access suit the breed\'s size
• Higher score means the breed fits well in your available space.', 'exercise': 'Exercise Requirements Score:
• Based on your daily exercise time and activity type
• Compares your activity level to the breed\'s exercise needs
• Higher score means your routine aligns well with the breed\'s energy requirements.', 'grooming': 'Grooming Needs Score:
• Evaluates breed\'s grooming needs (coat care, trimming, brushing)
• Compares these requirements with your grooming commitment level
• Higher score means the breed\'s grooming needs fit your willingness and capability.', 'experience': 'Experience Level Score:
• Based on your dog-owning experience level
• Considers breed\'s training complexity, temperament, and handling difficulty
• Higher score means the breed is more suitable for your experience level.', 'noise': 'Noise Control Score:
• Based on your noise tolerance preference
• Considers breed\'s typical noise level and barking tendencies
• Accounts for living environment and sensitivity to noise.', 'family': 'Family Compatibility Score:
• Evaluates how well the breed fits with your family situation
• Considers children, other pets, and family dynamics
• Higher score means better family compatibility.' } # 生成維度分數HTML dimension_html = "" for dim, label in dimension_labels.items(): score = scores.get(dim, overall_score * 0.9) percentage = formatter.format_unified_percentage(score) progress_bar = formatter.generate_unified_progress_bar(score) # 為 Find by Description 添加提示氣泡 tooltip_html = '' if is_description_search: tooltip_html = f'i{tooltip_content.get(dim, "")}' dimension_html += f'''
{label} {tooltip_html} {percentage}
{progress_bar}
''' # 生成品種資訊HTML characteristics = generate_breed_characteristics_data(breed_info) info_html = "" for label, value in characteristics: if label != 'Description': # Skip description as it's shown separately info_html += f'''
{label}
{value}
''' # 生成單個品種卡片HTML overall_percentage = formatter.format_unified_percentage(overall_score) overall_progress_bar = formatter.generate_unified_progress_bar(overall_score) brand_card_html = f'''
🏆 #{rank}

{breed.replace('_', ' ')}

Overall Match: {overall_percentage}
{overall_progress_bar}
{dimension_html}
{info_html}

📝 Breed Description

{breed_info.get('Description', 'Detailed description for this breed is not currently available.')}

🌐 Learn more about {breed.replace('_', ' ')} on AKC website
''' html_content += brand_card_html html_content += "
" return html_content