File size: 21,587 Bytes
1e4c9bc
38d9980
d161865
 
 
1e4c9bc
 
 
 
 
 
 
 
 
 
 
d161865
 
1e4c9bc
 
 
 
 
 
 
 
d161865
 
 
 
1e4c9bc
 
 
d161865
 
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d161865
1e4c9bc
 
 
 
 
d161865
 
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
d161865
80eb9ed
 
 
1e4c9bc
 
 
80eb9ed
 
 
 
1e4c9bc
80eb9ed
1e4c9bc
 
 
80eb9ed
 
 
d161865
 
 
 
 
 
 
 
1e4c9bc
 
 
d161865
1e4c9bc
 
 
 
 
 
 
 
 
d161865
1e4c9bc
d161865
1e4c9bc
d161865
80eb9ed
1e4c9bc
80eb9ed
d161865
80eb9ed
d161865
1e4c9bc
d161865
80eb9ed
1e4c9bc
d161865
80eb9ed
 
 
 
 
 
 
1e4c9bc
 
80eb9ed
 
 
d161865
80eb9ed
d161865
1e4c9bc
d161865
80eb9ed
1e4c9bc
d161865
80eb9ed
 
 
 
 
 
1e4c9bc
80eb9ed
1e4c9bc
80eb9ed
 
 
d161865
80eb9ed
d161865
1e4c9bc
d161865
80eb9ed
1e4c9bc
d161865
80eb9ed
 
 
 
 
 
 
1e4c9bc
80eb9ed
 
 
 
d161865
80eb9ed
d161865
1e4c9bc
d161865
80eb9ed
1e4c9bc
d161865
 
 
 
 
 
 
 
80eb9ed
 
d161865
 
 
 
80eb9ed
d161865
1e4c9bc
d161865
80eb9ed
d161865
 
 
 
 
 
 
 
1e4c9bc
d161865
 
 
 
 
 
 
 
 
80eb9ed
d161865
 
 
80eb9ed
d161865
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d161865
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
d161865
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
d161865
 
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d161865
 
1e4c9bc
 
 
d161865
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
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) + "<div class='recommendations-container'>"

    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"""
            <div class="breed-details">
                <div class="compatibility-scores">
                    <!-- Space Compatibility Score -->
                    <div class="score-item">
                        <span class="label">
                            Space Compatibility:{tooltip_html}
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars.get('space', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
                        </div>
                        <span class="percentage">{display_scores['space']:.1f}%</span>
                    </div>

                    <!-- Exercise Compatibility Score -->
                    <div class="score-item">
                        <span class="label">
                            Exercise Match:
                            <span class="tooltip">
                                <span class="tooltip-icon">ⓘ</span>
                                <span class="tooltip-text">
                                    <strong>Exercise Match Score:</strong><br>
                                    • Based on your daily exercise time and type<br>
                                    • Compares your activity level to the breed's exercise needs<br>
                                    • Higher score means your routine aligns well with the breed's energy requirements.
                                </span>
                            </span>
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars.get('exercise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
                        </div>
                        <span class="percentage">{display_scores['exercise']:.1f}%</span>
                    </div>

                    <!-- Grooming Compatibility Score -->
                    <div class="score-item">
                        <span class="label">
                            Grooming Match:
                            <span class="tooltip">
                                <span class="tooltip-icon">ⓘ</span>
                                <span class="tooltip-text">
                                    <strong>Grooming Match Score:</strong><br>
                                    • Evaluates breed's grooming needs (coat care, trimming, brushing)<br>
                                    • Compares these requirements with your grooming commitment level<br>
                                    • Higher score means the breed's grooming needs fit your willingness and capability.
                                </span>
                            </span>
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars.get('grooming', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
                        </div>
                        <span class="percentage">{display_scores['grooming']:.1f}%</span>
                    </div>

                    <!-- Experience Compatibility Score -->
                    <div class="score-item">
                        <span class="label">
                            Experience Match:
                            <span class="tooltip">
                                <span class="tooltip-icon">ⓘ</span>
                                <span class="tooltip-text">
                                    <strong>Experience Match Score:</strong><br>
                                    • Based on your dog-owning experience level<br>
                                    • Considers breed's training complexity, temperament, and handling difficulty<br>
                                    • Higher score means the breed is more suitable for your experience level.
                                </span>
                            </span>
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars.get('experience', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
                        </div>
                        <span class="percentage">{display_scores['experience']:.1f}%</span>
                    </div>

                    <!-- Noise Compatibility Score -->
                    <div class="score-item">
                        <span class="label">
                            Noise Compatibility:
                            <span class="tooltip">
                                <span class="tooltip-icon">ⓘ</span>
                                <span class="tooltip-text">
                                    <strong>Noise Compatibility Score:</strong><br>
                                    • Based on your noise tolerance preference<br>
                                    • Considers breed's typical noise level and barking tendencies<br>
                                    • Accounts for living environment and sensitivity to noise.
                                </span>
                            </span>
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars.get('noise', {'style': 'width: 0%; background-color: #e74c3c;'})['style']}"></div>
                        </div>
                        <span class="percentage">{display_scores['noise']:.1f}%</span>
                    </div>

                    {f'''
                    <div class="score-item bonus-score">
                        <span class="label">
                            Breed Bonus:
                            <span class="tooltip">
                                <span class="tooltip-icon">ⓘ</span>
                                <span class="tooltip-text">
                                    <strong>Breed Bonus Points:</strong><br>
{('<br>• '.join(bonus_reasons) if bonus_reasons else 'No additional bonus points')}<br><br>
                                    <strong>Bonus Factors Include:</strong><br>
                                    • Friendly temperament<br>
                                    • Child compatibility<br>
                                    • Longer lifespan<br>
                                    • Living space adaptability
                                </span>
                            </span>
                        </span>
                        <div class="progress-bar">
                            <div class="progress" style="{progress_bars['bonus']['style']}"></div>
                        </div>
                        <span class="percentage">{bonus_score*100:.1f}%</span>
                    </div>
                ''' if bonus_score > 0 else ''}
                </div>
        """
        
        # 使用格式器生成詳細區段
        html_content += formatter.generate_detailed_sections_html(
            breed, info, noise_characteristics, barking_triggers, noise_level,
            health_considerations, health_screenings
        )
        
        html_content += """
            </div>
        </div>
        """

    # 結束 HTML 內容
    html_content += "</div>"
    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 '''
        <div style="text-align: center; padding: 60px 20px; background: linear-gradient(135deg, #f8fafc, #e2e8f0); border-radius: 16px; margin: 20px 0;">
            <div style="font-size: 3em; margin-bottom: 16px;">🐕</div>
            <h3 style="color: #374151; margin-bottom: 12px;">No Recommendations Available</h3>
            <p style="color: #6b7280; font-size: 1.1em;">Please try adjusting your preferences or description, and we'll help you find the most suitable breeds.</p>
        </div>
        '''

    # 使用格式器的統一CSS樣式
    html_content = formatter.unified_css + "<div class='unified-recommendations'>"

    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:<br>• Evaluates how well the breed adapts to your living environment<br>• Considers if your home (apartment/house) and yard access suit the breed\'s size<br>• Higher score means the breed fits well in your available space.',
            'exercise': 'Exercise Requirements Score:<br>• Based on your daily exercise time and activity type<br>• Compares your activity level to the breed\'s exercise needs<br>• Higher score means your routine aligns well with the breed\'s energy requirements.',
            'grooming': 'Grooming Needs Score:<br>• Evaluates breed\'s grooming needs (coat care, trimming, brushing)<br>• Compares these requirements with your grooming commitment level<br>• Higher score means the breed\'s grooming needs fit your willingness and capability.',
            'experience': 'Experience Level Score:<br>• Based on your dog-owning experience level<br>• Considers breed\'s training complexity, temperament, and handling difficulty<br>• Higher score means the breed is more suitable for your experience level.',
            'noise': 'Noise Control Score:<br>• Based on your noise tolerance preference<br>• Considers breed\'s typical noise level and barking tendencies<br>• Accounts for living environment and sensitivity to noise.',
            'family': 'Family Compatibility Score:<br>• Evaluates how well the breed fits with your family situation<br>• Considers children, other pets, and family dynamics<br>• 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'<span class="tooltip"><span class="tooltip-icon">i</span><span class="tooltip-text"><strong>{tooltip_content.get(dim, "")}</strong></span></span>'

            dimension_html += f'''
            <div class="unified-dimension-item">
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
                    <span style="font-weight: 600; color: #374151;">{label} {tooltip_html}</span>
                    <span style="font-weight: 700; color: #1f2937; font-size: 1.1em;">{percentage}</span>
                </div>
                {progress_bar}
            </div>
            '''

        # 生成品種資訊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'''
                <div class="unified-info-item">
                    <div class="unified-info-label">{label}</div>
                    <div class="unified-info-value">{value}</div>
                </div>
                '''

        # 生成單個品種卡片HTML
        overall_percentage = formatter.format_unified_percentage(overall_score)
        overall_progress_bar = formatter.generate_unified_progress_bar(overall_score)

        brand_card_html = f'''
        <div class="unified-breed-card">
            <div class="unified-breed-header">
                <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
                    <div class="unified-rank-badge">🏆 #{rank}</div>
                    <h2 class="unified-breed-title">{breed.replace('_', ' ')}</h2>
                    <div style="margin-left: auto;">
                        <div class="unified-match-score">Overall Match: {overall_percentage}</div>
                    </div>
                </div>
            </div>

            <div class="unified-overall-section">
                {overall_progress_bar}
            </div>

            <div class="unified-dimension-grid">
                {dimension_html}
            </div>

            <div class="unified-breed-info">
                {info_html}
            </div>
            
            <div style="background: linear-gradient(135deg, #F8FAFC, #F1F5F9); padding: 20px; border-radius: 12px; margin: 20px 0; border: 1px solid #E2E8F0;">
                <h3 style="color: #1F2937; font-size: 1.3em; font-weight: 700; margin: 0 0 12px 0; display: flex; align-items: center;">
                    <span style="margin-right: 8px;">📝</span> Breed Description
                </h3>
                <p style="color: #4B5563; line-height: 1.6; margin: 0 0 16px 0; font-size: 1.05em;">
                    {breed_info.get('Description', 'Detailed description for this breed is not currently available.')}
                </p>
                <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-').replace(' ', '-')}/" 
                   target="_blank" 
                   class="akc-button"
                   style="display: inline-flex; align-items: center; padding: 12px 20px; 
                          background: linear-gradient(135deg, #3B82F6, #1D4ED8); color: white; 
                          text-decoration: none; border-radius: 10px; font-weight: 600; 
                          box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 
                          transition: all 0.3s ease; font-size: 1.05em;"
                   onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(59, 130, 246, 0.5)'"
                   onmouseout="this.style.transform='translateY(0px)'; this.style.boxShadow='0 4px 12px rgba(59, 130, 246, 0.3)'">
                    <span style="margin-right: 8px;">🌐</span>
                    Learn more about {breed.replace('_', ' ')} on AKC website
                </a>
            </div>
        </div>
        '''

        html_content += brand_card_html

    html_content += "</div>"
    return html_content