@@ -50,6 +50,7 @@ function computeAnalytics(reviews: ReviewSession[]) {
5050 . map ( ( [ name , value ] ) => ( { name, value } ) )
5151
5252 const feedbackTotalsByCategory : Record < string , { accepted : number ; rejected : number } > = { }
53+ const feedbackTotalsByRule : Record < string , { accepted : number ; rejected : number } > = { }
5354 const feedbackCoverageSeries = completed . map ( ( r , i ) => {
5455 let accepted = 0
5556 let rejected = 0
@@ -70,6 +71,17 @@ function computeAnalytics(reviews: ReviewSession[]) {
7071 current . rejected += 1
7172 }
7273 feedbackTotalsByCategory [ comment . category ] = current
74+
75+ const ruleId = comment . rule_id ?. trim ( )
76+ if ( ruleId ) {
77+ const currentRule = feedbackTotalsByRule [ ruleId ] ?? { accepted : 0 , rejected : 0 }
78+ if ( comment . feedback === 'accept' ) {
79+ currentRule . accepted += 1
80+ } else {
81+ currentRule . rejected += 1
82+ }
83+ feedbackTotalsByRule [ ruleId ] = currentRule
84+ }
7385 }
7486
7587 const totalComments = r . comments . length
@@ -100,6 +112,19 @@ function computeAnalytics(reviews: ReviewSession[]) {
100112 } )
101113 . sort ( ( left , right ) => right . total - left . total || right . accepted - left . accepted )
102114
115+ const feedbackRuleData = Object . entries ( feedbackTotalsByRule )
116+ . map ( ( [ name , totals ] ) => {
117+ const total = totals . accepted + totals . rejected
118+ return {
119+ name,
120+ accepted : totals . accepted ,
121+ rejected : totals . rejected ,
122+ total,
123+ acceptanceRate : total > 0 ? totals . accepted / total : 0 ,
124+ }
125+ } )
126+ . sort ( ( left , right ) => right . total - left . total || right . accepted - left . accepted )
127+
103128 const topAcceptedCategories = feedbackCategoryData
104129 . filter ( item => item . accepted > 0 )
105130 . sort ( ( left , right ) => right . accepted - left . accepted || right . total - left . total )
@@ -110,6 +135,16 @@ function computeAnalytics(reviews: ReviewSession[]) {
110135 . sort ( ( left , right ) => right . rejected - left . rejected || right . total - left . total )
111136 . slice ( 0 , 5 )
112137
138+ const topAcceptedRules = feedbackRuleData
139+ . filter ( item => item . accepted > 0 )
140+ . sort ( ( left , right ) => right . accepted - left . accepted || right . total - left . total )
141+ . slice ( 0 , 5 )
142+
143+ const topRejectedRules = feedbackRuleData
144+ . filter ( item => item . rejected > 0 )
145+ . sort ( ( left , right ) => right . rejected - left . rejected || right . total - left . total )
146+ . slice ( 0 , 5 )
147+
113148 // Aggregate stats
114149 const totalFindings = completed . reduce ( ( s , r ) => s + r . summary ! . total_comments , 0 )
115150 const avgFindings = completed . length > 0 ? totalFindings / completed . length : 0
@@ -140,6 +175,8 @@ function computeAnalytics(reviews: ReviewSession[]) {
140175 feedbackCoverageSeries,
141176 topAcceptedCategories,
142177 topRejectedCategories,
178+ topAcceptedRules,
179+ topRejectedRules,
143180 stats : {
144181 totalReviews : completed . length ,
145182 avgScore,
@@ -241,7 +278,7 @@ function TrendList({ items, emptyLabel }: { items: FeedbackEvalTrendGap[]; empty
241278 )
242279}
243280
244- function FeedbackCategoryList ( {
281+ function FeedbackBreakdownList ( {
245282 items,
246283 mode,
247284 emptyLabel,
@@ -300,6 +337,8 @@ export function Analytics() {
300337 feedbackCoverageSeries,
301338 topAcceptedCategories,
302339 topRejectedCategories,
340+ topAcceptedRules,
341+ topRejectedRules,
303342 stats,
304343 } = analytics
305344 const {
@@ -496,7 +535,7 @@ export function Analytics() {
496535 LEARNING LOOP
497536 </ div >
498537
499- < div className = "grid grid-cols-1 md:grid-cols-3 gap-3" >
538+ < div className = "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3" >
500539 < div className = "bg-surface-1 border border-border rounded-lg p-4" >
501540 < div className = "text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code mb-3" >
502541 FEEDBACK COVERAGE
@@ -585,7 +624,7 @@ export function Analytics() {
585624 < div className = "text-[10px] font-semibold text-text-muted tracking-[0.05em] font-code mb-2" >
586625 MOST ACCEPTED
587626 </ div >
588- < FeedbackCategoryList
627+ < FeedbackBreakdownList
589628 items = { topAcceptedCategories }
590629 mode = "accepted"
591630 emptyLabel = "No accepted categories yet"
@@ -595,7 +634,7 @@ export function Analytics() {
595634 < div className = "text-[10px] font-semibold text-text-muted tracking-[0.05em] font-code mb-2" >
596635 MOST REJECTED
597636 </ div >
598- < FeedbackCategoryList
637+ < FeedbackBreakdownList
599638 items = { topRejectedCategories }
600639 mode = "rejected"
601640 emptyLabel = "No rejected categories yet"
@@ -608,6 +647,40 @@ export function Analytics() {
608647 </ div >
609648 ) }
610649 </ div >
650+
651+ < div className = "bg-surface-1 border border-border rounded-lg p-4" >
652+ < div className = "text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code mb-3" >
653+ TOP LABELED RULES
654+ </ div >
655+ { stats . labeledFeedbackTotal > 0 ? (
656+ < div className = "grid grid-cols-1 gap-4" >
657+ < div >
658+ < div className = "text-[10px] font-semibold text-text-muted tracking-[0.05em] font-code mb-2" >
659+ MOST ACCEPTED
660+ </ div >
661+ < FeedbackBreakdownList
662+ items = { topAcceptedRules }
663+ mode = "accepted"
664+ emptyLabel = "No accepted rules yet"
665+ />
666+ </ div >
667+ < div className = "pt-3 border-t border-border-subtle" >
668+ < div className = "text-[10px] font-semibold text-text-muted tracking-[0.05em] font-code mb-2" >
669+ MOST REJECTED
670+ </ div >
671+ < FeedbackBreakdownList
672+ items = { topRejectedRules }
673+ mode = "rejected"
674+ emptyLabel = "No rejected rules yet"
675+ />
676+ </ div >
677+ </ div >
678+ ) : (
679+ < div className = "h-32 flex items-center justify-center text-center text-text-muted text-sm px-6" >
680+ Rule-level learning appears once findings with rule IDs receive thumbs.
681+ </ div >
682+ ) }
683+ </ div >
611684 </ div >
612685 </ >
613686 ) }
0 commit comments