mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
initial leaderboard functionality
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user