|
12 | 12 | from typing import TYPE_CHECKING, Literal |
13 | 13 |
|
14 | 14 | from ... import _coerce |
15 | | -from ..._html_badges import _source_kind_badge_html |
| 15 | +from ..._html_badges import _micro_badges, _source_kind_badge_html, _stat_card |
16 | 16 | from ..._html_data_attrs import _build_data_attrs |
17 | 17 | from ..._html_escape import _escape_html |
18 | 18 | from ..._html_filters import CLONE_TYPE_OPTIONS, SPREAD_OPTIONS, _render_select |
|
27 | 27 | from ...report.json_contract import clone_group_id |
28 | 28 | from ...report.suggestions import classify_clone_type |
29 | 29 | from .._components import Tone, insight_block |
| 30 | +from .._glossary import glossary_tip |
30 | 31 | from .._icons import ICONS |
31 | 32 | from .._tables import render_rows_table |
32 | 33 | from .._tabs import render_split_tabs |
@@ -740,12 +741,66 @@ def render_clones_panel(ctx: ReportContext) -> tuple[str, bool, int, int]: |
740 | 741 | "from active review." |
741 | 742 | ) |
742 | 743 | clones_tone: Tone = "warn" if ctx.clone_groups_total > 0 else "ok" |
| 744 | + |
| 745 | + # Stat cards |
| 746 | + avg_per_group = ( |
| 747 | + f"{ctx.clone_instances_total / max(1, ctx.clone_groups_total):.1f}" |
| 748 | + if ctx.clone_groups_total > 0 |
| 749 | + else "0" |
| 750 | + ) |
| 751 | + high_spread = sum( |
| 752 | + 1 |
| 753 | + for gs in (ctx.func_sorted, ctx.block_sorted, ctx.segment_sorted) |
| 754 | + for _, items in gs |
| 755 | + if len({str(it.get("filepath", "")) for it in items}) > 1 |
| 756 | + ) |
| 757 | + clone_cards = [ |
| 758 | + _stat_card( |
| 759 | + "Clone groups", |
| 760 | + ctx.clone_groups_total, |
| 761 | + detail=_micro_badges( |
| 762 | + ("functions", len(ctx.func_sorted)), |
| 763 | + ("blocks", len(ctx.block_sorted)), |
| 764 | + ("segments", len(ctx.segment_sorted)), |
| 765 | + ), |
| 766 | + value_tone="warn" if ctx.clone_groups_total > 0 else "good", |
| 767 | + glossary_tip_fn=glossary_tip, |
| 768 | + ), |
| 769 | + _stat_card( |
| 770 | + "Instances", |
| 771 | + ctx.clone_instances_total, |
| 772 | + detail=_micro_badges(("avg/group", avg_per_group)), |
| 773 | + value_tone="warn" if ctx.clone_instances_total > 0 else "good", |
| 774 | + glossary_tip_fn=glossary_tip, |
| 775 | + ), |
| 776 | + ] |
| 777 | + if novelty_enabled: |
| 778 | + clone_cards.append( |
| 779 | + _stat_card( |
| 780 | + "New groups", |
| 781 | + total_new, |
| 782 | + detail=_micro_badges(("known", total_known)), |
| 783 | + value_tone="bad" if total_new > 0 else "good", |
| 784 | + glossary_tip_fn=glossary_tip, |
| 785 | + ), |
| 786 | + ) |
| 787 | + clone_cards.append( |
| 788 | + _stat_card( |
| 789 | + "High spread", |
| 790 | + high_spread, |
| 791 | + value_tone="warn" if high_spread > 0 else "muted", |
| 792 | + glossary_tip_fn=glossary_tip, |
| 793 | + ), |
| 794 | + ) |
| 795 | + clone_cards_html = f'<div class="stat-cards">{"".join(clone_cards)}</div>' |
| 796 | + |
743 | 797 | panel = ( |
744 | 798 | insight_block( |
745 | 799 | question="Where is duplication concentrated right now?", |
746 | 800 | answer=clones_answer, |
747 | 801 | tone=clones_tone, |
748 | 802 | ) |
| 803 | + + clone_cards_html |
749 | 804 | + panel |
750 | 805 | ) |
751 | 806 |
|
|
0 commit comments