Skip to content

Commit 0729414

Browse files
committed
feat: add rule-level learning loop analytics
Expose which rule IDs users accept and reject most often so the reinforcement loop is inspectable at the same granularity it reranks findings. Made-with: Cursor
1 parent a4d574c commit 0729414

File tree

1 file changed

+77
-4
lines changed

1 file changed

+77
-4
lines changed

web/src/pages/Analytics.tsx

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)