Skip to content

Commit bbc580b

Browse files
committed
improve callout
1 parent 179c4fd commit bbc580b

1 file changed

Lines changed: 89 additions & 100 deletions

File tree

internal/server/static/index.html

Lines changed: 89 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -600,29 +600,42 @@
600600
margin-top: 24px;
601601
}
602602

603-
.efficiency-callout p {
603+
.efficiency-callout-success {
604+
background: linear-gradient(135deg, #e8f5e9 0%, #ffffff 100%);
605+
border: 2px solid #4caf50;
606+
border-radius: 12px;
607+
padding: 24px;
608+
margin-top: 24px;
609+
}
610+
611+
.efficiency-callout p,
612+
.efficiency-callout-success p {
604613
margin: 0 0 16px 0;
605614
font-size: 16px;
606615
line-height: 1.6;
607616
color: #1d1d1f;
608617
}
609618

610-
.efficiency-callout p:last-child {
619+
.efficiency-callout p:last-child,
620+
.efficiency-callout-success p:last-child {
611621
margin-bottom: 0;
612622
}
613623

614-
.efficiency-callout strong {
624+
.efficiency-callout strong,
625+
.efficiency-callout-success strong {
615626
color: #000;
616627
font-weight: 600;
617628
}
618629

619-
.efficiency-callout a {
630+
.efficiency-callout a,
631+
.efficiency-callout-success a {
620632
color: #007aff;
621633
text-decoration: none;
622634
font-weight: 600;
623635
}
624636

625-
.efficiency-callout a:hover {
637+
.efficiency-callout a:hover,
638+
.efficiency-callout-success a:hover {
626639
text-decoration: underline;
627640
}
628641

@@ -698,31 +711,12 @@
698711
margin-bottom: 20px;
699712
}
700713

701-
.progress-header {
702-
font-weight: 600;
703-
margin-bottom: 12px;
704-
color: #333;
705-
}
706-
707-
.progress-item {
708-
padding: 8px 0;
709-
font-size: 14px;
714+
.progress-status {
715+
font-size: 15px;
710716
color: #555;
711-
display: flex;
712-
align-items: center;
713-
gap: 8px;
714-
}
715-
716-
.progress-item .status {
717717
font-weight: 500;
718-
min-width: 80px;
719718
}
720719

721-
.progress-item.fetching .status { color: #007aff; }
722-
.progress-item.processing .status { color: #ff9500; }
723-
.progress-item.complete .status { color: #34c759; }
724-
.progress-item.error .status { color: #ff3b30; }
725-
726720
.progress-summary {
727721
margin-top: 12px;
728722
padding-top: 12px;
@@ -983,20 +977,25 @@ <h1>PR Cost Calculator</h1>
983977
<div class="form-row">
984978
<div class="form-group">
985979
<label for="repoSampleSize">Sample Size</label>
986-
<select id="repoSampleSize">
987-
<option value="25" selected>25 PRs (fast, ±20% accuracy)</option>
988-
<option value="50">50 PRs (slower, ±14% accuracy)</option>
989-
</select>
990-
<div class="help-text">Number of PRs to sample</div>
980+
<input
981+
type="number"
982+
id="repoSampleSize"
983+
value="25"
984+
min="1"
985+
max="50"
986+
>
987+
<div class="help-text">25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</div>
991988
</div>
992989
<div class="form-group">
993990
<label for="repoDays">Days Back</label>
994-
<select id="repoDays">
995-
<option value="10">10 days</option>
996-
<option value="30">30 days</option>
997-
<option value="90" selected>90 days</option>
998-
</select>
999-
<div class="help-text">Time period for analysis</div>
991+
<input
992+
type="number"
993+
id="repoDays"
994+
value="90"
995+
min="1"
996+
max="365"
997+
>
998+
<div class="help-text">Number of days to look back</div>
1000999
</div>
10011000
</div>
10021001
</div>
@@ -1020,20 +1019,25 @@ <h1>PR Cost Calculator</h1>
10201019
<div class="form-row">
10211020
<div class="form-group">
10221021
<label for="orgSampleSize">Sample Size</label>
1023-
<select id="orgSampleSize">
1024-
<option value="25" selected>25 PRs (fast, ±20% accuracy)</option>
1025-
<option value="50">50 PRs (slower, ±14% accuracy)</option>
1026-
</select>
1027-
<div class="help-text">Number of PRs to sample</div>
1022+
<input
1023+
type="number"
1024+
id="orgSampleSize"
1025+
value="25"
1026+
min="1"
1027+
max="50"
1028+
>
1029+
<div class="help-text">25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</div>
10281030
</div>
10291031
<div class="form-group">
10301032
<label for="orgDays">Days Back</label>
1031-
<select id="orgDays">
1032-
<option value="10">10 days</option>
1033-
<option value="30">30 days</option>
1034-
<option value="90" selected>90 days</option>
1035-
</select>
1036-
<div class="help-text">Time period for analysis</div>
1033+
<input
1034+
type="number"
1035+
id="orgDays"
1036+
value="90"
1037+
min="1"
1038+
max="365"
1039+
>
1040+
<div class="help-text">Number of days to look back</div>
10371041
</div>
10381042
</div>
10391043
</div>
@@ -1249,7 +1253,7 @@ <h3>Why calculate PR costs?</h3>
12491253
}
12501254
}
12511255

1252-
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, userCount = 0, salary = 250000, benefitsMultiplier = 1.2, analysisType = 'project', sourceName = '') {
1256+
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, userCount = 0, salary = 250000, benefitsMultiplier = 1.2, analysisType = 'project', sourceName = '', totalPRs = 0, daysInPeriod = 90) {
12531257
let html = '<div class="efficiency-section">';
12541258
html += '<h2>Workflow Efficiency</h2>';
12551259

@@ -1277,42 +1281,35 @@ <h3>Why calculate PR costs?</h3>
12771281

12781282
html += '</div>'; // Close efficiency-content
12791283

1280-
// Callout for B or lower (not B+, not A-, not A, not A+)
1281-
const showCallout = grade === 'B' || grade === 'B-' || grade === 'C+' || grade === 'C' ||
1282-
grade === 'C-' || grade === 'D+' || grade === 'D' || grade === 'D-' || grade === 'F';
1283-
1284-
if (showCallout && isAnnual) {
1285-
// Calculate savings potential at 94% efficiency
1286-
const totalAnnualCost = totalCost * (annualWasteCost / preventableCost);
1287-
const targetWaste = totalAnnualCost * 0.06; // 6% waste at 94% efficiency
1288-
const currentWaste = annualWasteCost;
1289-
let savings = currentWaste - targetWaste;
1290-
1291-
// Subtract our pricing: $5/month * 12 months * number of users
1292-
const pricingCost = 5 * 12 * userCount;
1293-
savings -= pricingCost;
1294-
1295-
if (savings > 0) {
1296-
// Calculate average waste per PR
1297-
const avgWasteHoursPerPR = extPreventableHours / e.total_prs;
1298-
const avgWasteCostPerPR = extPreventableCost / e.total_prs;
1299-
1300-
// Estimate PRs per author: total PRs in period / unique users in sample
1301-
const daysInPeriod = parseInt(days);
1302-
const prsPerPersonInPeriod = e.total_prs / userCount;
1303-
const prsPerPersonPerYear = prsPerPersonInPeriod * (365 / daysInPeriod);
1304-
1305-
// Calculate annual waste per author
1306-
const annualWasteHoursPerAuthor = avgWasteHoursPerPR * prsPerPersonPerYear;
1307-
const annualWasteCostPerAuthor = avgWasteCostPerPR * prsPerPersonPerYear;
1308-
1309-
// Weekly waste per author
1310-
const weeklyWastePerAuthor = annualWasteHoursPerAuthor / 52;
1311-
const hoursPerWeek = weeklyWastePerAuthor.toFixed(1);
1284+
if (isAnnual) {
1285+
// Calculate per-author metrics for callout
1286+
const avgWasteHoursPerPR = preventableHours / totalPRs;
1287+
const avgWasteCostPerPR = preventableCost / totalPRs;
1288+
const prsPerPersonInPeriod = totalPRs / userCount;
1289+
const prsPerPersonPerYear = prsPerPersonInPeriod * (365 / daysInPeriod);
1290+
const annualWasteHoursPerAuthor = avgWasteHoursPerPR * prsPerPersonPerYear;
1291+
const annualWasteCostPerAuthor = avgWasteCostPerPR * prsPerPersonPerYear;
1292+
const weeklyWastePerAuthor = annualWasteHoursPerAuthor / 52;
1293+
const hoursPerWeek = weeklyWastePerAuthor.toFixed(1);
1294+
1295+
// Check if B+ or higher (A+, A, A-, B+)
1296+
const isHighPerformer = grade === 'A+' || grade === 'A' || grade === 'A-' || grade === 'B+';
1297+
1298+
// Check if B or lower
1299+
const showStrongCallout = grade === 'B' || grade === 'B-' || grade === 'C+' || grade === 'C' ||
1300+
grade === 'C-' || grade === 'D+' || grade === 'D' || grade === 'D-' || grade === 'F';
1301+
1302+
if (isHighPerformer) {
1303+
// Green callout for high performers
1304+
html += '<div class="efficiency-callout-success">';
1305+
html += `<p style="font-size: 16px; margin-bottom: 0; line-height: 1.5;">You probably don't need this as your devs only lose <strong>${hoursPerWeek} hours/week due to pull request latency</strong>, but for everyone else, there is <a href="https://codegroove.dev/products/ready-to-review/" target="_blank">Ready-to-Review</a>. Free for OSS, $5/dev/month for private. Beta begins November 3rd: <a href="mailto:beta@codeGROOVE.dev">beta@codeGROOVE.dev</a></p>`;
1306+
html += '</div>';
1307+
} else if (showStrongCallout) {
1308+
// Yellow callout for B or lower
13121309
const costPerPersonFormatted = '$' + Math.round(annualWasteCostPerAuthor).toLocaleString('en-US');
13131310

13141311
html += '<div class="efficiency-callout">';
1315-
html += `<p style="font-size: 16px; margin-bottom: 16px; line-height: 1.5;">Your devs lose <strong>${hoursPerWeek} hours/week</strong> waiting on code reviews. That's <strong>${costPerPersonFormatted}/year per person</strong>. Ready-to-Review fixes this.</p>`;
1312+
html += `<p style="font-size: 16px; margin-bottom: 16px; line-height: 1.5;">Your devs lose <strong>${hoursPerWeek} hours/week due to pull request latency</strong>. That's <strong>${costPerPersonFormatted}/year per person</strong>. <a href="https://codegroove.dev/products/ready-to-review/" target="_blank">Ready-to-Review</a> fixes this.</p>`;
13161313
html += '<p style="font-size: 16px; margin-bottom: 0; line-height: 1.5;">Free for OSS, $5/dev/month for private. Beta begins November 3rd: <a href="mailto:beta@codeGROOVE.dev">beta@codeGROOVE.dev</a></p>';
13171314
html += '</div>';
13181315
}
@@ -1754,7 +1751,6 @@ <h3>Why calculate PR costs?</h3>
17541751
// EventSource doesn't support POST, so we need a different approach
17551752
// We'll use fetch to initiate, but handle it as a proper SSE stream
17561753
return new Promise((resolve, reject) => {
1757-
const progressMap = new Map();
17581754
let progressContainer;
17591755

17601756
// Make the POST request with fetch
@@ -1837,7 +1833,7 @@ <h3>Why calculate PR costs?</h3>
18371833
const salary = request.config?.annual_salary || 250000;
18381834
const benefitsMultiplier = request.config?.benefits_multiplier || 1.2;
18391835
const analysisType = mode === 'org' ? 'organization' : 'project';
1840-
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, true, annualWasteCost, annualWasteHours, e.unique_users || 0, salary, benefitsMultiplier, analysisType, sourceName);
1836+
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, true, annualWasteCost, annualWasteHours, e.unique_users || 0, salary, benefitsMultiplier, analysisType, sourceName, e.total_prs, parseInt(days));
18411837

18421838
// Calculate average PR efficiency
18431839
const totalPRs = e.total_prs;
@@ -1871,36 +1867,29 @@ <h3>Why calculate PR costs?</h3>
18711867
return;
18721868
}
18731869

1874-
// Handle PR-specific progress
1870+
// Handle PR-specific progress - single line status update
18751871
if (data.pr) {
18761872
if (!progressContainer) {
18771873
progressContainer = document.createElement('div');
18781874
progressContainer.className = 'progress-container';
1879-
progressContainer.innerHTML = '<div class="progress-header">Processing PRs...</div>';
1875+
progressContainer.innerHTML = '<div class="progress-status"></div>';
18801876
resultDiv.innerHTML = '';
18811877
resultDiv.appendChild(progressContainer);
18821878
}
18831879

1884-
let progressItem = progressMap.get(data.pr);
1885-
if (!progressItem) {
1886-
progressItem = document.createElement('div');
1887-
progressItem.className = 'progress-item';
1888-
progressContainer.appendChild(progressItem);
1889-
progressMap.set(data.pr, progressItem);
1890-
}
1891-
1880+
const statusLine = progressContainer.querySelector('.progress-status');
18921881
const repo = data.owner && data.repo ? `${data.owner}/${data.repo}` : '';
1893-
const prLink = repo ? `${repo}#${data.pr}` : `#${data.pr}`;
1882+
const prLink = repo ? `${repo}#${data.pr}` : `PR #${data.pr}`;
1883+
const progress = data.progress || '';
18941884

1895-
progressItem.className = `progress-item ${data.type}`;
18961885
if (data.type === 'fetching') {
1897-
progressItem.innerHTML = `<span class="status">Fetching</span> ${prLink} ${data.progress || ''}`;
1886+
statusLine.innerHTML = `Fetching ${prLink}... ${progress}`;
18981887
} else if (data.type === 'processing') {
1899-
progressItem.innerHTML = `<span class="status">Processing</span> ${prLink} ${data.progress || ''}`;
1888+
statusLine.innerHTML = `Processing ${prLink}... ${progress}`;
19001889
} else if (data.type === 'complete') {
1901-
progressItem.innerHTML = `<span class="status">✓ Complete</span> ${prLink} ${data.progress || ''}`;
1890+
statusLine.innerHTML = `✓ Completed ${prLink} ${progress}`;
19021891
} else if (data.type === 'error') {
1903-
progressItem.innerHTML = `<span class="status">✗ Error</span> ${prLink}: ${data.error}`;
1892+
statusLine.innerHTML = `✗ Error with ${prLink}: ${data.error}`;
19041893
}
19051894
}
19061895
} catch (parseError) {

0 commit comments

Comments
 (0)