From 121621588e1a784887f5e1de171fb340f83ac152 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Fri, 12 Dec 2025 00:31:48 -0600 Subject: [PATCH] initial leaderboard functionality --- scripts/github-community-analytics.js | 280 ++++++++++++++++++ src/main/index.ts | 193 ++++++++++++ src/main/preload.ts | 68 +++++ src/renderer/App.tsx | 85 +++++- src/renderer/components/AboutModal.tsx | 57 +++- src/renderer/components/FilePreview.tsx | 30 +- .../components/FirstRunCelebration.tsx | 32 +- .../components/StandingOvationOverlay.tsx | 23 ++ src/renderer/constants/modalPriorities.ts | 3 + src/renderer/hooks/useSettings.ts | 38 ++- src/renderer/types/index.ts | 24 ++ 11 files changed, 809 insertions(+), 24 deletions(-) diff --git a/scripts/github-community-analytics.js b/scripts/github-community-analytics.js index dfa03a7d..0b7b0553 100755 --- a/scripts/github-community-analytics.js +++ b/scripts/github-community-analytics.js @@ -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 ` + + + + + Maestro Community Dashboard + + + + +
+

Maestro Community Dashboard

+

+
+
+
-
Stars
+
-
Forks
+
-
Unique Members
+
-
Community Reach
+
+
+

Star Growth Over Time

+

Fork Growth Over Time

+
+
+

Top Locations

+

Top Companies

+
+
+

Top Influencers

UserCompanyFollowers
+

Recent Activity

UserActionDate
+

Highly Engaged (Starred + Forked)

UserLocationFollowers
+

All Community Members

UserStatusJoined GitHub
+
+ + +`; +} + 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 { diff --git a/src/main/index.ts b/src/main/index.ts index 18034a00..3cb08134 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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) diff --git a/src/main/preload.ts b/src/main/preload.ts index 9cc3fbaf..1791e470 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -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 { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index ce58615e..41fbdf90 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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([]); // 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 && ( + 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} /> )} diff --git a/src/renderer/components/AboutModal.tsx b/src/renderer/components/AboutModal.tsx index 9eb78f08..477b2092 100644 --- a/src/renderer/components/AboutModal.tsx +++ b/src/renderer/components/AboutModal.tsx @@ -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(); const [globalStats, setGlobalStats] = useState(null); @@ -275,18 +277,45 @@ export function AboutModal({ theme, sessions, autoRunStats, onClose }: AboutModa )} - {/* Project Link */} - + {/* Action Links */} +
+ {/* Project Link */} + + + {/* Leaderboard Registration */} + {onOpenLeaderboardRegistration && ( + + )} +
{/* Made in Austin */}
diff --git a/src/renderer/components/FilePreview.tsx b/src/renderer/components/FilePreview.tsx index e3592616..7e7ad7fc 100644 --- a/src/renderer/components/FilePreview.tsx +++ b/src/renderer/components/FilePreview.tsx @@ -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> | 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
{/* File Stats subbar - hidden on scroll */} - {(fileStats || tokenCount !== null) && showStatsBar && ( + {(fileStats || tokenCount !== null || taskCounts) && showStatsBar && (
)} + {taskCounts && ( +
+ Tasks:{' '} + {taskCounts.closed} + / + {taskCounts.open + taskCounts.closed} +
+ )}
)} diff --git a/src/renderer/components/FirstRunCelebration.tsx b/src/renderer/components/FirstRunCelebration.tsx index 012102fc..b2aab412 100644 --- a/src/renderer/components/FirstRunCelebration.tsx +++ b/src/renderer/components/FirstRunCelebration.tsx @@ -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(null); const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack(); @@ -408,7 +414,7 @@ export function FirstRunCelebration({ {/* Button */} -
+
+ )} +

Press Enter or Escape to dismiss diff --git a/src/renderer/components/StandingOvationOverlay.tsx b/src/renderer/components/StandingOvationOverlay.tsx index 251f642f..c2d01d66 100644 --- a/src/renderer/components/StandingOvationOverlay.tsx +++ b/src/renderer/components/StandingOvationOverlay.tsx @@ -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(); @@ -616,6 +620,25 @@ export function StandingOvationOverlay({

)}
+ + {/* Leaderboard Registration */} + {onOpenLeaderboardRegistration && !isLeaderboardRegistered && ( + + )} diff --git a/src/renderer/constants/modalPriorities.ts b/src/renderer/constants/modalPriorities.ts index 3e7ae9cb..b9c6a660 100644 --- a/src/renderer/constants/modalPriorities.ts +++ b/src/renderer/constants/modalPriorities.ts @@ -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, diff --git a/src/renderer/hooks/useSettings.ts b/src/renderer/hooks/useSettings.ts index ea5a6a13..dbefb1c8 100644 --- a/src/renderer/hooks/useSettings.ts +++ b/src/renderer/hooks/useSettings.ts @@ -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(DEFAULT_ONBOARDING_STATS); + // Leaderboard Registration (persistent) + const [leaderboardRegistration, setLeaderboardRegistrationState] = useState(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, ]); } diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 79b98f6e..66f86d14 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -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; +} +