initial leaderboard functionality

This commit is contained in:
Pedram Amini
2025-12-12 00:31:48 -06:00
parent c275b9744c
commit 121621588e
11 changed files with 809 additions and 24 deletions

View File

@@ -278,6 +278,279 @@ ${report.topInfluencers.map(u => `| [@${u.username}](https://github.com/${u.user
return md;
}
function generateHtmlDashboard(report, stargazers, forkers, userDetails) {
const embeddedData = {
report,
stargazers,
forkers,
userDetails: userDetails || {}
};
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maestro Community Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
padding: 24px;
min-height: 100vh;
}
.header { text-align: center; margin-bottom: 32px; }
.header h1 { font-size: 2.5rem; color: #58a6ff; margin-bottom: 8px; }
.header p { color: #8b949e; font-size: 0.9rem; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 24px;
text-align: center;
}
.stat-card .value { font-size: 2.5rem; font-weight: bold; color: #58a6ff; }
.stat-card .label { color: #8b949e; margin-top: 4px; }
.stat-card.stars .value { color: #f0c14b; }
.stat-card.forks .value { color: #a371f7; }
.stat-card.users .value { color: #3fb950; }
.stat-card.reach .value { color: #ff7b72; }
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.chart-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 24px;
}
.chart-card h3 { margin-bottom: 16px; color: #c9d1d9; }
.chart-container { position: relative; height: 300px; }
.tables-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 24px;
}
.table-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 24px;
max-height: 500px;
overflow-y: auto;
}
.table-card h3 {
margin-bottom: 16px;
color: #c9d1d9;
position: sticky;
top: 0;
background: #161b22;
padding-bottom: 8px;
}
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #30363d; }
th { color: #8b949e; font-weight: 500; font-size: 0.85rem; }
td { font-size: 0.9rem; }
tr:hover { background: #1f2428; }
.user-cell { display: flex; align-items: center; gap: 10px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; }
a { color: #58a6ff; text-decoration: none; }
a:hover { text-decoration: underline; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.badge.starred { background: #f0c14b22; color: #f0c14b; }
.badge.forked { background: #a371f722; color: #a371f7; }
.badge.both { background: #3fb95022; color: #3fb950; }
.location-bar { display: flex; align-items: center; margin-bottom: 8px; }
.location-bar .name {
width: 150px;
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.location-bar .bar {
flex: 1;
height: 20px;
background: #30363d;
border-radius: 4px;
margin: 0 12px;
overflow: hidden;
}
.location-bar .bar-fill {
height: 100%;
background: linear-gradient(90deg, #58a6ff, #a371f7);
border-radius: 4px;
}
.location-bar .count { width: 30px; text-align: right; font-size: 0.85rem; color: #8b949e; }
@media (max-width: 600px) {
.charts-grid, .tables-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<h1>Maestro Community Dashboard</h1>
<p id="generated-at"></p>
</div>
<div class="stats-grid">
<div class="stat-card stars"><div class="value" id="total-stars">-</div><div class="label">Stars</div></div>
<div class="stat-card forks"><div class="value" id="total-forks">-</div><div class="label">Forks</div></div>
<div class="stat-card users"><div class="value" id="total-users">-</div><div class="label">Unique Members</div></div>
<div class="stat-card reach"><div class="value" id="total-reach">-</div><div class="label">Community Reach</div></div>
</div>
<div class="charts-grid">
<div class="chart-card"><h3>Star Growth Over Time</h3><div class="chart-container"><canvas id="star-chart"></canvas></div></div>
<div class="chart-card"><h3>Fork Growth Over Time</h3><div class="chart-container"><canvas id="fork-chart"></canvas></div></div>
</div>
<div class="charts-grid">
<div class="chart-card"><h3>Top Locations</h3><div id="locations-chart"></div></div>
<div class="chart-card"><h3>Top Companies</h3><div id="companies-chart"></div></div>
</div>
<div class="tables-grid">
<div class="table-card"><h3>Top Influencers</h3><table id="influencers-table"><thead><tr><th>User</th><th>Company</th><th>Followers</th></tr></thead><tbody></tbody></table></div>
<div class="table-card"><h3>Recent Activity</h3><table id="activity-table"><thead><tr><th>User</th><th>Action</th><th>Date</th></tr></thead><tbody></tbody></table></div>
<div class="table-card"><h3>Highly Engaged (Starred + Forked)</h3><table id="engaged-table"><thead><tr><th>User</th><th>Location</th><th>Followers</th></tr></thead><tbody></tbody></table></div>
<div class="table-card"><h3>All Community Members</h3><table id="members-table"><thead><tr><th>User</th><th>Status</th><th>Joined GitHub</th></tr></thead><tbody></tbody></table></div>
</div>
<script>
const DATA = ${JSON.stringify(embeddedData)};
Chart.defaults.color = '#8b949e';
Chart.defaults.borderColor = '#30363d';
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function formatDate(dateStr) {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function escapeText(str) { return str ? String(str) : ''; }
function createLink(href, text) {
const a = document.createElement('a');
a.href = href; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = text;
return a;
}
function createBadge(type) {
const span = document.createElement('span');
span.className = 'badge ' + type; span.textContent = type;
return span;
}
function createGrowthChart(canvasId, data, color) {
new Chart(document.getElementById(canvasId).getContext('2d'), {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{ label: 'Cumulative', data: data.map(d => d.cumulative), borderColor: color, backgroundColor: color + '22', fill: true, tension: 0.3, pointRadius: 3, pointHoverRadius: 6 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 10 } }, y: { beginAtZero: true, grid: { color: '#30363d' } } } }
});
}
function createBarChart(containerId, data, maxItems = 10) {
const container = document.getElementById(containerId);
container.replaceChildren();
const maxCount = Math.max(...data.slice(0, maxItems).map(d => d[1]));
data.slice(0, maxItems).forEach(([name, count]) => {
const row = document.createElement('div'); row.className = 'location-bar';
const nameDiv = document.createElement('div'); nameDiv.className = 'name'; nameDiv.title = escapeText(name); nameDiv.textContent = escapeText(name);
const barDiv = document.createElement('div'); barDiv.className = 'bar';
const barFill = document.createElement('div'); barFill.className = 'bar-fill'; barFill.style.width = (count / maxCount * 100) + '%';
barDiv.appendChild(barFill);
const countDiv = document.createElement('div'); countDiv.className = 'count'; countDiv.textContent = count;
row.appendChild(nameDiv); row.appendChild(barDiv); row.appendChild(countDiv);
container.appendChild(row);
});
}
function populateTable(tableId, rows, cellFn) {
const tbody = document.querySelector('#' + tableId + ' tbody');
tbody.replaceChildren();
rows.forEach(row => {
const tr = document.createElement('tr');
cellFn(row).forEach(cell => tr.appendChild(cell));
tbody.appendChild(tr);
});
}
(function init() {
const { report, stargazers, forkers, userDetails } = DATA;
document.getElementById('generated-at').textContent = 'Last updated: ' + formatDate(report.generatedAt);
document.getElementById('total-stars').textContent = report.summary.totalStars;
document.getElementById('total-forks').textContent = report.summary.totalForks;
document.getElementById('total-users').textContent = report.summary.uniqueUsers;
document.getElementById('total-reach').textContent = formatNumber(report.demographics?.totalCommunityFollowers || 0);
createGrowthChart('star-chart', report.starGrowth, '#f0c14b');
createGrowthChart('fork-chart', report.forkGrowth, '#a371f7');
if (report.demographics) {
createBarChart('locations-chart', report.demographics.topLocations);
createBarChart('companies-chart', report.demographics.topCompanies);
}
populateTable('influencers-table', report.topInfluencers || [], u => {
const td1 = document.createElement('td'); td1.appendChild(createLink('https://github.com/' + u.username, '@' + u.username));
const td2 = document.createElement('td'); td2.textContent = escapeText(u.company) || '-';
const td3 = document.createElement('td'); td3.textContent = formatNumber(u.followers);
return [td1, td2, td3];
});
const activity = [...stargazers.map(s => ({ ...s, action: 'starred', date: s.starredAt })), ...forkers.map(f => ({ ...f, action: 'forked', date: f.forkedAt }))].sort((a, b) => new Date(b.date) - new Date(a.date)).slice(0, 20);
populateTable('activity-table', activity, a => {
const td1 = document.createElement('td'); td1.className = 'user-cell';
const img = document.createElement('img'); img.className = 'avatar'; img.src = a.avatarUrl; img.alt = '';
td1.appendChild(img); td1.appendChild(createLink(a.profileUrl, '@' + a.username));
const td2 = document.createElement('td'); td2.appendChild(createBadge(a.action));
const td3 = document.createElement('td'); td3.textContent = formatDate(a.date);
return [td1, td2, td3];
});
populateTable('engaged-table', report.engagedUsers || [], username => {
const details = userDetails[username] || {};
const td1 = document.createElement('td'); td1.appendChild(createLink('https://github.com/' + username, '@' + username));
const td2 = document.createElement('td'); td2.textContent = escapeText(details.location) || '-';
const td3 = document.createElement('td'); td3.textContent = formatNumber(details.followers || 0);
return [td1, td2, td3];
});
const starSet = new Set(stargazers.map(s => s.username));
const forkSet = new Set(forkers.map(f => f.username));
const allMembers = [...new Set([...starSet, ...forkSet])].sort();
populateTable('members-table', allMembers, username => {
const details = userDetails[username] || {};
const starred = starSet.has(username), forked = forkSet.has(username);
const td1 = document.createElement('td'); td1.appendChild(createLink('https://github.com/' + username, '@' + username));
const td2 = document.createElement('td');
if (starred && forked) td2.appendChild(createBadge('both'));
else if (starred) td2.appendChild(createBadge('starred'));
else if (forked) td2.appendChild(createBadge('forked'));
const td3 = document.createElement('td'); td3.textContent = details.createdAt ? formatDate(details.createdAt) : '-';
return [td1, td2, td3];
});
})();
</script>
</body>
</html>`;
}
async function main() {
const args = process.argv.slice(2);
const fetchDetails = args.includes('--fetch-details');
@@ -351,6 +624,13 @@ async function main() {
markdown
);
// Generate self-contained HTML dashboard
const htmlDashboard = generateHtmlDashboard(report, stargazers, forkers, userDetails);
fs.writeFileSync(
path.join(OUTPUT_DIR, 'index.html'),
htmlDashboard
);
if (jsonOutput) {
console.log(JSON.stringify(report, null, 2));
} else {

View File

@@ -4993,6 +4993,199 @@ function setupIpcHandlers() {
}
}
);
// ==========================================================================
// Leaderboard API
// ==========================================================================
// Submit leaderboard entry to runmaestro.ai
ipcMain.handle(
'leaderboard:submit',
async (
_event,
data: {
email: string;
displayName: string;
githubUsername?: string;
twitterHandle?: string;
linkedinHandle?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
longestRunMs?: number;
longestRunDate?: string;
}
): Promise<{
success: boolean;
message: string;
requiresConfirmation?: boolean;
confirmationUrl?: string;
error?: string;
}> => {
try {
logger.info('Submitting leaderboard entry', 'Leaderboard', {
displayName: data.displayName,
email: data.email.substring(0, 3) + '***',
badgeLevel: data.badgeLevel,
});
const response = await fetch('https://runmaestro.ai/api/m4estr0/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': `Maestro/${app.getVersion()}`,
},
body: JSON.stringify(data),
});
const result = await response.json() as {
success?: boolean;
message?: string;
requiresConfirmation?: boolean;
confirmationUrl?: string;
error?: string;
};
if (response.ok) {
logger.info('Leaderboard submission successful', 'Leaderboard', {
requiresConfirmation: result.requiresConfirmation,
});
return {
success: true,
message: result.message || 'Submission received',
requiresConfirmation: result.requiresConfirmation,
confirmationUrl: result.confirmationUrl,
};
} else {
logger.warn('Leaderboard submission failed', 'Leaderboard', {
status: response.status,
error: result.error || result.message,
});
return {
success: false,
message: result.message || 'Submission failed',
error: result.error || `Server error: ${response.status}`,
};
}
} catch (error) {
logger.error('Error submitting to leaderboard', 'Leaderboard', error);
return {
success: false,
message: 'Failed to connect to leaderboard server',
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
);
// Get leaderboard entries
ipcMain.handle(
'leaderboard:get',
async (
_event,
options?: { limit?: number }
): Promise<{
success: boolean;
entries?: Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
}>;
error?: string;
}> => {
try {
const limit = options?.limit || 50;
const response = await fetch(`https://runmaestro.ai/api/leaderboard?limit=${limit}`, {
headers: {
'User-Agent': `Maestro/${app.getVersion()}`,
},
});
if (response.ok) {
const data = await response.json() as { entries?: unknown[] };
return { success: true, entries: data.entries as Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
}> };
} else {
return {
success: false,
error: `Server error: ${response.status}`,
};
}
} catch (error) {
logger.error('Error fetching leaderboard', 'Leaderboard', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
);
// Get longest runs leaderboard
ipcMain.handle(
'leaderboard:getLongestRuns',
async (
_event,
options?: { limit?: number }
): Promise<{
success: boolean;
entries?: Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
longestRunMs: number;
runDate: string;
}>;
error?: string;
}> => {
try {
const limit = options?.limit || 50;
const response = await fetch(`https://runmaestro.ai/api/longest-runs?limit=${limit}`, {
headers: {
'User-Agent': `Maestro/${app.getVersion()}`,
},
});
if (response.ok) {
const data = await response.json() as { entries?: unknown[] };
return { success: true, entries: data.entries as Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
longestRunMs: number;
runDate: string;
}> };
} else {
return {
success: false,
error: `Server error: ${response.status}`,
};
}
} catch (error) {
logger.error('Error fetching longest runs leaderboard', 'Leaderboard', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
);
}
// Handle process output streaming (set up after initialization)

View File

@@ -641,6 +641,27 @@ contextBridge.exposeInMainWorld('maestro', {
import: (sessionId: string, autoRunFolderPath: string) =>
ipcRenderer.invoke('playbooks:import', sessionId, autoRunFolderPath),
},
// Leaderboard API (runmaestro.ai integration)
leaderboard: {
submit: (data: {
email: string;
displayName: string;
githubUsername?: string;
twitterHandle?: string;
linkedinHandle?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
longestRunMs?: number;
longestRunDate?: string;
}) => ipcRenderer.invoke('leaderboard:submit', data),
get: (options?: { limit?: number }) =>
ipcRenderer.invoke('leaderboard:get', options),
getLongestRuns: (options?: { limit?: number }) =>
ipcRenderer.invoke('leaderboard:getLongestRuns', options),
},
});
// Type definitions for TypeScript
@@ -1135,6 +1156,53 @@ export interface MaestroAPI {
error?: string;
}>;
};
leaderboard: {
submit: (data: {
email: string;
displayName: string;
githubUsername?: string;
twitterHandle?: string;
linkedinHandle?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
longestRunMs?: number;
longestRunDate?: string;
}) => Promise<{
success: boolean;
message: string;
requiresConfirmation?: boolean;
confirmationUrl?: string;
error?: string;
}>;
get: (options?: { limit?: number }) => Promise<{
success: boolean;
entries?: Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
badgeLevel: number;
badgeName: string;
cumulativeTimeMs: number;
totalRuns: number;
}>;
error?: string;
}>;
getLongestRuns: (options?: { limit?: number }) => Promise<{
success: boolean;
entries?: Array<{
rank: number;
displayName: string;
githubUsername?: string;
avatarUrl?: string;
longestRunMs: number;
runDate: string;
}>;
error?: string;
}>;
};
}
declare global {

View File

@@ -25,6 +25,7 @@ import { PromptComposerModal } from './components/PromptComposerModal';
import { ExecutionQueueBrowser } from './components/ExecutionQueueBrowser';
import { StandingOvationOverlay } from './components/StandingOvationOverlay';
import { FirstRunCelebration } from './components/FirstRunCelebration';
import { LeaderboardRegistrationModal } from './components/LeaderboardRegistrationModal';
import { PlaygroundPanel } from './components/PlaygroundPanel';
import { AutoRunSetupModal } from './components/AutoRunSetupModal';
import { DebugWizardModal } from './components/DebugWizardModal';
@@ -181,6 +182,7 @@ export default function MaestroConsole() {
firstAutoRunCompleted, setFirstAutoRunCompleted,
recordWizardStart, recordWizardComplete, recordWizardAbandon, recordWizardResume,
recordTourStart, recordTourComplete, recordTourSkip,
leaderboardRegistration, setLeaderboardRegistration, isLeaderboardRegistered,
} = settings;
// --- STATE ---
@@ -262,6 +264,7 @@ export default function MaestroConsole() {
const [lightboxImages, setLightboxImages] = useState<string[]>([]); // Context images for navigation
const [aboutModalOpen, setAboutModalOpen] = useState(false);
const [updateCheckModalOpen, setUpdateCheckModalOpen] = useState(false);
const [leaderboardRegistrationOpen, setLeaderboardRegistrationOpen] = useState(false);
const [standingOvationData, setStandingOvationData] = useState<{
badge: typeof CONDUCTOR_BADGES[number];
isNewRecord: boolean;
@@ -2698,6 +2701,54 @@ export default function MaestroConsole() {
}, 500);
}
}
// Submit to leaderboard if registered and email confirmed
if (isLeaderboardRegistered && leaderboardRegistration) {
// Calculate updated stats after this run (simulating what recordAutoRunComplete updated)
const updatedCumulativeTimeMs = autoRunStats.cumulativeTimeMs + info.elapsedTimeMs;
const updatedTotalRuns = autoRunStats.totalRuns + 1;
const updatedLongestRunMs = Math.max(autoRunStats.longestRunMs || 0, info.elapsedTimeMs);
const updatedBadge = CONDUCTOR_BADGES.find(b =>
b.thresholdMs <= updatedCumulativeTimeMs
);
const updatedBadgeLevel = updatedBadge?.level || 0;
const updatedBadgeName = updatedBadge?.name || 'No Badge Yet';
// Format longest run date
let longestRunDate: string | undefined;
if (isNewRecord) {
longestRunDate = new Date().toISOString().split('T')[0];
} else if (autoRunStats.longestRunTimestamp > 0) {
longestRunDate = new Date(autoRunStats.longestRunTimestamp).toISOString().split('T')[0];
}
// Submit to leaderboard in background
window.maestro.leaderboard.submit({
email: leaderboardRegistration.email,
displayName: leaderboardRegistration.displayName,
githubUsername: leaderboardRegistration.githubUsername,
twitterHandle: leaderboardRegistration.twitterHandle,
linkedinHandle: leaderboardRegistration.linkedinHandle,
badgeLevel: updatedBadgeLevel,
badgeName: updatedBadgeName,
cumulativeTimeMs: updatedCumulativeTimeMs,
totalRuns: updatedTotalRuns,
longestRunMs: updatedLongestRunMs,
longestRunDate,
}).then(result => {
if (result.success) {
// Update last submission timestamp
setLeaderboardRegistration({
...leaderboardRegistration,
lastSubmissionAt: Date.now(),
emailConfirmed: !result.requiresConfirmation,
});
}
// Silent failure - don't bother the user if submission fails
}).catch(() => {
// Silent failure - leaderboard submission is not critical
});
}
}
},
onPRResult: (info) => {
@@ -3990,11 +4041,13 @@ export default function MaestroConsole() {
}
}
// Ungrouped sessions (sorted alphabetically)
const ungroupedSessions = sessions
.filter(s => !s.groupId)
.sort((a, b) => compareNamesIgnoringEmojis(a.name, b.name));
visualOrder.push(...ungroupedSessions);
// Ungrouped sessions (sorted alphabetically) - only if not collapsed
if (!settings.ungroupedCollapsed) {
const ungroupedSessions = sessions
.filter(s => !s.groupId)
.sort((a, b) => compareNamesIgnoringEmojis(a.name, b.name));
visualOrder.push(...ungroupedSessions);
}
} else {
// Sidebar collapsed: cycle through all sessions in their sorted order
visualOrder.push(...sortedSessions);
@@ -6796,6 +6849,24 @@ export default function MaestroConsole() {
sessions={sessions}
autoRunStats={autoRunStats}
onClose={() => setAboutModalOpen(false)}
onOpenLeaderboardRegistration={() => {
setAboutModalOpen(false);
setLeaderboardRegistrationOpen(true);
}}
isLeaderboardRegistered={isLeaderboardRegistered}
/>
)}
{/* --- LEADERBOARD REGISTRATION MODAL --- */}
{leaderboardRegistrationOpen && (
<LeaderboardRegistrationModal
theme={theme}
autoRunStats={autoRunStats}
existingRegistration={leaderboardRegistration}
onClose={() => setLeaderboardRegistrationOpen(false)}
onSave={(registration) => {
setLeaderboardRegistration(registration);
}}
/>
)}
@@ -6815,6 +6886,8 @@ export default function MaestroConsole() {
completedTasks={firstRunCelebrationData.completedTasks}
totalTasks={firstRunCelebrationData.totalTasks}
onClose={() => setFirstRunCelebrationData(null)}
onOpenLeaderboardRegistration={() => setLeaderboardRegistrationOpen(true)}
isLeaderboardRegistered={isLeaderboardRegistered}
/>
)}
@@ -6832,6 +6905,8 @@ export default function MaestroConsole() {
acknowledgeBadge(standingOvationData.badge.level);
setStandingOvationData(null);
}}
onOpenLeaderboardRegistration={() => setLeaderboardRegistrationOpen(true)}
isLeaderboardRegistered={isLeaderboardRegistered}
/>
)}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { X, Wand2, ExternalLink, FileCode, BarChart3, Loader2 } from 'lucide-react';
import { X, Wand2, ExternalLink, FileCode, BarChart3, Loader2, Trophy } from 'lucide-react';
import type { Theme, Session, AutoRunStats } from '../types';
import { useLayerStack } from '../contexts/LayerStackContext';
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
@@ -23,9 +23,11 @@ interface AboutModalProps {
sessions: Session[];
autoRunStats: AutoRunStats;
onClose: () => void;
onOpenLeaderboardRegistration?: () => void;
isLeaderboardRegistered?: boolean;
}
export function AboutModal({ theme, sessions, autoRunStats, onClose }: AboutModalProps) {
export function AboutModal({ theme, sessions, autoRunStats, onClose, onOpenLeaderboardRegistration, isLeaderboardRegistered }: AboutModalProps) {
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
const layerIdRef = useRef<string>();
const [globalStats, setGlobalStats] = useState<ClaudeGlobalStats | null>(null);
@@ -275,18 +277,45 @@ export function AboutModal({ theme, sessions, autoRunStats, onClose }: AboutModa
)}
</div>
{/* Project Link */}
<button
onClick={() => window.maestro.shell.openExternal('https://github.com/pedramamini/Maestro')}
className="w-full flex items-center justify-between p-3 rounded border hover:bg-white/5 transition-colors"
style={{ borderColor: theme.colors.border }}
>
<div className="flex items-center gap-2">
<FileCode className="w-4 h-4" style={{ color: theme.colors.accent }} />
<span className="text-sm font-medium" style={{ color: theme.colors.textMain }}>View on GitHub</span>
</div>
<ExternalLink className="w-4 h-4" style={{ color: theme.colors.textDim }} />
</button>
{/* Action Links */}
<div className="flex gap-2">
{/* Project Link */}
<button
onClick={() => window.maestro.shell.openExternal('https://github.com/pedramamini/Maestro')}
className="flex-1 flex items-center justify-between p-3 rounded border hover:bg-white/5 transition-colors"
style={{ borderColor: theme.colors.border }}
>
<div className="flex items-center gap-2">
<FileCode className="w-4 h-4" style={{ color: theme.colors.accent }} />
<span className="text-sm font-medium" style={{ color: theme.colors.textMain }}>GitHub</span>
</div>
<ExternalLink className="w-4 h-4" style={{ color: theme.colors.textDim }} />
</button>
{/* Leaderboard Registration */}
{onOpenLeaderboardRegistration && (
<button
onClick={onOpenLeaderboardRegistration}
className="flex-1 flex items-center justify-between p-3 rounded border hover:bg-white/5 transition-colors"
style={{
borderColor: isLeaderboardRegistered ? theme.colors.success : theme.colors.accent,
backgroundColor: isLeaderboardRegistered ? `${theme.colors.success}10` : undefined,
}}
>
<div className="flex items-center gap-2">
<Trophy className="w-4 h-4" style={{ color: isLeaderboardRegistered ? theme.colors.success : '#FFD700' }} />
<span className="text-sm font-medium" style={{ color: theme.colors.textMain }}>
{isLeaderboardRegistered ? 'Leaderboard' : 'Join Leaderboard'}
</span>
</div>
{isLeaderboardRegistered ? (
<span className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: theme.colors.success, color: '#fff' }}>Active</span>
) : (
<ExternalLink className="w-4 h-4" style={{ color: theme.colors.textDim }} />
)}
</button>
)}
</div>
{/* Made in Austin */}
<div className="pt-1 text-center flex flex-col items-center gap-1">

View File

@@ -97,6 +97,17 @@ const formatTokenCount = (count: number): string => {
return count.toLocaleString();
};
// Count markdown tasks (checkboxes)
const countMarkdownTasks = (content: string): { open: number; closed: number } => {
// Match markdown checkboxes: - [ ] or - [x] (also * [ ] and * [x])
const openMatches = content.match(/^[\s]*[-*]\s*\[\s*\]/gm);
const closedMatches = content.match(/^[\s]*[-*]\s*\[[xX]\]/gm);
return {
open: openMatches?.length || 0,
closed: closedMatches?.length || 0
};
};
// Lazy-loaded tokenizer encoder (cl100k_base is used by Claude/GPT-4)
let encoderPromise: Promise<ReturnType<typeof getEncoding>> | null = null;
const getEncoder = () => {
@@ -322,6 +333,15 @@ export function FilePreview({ file, onClose, theme, markdownRawMode, setMarkdown
const isMarkdown = language === 'markdown';
const isImage = isImageFile(file.name);
// Calculate task counts for markdown files
const taskCounts = useMemo(() => {
if (!isMarkdown || !file?.content) return null;
const counts = countMarkdownTasks(file.content);
// Only return if there are any tasks
if (counts.open === 0 && counts.closed === 0) return null;
return counts;
}, [isMarkdown, file?.content]);
// Extract directory path without filename
const directoryPath = file.path.substring(0, file.path.lastIndexOf('/'));
@@ -796,7 +816,7 @@ export function FilePreview({ file, onClose, theme, markdownRawMode, setMarkdown
</div>
</div>
{/* File Stats subbar - hidden on scroll */}
{(fileStats || tokenCount !== null) && showStatsBar && (
{(fileStats || tokenCount !== null || taskCounts) && showStatsBar && (
<div
className="flex items-center gap-4 px-6 py-1.5 border-b transition-all duration-200"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgActivity }}
@@ -825,6 +845,14 @@ export function FilePreview({ file, onClose, theme, markdownRawMode, setMarkdown
</div>
</>
)}
{taskCounts && (
<div className="text-[10px]" style={{ color: theme.colors.textDim }}>
<span className="opacity-60">Tasks:</span>{' '}
<span style={{ color: theme.colors.success }}>{taskCounts.closed}</span>
<span className="opacity-60">/</span>
<span style={{ color: theme.colors.textMain }}>{taskCounts.open + taskCounts.closed}</span>
</div>
)}
</div>
)}
</div>

View File

@@ -28,6 +28,10 @@ interface FirstRunCelebrationProps {
totalTasks: number;
/** Callback when modal is dismissed */
onClose: () => void;
/** Callback to open leaderboard registration */
onOpenLeaderboardRegistration?: () => void;
/** Whether the user is already registered for the leaderboard */
isLeaderboardRegistered?: boolean;
}
/**
@@ -66,6 +70,8 @@ export function FirstRunCelebration({
completedTasks,
totalTasks,
onClose,
onOpenLeaderboardRegistration,
isLeaderboardRegistered,
}: FirstRunCelebrationProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
@@ -408,7 +414,7 @@ export function FirstRunCelebration({
</div>
{/* Button */}
<div className="px-8 pb-8">
<div className="px-8 pb-8 space-y-3">
<button
onClick={handleClose}
disabled={isClosing}
@@ -424,8 +430,30 @@ export function FirstRunCelebration({
{isClosing ? '🎉 Let\'s Go! 🎉' : 'Got It!'}
</button>
{/* Leaderboard Registration */}
{onOpenLeaderboardRegistration && !isLeaderboardRegistered && (
<button
onClick={() => {
handleClose();
setTimeout(() => {
onOpenLeaderboardRegistration();
}, 1100); // Wait for close animation
}}
disabled={isClosing}
className="w-full py-2.5 rounded-lg font-medium transition-all flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50"
style={{
backgroundColor: `${goldColor}20`,
color: goldColor,
border: `1px solid ${goldColor}60`,
}}
>
<Trophy className="w-4 h-4" />
Join Global Leaderboard
</button>
)}
<p
className="text-xs text-center mt-3"
className="text-xs text-center"
style={{ color: theme.colors.textDim }}
>
Press Enter or Escape to dismiss

View File

@@ -16,6 +16,8 @@ interface StandingOvationOverlayProps {
recordTimeMs?: number;
cumulativeTimeMs: number;
onClose: () => void;
onOpenLeaderboardRegistration?: () => void;
isLeaderboardRegistered?: boolean;
}
/**
@@ -30,6 +32,8 @@ export function StandingOvationOverlay({
recordTimeMs,
cumulativeTimeMs,
onClose,
onOpenLeaderboardRegistration,
isLeaderboardRegistered,
}: StandingOvationOverlayProps) {
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
const layerIdRef = useRef<string>();
@@ -616,6 +620,25 @@ export function StandingOvationOverlay({
</div>
)}
</div>
{/* Leaderboard Registration */}
{onOpenLeaderboardRegistration && !isLeaderboardRegistered && (
<button
onClick={() => {
onClose();
onOpenLeaderboardRegistration();
}}
className="w-full py-2.5 rounded-lg font-medium transition-all flex items-center justify-center gap-2 hover:opacity-90"
style={{
backgroundColor: `${goldColor}20`,
color: goldColor,
border: `1px solid ${goldColor}60`,
}}
>
<Trophy className="w-4 h-4" />
Join Global Leaderboard
</button>
)}
</div>
</div>
</div>

View File

@@ -86,6 +86,9 @@ export const MODAL_PRIORITIES = {
/** Keyboard shortcuts help modal */
SHORTCUTS_HELP: 650,
/** Leaderboard registration modal */
LEADERBOARD_REGISTRATION: 620,
/** About/info modal */
ABOUT: 600,

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import type { LLMProvider, ThemeId, Shortcut, CustomAICommand, GlobalStats, AutoRunStats, OnboardingStats } from '../types';
import type { LLMProvider, ThemeId, Shortcut, CustomAICommand, GlobalStats, AutoRunStats, OnboardingStats, LeaderboardRegistration } from '../types';
import { DEFAULT_SHORTCUTS } from '../constants/shortcuts';
// Default global stats
@@ -208,6 +208,11 @@ export interface UseSettingsReturn {
averageConversationExchanges: number;
averagePhasesPerWizard: number;
};
// Leaderboard Registration (persistent)
leaderboardRegistration: LeaderboardRegistration | null;
setLeaderboardRegistration: (value: LeaderboardRegistration | null) => void;
isLeaderboardRegistered: boolean;
}
export function useSettings(): UseSettingsReturn {
@@ -287,6 +292,9 @@ export function useSettings(): UseSettingsReturn {
// Onboarding Stats (persistent, local-only analytics)
const [onboardingStats, setOnboardingStatsState] = useState<OnboardingStats>(DEFAULT_ONBOARDING_STATS);
// Leaderboard Registration (persistent)
const [leaderboardRegistration, setLeaderboardRegistrationState] = useState<LeaderboardRegistration | null>(null);
// Wrapper functions that persist to electron-store
// PERF: All wrapped in useCallback to prevent re-renders
const setLlmProvider = useCallback((value: LLMProvider) => {
@@ -611,8 +619,11 @@ export function useSettings(): UseSettingsReturn {
// UI collapse state setters
const setUngroupedCollapsed = useCallback((value: boolean) => {
console.log('[useSettings] setUngroupedCollapsed called with:', value);
setUngroupedCollapsedState(value);
window.maestro.settings.set('ungroupedCollapsed', value);
window.maestro.settings.set('ungroupedCollapsed', value)
.then(() => console.log('[useSettings] ungroupedCollapsed persisted successfully'))
.catch((err: unknown) => console.error('[useSettings] Failed to persist ungroupedCollapsed:', err));
}, []);
// Onboarding setters
@@ -792,6 +803,17 @@ export function useSettings(): UseSettingsReturn {
onboardingStats.averagePhasesPerWizard,
]);
// Leaderboard Registration setter
const setLeaderboardRegistration = useCallback((value: LeaderboardRegistration | null) => {
setLeaderboardRegistrationState(value);
window.maestro.settings.set('leaderboardRegistration', value);
}, []);
// Computed property for checking if registered
const isLeaderboardRegistered = useMemo(() => {
return leaderboardRegistration !== null && leaderboardRegistration.emailConfirmed;
}, [leaderboardRegistration]);
// Load settings from electron-store on mount
useEffect(() => {
const loadSettings = async () => {
@@ -831,6 +853,7 @@ export function useSettings(): UseSettingsReturn {
const savedTourCompleted = await window.maestro.settings.get('tourCompleted');
const savedFirstAutoRunCompleted = await window.maestro.settings.get('firstAutoRunCompleted');
const savedOnboardingStats = await window.maestro.settings.get('onboardingStats');
const savedLeaderboardRegistration = await window.maestro.settings.get('leaderboardRegistration');
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI);
if (savedEnterToSendTerminal !== undefined) setEnterToSendTerminalState(savedEnterToSendTerminal);
@@ -950,6 +973,11 @@ export function useSettings(): UseSettingsReturn {
setOnboardingStatsState({ ...DEFAULT_ONBOARDING_STATS, ...savedOnboardingStats });
}
// Load leaderboard registration
if (savedLeaderboardRegistration !== undefined) {
setLeaderboardRegistrationState(savedLeaderboardRegistration as LeaderboardRegistration | null);
}
// Mark settings as loaded
setSettingsLoaded(true);
};
@@ -1050,6 +1078,9 @@ export function useSettings(): UseSettingsReturn {
recordTourComplete,
recordTourSkip,
getOnboardingAnalytics,
leaderboardRegistration,
setLeaderboardRegistration,
isLeaderboardRegistered,
}), [
// State values
settingsLoaded,
@@ -1137,5 +1168,8 @@ export function useSettings(): UseSettingsReturn {
recordTourComplete,
recordTourSkip,
getOnboardingAnalytics,
leaderboardRegistration,
setLeaderboardRegistration,
isLeaderboardRegistered,
]);
}

View File

@@ -451,3 +451,27 @@ export interface CustomAICommand {
isBuiltIn?: boolean; // If true, cannot be deleted (only edited)
}
// Leaderboard registration data for runmaestro.ai integration
export interface LeaderboardRegistration {
// Required fields
email: string; // User's email (will be confirmed)
displayName: string; // Display name on leaderboard
// Optional social handles (without @)
twitterHandle?: string; // X/Twitter handle
githubUsername?: string; // GitHub username
linkedinHandle?: string; // LinkedIn handle
// Registration state
registeredAt: number; // Timestamp when registered
emailConfirmed: boolean; // Whether email has been confirmed
lastSubmissionAt?: number; // Last successful submission timestamp
}
// Response from leaderboard submission API
export interface LeaderboardSubmitResponse {
success: boolean;
message: string;
requiresConfirmation?: boolean;
confirmationUrl?: string;
error?: string;
}