文件预览

html_reporter.py

查看 Code Analysis Skills 技能包中的文件内容。

文件内容

src/reporters/html_reporter.py

"""
HTML Reporter - Generates comprehensive analysis reports in HTML format.

Uses Jinja2 templates for rich, styled HTML output with:
  - Developer evaluations (score, grade, strengths, weaknesses, suggestions)
  - Slacking index visualization
  - Score bar charts
  - Comparison tables and leaderboards
"""

from typing import Dict

from jinja2 import Template

from src.reporters.base_reporter import BaseReporter

HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Code Analysis Report</title>
    <style>
        :root {
            --primary: #4f46e5;
            --primary-light: #818cf8;
            --bg: #f8fafc;
            --card-bg: #ffffff;
            --text: #1e293b;
            --text-muted: #64748b;
            --border: #e2e8f0;
            --success: #22c55e;
            --warning: #f59e0b;
            --danger: #ef4444;
            --info: #3b82f6;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: var(--bg);
            color: var(--text);
            line-height: 1.6;
            padding: 2rem;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        h1 {
            font-size: 2rem;
            color: var(--primary);
            margin-bottom: 1.5rem;
            padding-bottom: 0.5rem;
            border-bottom: 3px solid var(--primary);
        }
        h2 {
            font-size: 1.5rem;
            margin: 2rem 0 1rem;
            padding: 0.5rem 1rem;
            background: var(--primary);
            color: white;
            border-radius: 8px;
        }
        h3 {
            font-size: 1.2rem;
            color: var(--primary);
            margin: 1.5rem 0 0.5rem;
        }
        h4 {
            font-size: 1rem;
            color: var(--text-muted);
            margin: 1rem 0 0.5rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }
        .card {
            background: var(--card-bg);
            border: 1px solid var(--border);
            border-radius: 12px;
            padding: 1.5rem;
            margin-bottom: 1.5rem;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin: 0.5rem 0 1rem;
        }
        th, td {
            padding: 0.6rem 1rem;
            text-align: left;
            border-bottom: 1px solid var(--border);
        }
        th {
            background: var(--bg);
            font-weight: 600;
            font-size: 0.875rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            color: var(--text-muted);
        }
        tr:hover { background: #f1f5f9; }
        .metric-value { font-weight: 600; color: var(--primary); }
        .comparison-table th { background: var(--primary); color: white; }
        .comparison-table tr:nth-child(even) { background: #f8fafc; }
        .badge {
            display: inline-block;
            padding: 0.15rem 0.5rem;
            border-radius: 999px;
            font-size: 0.75rem;
            font-weight: 600;
        }
        .badge-good { background: #dcfce7; color: #166534; }
        .badge-warn { background: #fef3c7; color: #92400e; }
        .badge-bad { background: #fee2e2; color: #991b1b; }
        .badge-info { background: #dbeafe; color: #1e40af; }

        /* Evaluation styles */
        .eval-header {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-bottom: 1rem;
            flex-wrap: wrap;
        }
        .score-circle {
            width: 80px;
            height: 80px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1.5rem;
            font-weight: 700;
            color: white;
            flex-shrink: 0;
        }
        .score-s { background: linear-gradient(135deg, #f59e0b, #ef4444); }
        .score-a { background: linear-gradient(135deg, #22c55e, #16a34a); }
        .score-b { background: linear-gradient(135deg, #3b82f6, #2563eb); }
        .score-c { background: linear-gradient(135deg, #f59e0b, #d97706); }
        .score-d { background: linear-gradient(135deg, #f97316, #ea580c); }
        .score-e { background: linear-gradient(135deg, #ef4444, #dc2626); }
        .score-f { background: linear-gradient(135deg, #991b1b, #7f1d1d); }
        .grade-label {
            font-size: 2rem;
            font-weight: 800;
            margin-left: 0.5rem;
        }
        .verdict {
            font-style: italic;
            color: var(--text-muted);
            margin: 0.5rem 0 1rem;
            padding: 0.5rem 1rem;
            border-left: 4px solid var(--primary);
            background: #f1f5f9;
            border-radius: 0 8px 8px 0;
        }
        .score-bar-container {
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }
        .score-bar {
            height: 12px;
            border-radius: 6px;
            background: #e2e8f0;
            flex: 1;
            max-width: 200px;
            overflow: hidden;
        }
        .score-bar-fill {
            height: 100%;
            border-radius: 6px;
            transition: width 0.3s ease;
        }
        .fill-high { background: linear-gradient(90deg, #22c55e, #16a34a); }
        .fill-mid { background: linear-gradient(90deg, #f59e0b, #d97706); }
        .fill-low { background: linear-gradient(90deg, #ef4444, #dc2626); }

        .strength-item { color: #166534; margin: 0.3rem 0; }
        .weakness-item { color: #991b1b; margin: 0.3rem 0; }
        .suggestion-item { color: #1e40af; margin: 0.3rem 0; }

        .strength-item::before { content: "✅ "; }
        .weakness-item::before { content: "❌ "; }
        .suggestion-item::before { content: "💡 "; }

        /* Slacking Index */
        .slacking-meter {
            width: 100%;
            height: 30px;
            background: linear-gradient(90deg, #22c55e, #f59e0b, #ef4444);
            border-radius: 15px;
            position: relative;
            margin: 1rem 0;
        }
        .slacking-indicator {
            position: absolute;
            top: -5px;
            width: 20px;
            height: 40px;
            background: white;
            border: 3px solid #1e293b;
            border-radius: 4px;
            transform: translateX(-50%);
        }
        .slacking-label {
            font-size: 1.2rem;
            font-weight: 700;
            margin: 0.5rem 0;
        }

        /* Leaderboard */
        .leaderboard-rank {
            font-size: 1.2rem;
            font-weight: 700;
        }
        .rank-1 { color: #f59e0b; }
        .rank-2 { color: #94a3b8; }
        .rank-3 { color: #b45309; }

        footer {
            text-align: center;
            margin-top: 3rem;
            padding-top: 1rem;
            border-top: 1px solid var(--border);
            color: var(--text-muted);
            font-size: 0.875rem;
        }

        @media print {
            body { padding: 0.5rem; }
            .card { break-inside: avoid; }
            h2 { break-before: page; }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 Code Analysis Report</h1>

        {% for repo_name, repo_metrics in metrics.items() %}
        <h2>📁 {{ repo_name }}</h2>

        {% set all_authors = [] %}
        {% for key, analyzer_data in repo_metrics.items() %}
            {% if key != 'evaluations' and analyzer_data is mapping %}
                {% for author in analyzer_data.keys() %}
                    {% if author not in all_authors %}
                        {% if all_authors.append(author) %}{% endif %}
                    {% endif %}
                {% endfor %}
            {% endif %}
        {% endfor %}

        {% for author in all_authors | sort %}
        <div class="card">
            <h3>👤 {{ author }}</h3>

            {# ── Developer Evaluation ── #}
            {% set ev = repo_metrics.get('evaluations', {}).get(author, {}) %}
            {% if ev %}
            <h4>🏆 Developer Evaluation</h4>
            <div class="eval-header">
                <div class="score-circle score-{{ ev.grade | lower }}">
                    {{ ev.overall_score }}
                </div>
                <div>
                    <span class="grade-label">{{ ev.grade }}</span>
                    <div class="verdict">{{ ev.verdict }}</div>
                </div>
            </div>

            {# Dimension score bars #}
            <table>
                <tr><th>Dimension</th><th>Score</th><th>Bar</th></tr>
                {% set dim_names = {
                    'commit_discipline': '📝 Commit Discipline',
                    'work_consistency': '⏰ Work Consistency',
                    'efficiency': '🚀 Efficiency',
                    'code_quality': '🔍 Code Quality',
                    'code_style': '🎨 Code Style',
                    'engagement': '💪 Engagement'
                } %}
                {% for dim, score in ev.get('dimension_scores', {}).items() %}
                <tr>
                    <td>{{ dim_names.get(dim, dim) }}</td>
                    <td class="metric-value">{{ "%.0f" | format(score) }}/100</td>
                    <td>
                        <div class="score-bar-container">
                            <div class="score-bar">
                                <div class="score-bar-fill {% if score >= 70 %}fill-high{% elif score >= 40 %}fill-mid{% else %}fill-low{% endif %}"
                                     style="width: {{ score }}%"></div>
                            </div>
                        </div>
                    </td>
                </tr>
                {% endfor %}
            </table>

            {# Strengths #}
            {% if ev.strengths %}
            <h4 style="color: #166534">Strengths</h4>
            {% for s in ev.strengths %}
            <div class="strength-item">{{ s }}</div>
            {% endfor %}
            {% endif %}

            {# Weaknesses #}
            {% if ev.weaknesses %}
            <h4 style="color: #991b1b; margin-top: 1rem">Weaknesses</h4>
            {% for w in ev.weaknesses %}
            <div class="weakness-item">{{ w }}</div>
            {% endfor %}
            {% endif %}

            {# Suggestions #}
            {% if ev.suggestions %}
            <h4 style="color: #1e40af; margin-top: 1rem">Suggestions</h4>
            {% for sg in ev.suggestions %}
            <div class="suggestion-item">{{ sg }}</div>
            {% endfor %}
            {% endif %}
            {% endif %}

            {# ── Slacking Index ── #}
            {% set sl = repo_metrics.get('slacking', {}).get(author, {}) %}
            {% if sl %}
            <h4>🐟 Slacking Index (摸鱼指数)</h4>
            <div class="slacking-label">
                {{ sl.slacking_index }}/100 — {{ sl.slacking_level_cn }} ({{ sl.slacking_level }})
            </div>
            <div class="slacking-meter">
                <div class="slacking-indicator" style="left: {{ sl.slacking_index }}%"></div>
            </div>
            <table>
                <tr><th>Signal</th><th>Value</th></tr>
                <tr><td>Activity Ratio</td><td>{{ "%.1f%%" | format(sl.activity_ratio * 100) }}</td></tr>
                <tr><td>Trivial Commit Ratio</td><td>{{ "%.1f%%" | format(sl.trivial_commit_ratio * 100) }}</td></tr>
                <tr><td>Large Gap Ratio</td><td>{{ "%.1f%%" | format(sl.large_gap_ratio * 100) }}</td></tr>
                <tr><td>Lines/Active Day</td><td>{{ sl.lines_per_active_day }}</td></tr>
                <tr><td>Non-code Commit Ratio</td><td>{{ "%.1f%%" | format(sl.non_code_commit_ratio * 100) }}</td></tr>
            </table>
            {% endif %}

            {# ── Commit Patterns ── #}
            {% set cd = repo_metrics.get('commit_patterns', {}).get(author, {}) %}
            {% if cd %}
            <h4>📝 Commit Patterns</h4>
            <table>
                <tr><th>Metric</th><th>Value</th></tr>
                <tr><td>Total Commits</td><td class="metric-value">{{ cd.total_commits }}</td></tr>
                <tr><td>Merge Ratio</td><td>{{ "%.1f%%" | format(cd.merge_ratio * 100) }}</td></tr>
                <tr><td>Active Span</td><td>{{ cd.active_span_days }} days</td></tr>
                <tr><td>Avg Commits/Day</td><td class="metric-value">{{ cd.avg_commits_per_active_day }}</td></tr>
                <tr><td>Avg Lines Added</td><td>{{ cd.avg_lines_added }}</td></tr>
                <tr><td>Avg Lines Deleted</td><td>{{ cd.avg_lines_deleted }}</td></tr>
                <tr><td>Total Lines Added</td><td>{{ "{:,}".format(cd.total_lines_added) }}</td></tr>
                <tr><td>Total Lines Deleted</td><td>{{ "{:,}".format(cd.total_lines_deleted) }}</td></tr>
            </table>
            {% endif %}

            {# ── Work Habits ── #}
            {% set hd = repo_metrics.get('work_habits', {}).get(author, {}) %}
            {% if hd %}
            <h4>⏰ Work Habits</h4>
            <table>
                <tr><th>Metric</th><th>Value</th></tr>
                <tr><td>Peak Hour</td><td class="metric-value">{{ hd.peak_hour }}:00</td></tr>
                <tr><td>Weekend Ratio</td><td>{{ "%.1f%%" | format(hd.weekend_ratio * 100) }}</td></tr>
                <tr><td>Late Night Ratio</td><td>
                    {{ "%.1f%%" | format(hd.late_night_ratio * 100) }}
                    {% if hd.late_night_ratio > 0.3 %}
                        <span class="badge badge-warn">High</span>
                    {% endif %}
                </td></tr>
                <tr><td>Longest Streak</td><td>{{ hd.longest_streak_days }} days</td></tr>
                <tr><td>Avg Gap</td><td>{{ hd.avg_gap_between_commits_hours }} hrs</td></tr>
            </table>
            {% endif %}

            {# ── Efficiency ── #}
            {% set ed = repo_metrics.get('efficiency', {}).get(author, {}) %}
            {% if ed %}
            <h4>🚀 Efficiency</h4>
            <table>
                <tr><th>Metric</th><th>Value</th></tr>
                <tr><td>Churn Rate</td><td>{{ "%.1f%%" | format(ed.churn_rate * 100) }}</td></tr>
                <tr><td>Rework Ratio</td><td>
                    {{ "%.1f%%" | format(ed.rework_ratio * 100) }}
                    {% if ed.rework_ratio > 0.3 %}
                        <span class="badge badge-warn">High Rework</span>
                    {% endif %}
                </td></tr>
                <tr><td>Lines/Commit</td><td>{{ ed.lines_per_commit }}</td></tr>
                <tr><td>Files Touched</td><td>{{ ed.unique_files_touched }}</td></tr>
                <tr><td>Ownership Ratio</td><td>{{ "%.1f%%" | format(ed.ownership_ratio * 100) }}</td></tr>
                <tr><td>Bus Factor</td><td>{{ ed.repo_avg_bus_factor }}</td></tr>
            </table>
            {% endif %}

            {# ── Code Style ── #}
            {% set sd = repo_metrics.get('code_style', {}).get(author, {}) %}
            {% if sd %}
            <h4>🎨 Code Style</h4>
            <table>
                <tr><th>Metric</th><th>Value</th></tr>
                <tr><td>Conventional Commit Ratio</td><td>{{ "%.1f%%" | format(sd.conventional_commit_ratio * 100) }}</td></tr>
                <tr><td>Issue Reference Ratio</td><td>{{ "%.1f%%" | format(sd.issue_reference_ratio * 100) }}</td></tr>
                <tr><td>Avg Change Size</td><td>{{ sd.avg_change_size_lines }} lines</td></tr>
            </table>
            {% endif %}

            {# ── Code Quality ── #}
            {% set qd = repo_metrics.get('code_quality', {}).get(author, {}) %}
            {% if qd %}
            <h4>🔍 Code Quality</h4>
            <table>
                <tr><th>Metric</th><th>Value</th></tr>
                <tr><td>Bug Fix Ratio</td><td>
                    {{ "%.1f%%" | format(qd.bug_fix_ratio * 100) }}
                    {% if qd.bug_fix_ratio > 0.5 %}
                        <span class="badge badge-bad">High</span>
                    {% elif qd.bug_fix_ratio > 0.3 %}
                        <span class="badge badge-warn">Moderate</span>
                    {% else %}
                        <span class="badge badge-good">Low</span>
                    {% endif %}
                </td></tr>
                <tr><td>Revert Ratio</td><td>{{ "%.1f%%" | format(qd.revert_ratio * 100) }}</td></tr>
                <tr><td>Large Commit Ratio</td><td>{{ "%.1f%%" | format(qd.large_commit_ratio * 100) }}</td></tr>
                <tr><td>Test Modification Ratio</td><td>{{ "%.1f%%" | format(qd.test_modification_ratio * 100) }}</td></tr>
                <tr><td>Avg Commit Size</td><td>{{ qd.avg_commit_size }} lines</td></tr>
                {% if qd.avg_python_complexity > 0 %}
                <tr><td>Avg Python Complexity</td><td>{{ qd.avg_python_complexity }}</td></tr>
                {% endif %}
            </table>
            {% endif %}
        </div>
        {% endfor %}
        {% endfor %}

        {# ── Leaderboard ── #}
        {% set has_evals = false %}
        {% for repo_name, repo_metrics in metrics.items() %}
            {% if repo_metrics.get('evaluations') %}
                {% set has_evals = true %}
            {% endif %}
        {% endfor %}

        {% for repo_name, repo_metrics in metrics.items() %}
        {% if repo_metrics.get('evaluations') %}
        <h2>🏆 Developer Leaderboard</h2>
        <div class="card">
            <table class="comparison-table">
                <tr><th>Rank</th><th>Developer</th><th>Score</th><th>Grade</th><th>Verdict</th></tr>
                {% for author, ev in repo_metrics.get('evaluations', {}).items() | sort(attribute='1.overall_score', reverse=true) %}
                <tr>
                    <td class="leaderboard-rank {% if loop.index <= 3 %}rank-{{ loop.index }}{% endif %}">
                        {% if loop.index == 1 %}🥇{% elif loop.index == 2 %}🥈{% elif loop.index == 3 %}🥉{% else %}{{ loop.index }}{% endif %}
                    </td>
                    <td><strong>{{ author }}</strong></td>
                    <td class="metric-value">{{ ev.overall_score }}</td>
                    <td><span class="badge badge-info">{{ ev.grade }}</span></td>
                    <td>{{ ev.verdict }}</td>
                </tr>
                {% endfor %}
            </table>
        </div>
        {% endif %}

        {# ── Slacking Leaderboard ── #}
        {% if repo_metrics.get('slacking') %}
        <h2>🐟 Slacking Index Leaderboard (摸鱼排行榜)</h2>
        <div class="card">
            <table class="comparison-table">
                <tr><th>Rank</th><th>Developer</th><th>Index</th><th>Level</th><th>Lines/Day</th></tr>
                {% for author, sl in repo_metrics.get('slacking', {}).items() | sort(attribute='1.slacking_index', reverse=true) %}
                <tr>
                    <td>{{ loop.index }}</td>
                    <td><strong>{{ author }}</strong></td>
                    <td class="metric-value">{{ sl.slacking_index }}/100</td>
                    <td>{{ sl.slacking_level_cn }}</td>
                    <td>{{ sl.lines_per_active_day }}</td>
                </tr>
                {% endfor %}
            </table>
        </div>
        {% endif %}
        {% endfor %}

        <footer>
            Generated by <strong>Code Analysis Skills</strong> | Powered by ClawHub
        </footer>
    </div>
</body>
</html>
"""


class HtmlReporter(BaseReporter):
    """Generates styled HTML reports from analysis metrics."""

    def generate(self, metrics: Dict) -> str:
        """Generate an HTML report using Jinja2 template."""
        template = Template(HTML_TEMPLATE)
        return template.render(metrics=metrics)