diff --git a/src/api/ConfigurationsApi.ts b/src/api/ConfigurationsApi.ts index 0c6ef57..fa5efbf 100644 --- a/src/api/ConfigurationsApi.ts +++ b/src/api/ConfigurationsApi.ts @@ -1,15 +1,5 @@ import { useApiQuery } from './ApiUtils'; -import { type TierConfigResponse, type GeneralConfigResponse } from './models'; - -/** - * Get tier configuration data (requirements and scoring parameters) - * Uses the /configurations/tiers endpoint - */ -export const useTierConfigurations = () => - useApiQuery( - 'useTierConfigurations', - '/configurations/tiers', - ); +import { type GeneralConfigResponse } from './models'; /** * Get general configuration data (branding, scoring parameters, thresholds) diff --git a/src/api/PredictionsApi.ts b/src/api/PredictionsApi.ts deleted file mode 100644 index 7fb4f67..0000000 --- a/src/api/PredictionsApi.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useApiQuery } from './ApiUtils'; -import { type MinerPredictionScore } from './models/Predictions'; - -const usePredictionsQuery = ( - queryName: string, - url: string, - refetchInterval?: number, - queryParams?: Record, - enabled?: boolean, -) => - useApiQuery( - queryName, - `/predictions${url}`, - refetchInterval, - queryParams, - enabled, - ); - -/** - * Get EMA prediction scores for all currently active miners, ordered by score descending. - */ -export const usePredictionScores = () => - usePredictionsQuery('usePredictionScores', '/scores'); diff --git a/src/api/ReposApi.ts b/src/api/ReposApi.ts index b02af14..33155c6 100644 --- a/src/api/ReposApi.ts +++ b/src/api/ReposApi.ts @@ -1,7 +1,7 @@ // Repository API hooks - uses /repos endpoints import { useApiQuery } from './ApiUtils'; import { type RepositoryMaintainer, type RepositoryIssue } from './models'; -import { type CommitLog } from './models/Dashboard'; +import { type CommitLog, type Repository } from './models/Dashboard'; /** * Helper to create /repos endpoint queries @@ -19,6 +19,16 @@ const useReposQuery = ( queryParams, ); +/** + * Get config for a specific repository (weight, additional branches, etc.) + * @param repo - Full repository name (e.g., "opentensor/btcli") + */ +export const useRepositoryConfig = (repo: string) => + useReposQuery( + 'useRepositoryConfig', + `/${encodeURIComponent(repo)}`, + ); + /** * Get maintainers (assignees) for a specific repository * @param repo - Full repository name (e.g., "opentensor/btcli") diff --git a/src/api/index.ts b/src/api/index.ts index 9a6afd6..146362a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,7 +3,6 @@ export * from './ConfigurationsApi'; export * from './DashboardApi'; export * from './IssuesApi'; export * from './MinerApi'; -export * from './PredictionsApi'; export * from './PrsApi'; export * from './ReposApi'; export * from './SearchApi'; diff --git a/src/api/models/Configurations.ts b/src/api/models/Configurations.ts index 6c64ebb..694e318 100644 --- a/src/api/models/Configurations.ts +++ b/src/api/models/Configurations.ts @@ -1,22 +1,3 @@ -export interface TierConfig { - name: string; - level: number; - requiredCredibility: number; - requiredMinTokenScore: number | null; - requiredMinTokenScorePerRepo: number; - requiredQualifiedUniqueRepos: number; - credibilityScalar: number; - mergedPrBaseScore: number; - contributionScoreForFullBonus: number; - contributionScoreMaxBonus: number; - openPrCollateralPercentage: number; -} - -export interface TierConfigResponse { - tiers: TierConfig[]; - tierOrder: string[]; -} - export interface LanguageFileScoring { defaultProgrammingLanguageWeight: number; testFileContributionWeight: number; diff --git a/src/api/models/Dashboard.ts b/src/api/models/Dashboard.ts index 6893540..e3006fa 100644 --- a/src/api/models/Dashboard.ts +++ b/src/api/models/Dashboard.ts @@ -5,7 +5,6 @@ export type RepoChanges = { deletions: number; linesChanged: number; weight: string; // bc float - tier: string; // Bronze, Silver, Gold inactiveAt: string | null; }; @@ -14,7 +13,6 @@ export type Repository = { owner: string; name: string; weight: string; // bc float - tier: string; // Bronze, Silver, Gold additionalAcceptableBranches?: string[] | null; inactiveAt?: string | null; }; @@ -34,13 +32,6 @@ export type CommitsTrend = { * Dashboard statistics * * API endpoint: GET /dash/stats - * Optional query parameter: tier (Bronze, Silver, or Gold) - filters all stats to specific repository tier - * - * Examples: - * - GET /dash/stats - returns overall stats - * - GET /dash/stats?tier=Bronze - returns stats filtered to Bronze tier repositories only - * - GET /dash/stats?tier=Silver - returns stats filtered to Silver tier repositories only - * - GET /dash/stats?tier=Gold - returns stats filtered to Gold tier repositories only */ export type Stats = { uniqueRepositories: string | number; @@ -48,7 +39,6 @@ export type Stats = { recentLinesChanged: string | number; totalIssues: number; totalCommits: string | number; - tier?: string; // Returned when tier filter is applied prices?: { tao: { data: { @@ -93,7 +83,6 @@ export type CommitLog = { githubId?: string; // Numeric GitHub ID - only present in /miners endpoints, not /dash/commits score: string; // Backend returns as string baseScore?: string; // Backend returns as string - tier: string | null; // Bronze, Silver, Gold - from joined repositories table (null if repo not registered) // Score multiplier fields (from /miners/{id}/prs endpoint) repoWeightMultiplier?: string; @@ -101,23 +90,21 @@ export type CommitLog = { openPrSpamMultiplier?: string; pioneerDividend?: number; pioneerRank?: number; - repositoryUniquenessMultiplier?: string; timeDecayMultiplier?: string; credibilityMultiplier?: string; // Token scoring fields totalNodesScored?: number; - rawCredibility?: number; - credibilityScalar?: number; tokenScore?: number; structuralCount?: number; structuralScore?: number; leafCount?: number; leafScore?: number; - // TODO: these values do not come in the /dash/commits endpoint, refactor to perhaps make a new model to include these attributes - // Payout predictions (from /miners/all/prs endpoint) - // Note: dollar values are null for open PRs, only calculated for merged PRs + // Review quality + reviewQualityMultiplier?: string; + + // Payout predictions potentialScore?: number; predictedAlphaPerDay?: number | null; predictedTaoPerDay?: number | null; @@ -137,63 +124,27 @@ export type MinerEvaluation = { totalOpenPrs: number; totalPrs: number; uniqueReposCount: number; - qualifiedUniqueReposCount?: number; - // Tier system properties - currentTier?: string; totalCollateralScore?: number; totalClosedPrs?: number; totalMergedPrs?: number; + // Eligibility gate + isEligible?: boolean; + credibility?: number; + // Issue discovery scoring + issueDiscoveryScore?: number; + issueTokenScore?: number; + issueCredibility?: number; + isIssueEligible?: boolean; + totalSolvedIssues?: number; + totalValidSolvedIssues?: number; + totalClosedIssues?: number; + totalOpenIssues?: number; // Total token scoring fields totalTokenScore?: number; totalStructuralCount?: number; totalStructuralScore?: number; totalLeafCount?: number; totalLeafScore?: number; - // Bronze tier - bronzeMergedPrs?: number; - bronzeClosedPrs?: number; - bronzeTotalPrs?: number; - bronzeCollateralScore?: number; - bronzeScore?: number; - bronzeTokenScore?: number; - bronzeStructuralCount?: number; - bronzeStructuralScore?: number; - bronzeLeafCount?: number; - bronzeLeafScore?: number; - // Silver tier - silverMergedPrs?: number; - silverClosedPrs?: number; - silverTotalPrs?: number; - silverCollateralScore?: number; - silverScore?: number; - silverTokenScore?: number; - silverStructuralCount?: number; - silverStructuralScore?: number; - silverLeafCount?: number; - silverLeafScore?: number; - // Gold tier - goldMergedPrs?: number; - goldClosedPrs?: number; - goldTotalPrs?: number; - goldCollateralScore?: number; - goldScore?: number; - goldTokenScore?: number; - goldStructuralCount?: number; - goldStructuralScore?: number; - goldLeafCount?: number; - goldLeafScore?: number; - // Credibility metrics (PR success rates as decimals 0-1) - credibility?: number; - bronzeCredibility?: number; - silverCredibility?: number; - goldCredibility?: number; - // Unique repo contribution counts per tier - bronzeUniqueRepos?: number; - bronzeQualifiedUniqueRepos?: number; - silverUniqueRepos?: number; - silverQualifiedUniqueRepos?: number; - goldUniqueRepos?: number; - goldQualifiedUniqueRepos?: number; // Timestamps evaluatedAt: string; createdAt: string; @@ -256,7 +207,6 @@ export type PullRequestDetails = { openPrSpamMultiplier: string; // float returned as string pioneerDividend: number; pioneerRank: number; - repositoryUniquenessMultiplier: string; // float returned as string timeDecayMultiplier: string; // float returned as string credibilityMultiplier: string; // float returned as string reviewQualityMultiplier?: string; // float returned as string @@ -267,8 +217,6 @@ export type PullRequestDetails = { commits: number; // Token scoring fields totalNodesScored: number; - rawCredibility: number; - credibilityScalar: number; tokenScore: string; structuralCount: number; structuralScore: number; @@ -279,9 +227,7 @@ export type PullRequestDetails = { lastEditedAt: string | null; createdAt: string; updatedAt: string; - tier: string; // Bronze, Silver, Gold // Predicted daily payouts based on potential score - // Note: dollar values are null for open PRs, only calculated for merged PRs potentialScore?: number; predictedAlphaPerDay?: number | null; predictedTaoPerDay?: number | null; diff --git a/src/api/models/Predictions.ts b/src/api/models/Predictions.ts deleted file mode 100644 index 6ce0791..0000000 --- a/src/api/models/Predictions.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface MinerPredictionScore { - githubId: string; - emaScore: number; - rounds: number; - updatedAt: string; - uid: number; - hotkey: string; -} diff --git a/src/api/models/index.ts b/src/api/models/index.ts index 6ea63b3..e56a2f6 100644 --- a/src/api/models/index.ts +++ b/src/api/models/index.ts @@ -2,4 +2,3 @@ export * from './Dashboard'; export * from './Issues'; export * from './Miner'; export * from './Configurations'; -export * from './Predictions'; diff --git a/src/components/dashboard/GlobalActivity.tsx b/src/components/dashboard/GlobalActivity.tsx index cf35245..64d3e2b 100644 --- a/src/components/dashboard/GlobalActivity.tsx +++ b/src/components/dashboard/GlobalActivity.tsx @@ -10,17 +10,14 @@ import { import PublicIcon from '@mui/icons-material/Public'; import CodeOffIcon from '@mui/icons-material/CodeOff'; import { subDays, format } from 'date-fns'; -import { useAllMiners, useAllPrs, useReposAndWeights } from '../../api'; +import { useAllMiners, useAllPrs } from '../../api'; import { STATUS_COLORS } from '../../theme'; -import TierRepoCard from './TierRepoCard'; import ContributionHeatmap from './ContributionHeatmap'; import PRStatusChart from './PRStatusChart'; -import TierPerformanceTable from './TierPerformanceTable'; const GlobalActivity: React.FC = () => { const { data: allMinerStats, isLoading: isLoadingStats } = useAllMiners(); const { data: allPrs, isLoading: isLoadingPRs } = useAllPrs(); - const { data: repos } = useReposAndWeights(); // Calculate Heatmap Data const { contributionData, contributionsLast30Days, totalDaysShown } = @@ -90,102 +87,29 @@ const GlobalActivity: React.FC = () => { }; }, [allPrs]); - // Calculate Tier Stats - const { activeStats, inactiveStats, tierStats } = useMemo(() => { + // Calculate Active/Inactive Stats + const { activeStats, inactiveStats } = useMemo(() => { const defaultStats = { merged: 0, open: 0, closed: 0, total: 0 }; - const defaultTierStats = { - merged: 0, - open: 0, - closed: 0, - total: 0, - credibility: 0, - totalScore: 0, - uniqueRepos: 0, - avgScorePerMiner: 0, - totalPRs: 0, - }; if (!Array.isArray(allMinerStats)) { return { activeStats: { ...defaultStats, credibility: 0 }, inactiveStats: { ...defaultStats, credibility: 0 }, - tierStats: { - Gold: { ...defaultTierStats }, - Silver: { ...defaultTierStats }, - Bronze: { ...defaultTierStats }, - Candidate: { ...defaultTierStats }, - }, }; } const active = { ...defaultStats }; const inactive = { ...defaultStats }; - const tiers: Record = { - Gold: { ...defaultTierStats }, - Silver: { ...defaultTierStats }, - Bronze: { ...defaultTierStats }, - }; allMinerStats.forEach((m) => { - const isActive = - m.currentTier && ['Bronze', 'Silver', 'Gold'].includes(m.currentTier); - const target = isActive ? active : inactive; + const target = m.isEligible ? active : inactive; target.merged += m.totalMergedPrs || 0; target.open += m.totalOpenPrs || 0; target.closed += m.totalClosedPrs || 0; target.total += 1; - - if (m.currentTier && tiers[m.currentTier]) { - const t = tiers[m.currentTier]; - const tierKey = m.currentTier.toLowerCase(); - t.merged += (m[`${tierKey}MergedPrs` as keyof typeof m] as number) || 0; - t.closed += (m[`${tierKey}ClosedPrs` as keyof typeof m] as number) || 0; - t.totalScore += Number(m[`${tierKey}Score` as keyof typeof m]) || 0; - t.open += - ((m[`${tierKey}TotalPrs` as keyof typeof m] as number) || 0) - - ((m[`${tierKey}MergedPrs` as keyof typeof m] as number) || 0) - - ((m[`${tierKey}ClosedPrs` as keyof typeof m] as number) || 0); - t.total += 1; - } - }); - - // Calculate credibility and metrics for each tier - const calculateCredibility = (stats: typeof defaultTierStats) => { - const totalResolved = stats.merged + stats.closed; - return totalResolved > 0 ? stats.merged / totalResolved : 0; - }; - - ['Gold', 'Silver', 'Bronze'].forEach((tier) => { - const t = tiers[tier]; - t.credibility = calculateCredibility(t); - t.totalPRs = t.merged + t.open + t.closed; - t.avgScorePerMiner = t.total > 0 ? t.totalScore / t.total : 0; }); - // Candidate tier - const candidateTier = { - ...inactive, - credibility: - inactive.merged + inactive.closed > 0 - ? inactive.merged / (inactive.merged + inactive.closed) - : 0, - totalScore: allMinerStats - .filter( - (m) => - !m.currentTier || - !['Bronze', 'Silver', 'Gold'].includes(m.currentTier), - ) - .reduce((sum, m) => sum + (Number(m.totalScore) || 0), 0), - uniqueRepos: 0, - totalPRs: inactive.merged + inactive.open + inactive.closed, - avgScorePerMiner: 0, - }; - candidateTier.avgScorePerMiner = - candidateTier.total > 0 - ? candidateTier.totalScore / candidateTier.total - : 0; - return { activeStats: { ...active, @@ -201,71 +125,9 @@ const GlobalActivity: React.FC = () => { ? inactive.merged / (inactive.merged + inactive.closed) : 0, }, - tierStats: { ...tiers, Candidate: candidateTier }, }; }, [allMinerStats]); - // Calculate max values for opacity scaling - const { maxValues, getOpacity } = useMemo(() => { - const tierNames = ['Gold', 'Silver', 'Bronze']; - const maxVals = tierNames.reduce( - (acc, tier) => { - const s = (tierStats[tier as keyof typeof tierStats] as any) || {}; - return { - total: Math.max(acc.total, s.total || 0), - merged: Math.max(acc.merged, s.merged || 0), - open: Math.max(acc.open, s.open || 0), - closed: Math.max(acc.closed, s.closed || 0), - totalScore: Math.max(acc.totalScore, s.totalScore || 0), - avgScorePerMiner: Math.max( - acc.avgScorePerMiner, - s.avgScorePerMiner || 0, - ), - }; - }, - { - total: 0, - merged: 0, - open: 0, - closed: 0, - totalScore: 0, - avgScorePerMiner: 0, - }, - ); - - const getOp = (value: number, max: number) => { - if (max === 0) return 0.6; - return (0.6 + 0.4 * Math.min(value / max, 1)).toFixed(2); - }; - - return { maxValues: maxVals, getOpacity: getOp }; - }, [tierStats]); - - // Group repos by tier - const topReposByTier = useMemo(() => { - if (!repos || !Array.isArray(repos)) - return { Gold: [], Silver: [], Bronze: [] }; - - const result: Record> = { - Gold: [], - Silver: [], - Bronze: [], - }; - - repos.forEach((repo) => { - if (repo.tier && result[repo.tier]) { - result[repo.tier].push({ fullName: repo.fullName, owner: repo.owner }); - } - }); - - // Shuffle for variety - Object.keys(result).forEach((tier) => { - result[tier] = result[tier].sort(() => Math.random() - 0.5); - }); - - return result; - }, [repos]); - if (isLoadingPRs || isLoadingStats) { return ( @@ -337,7 +199,7 @@ const GlobalActivity: React.FC = () => { /> - {/* Active & Candidate Stats */} + {/* Active & Inactive Stats */} ({ @@ -372,36 +234,6 @@ const GlobalActivity: React.FC = () => { - - {/* Tier Repo Cards */} - - ({ - display: 'flex', - flexDirection: 'column', - gap: 1, - height: '100%', - [theme.breakpoints.between('lg', 'xl')]: { gap: 0.5 }, - })} - > - {['Gold', 'Silver', 'Bronze'].map((tier) => ( - - ))} - - - - {/* Tier Performance Table */} - - - )} diff --git a/src/components/dashboard/LeaderboardCharts.tsx b/src/components/dashboard/LeaderboardCharts.tsx index 617dbce..139fc1f 100644 --- a/src/components/dashboard/LeaderboardCharts.tsx +++ b/src/components/dashboard/LeaderboardCharts.tsx @@ -7,14 +7,13 @@ import { FormControlLabel, Switch, Typography, - Stack, - Button, CircularProgress, } from '@mui/material'; import BarChartIcon from '@mui/icons-material/BarChart'; import ReactECharts from 'echarts-for-react'; import { useAllPrs, useReposAndWeights } from '../../api'; -import { TIER_COLORS, STATUS_COLORS } from '../../theme'; + +const CHART_DOT_COLOR = 'rgba(88, 166, 255, 0.9)'; const truncateText = (text: string, maxLength: number): string => { if (!text) return ''; @@ -24,9 +23,6 @@ const truncateText = (text: string, maxLength: number): string => { const LeaderboardCharts: React.FC = () => { const [activeTab, setActiveTab] = useState(0); const [useLogScale, setUseLogScale] = useState(true); - const [tierFilter, setTierFilter] = useState< - 'all' | 'Gold' | 'Silver' | 'Bronze' - >('all'); const { data: allPRs, isLoading: isLoadingPRs } = useAllPrs(); const { data: repos, isLoading: isLoadingRepos } = useReposAndWeights(); @@ -56,7 +52,6 @@ const LeaderboardCharts: React.FC = () => { totalPRs: 0, uniqueMiners: new Set(), weight: 0, - tier: '', }; current.totalScore += parseFloat(pr.score || '0'); current.totalPRs += 1; @@ -70,7 +65,6 @@ const LeaderboardCharts: React.FC = () => { stats.weight = repoData.weight ? parseFloat(String(repoData.weight)) : 0; - stats.tier = repoData.tier || ''; } }); @@ -80,56 +74,8 @@ const LeaderboardCharts: React.FC = () => { .map((repo, index) => ({ ...repo, rank: index + 1 })); }, [allPRs, repos]); - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return 'rgba(139, 148, 158, 0.9)'; - } - }; - - const TierFilterButton = ({ - label, - value, - color, - }: { - label: string; - value: typeof tierFilter; - color: string; - }) => ( - - ); - const getPRsChartOption = () => { - const filteredData = - tierFilter === 'all' - ? topPRs - : topPRs.filter((pr) => pr.tier === tierFilter); - const chartData = filteredData + const chartData = topPRs .filter((item) => parseFloat(item?.score || '0') >= 1) .slice(0, 50); @@ -138,16 +84,15 @@ const LeaderboardCharts: React.FC = () => { ); const dotData = chartData.map((item) => ({ value: Number(parseFloat(item?.score || '0')), - tier: item?.tier || 'N/A', title: item?.pullRequestTitle || '', author: item?.author || '', repository: item?.repository || '', prNumber: item?.pullRequestNumber || 0, rank: item?.rank || 0, itemStyle: { - color: getTierColor(item?.tier || ''), + color: CHART_DOT_COLOR, shadowBlur: 10, - shadowColor: getTierColor(item?.tier || ''), + shadowColor: CHART_DOT_COLOR, }, })); @@ -155,7 +100,7 @@ const LeaderboardCharts: React.FC = () => { backgroundColor: 'transparent', title: { text: 'Pull Request Performance Ranking', - subtext: 'Individual PR scores with tier classification', + subtext: 'Individual PR scores ranked by performance', left: 'center', top: 20, textStyle: { @@ -188,11 +133,10 @@ const LeaderboardCharts: React.FC = () => { formatter: (params: any) => { const data = params[0]?.data || params[1]?.data; if (!data) return ''; - const tierColor = getTierColor(data.tier); return `
- +
PR #${data.prNumber}
${data.author}
@@ -201,10 +145,6 @@ const LeaderboardCharts: React.FC = () => {
${data.title}
-
-
- ${data.tier} Tier -
Rank: @@ -280,7 +220,7 @@ const LeaderboardCharts: React.FC = () => { data: dotData.map((item) => ({ ...item, itemStyle: { - color: getTierColor(item.tier), + color: CHART_DOT_COLOR, opacity: 0.4, borderRadius: [2, 2, 0, 0], }, @@ -310,11 +250,7 @@ const LeaderboardCharts: React.FC = () => { }; const getReposChartOption = () => { - const filteredData = - tierFilter === 'all' - ? repoStats - : repoStats.filter((repo) => repo.tier === tierFilter); - const chartData = filteredData + const chartData = repoStats .filter((item) => item.totalScore >= 1) .slice(0, 50); @@ -323,16 +259,15 @@ const LeaderboardCharts: React.FC = () => { ); const dotData = chartData.map((item) => ({ value: item.totalScore, - tier: item.tier, repository: item.repository, totalPRs: item.totalPRs, uniqueMiners: item.uniqueMiners.size, weight: item.weight, rank: item.rank, itemStyle: { - color: getTierColor(item.tier), + color: CHART_DOT_COLOR, shadowBlur: 10, - shadowColor: getTierColor(item.tier), + shadowColor: CHART_DOT_COLOR, }, })); @@ -373,7 +308,6 @@ const LeaderboardCharts: React.FC = () => { formatter: (params: any) => { const data = params[0]?.data || params[1]?.data; if (!data) return ''; - const tierColor = getTierColor(data.tier); const repoOwner = data.repository.split('/')[0]; const repoName = data.repository.split('/')[1] || data.repository; const avatarBg = @@ -385,16 +319,12 @@ const LeaderboardCharts: React.FC = () => { return `
- +
${repoName}
${repoOwner}
-
-
- ${data.tier} Tier -
Rank: @@ -474,7 +404,7 @@ const LeaderboardCharts: React.FC = () => { data: dotData.map((item) => ({ ...item, itemStyle: { - color: getTierColor(item.tier), + color: CHART_DOT_COLOR, opacity: 0.4, borderRadius: [2, 2, 0, 0], }, @@ -548,74 +478,37 @@ const LeaderboardCharts: React.FC = () => { - - - - - - setUseLogScale(e.target.checked)} + size="small" + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { + color: 'primary.main', + }, + '& .MuiSwitch-track': { + backgroundColor: 'rgba(255, 255, 255, 0.3)', + }, + }} /> - - setUseLogScale(e.target.checked)} - size="small" - sx={{ - '& .MuiSwitch-switchBase.Mui-checked': { - color: 'primary.main', - }, - '& .MuiSwitch-track': { - backgroundColor: 'rgba(255, 255, 255, 0.3)', - }, - }} - /> - } - label={ - - Log Scale - - } - sx={{ ml: 'auto' }} - /> - + } + label={ + + Log Scale + + } + sx={{ ml: 'auto' }} + /> {isLoading ? ( diff --git a/src/components/dashboard/TierPerformanceTable.tsx b/src/components/dashboard/TierPerformanceTable.tsx deleted file mode 100644 index d2f93f9..0000000 --- a/src/components/dashboard/TierPerformanceTable.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import React from 'react'; -import { Box, Card, Typography } from '@mui/material'; -import ReactECharts from 'echarts-for-react'; -import { TIER_COLORS, STATUS_COLORS, CHART_COLORS } from '../../theme'; - -interface TierStats { - total: number; - merged: number; - open: number; - closed: number; - credibility: number; - totalScore: number; - avgScorePerMiner: number; -} - -interface TierPerformanceTableProps { - tierStats: Partial>; - maxValues: { - total: number; - merged: number; - open: number; - closed: number; - totalScore: number; - avgScorePerMiner: number; - }; - getOpacity: (value: number, max: number) => string | number; -} - -const TIER_ORDER = ['Gold', 'Silver', 'Bronze', 'Candidate'] as const; - -const getTierColor = (tier: string): string => { - const colors: Record = { - Gold: TIER_COLORS.gold, - Silver: TIER_COLORS.silver, - Bronze: TIER_COLORS.bronze, - Candidate: '#ffffff', - }; - return colors[tier] || '#ffffff'; -}; - -const TierPerformanceTable: React.FC = ({ - tierStats, - maxValues, - getOpacity, -}) => ( - - - - {/* Table Header */} - - - - Tier - - - - - - Miners - - - - - - M.O.C Ratio - - - - - - Score - - - Avg/Miner - - - - - {/* Table Rows */} - {TIER_ORDER.map((tier) => { - const stats = tierStats[tier] || { - total: 0, - merged: 0, - open: 0, - closed: 0, - credibility: 0, - totalScore: 0, - avgScorePerMiner: 0, - }; - const isCandidate = tier === 'Candidate'; - const color = getTierColor(tier); - - return ( - - ); - })} - - - -); - -interface TierRowProps { - tier: string; - stats: TierStats; - color: string; - isCandidate: boolean; - maxValues: TierPerformanceTableProps['maxValues']; - getOpacity: TierPerformanceTableProps['getOpacity']; -} - -const TierRow: React.FC = ({ - tier, - stats, - color, - isCandidate, - maxValues, - getOpacity, -}) => ( - - {/* Tier Name */} - - {!isCandidate && ( - - )} - - {isCandidate ? 'Unranked' : tier} - - - - {/* Miners Count */} - - - {stats.total} - - - - {/* M.O.C Ratio */} - - - - - - - - {/* Score Group */} - - - - - -); - -const StatCell: React.FC<{ - value: number | string; - color: string; - opacity: string | number; -}> = ({ value, color, opacity }) => ( - - - {value} - - -); - -const MiniGauge: React.FC<{ stats: TierStats }> = ({ stats }) => { - const hasData = stats.merged + stats.closed > 0; - const credibilityColor = !hasData - ? 'rgba(255,255,255,0.3)' - : stats.credibility >= 0.7 - ? CHART_COLORS.merged - : stats.credibility >= 0.4 - ? STATUS_COLORS.warning - : CHART_COLORS.closed; - - const option = { - backgroundColor: 'transparent', - title: { - text: hasData ? `${(stats.credibility * 100).toFixed(0)}` : 'N/A', - left: 'center', - top: 'middle', - textStyle: { - color: credibilityColor, - fontSize: hasData ? 10 : 9, - fontWeight: 'bold', - fontFamily: '"JetBrains Mono", monospace', - }, - }, - series: [ - { - type: 'pie', - radius: ['65%', '80%'], - center: ['50%', '50%'], - avoidLabelOverlap: false, - itemStyle: { - borderRadius: 1, - borderColor: '#0d1117', - borderWidth: 0.5, - }, - label: { show: false }, - emphasis: { scale: false }, - labelLine: { show: false }, - data: hasData - ? [ - { - value: stats.merged, - itemStyle: { color: CHART_COLORS.merged }, - }, - { value: stats.open, itemStyle: { color: CHART_COLORS.open } }, - { - value: stats.closed, - itemStyle: { color: CHART_COLORS.closed }, - }, - ] - : [{ value: 1, itemStyle: { color: 'rgba(255,255,255,0.1)' } }], - }, - ], - }; - - return ( - - - - - - ); -}; - -export default TierPerformanceTable; diff --git a/src/components/dashboard/TierRepoCard.tsx b/src/components/dashboard/TierRepoCard.tsx deleted file mode 100644 index 4c43faf..0000000 --- a/src/components/dashboard/TierRepoCard.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Box, Card, Typography, Tooltip } from '@mui/material'; -import { TIER_COLORS } from '../../theme'; - -interface TierRepoCardProps { - tier: string; - repos: Array<{ fullName: string; owner: string }>; -} - -const TierRepoCard: React.FC = ({ tier, repos }) => { - const navigate = useNavigate(); - const containerRef = React.useRef(null); - const [maxItems, setMaxItems] = React.useState(9); - - React.useLayoutEffect(() => { - if (!containerRef.current) return; - - const updateMaxItems = () => { - if (!containerRef.current) return; - const width = containerRef.current.offsetWidth; - const padding = 24; - const calculated = Math.floor((width - padding) / 30); - setMaxItems(Math.max(5, Math.min(20, calculated))); - }; - - const observer = new ResizeObserver(updateMaxItems); - observer.observe(containerRef.current); - updateMaxItems(); - - return () => observer.disconnect(); - }, []); - - const tierColor = TIER_COLORS[tier.toLowerCase() as keyof typeof TIER_COLORS]; - const showOverflow = repos.length > maxItems; - const effectiveLimit = showOverflow ? maxItems - 1 : maxItems; - const displayedRepos = repos.slice(0, effectiveLimit); - const remainingCount = repos.length - displayedRepos.length; - - return ( - - - {tier} Tier Repositories - - - - {displayedRepos.map((repo) => ( - - - navigate( - `/miners/repository?name=${encodeURIComponent(repo.fullName)}`, - { state: { backLabel: 'Back to Dashboard' } }, - ) - } - sx={{ textDecoration: 'none', cursor: 'pointer' }} - > - ) => { - e.currentTarget.style.display = 'none'; - }} - sx={{ - width: 42, - height: 42, - borderRadius: '50%', - border: `2px solid ${tierColor}40`, - marginLeft: '-12px', - backgroundColor: - repo.owner === 'opentensor' - ? '#ffffff' - : repo.owner === 'bitcoin' - ? '#F7931A' - : '#161b22', - transition: 'all 0.2s', - position: 'relative', - zIndex: 1, - '&:hover': { - transform: 'scale(1.2)', - borderColor: tierColor, - boxShadow: `0 0 12px ${tierColor}60`, - zIndex: 100, - }, - }} - /> - - - ))} - {remainingCount > 0 && ( - - navigate(`/top-repos?tier=${tier}`)} - sx={{ - width: 42, - height: 42, - minWidth: 42, - minHeight: 42, - flexShrink: 0, - borderRadius: '50%', - backgroundColor: 'rgba(255, 255, 255, 0.1)', - border: '2px solid #0d1117', - marginLeft: '-12px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - cursor: 'pointer', - transition: 'all 0.2s', - position: 'relative', - zIndex: 1, - '&:hover': { - transform: 'scale(1.2)', - backgroundColor: 'rgba(255, 255, 255, 0.15)', - borderColor: 'rgba(255, 255, 255, 0.3)', - zIndex: 100, - }, - }} - > - - +{remainingCount} - - - - )} - - - ); -}; - -export default TierRepoCard; diff --git a/src/components/dashboard/index.ts b/src/components/dashboard/index.ts index fef3893..c39361b 100644 --- a/src/components/dashboard/index.ts +++ b/src/components/dashboard/index.ts @@ -21,9 +21,3 @@ export * from './PRStatusChart'; export { default as RepositoriesTable } from './RepositoriesTable'; export * from './RepositoriesTable'; - -export { default as TierPerformanceTable } from './TierPerformanceTable'; -export * from './TierPerformanceTable'; - -export { default as TierRepoCard } from './TierRepoCard'; -export * from './TierRepoCard'; diff --git a/src/components/issues/IssueConversation.tsx b/src/components/issues/IssueConversation.tsx index c9c1369..1a5ba1d 100644 --- a/src/components/issues/IssueConversation.tsx +++ b/src/components/issues/IssueConversation.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { Box, Typography, Avatar, Paper, Link, Chip } from '@mui/material'; import { useTheme } from '@mui/material/styles'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; import { type IssueDetails } from '../../api/models/Issues'; import { STATUS_COLORS } from '../../theme'; -import 'github-markdown-css/github-markdown-dark.css'; // Import standard GitHub Dark styles +import 'github-markdown-css/github-markdown-dark.css'; interface IssueConversationProps { issue: IssueDetails; @@ -19,7 +22,7 @@ const IssueConversation: React.FC = ({ issue }) => { avatarUrl: `https://avatars.githubusercontent.com/${issue.authorLogin}`, htmlUrl: `https://github.com/${issue.authorLogin}`, }, - body: issue.body || '*No description provided.*', + body: issue.body || 'No description provided.', createdAt: issue.createdAt, authorAssociation: 'OWNER', // Assuming creator is owner for display purposes, or fetch actual association if available isDescription: true, @@ -55,7 +58,7 @@ const IssueConversation: React.FC = ({ issue }) => { flexDirection: 'column', gap: 3, pt: 2, - maxWidth: '960px', // Widen slightly for better code block readability + maxWidth: '960px', mx: 'auto', position: 'relative', }} @@ -239,15 +242,29 @@ const IssueConversation: React.FC = ({ issue }) => { }, '& h1': { fontSize: '2em' }, '& h2': { fontSize: '1.5em' }, + '& h3': { fontSize: '1.25em' }, + '& p': { mb: 2, mt: 0 }, '& a': { color: colors.accent.fg, textDecoration: 'none' }, '& a:hover': { textDecoration: 'underline' }, + '& ul, & ol': { + pl: '2em', + mb: 2, + listStyleType: 'disc', + }, + '& ol': { listStyleType: 'decimal' }, + '& li': { mb: 0.5 }, + '& li + li': { mt: '0.25em' }, '& blockquote': { padding: '0 1em', color: colors.fg.muted, borderLeft: `0.25em solid ${colors.border.default}`, my: 2, + mx: 0, + }, + '& input[type="checkbox"]': { + mr: 0.5, + verticalAlign: 'middle', }, - // Updated Code Block Styling '& code': { padding: '0.2em 0.4em', margin: 0, @@ -259,8 +276,36 @@ const IssueConversation: React.FC = ({ issue }) => { '& pre': { mt: 2, mb: 2, + p: 2, borderRadius: '6px', - overflow: 'hidden', // Let SyntaxHighlighter handle scroll + overflow: 'auto', + backgroundColor: theme.palette.surface.elevated, + border: `1px solid ${colors.border.default}`, + '& code': { + backgroundColor: 'transparent', + p: 0, + fontSize: '100%', + }, + }, + '& table': { + borderCollapse: 'collapse', + width: '100%', + mb: 2, + overflowX: 'auto', + }, + '& th, & td': { + border: `1px solid ${colors.border.default}`, + padding: '6px 13px', + }, + '& th': { fontWeight: 600 }, + '& tr:nth-of-type(2n)': { + backgroundColor: theme.palette.surface.elevated, + }, + '& hr': { + height: '0.25em', + my: 3, + backgroundColor: colors.border.default, + border: 0, }, '& img': { maxWidth: '100%', @@ -274,21 +319,16 @@ const IssueConversation: React.FC = ({ issue }) => { fontSize: '14px', lineHeight: 1.6, }, - '& .markdown-body pre': { - backgroundColor: theme.palette.surface.elevated, // Distinct code block background - border: `1px solid ${colors.border.default}`, - borderRadius: '6px', - }, - '& .markdown-body code': { - fontFamily: '"JetBrains Mono", monospace', - }, }} > -
+
+ + {item.body} + +
diff --git a/src/components/layout/GlobalSearchBar.tsx b/src/components/layout/GlobalSearchBar.tsx index 3614472..d9559d2 100644 --- a/src/components/layout/GlobalSearchBar.tsx +++ b/src/components/layout/GlobalSearchBar.tsx @@ -153,23 +153,11 @@ const SearchActionRow: React.FC = ({ ); -const getMinerSubtitle = (miner: { - currentTier: string; - leaderboardRank: number; -}) => { - const parts: string[] = []; - +const getMinerSubtitle = (miner: { leaderboardRank: number }) => { if (miner.leaderboardRank > 0) { - parts.push(`Rank #${miner.leaderboardRank}`); - } - - const currentTier = miner.currentTier.trim(); - - if (currentTier) { - parts.push(currentTier); + return `Rank #${miner.leaderboardRank}`; } - - return parts.join(' · '); + return ''; }; const GlobalSearchBar: React.FC = () => { @@ -391,9 +379,7 @@ const GlobalSearchBar: React.FC = () => { navigateAndClose( `/miners/repository?name=${encodeURIComponent(repo.fullName)}`, diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index f0be4f6..21f6ac0 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -24,8 +24,9 @@ const Sidebar: React.FC = ({ onNavigate }) => { const navItems = [ { label: 'dashboard', path: '/dashboard' }, - { label: 'issues', path: '/issues', badge: 'new' }, - { label: 'leaderboard', path: '/top-miners' }, + { label: 'oss contributions', path: '/top-miners' }, + { label: 'discoveries', path: '/discoveries', badge: 'new' }, + { label: 'bounties', path: '/issues' }, { label: 'repositories', path: '/repositories' }, { label: 'onboard', path: '/onboard' }, ]; @@ -78,14 +79,12 @@ const Sidebar: React.FC = ({ onNavigate }) => { fontFamily: '"JetBrains Mono", monospace', fontSize: '0.95rem', textTransform: 'none', - backgroundColor: - location.pathname === item.path - ? 'rgba(255, 255, 255, 0.1)' - : 'transparent', - borderLeft: - location.pathname === item.path - ? '2px solid #ffffff' - : '2px solid transparent', + backgroundColor: location.pathname.startsWith(item.path) + ? 'rgba(255, 255, 255, 0.1)' + : 'transparent', + borderLeft: location.pathname.startsWith(item.path) + ? '2px solid #ffffff' + : '2px solid transparent', borderRadius: 0, textAlign: 'left', '&:hover': { diff --git a/src/components/leaderboard/LeaderboardSidebar.tsx b/src/components/leaderboard/LeaderboardSidebar.tsx index 001e4ee..9b9c5d9 100644 --- a/src/components/leaderboard/LeaderboardSidebar.tsx +++ b/src/components/leaderboard/LeaderboardSidebar.tsx @@ -10,11 +10,13 @@ export type { MinerStats } from './types'; interface LeaderboardSidebarProps { miners: MinerStats[]; onSelectMiner: (githubId: string) => void; + variant?: 'oss' | 'discoveries'; } export const LeaderboardSidebar: React.FC = ({ miners, onSelectMiner, + variant = 'oss', }) => { // State for toggling lists const [leaderboardType, setLeaderboardType] = useState<'earners' | 'active'>( @@ -42,7 +44,7 @@ export const LeaderboardSidebar: React.FC = ({ const networkStats = useMemo( () => ({ totalMiners: miners.length, - activeTier: miners.filter((m) => m.currentTier).length, + eligible: miners.filter((m) => m.isEligible).length, totalPRs: miners.reduce((acc, m) => acc + (m.totalPRs || 0), 0), dailyPool: miners.reduce((acc, m) => acc + (m.usdPerDay || 0), 0), }), @@ -64,8 +66,14 @@ export const LeaderboardSidebar: React.FC = ({ }} > - - + + = ({ } sx={{ flexShrink: 0 }} > - + {(leaderboardType === 'earners' ? topEarners : mostActive).map( (miner, i) => ( = ({ interface LeaderboardTabsProps { activeTab: 'earners' | 'active'; onTabChange: (tab: 'earners' | 'active') => void; + variant?: 'oss' | 'discoveries'; } const LeaderboardTabs: React.FC = ({ activeTab, onTabChange, + variant = 'oss', }) => ( = ({ > {[ { label: '$', value: 'earners' as const }, - { label: 'PRs', value: 'active' as const }, + { + label: variant === 'discoveries' ? 'Issues' : 'PRs', + value: 'active' as const, + }, ].map((option) => ( = ({ interface LeaderboardHeaderProps { type: 'earners' | 'active'; + variant?: 'oss' | 'discoveries'; } -const LeaderboardHeader: React.FC = ({ type }) => ( +const LeaderboardHeader: React.FC = ({ + type, + variant = 'oss', +}) => ( = ({ type }) => ( textTransform: 'uppercase', }} > - {type === 'earners' ? '$/Day' : 'PRs'} + {type === 'earners' + ? '$/Day' + : variant === 'discoveries' + ? 'Issues' + : 'PRs'} ); diff --git a/src/components/leaderboard/MinerCard.tsx b/src/components/leaderboard/MinerCard.tsx index db5826f..d159867 100644 --- a/src/components/leaderboard/MinerCard.tsx +++ b/src/components/leaderboard/MinerCard.tsx @@ -3,7 +3,7 @@ import { Box, Card, Typography, Avatar } from '@mui/material'; import ReactECharts from 'echarts-for-react'; import { useMinerGithubData, useMinerPRs } from '../../api'; import { CHART_COLORS, STATUS_COLORS } from '../../theme'; -import { type MinerStats, getTierColors, FONTS } from './types'; +import { type MinerStats, FONTS } from './types'; interface MinerCardProps { miner: MinerStats; @@ -11,15 +11,13 @@ interface MinerCardProps { } export const MinerCard: React.FC = ({ miner, onClick }) => { - const tierColors = getTierColors(miner.currentTier); - // Helper to check for numeric IDs or missing values const isNumericId = (val: string | undefined) => !val || /^\d+$/.test(val); // Fetch profile if author is missing or looks like an ID const shouldFetch = isNumericId(miner.author); const { data: githubData } = useMinerGithubData(miner.githubId, shouldFetch); - // Also fetch PRs as fallback if github data is missing (common for unranked miners) + // Also fetch PRs as fallback if github data is missing const { data: prs } = useMinerPRs(miner.githubId, shouldFetch); const username = @@ -30,11 +28,12 @@ export const MinerCard: React.FC = ({ miner, onClick }) => { ''; const credibilityPercent = (miner.credibility || 0) * 100; - const borderColor = miner.currentTier - ? tierColors.border + const isEligible = miner.isEligible ?? false; + const borderColor = isEligible + ? 'rgba(63, 185, 80, 0.3)' : 'rgba(48, 54, 61, 0.4)'; - if (!miner.currentTier) { + if (!isEligible) { return ( = ({ miner, onClick }) => { py: 0.1, }} > - Unranked + Ineligible ); @@ -118,19 +117,15 @@ export const MinerCard: React.FC = ({ miner, onClick }) => { boxShadow: '0 2px 8px rgba(0,0,0,0.1)', '&:hover': { backgroundColor: 'rgba(22, 27, 34, 0.6)', - borderColor: tierColors.text, + borderColor: 'rgba(63, 185, 80, 0.5)', transform: 'translateY(-2px)', - boxShadow: `0 8px 24px -6px rgba(0, 0, 0, 0.6), 0 0 0 1px ${tierColors.border}40`, + boxShadow: '0 8px 24px -6px rgba(0, 0, 0, 0.6)', }, }} elevation={0} > {/* Header: Identity + Rank */} - + {/* Main Stats: Earnings & Credibility */} @@ -144,13 +139,11 @@ export const MinerCard: React.FC = ({ miner, onClick }) => { interface MinerCardHeaderProps { username: string; miner: MinerStats; - tierColors: ReturnType; } const MinerCardHeader: React.FC = ({ username, miner, - tierColors, }) => ( = ({ sx={{ width: 36, height: 36, - border: `2px solid ${tierColors.border}`, - boxShadow: `0 0 10px ${tierColors.border}20`, + border: '2px solid rgba(63, 185, 80, 0.3)', }} /> = ({ bottom: -4, right: -4, backgroundColor: '#0d1117', - border: `1px solid ${tierColors.border}`, + border: '1px solid rgba(63, 185, 80, 0.3)', borderRadius: '4px', px: 0.5, py: 0, @@ -187,7 +179,7 @@ const MinerCardHeader: React.FC = ({ fontFamily: FONTS.mono, fontSize: '0.6rem', fontWeight: 700, - color: tierColors.text, + color: 'rgba(255, 255, 255, 0.7)', }} > #{miner.rank} @@ -210,19 +202,6 @@ const MinerCardHeader: React.FC = ({ - - {miner.currentTier} - ); diff --git a/src/components/leaderboard/MinerSection.tsx b/src/components/leaderboard/MinerSection.tsx index ee0fe4e..0577ac7 100644 --- a/src/components/leaderboard/MinerSection.tsx +++ b/src/components/leaderboard/MinerSection.tsx @@ -2,12 +2,12 @@ import React, { useState } from 'react'; import { Box, Card, Typography, Grid } from '@mui/material'; import { MinerCard } from './MinerCard'; import { STATUS_COLORS } from '../../theme'; -import { type MinerStats, type TierColorSet, FONTS } from './types'; +import { type MinerStats, type RankColorSet, FONTS } from './types'; interface MinerSectionProps { title?: string; miners: MinerStats[]; - color: TierColorSet; + color: RankColorSet; onSelectMiner: (githubId: string) => void; defaultExpanded?: boolean; } @@ -22,7 +22,7 @@ export const MinerSection: React.FC = ({ const [expanded, setExpanded] = useState(defaultExpanded); // Determine how many items to show - // User Requirement: "see at least top 3 in every tier without expanding the view" + // Show at least top 3 in every section without expanding const INITIAL_DISPLAY_COUNT = 3; // If not expanded, show INITIAL_DISPLAY_COUNT. If expanded, show all. @@ -78,7 +78,7 @@ export const MinerSection: React.FC = ({ interface SectionHeaderProps { title: string; - color: TierColorSet; + color: RankColorSet; } const SectionHeader: React.FC = ({ title, color }) => ( @@ -112,7 +112,7 @@ interface SectionFooterProps { expanded: boolean; onToggle: () => void; remainingCount: number; - color: TierColorSet; + color: RankColorSet; } const SectionFooter: React.FC = ({ diff --git a/src/components/leaderboard/TopMinersTable.tsx b/src/components/leaderboard/TopMinersTable.tsx index 8310215..f5d0261 100644 --- a/src/components/leaderboard/TopMinersTable.tsx +++ b/src/components/leaderboard/TopMinersTable.tsx @@ -10,12 +10,7 @@ import SearchIcon from '@mui/icons-material/Search'; import { SectionCard } from './SectionCard'; import { MinerSection } from './MinerSection'; import { STATUS_COLORS } from '../../theme'; -import { - type MinerStats, - type SortOption, - getTierColors, - FONTS, -} from './types'; +import { type MinerStats, type SortOption, FONTS } from './types'; // Re-export MinerStats for backward compatibility export type { MinerStats } from './types'; @@ -66,19 +61,15 @@ const TopMinersTable: React.FC = ({ ); } - // 2. Group by Tier - const gold = result.filter((m) => m.currentTier === 'Gold'); - const silver = result.filter((m) => m.currentTier === 'Silver'); - const bronze = result.filter((m) => m.currentTier === 'Bronze'); - const others = result.filter((m) => !m.currentTier); + // 2. Group by eligibility + const eligible = result.filter((m) => m.isEligible); + const ineligible = result.filter((m) => !m.isEligible); // 3. Sort each Group return { - gold: sortMinersList(gold, sortOption), - silver: sortMinersList(silver, sortOption), - bronze: sortMinersList(bronze, sortOption), - others: sortMinersList( - others, + eligible: sortMinersList(eligible, sortOption), + ineligible: sortMinersList( + ineligible, sortOption === 'totalScore' ? 'credibility' : sortOption, ), totalFiltered: result.length, @@ -117,41 +108,26 @@ const TopMinersTable: React.FC = ({ - {/* GOLD SECTION */} - {groupedMiners.gold.length > 0 && ( + {/* ELIGIBLE SECTION */} + {groupedMiners.eligible.length > 0 && ( - )} - - {/* SILVER SECTION */} - {groupedMiners.silver.length > 0 && ( - - )} - - {/* BRONZE SECTION */} - {groupedMiners.bronze.length > 0 && ( - )} - {/* INACTIVE / OTHER SECTION */} - {groupedMiners.others.length > 0 && ( + {/* INELIGIBLE SECTION */} + {groupedMiners.ineligible.length > 0 && ( = ({ const [showChart, setShowChart] = useState(false); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - const [tierFilter, setTierFilter] = useState< - 'all' | 'Gold' | 'Silver' | 'Bronze' - >('all'); const [statusFilter, setStatusFilter] = useState< 'all' | 'open' | 'closed' | 'merged' >('all'); @@ -79,11 +76,6 @@ const TopPRsTable: React.FC = ({ const filteredPRs = useMemo(() => { let filtered = rankedPRs; - // Apply tier filter - if (tierFilter !== 'all') { - filtered = filtered.filter((pr) => pr.tier === tierFilter); - } - // Apply status filter if (statusFilter !== 'all') { if (statusFilter === 'merged') { @@ -113,7 +105,7 @@ const TopPRsTable: React.FC = ({ } return filtered; - }, [rankedPRs, searchQuery, tierFilter, statusFilter]); + }, [rankedPRs, searchQuery, statusFilter]); const statusCounts = useMemo( () => ({ @@ -129,70 +121,6 @@ const TopPRsTable: React.FC = ({ [rankedPRs], ); - const tierCounts = useMemo( - () => ({ - all: rankedPRs.length, - gold: rankedPRs.filter((pr) => pr.tier === 'Gold').length, - silver: rankedPRs.filter((pr) => pr.tier === 'Silver').length, - bronze: rankedPRs.filter((pr) => pr.tier === 'Bronze').length, - }), - [rankedPRs], - ); - - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return theme.palette.tier.gold; - case 'Silver': - return theme.palette.tier.silver; - case 'Bronze': - return theme.palette.tier.bronze; - default: - return theme.palette.status.neutral; - } - }; - - const TierFilterButton = ({ - label, - value, - count, - color, - }: { - label: string; - value: typeof tierFilter; - count: number; - color: string; - }) => ( - - ); - const FilterButton = ({ label, value, @@ -240,18 +168,7 @@ const TopPRsTable: React.FC = ({ const textColor = 'rgba(255, 255, 255, 0.85)'; const gridColor = 'rgba(255, 255, 255, 0.08)'; - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return theme.palette.tier.gold; - case 'Silver': - return theme.palette.tier.silver; - case 'Bronze': - return theme.palette.tier.bronze; - default: - return theme.palette.status.neutral; - } - }; + const chartColor = theme.palette.primary.main; const xAxisData = chartData.map( (item) => `#${item?.pullRequestNumber || ''}`, @@ -259,7 +176,6 @@ const TopPRsTable: React.FC = ({ const stemData = chartData.map((item) => ({ value: Number(parseFloat(item?.score || '0')), - tier: item?.tier || 'N/A', title: item?.pullRequestTitle || '', author: item?.author || '', repository: item?.repository || '', @@ -269,16 +185,15 @@ const TopPRsTable: React.FC = ({ const dotData = stemData.map((item) => ({ value: item.value, - tier: item.tier, title: item.title, author: item.author, repository: item.repository, prNumber: item.prNumber, rank: item.rank, itemStyle: { - color: getTierColor(item.tier), + color: chartColor, shadowBlur: 10, - shadowColor: getTierColor(item.tier), + shadowColor: chartColor, }, })); @@ -286,7 +201,7 @@ const TopPRsTable: React.FC = ({ backgroundColor: 'transparent', title: { text: 'Pull Request Performance Ranking', - subtext: 'Individual PR scores with tier classification', + subtext: 'Individual PR scores ranked by performance', left: 'center', top: 20, textStyle: { @@ -321,7 +236,6 @@ const TopPRsTable: React.FC = ({ formatter: (params: any) => { const data = params[0]?.data || params[1]?.data; if (!data) return ''; - const tierColor = getTierColor(data.tier); return `
@@ -331,10 +245,6 @@ const TopPRsTable: React.FC = ({
${data.title}
-
-
- ${data.tier} Tier -
Rank: @@ -435,7 +345,7 @@ const TopPRsTable: React.FC = ({ data: dotData.map((item) => ({ ...item, itemStyle: { - color: getTierColor(item.tier), + color: chartColor, opacity: 0.4, borderRadius: [2, 2, 0, 0], }, @@ -694,46 +604,6 @@ const TopPRsTable: React.FC = ({ flexWrap: 'wrap', }} > - - - TIER - - - - - - - - - = ({ {truncateText(pr.repository || '', 30)} - @@ -1160,19 +1021,19 @@ const getRankIcon = (rank: number) => ( border: '1px solid', borderColor: rank === 1 - ? alpha(TIER_COLORS.gold, 0.4) + ? alpha(RANK_COLORS.first, 0.4) : rank === 2 - ? alpha(TIER_COLORS.silver, 0.4) + ? alpha(RANK_COLORS.second, 0.4) : rank === 3 - ? alpha(TIER_COLORS.bronze, 0.4) + ? alpha(RANK_COLORS.third, 0.4) : 'rgba(255, 255, 255, 0.15)', boxShadow: rank === 1 - ? `0 0 12px ${alpha(TIER_COLORS.gold, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.gold, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.first, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.first, 0.2)}` : rank === 2 - ? `0 0 12px ${alpha(TIER_COLORS.silver, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.silver, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.second, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.second, 0.2)}` : rank === 3 - ? `0 0 12px ${alpha(TIER_COLORS.bronze, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.bronze, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.third, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.third, 0.2)}` : 'none', }} > @@ -1181,11 +1042,11 @@ const getRankIcon = (rank: number) => ( sx={{ color: rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : rank === 2 - ? TIER_COLORS.silver + ? RANK_COLORS.second : rank === 3 - ? TIER_COLORS.bronze + ? RANK_COLORS.third : 'rgba(255, 255, 255, 0.6)', fontFamily: '"JetBrains Mono", monospace', fontSize: '0.65rem', diff --git a/src/components/leaderboard/TopRepositoriesTable.tsx b/src/components/leaderboard/TopRepositoriesTable.tsx index 78744ea..2c997de 100644 --- a/src/components/leaderboard/TopRepositoriesTable.tsx +++ b/src/components/leaderboard/TopRepositoriesTable.tsx @@ -28,8 +28,6 @@ import { MenuItem, FormControl, Button, - Stack, - Chip, Switch, FormControlLabel, } from '@mui/material'; @@ -38,7 +36,7 @@ import BarChartIcon from '@mui/icons-material/BarChart'; import TableChartIcon from '@mui/icons-material/TableChart'; import ReactECharts from 'echarts-for-react'; import { useSearchParams } from 'react-router-dom'; -import { TIER_COLORS, STATUS_COLORS } from '../../theme'; +import { RANK_COLORS } from '../../theme'; interface RepoStats { repository: string; @@ -46,7 +44,6 @@ interface RepoStats { totalPRs: number; uniqueMiners: Set; weight: number; - tier: string; rank?: number; inactiveAt?: string | null; } @@ -64,7 +61,6 @@ interface TopRepositoriesTableProps { repositories: RepoStats[]; isLoading?: boolean; onSelectRepository: (repositoryFullName: string) => void; - initialTierFilter?: 'Gold' | 'Silver' | 'Bronze'; } // Utility function to truncate text @@ -73,7 +69,6 @@ const truncateText = (text: string, maxLength: number): string => { return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; }; -const VALID_TIERS = ['all', 'Gold', 'Silver', 'Bronze'] as const; const VALID_SORT_COLUMNS: SortColumn[] = [ 'rank', 'repository', @@ -88,17 +83,10 @@ const TopRepositoriesTable: React.FC = ({ repositories, isLoading, onSelectRepository, - initialTierFilter, }) => { const [searchParams, setSearchParams] = useSearchParams(); // Read initial state from URL params, falling back to defaults - const urlTier = searchParams.get('tier') as - | 'all' - | 'Gold' - | 'Silver' - | 'Bronze' - | null; const urlRows = parseInt(searchParams.get('rows') || '', 10); const urlPage = parseInt(searchParams.get('page') || '', 10); const urlSort = searchParams.get('sort') as SortColumn; @@ -117,13 +105,6 @@ const TopRepositoriesTable: React.FC = ({ const [sortDirection, setSortDirection] = useState( urlDir === 'asc' || urlDir === 'desc' ? urlDir : 'desc', ); - const [tierFilter, setTierFilter] = useState< - 'all' | 'Gold' | 'Silver' | 'Bronze' - >( - urlTier && VALID_TIERS.includes(urlTier) - ? urlTier - : initialTierFilter || 'all', - ); const [useLogScale, setUseLogScale] = useState(true); const isInitialMount = useRef(true); const trimmedSearch = searchQuery.trim(); @@ -133,14 +114,12 @@ const TopRepositoriesTable: React.FC = ({ const syncToUrl = useCallback( (overrides?: Record) => { const params: Record = {}; - const tier = overrides?.tier ?? tierFilter; const rows = overrides?.rows ?? String(rowsPerPage); const pg = overrides?.page ?? String(page); const sort = overrides?.sort ?? sortColumn; const dir = overrides?.dir ?? sortDirection; const search = overrides?.search ?? searchQuery; - if (tier !== 'all') params.tier = tier; if (rows !== '10') params.rows = rows; if (pg !== '0') params.page = pg; if (sort !== 'weight') params.sort = sort; @@ -150,7 +129,6 @@ const TopRepositoriesTable: React.FC = ({ setSearchParams(params, { replace: true }); }, [ - tierFilter, rowsPerPage, page, sortColumn, @@ -196,11 +174,6 @@ const TopRepositoriesTable: React.FC = ({ const filteredRepositories = useMemo(() => { let filtered = rankedRepositories; - // Apply tier filter - if (tierFilter !== 'all') { - filtered = filtered.filter((repo) => repo.tier === tierFilter); - } - // Apply search filter if (searchQuery) { const lowerQuery = searchQuery.toLowerCase(); @@ -210,95 +183,42 @@ const TopRepositoriesTable: React.FC = ({ } return filtered; - }, [rankedRepositories, searchQuery, tierFilter]); + }, [rankedRepositories, searchQuery]); const getChartOption = () => { const chartData = filteredRepositories.slice(0, 50); // Limit for performance const textColor = 'rgba(255, 255, 255, 0.85)'; const gridColor = 'rgba(255, 255, 255, 0.08)'; - const getTierColorGradient = (tier: string) => { - switch (tier) { - case 'Gold': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(255, 215, 0, 0.9)' }, - { offset: 0.5, color: 'rgba(255, 215, 0, 0.7)' }, - { offset: 1, color: 'rgba(255, 200, 0, 0.5)' }, - ], - }; - case 'Silver': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(192, 192, 192, 0.9)' }, - { offset: 0.5, color: 'rgba(192, 192, 192, 0.7)' }, - { offset: 1, color: 'rgba(170, 170, 170, 0.5)' }, - ], - }; - case 'Bronze': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(205, 127, 50, 0.9)' }, - { offset: 0.5, color: 'rgba(205, 127, 50, 0.7)' }, - { offset: 1, color: 'rgba(184, 115, 51, 0.5)' }, - ], - }; - default: - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(139, 148, 158, 0.8)' }, - { offset: 0.5, color: 'rgba(139, 148, 158, 0.6)' }, - { offset: 1, color: 'rgba(100, 108, 118, 0.4)' }, - ], - }; - } + const barGradient = { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(139, 148, 158, 0.8)' }, + { offset: 0.5, color: 'rgba(139, 148, 158, 0.6)' }, + { offset: 1, color: 'rgba(100, 108, 118, 0.4)' }, + ], }; const xAxisData = chartData.map((item) => ({ name: (item?.repository || '').split('/')[1] || item?.repository || '', fullName: item?.repository || '', - tier: item?.tier || 'N/A', })); const seriesData = chartData.map((item, index) => ({ value: Number(item?.totalScore) || 0, rank: item?.rank || index + 1, - tier: item?.tier || 'N/A', repository: item?.repository || '', weight: item?.weight || 0, prs: item?.totalPRs || 0, contributors: item?.uniqueMiners?.size || 0, itemStyle: { - color: getTierColorGradient(item?.tier || ''), + color: barGradient, borderRadius: [6, 6, 0, 0], - shadowColor: - item?.tier === 'Gold' - ? 'rgba(255, 215, 0, 0.4)' - : item?.tier === 'Silver' - ? 'rgba(192, 192, 192, 0.3)' - : item?.tier === 'Bronze' - ? 'rgba(205, 127, 50, 0.3)' - : 'rgba(100, 100, 100, 0.2)', + shadowColor: 'rgba(100, 100, 100, 0.2)', shadowBlur: 12, }, })); @@ -342,23 +262,12 @@ const TopRepositoriesTable: React.FC = ({ formatter: (params: any) => { const data = params[0]; const item = seriesData[data.dataIndex]; - const tierColor = - item.tier === 'Gold' - ? TIER_COLORS.gold - : item.tier === 'Silver' - ? TIER_COLORS.silver - : item.tier === 'Bronze' - ? TIER_COLORS.bronze - : STATUS_COLORS.open; return `
#${item.rank} ${item.repository}
-
- ${item.tier} Tier -
Total Score: ${item.value.toFixed(2)}
Weight: ${item.weight.toFixed(2)}
@@ -539,71 +448,6 @@ const TopRepositoriesTable: React.FC = ({ ); - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.open; - } - }; - - const tierCounts = useMemo( - () => ({ - all: rankedRepositories.length, - gold: rankedRepositories.filter((r) => r.tier === 'Gold').length, - silver: rankedRepositories.filter((r) => r.tier === 'Silver').length, - bronze: rankedRepositories.filter((r) => r.tier === 'Bronze').length, - }), - [rankedRepositories], - ); - - const TierFilterButton = ({ - label, - value, - count, - color, - }: { - label: string; - value: typeof tierFilter; - count: number; - color: string; - }) => ( - - ); - useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; @@ -649,33 +493,6 @@ const TopRepositoriesTable: React.FC = ({ flexWrap: 'wrap', }} > - - - - - - - = ({ {truncateText(repo.repository || '', 40)} - ( border: '1px solid', borderColor: rank === 1 - ? alpha(TIER_COLORS.gold, 0.4) + ? alpha(RANK_COLORS.first, 0.4) : rank === 2 - ? alpha(TIER_COLORS.silver, 0.4) + ? alpha(RANK_COLORS.second, 0.4) : rank === 3 - ? alpha(TIER_COLORS.bronze, 0.4) + ? alpha(RANK_COLORS.third, 0.4) : 'rgba(255, 255, 255, 0.15)', boxShadow: rank === 1 - ? `0 0 12px ${alpha(TIER_COLORS.gold, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.gold, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.first, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.first, 0.2)}` : rank === 2 - ? `0 0 12px ${alpha(TIER_COLORS.silver, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.silver, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.second, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.second, 0.2)}` : rank === 3 - ? `0 0 12px ${alpha(TIER_COLORS.bronze, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.bronze, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.third, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.third, 0.2)}` : 'none', }} > @@ -1172,11 +980,11 @@ const getRankIcon = (rank: number) => ( sx={{ color: rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : rank === 2 - ? TIER_COLORS.silver + ? RANK_COLORS.second : rank === 3 - ? TIER_COLORS.bronze + ? RANK_COLORS.third : 'rgba(255, 255, 255, 0.6)', fontFamily: '"JetBrains Mono", monospace', fontSize: '0.65rem', diff --git a/src/components/leaderboard/index.ts b/src/components/leaderboard/index.ts index 5b621a6..f5d0553 100644 --- a/src/components/leaderboard/index.ts +++ b/src/components/leaderboard/index.ts @@ -8,5 +8,5 @@ export { MinerSection } from './MinerSection'; export { SectionCard } from './SectionCard'; // Types and utilities -export type { MinerStats, SortOption, TierFilter, TierColorSet } from './types'; -export { FONTS, getTierColors, getRankColors } from './types'; +export type { MinerStats, SortOption, RankColorSet } from './types'; +export { FONTS, getRankColors } from './types'; diff --git a/src/components/leaderboard/types.ts b/src/components/leaderboard/types.ts index b65ddfc..d2d376a 100644 --- a/src/components/leaderboard/types.ts +++ b/src/components/leaderboard/types.ts @@ -1,5 +1,4 @@ -import { TIER_COLORS } from '../../theme'; -import { alpha } from '@mui/material'; +import { RANK_COLORS } from '../../theme'; export interface MinerStats { githubId: string; @@ -14,7 +13,7 @@ export interface MinerStats { rank?: number; uniqueReposCount?: number; credibility?: number; - currentTier?: string; + isEligible?: boolean; usdPerDay?: number; totalMergedPrs?: number; totalOpenPrs?: number; @@ -26,51 +25,20 @@ export type SortOption = | 'usdPerDay' | 'totalPRs' | 'credibility'; -export type TierFilter = 'all' | 'Gold' | 'Silver' | 'Bronze'; export const FONTS = { mono: '"JetBrains Mono", monospace', - // Use mono font consistently for data-driven UI } as const; -export interface TierColorSet { +export interface RankColorSet { border: string; text: string; bg: string; } -export const getTierColors = (tier: string | undefined): TierColorSet => { - switch (tier) { - case 'Gold': - return { - border: alpha(TIER_COLORS.gold, 0.5), - text: TIER_COLORS.gold, - bg: alpha(TIER_COLORS.gold, 0.1), - }; - case 'Silver': - return { - border: alpha(TIER_COLORS.silver, 0.5), - text: TIER_COLORS.silver, - bg: alpha(TIER_COLORS.silver, 0.1), - }; - case 'Bronze': - return { - border: alpha(TIER_COLORS.bronze, 0.5), - text: TIER_COLORS.bronze, - bg: alpha(TIER_COLORS.bronze, 0.1), - }; - default: - return { - border: 'rgba(255, 255, 255, 0.15)', - text: 'rgba(255, 255, 255, 0.5)', - bg: 'rgba(255, 255, 255, 0.02)', - }; - } -}; - export const getRankColors = (rank: number) => { - if (rank === 1) return { color: TIER_COLORS.gold, icon: '🥇' }; - if (rank === 2) return { color: TIER_COLORS.silver, icon: '🥈' }; - if (rank === 3) return { color: TIER_COLORS.bronze, icon: '🥉' }; + if (rank === 1) return { color: RANK_COLORS.first, icon: '🥇' }; + if (rank === 2) return { color: RANK_COLORS.second, icon: '🥈' }; + if (rank === 3) return { color: RANK_COLORS.third, icon: '🥉' }; return { color: 'rgba(255, 255, 255, 0.6)', icon: null }; }; diff --git a/src/components/miners/IssueDiscoveryScoreCard.tsx b/src/components/miners/IssueDiscoveryScoreCard.tsx new file mode 100644 index 0000000..9101659 --- /dev/null +++ b/src/components/miners/IssueDiscoveryScoreCard.tsx @@ -0,0 +1,222 @@ +import React from 'react'; +import { + Card, + Typography, + Box, + Grid, + CircularProgress, + Tooltip, + alpha, +} from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { useMinerStats } from '../../api'; +import { CREDIBILITY_COLORS, RISK_COLORS } from '../../theme'; + +const tooltipSlotProps = { + tooltip: { + sx: { + backgroundColor: 'surface.tooltip', + color: 'text.primary', + fontSize: '0.75rem', + fontFamily: '"JetBrains Mono", monospace', + padding: '8px 12px', + borderRadius: '6px', + border: '1px solid', + borderColor: 'border.light', + maxWidth: 260, + }, + }, + arrow: { sx: { color: 'surface.tooltip' } }, +}; + +interface StatTileProps { + label: string; + value: string; + sub?: string; + color?: string; + tooltip?: string; +} + +const StatTile: React.FC = ({ + label, + value, + sub, + color, + tooltip, +}) => ( + + + {tooltip ? ( + + + {label} + + + + ) : ( + {label} + )} + + + {value} + + {sub && ( + alpha(t.palette.text.primary, 0.4), + mt: 0.25, + }} + > + {sub} + + )} + +); + +const credibilityColor = (cred: number) => { + if (cred >= 0.9) return CREDIBILITY_COLORS.excellent; + if (cred >= 0.7) return CREDIBILITY_COLORS.good; + if (cred >= 0.5) return CREDIBILITY_COLORS.moderate; + if (cred >= 0.3) return CREDIBILITY_COLORS.low; + return CREDIBILITY_COLORS.poor; +}; + +interface IssueDiscoveryScoreCardProps { + githubId: string; +} + +const IssueDiscoveryScoreCard: React.FC = ({ + githubId, +}) => { + const { data: minerStats, isLoading, error } = useMinerStats(githubId); + + if (isLoading) { + return ( + + + + ); + } + + if (error || !minerStats) { + return null; + } + + const discoveryScore = Number(minerStats.issueDiscoveryScore) || 0; + const issueTokenScore = Number(minerStats.issueTokenScore) || 0; + const issueCred = Number(minerStats.issueCredibility) || 0; + const solvedIssues = Number(minerStats.totalSolvedIssues) || 0; + const validSolvedIssues = Number(minerStats.totalValidSolvedIssues) || 0; + const closedIssues = Number(minerStats.totalClosedIssues) || 0; + const openIssues = Number(minerStats.totalOpenIssues) || 0; + + // Open issue spam threshold: min(5 + floor(tokenScore / 300), 30) + const openIssueThreshold = Math.min( + 5 + Math.floor(issueTokenScore / 300), + 30, + ); + + const openIssueColor = + openIssues >= openIssueThreshold + ? RISK_COLORS.exceeded + : openIssues >= openIssueThreshold - 1 + ? RISK_COLORS.critical + : openIssues >= openIssueThreshold - 2 + ? RISK_COLORS.approaching + : undefined; + + return ( + + + Issue Discovery + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default IssueDiscoveryScoreCard; diff --git a/src/components/miners/MinerFocusCard.tsx b/src/components/miners/MinerFocusCard.tsx index c6cfb22..9dfece9 100644 --- a/src/components/miners/MinerFocusCard.tsx +++ b/src/components/miners/MinerFocusCard.tsx @@ -1,12 +1,8 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Box, Card, Typography, useTheme } from '@mui/material'; -import { - TrendingUp as TrendingUpIcon, - LockOpen as LockOpenIcon, -} from '@mui/icons-material'; -import { useMinerStats, useTierConfigurations } from '../../api'; -import { TIER_COLORS } from '../../theme'; -import { parseNumber, TIER_LEVELS } from '../../utils/ExplorerUtils'; +import { TrendingUp as TrendingUpIcon } from '@mui/icons-material'; +import { useMinerStats } from '../../api'; +import { parseNumber } from '../../utils/ExplorerUtils'; interface MinerFocusCardProps { githubId: string; @@ -15,146 +11,13 @@ interface MinerFocusCardProps { export const MinerFocusCard: React.FC = ({ githubId }) => { const theme = useTheme(); const { data: minerStats } = useMinerStats(githubId); - const { data: tierData } = useTierConfigurations(); - const focus = useMemo(() => { - if (!minerStats) return null; - const currentTier = (minerStats.currentTier || '').trim(); - const level = TIER_LEVELS[currentTier.toLowerCase()] || 0; - const totalPrs = parseNumber(minerStats.totalPrs); - const isNewMiner = level <= 1 && totalPrs < 15; - const nextTierLevel = level + 1; - const nextTierName = - nextTierLevel === 1 ? 'Bronze' : nextTierLevel === 2 ? 'Silver' : 'Gold'; - const tierConfigs = tierData?.tiers ?? []; - const nextConfig = tierConfigs.find( - (t) => t.name.toLowerCase() === nextTierName.toLowerCase(), - ); - if (isNewMiner && nextConfig && nextTierLevel <= 3) { - const tokenScore = - nextTierLevel === 1 - ? parseNumber(minerStats.bronzeTokenScore) - : nextTierLevel === 2 - ? parseNumber(minerStats.silverTokenScore) - : parseNumber(minerStats.goldTokenScore); - const qualifiedRepos = - nextTierLevel === 1 - ? parseNumber(minerStats.bronzeQualifiedUniqueRepos) - : nextTierLevel === 2 - ? parseNumber(minerStats.silverQualifiedUniqueRepos) - : parseNumber(minerStats.goldQualifiedUniqueRepos); - const credibility = - nextTierLevel === 1 - ? parseNumber(minerStats.bronzeCredibility) - : nextTierLevel === 2 - ? parseNumber(minerStats.silverCredibility) - : parseNumber(minerStats.goldCredibility); - const reqToken = nextConfig.requiredMinTokenScore ?? 0; - const reqRepos = nextConfig.requiredQualifiedUniqueRepos || 3; - const reqCred = nextConfig.requiredCredibility || 0.7; - const tokenPct = - reqToken > 0 ? Math.min((tokenScore / reqToken) * 100, 100) : 100; - const reposPct = Math.min((qualifiedRepos / reqRepos) * 100, 100); - const credPct = Math.min((credibility / reqCred) * 100, 100); - const overallPct = - reqToken > 0 - ? (tokenPct + reposPct + credPct) / 3 - : (reposPct + credPct) / 2; - const steps: string[] = []; - if (tokenPct < 100 && reqToken > 0) - steps.push( - `Reach ${reqToken} token score in ${nextTierName} repos (${Math.round(tokenPct)}% there)`, - ); - if (reposPct < 100) - steps.push( - `Get ${reqRepos} qualified ${nextTierName} repos (${qualifiedRepos}/${reqRepos})`, - ); - if (credPct < 100) - steps.push( - `Reach ${(reqCred * 100).toFixed(0)}% credibility in ${nextTierName} (${(credibility * 100).toFixed(0)}% now)`, - ); - return { - type: 'unlock' as const, - nextTierName, - overallPct: Math.round(overallPct), - steps: steps.slice(0, 2), - color: - nextTierName === 'Gold' - ? TIER_COLORS.gold - : nextTierName === 'Silver' - ? TIER_COLORS.silver - : TIER_COLORS.bronze, - }; - } - return { - type: 'earnings' as const, - usdPerDay: parseNumber(minerStats.usdPerDay), - totalScore: parseNumber(minerStats.totalScore), - currentTier: currentTier || 'Unranked', - }; - }, [minerStats, tierData]); + if (!minerStats) return null; - if (!focus || !minerStats) return null; - - if (focus.type === 'unlock') { - return ( - - - - - - Your next milestone: Unlock {focus.nextTierName} - - - {focus.overallPct === 0 - ? `Focus on the steps below to unlock ${focus.nextTierName}.` - : `You're about ${focus.overallPct}% of the way there. Focus on:`} - - - {focus.steps.map((step, i) => ( -
  • {step}
  • - ))} -
    -
    -
    -
    - ); - } - - const usdDisplay = Number(focus.usdPerDay).toFixed(2); + const usdPerDay = parseNumber(minerStats.usdPerDay); + const totalScore = parseNumber(minerStats.totalScore); + const isEligible = minerStats.isEligible ?? false; + const usdDisplay = Number(usdPerDay).toFixed(2); return ( = ({ githubId }) => { mt: 0.5, }} > - {focus.currentTier} tier · ${usdDisplay}/day est. · Score{' '} - {focus.totalScore.toFixed(2)} + {isEligible ? 'Eligible' : 'Ineligible'} · ${usdDisplay}/day est. · + Score {totalScore.toFixed(2)} diff --git a/src/components/miners/MinerInsightsCard.tsx b/src/components/miners/MinerInsightsCard.tsx index 5a9b018..6666678 100644 --- a/src/components/miners/MinerInsightsCard.tsx +++ b/src/components/miners/MinerInsightsCard.tsx @@ -4,20 +4,16 @@ import { CheckCircle as AchievementIcon, ErrorOutline as WarningIcon, Lightbulb as TipIcon, - TrackChanges as ProgressIcon, } from '@mui/icons-material'; import { useGeneralConfig, useMinerStats, - useTierConfigurations, type MinerEvaluation, type RepositoryPrScoring, - type TierConfig, } from '../../api'; -import { STATUS_COLORS, TIER_COLORS } from '../../theme'; +import { STATUS_COLORS } from '../../theme'; import { calculateDynamicOpenPrThreshold, - getTierLevel, parseNumber, } from '../../utils/ExplorerUtils'; @@ -25,7 +21,7 @@ interface MinerInsightsCardProps { githubId: string; } -type InsightType = 'warning' | 'tip' | 'achievement' | 'progress'; +type InsightType = 'warning' | 'tip' | 'achievement'; interface InsightItem { id: string; @@ -35,18 +31,6 @@ interface InsightItem { priority: number; } -const fieldByTier = ( - tierName: string, - suffix: 'QualifiedUniqueRepos' | 'TokenScore' | 'Credibility', -): keyof MinerEvaluation => - `${tierName.toLowerCase()}${suffix}` as keyof MinerEvaluation; - -const getNextTierName = (tierLevel: number): 'Bronze' | 'Silver' | 'Gold' => { - if (tierLevel <= 0) return 'Bronze'; - if (tierLevel === 1) return 'Silver'; - return 'Gold'; -}; - const getOpenPrInsight = ( minerStats: MinerEvaluation, prScoring: RepositoryPrScoring | undefined, @@ -112,58 +96,19 @@ const getCredibilityInsight = (minerStats: MinerEvaluation): InsightItem => { }; }; -const getTierProgressInsight = ( +const getEligibilityInsight = ( minerStats: MinerEvaluation, - tierConfigList: TierConfig[] | undefined, -): InsightItem => { - const currentTierLevel = getTierLevel(minerStats.currentTier); - - if (currentTierLevel >= 3) { - return { - id: 'tier-maxed', - type: 'achievement', - title: 'Top tier unlocked', - description: - 'You are currently in Gold. Keep credibility and merge consistency high to preserve payout quality.', - priority: 40, - }; - } - - const nextTierName = getNextTierName(currentTierLevel); - const nextTierConfig = tierConfigList?.find( - (tierConfig) => - tierConfig.name.toLowerCase() === nextTierName.toLowerCase(), - ); - - const qualifiedRepos = parseNumber( - minerStats[fieldByTier(nextTierName, 'QualifiedUniqueRepos')], - ); - const tierTokenScore = parseNumber( - minerStats[fieldByTier(nextTierName, 'TokenScore')], - ); - const tierCredibility = parseNumber( - minerStats[fieldByTier(nextTierName, 'Credibility')], - ); - - const requiredRepos = parseNumber( - nextTierConfig?.requiredQualifiedUniqueRepos, - 3, - ); - const requiredToken = parseNumber(nextTierConfig?.requiredMinTokenScore, 0); - const requiredCredibility = parseNumber( - nextTierConfig?.requiredCredibility, - 0.7, - ); +): InsightItem | null => { + const isEligible = minerStats.isEligible ?? false; - const missingRepos = Math.max(requiredRepos - qualifiedRepos, 0); - const missingToken = Math.max(requiredToken - tierTokenScore, 0); - const missingCredibility = Math.max(requiredCredibility - tierCredibility, 0); + if (isEligible) return null; return { - id: `tier-progress-${nextTierName.toLowerCase()}`, - type: 'progress', - title: `Unlock ${nextTierName}`, - description: `Need ${missingRepos} more qualified repo${missingRepos === 1 ? '' : 's'}, ${missingToken.toFixed(2)} token score, and ${(missingCredibility * 100).toFixed(1)}% credibility in ${nextTierName} scope.`, + id: 'eligibility-ineligible', + type: 'warning', + title: 'Not yet eligible', + description: + 'You are currently ineligible for rewards. Improve your credibility, increase your token score, and contribute to more repositories to become eligible.', priority: 90, }; }; @@ -199,13 +144,6 @@ const getInsightStyle = (type: InsightType) => { background: alpha(STATUS_COLORS.success, 0.1), icon: , }; - case 'progress': - return { - color: TIER_COLORS.gold, - border: alpha(TIER_COLORS.gold, 0.35), - background: alpha(TIER_COLORS.gold, 0.1), - icon: , - }; default: return { color: STATUS_COLORS.info, @@ -219,7 +157,6 @@ const getInsightStyle = (type: InsightType) => { const MinerInsightsCard: React.FC = ({ githubId }) => { const { data: minerStats } = useMinerStats(githubId); const { data: generalConfig } = useGeneralConfig(); - const { data: tierConfigData } = useTierConfigurations(); const insights = useMemo(() => { if (!minerStats) return []; @@ -235,11 +172,13 @@ const MinerInsightsCard: React.FC = ({ githubId }) => { const collateralInsight = getCollateralInsight(minerStats); if (collateralInsight) assembled.push(collateralInsight); - assembled.push(getTierProgressInsight(minerStats, tierConfigData?.tiers)); + const eligibilityInsight = getEligibilityInsight(minerStats); + if (eligibilityInsight) assembled.push(eligibilityInsight); + assembled.push(getCredibilityInsight(minerStats)); return assembled.sort((a, b) => b.priority - a.priority).slice(0, 4); - }, [minerStats, generalConfig, tierConfigData]); + }, [minerStats, generalConfig]); if (!minerStats) return null; @@ -272,7 +211,7 @@ const MinerInsightsCard: React.FC = ({ githubId }) => { fontSize: '0.85rem', }} > - Prioritized recommendations based on your tier progress, credibility, + Prioritized recommendations based on your eligibility, credibility, collateral, and open-PR posture. diff --git a/src/components/miners/MinerPRsTable.tsx b/src/components/miners/MinerPRsTable.tsx index 8edd481..bbb1244 100644 --- a/src/components/miners/MinerPRsTable.tsx +++ b/src/components/miners/MinerPRsTable.tsx @@ -25,16 +25,10 @@ import { NavigateBefore as PrevIcon, NavigateNext as NextIcon, } from '@mui/icons-material'; -import { useMinerPRs, useReposAndWeights, type CommitLog } from '../../api'; +import { useMinerPRs, type CommitLog } from '../../api'; import { useNavigate } from 'react-router-dom'; -import { TIER_COLORS } from '../../theme'; import ExplorerFilterButton from './ExplorerFilterButton'; -import { - type MinerTierFilter, - type MinerStatusFilter, - countPrTiers, - filterPrsByTier, -} from '../../utils/ExplorerUtils'; +import { type MinerStatusFilter } from '../../utils/ExplorerUtils'; type PrSortField = 'number' | 'score' | 'lines' | 'date'; type SortDir = 'asc' | 'desc'; @@ -64,48 +58,26 @@ const getScoreTooltip = (pr: CommitLog): string | null => { const parts: string[] = [`Base: ${base.toFixed(2)}`]; if (pr.tokenScore != null) parts.push(`Tokens: ${Number(pr.tokenScore).toFixed(2)}`); - if (pr.rawCredibility != null) - parts.push(`Cred: ${(pr.rawCredibility * 100).toFixed(0)}%`); - if (pr.credibilityScalar != null) - parts.push(`Cred scalar: ${pr.credibilityScalar.toFixed(2)}×`); + if (pr.credibilityMultiplier != null) + parts.push(`Cred: ${Number(pr.credibilityMultiplier).toFixed(2)}×`); return parts.join(' · '); }; interface MinerPRsTableProps { githubId: string; - /** When set externally (e.g. from TierDetailsPage), overrides internal tier filter. */ - tierFilter?: string; } -const MinerPRsTable: React.FC = ({ - githubId, - tierFilter: externalTierFilter, -}) => { +const MinerPRsTable: React.FC = ({ githubId }) => { const theme = useTheme(); const navigate = useNavigate(); const { data: prs, isLoading } = useMinerPRs(githubId); - const { data: repos } = useReposAndWeights(); const [selectedAuthor, setSelectedAuthor] = useState(null); const [statusFilter, setStatusFilter] = useState('all'); - const [internalTierFilter, setTierFilter] = useState('all'); - const tierFilter: MinerTierFilter = - (externalTierFilter?.toLowerCase() as MinerTierFilter) || - internalTierFilter; const [searchQuery, setSearchQuery] = useState(''); const [sortField, setSortField] = useState('date'); const [sortDir, setSortDir] = useState('desc'); const [page, setPage] = useState(0); - const repoTiers = useMemo(() => { - const map = new Map(); - if (Array.isArray(repos)) { - repos.forEach((repo) => { - if (repo?.fullName) map.set(repo.fullName, repo.tier || ''); - }); - } - return map; - }, [repos]); - const handleSort = useCallback( (field: PrSortField) => { if (sortField === field) { @@ -138,7 +110,6 @@ const MinerPRsTable: React.FC = ({ (pr) => pr.prState === 'CLOSED' && !pr.mergedAt, ); } - filtered = filterPrsByTier(filtered, tierFilter, repoTiers); if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); filtered = filtered.filter( @@ -149,7 +120,7 @@ const MinerPRsTable: React.FC = ({ ); } return filtered; - }, [prs, selectedAuthor, statusFilter, tierFilter, repoTiers, searchQuery]); + }, [prs, selectedAuthor, statusFilter, searchQuery]); const sortedPRs = useMemo(() => { const sorted = [...filteredPRs]; @@ -201,11 +172,6 @@ const MinerPRsTable: React.FC = ({ }; }, [prs]); - const tierCounts = useMemo(() => { - if (!prs) return { all: 0, gold: 0, silver: 0, bronze: 0 }; - return countPrTiers(prs, repoTiers); - }, [prs, repoTiers]); - if (isLoading) { return ( @@ -260,10 +226,7 @@ const MinerPRsTable: React.FC = ({ }} > ({filteredPRs.length} - {selectedAuthor || - statusFilter !== 'all' || - tierFilter !== 'all' || - searchQuery.trim() + {selectedAuthor || statusFilter !== 'all' || searchQuery.trim() ? ` of ${prs?.length || 0}` : ''} ) @@ -330,50 +293,6 @@ const MinerPRsTable: React.FC = ({ }} /> - - {/* Tier Filter Buttons */} - - { - setTierFilter('all'); - setPage(0); - }} - /> - { - setTierFilter('gold'); - setPage(0); - }} - /> - { - setTierFilter('silver'); - setPage(0); - }} - /> - { - setTierFilter('bronze'); - setPage(0); - }} - /> - diff --git a/src/components/miners/MinerRepositoriesTable.tsx b/src/components/miners/MinerRepositoriesTable.tsx index 301554c..b61e2c1 100644 --- a/src/components/miners/MinerRepositoriesTable.tsx +++ b/src/components/miners/MinerRepositoriesTable.tsx @@ -17,14 +17,8 @@ import { useTheme, } from '@mui/material'; import { Search as SearchIcon } from '@mui/icons-material'; -import { - useMinerPRs, - useReposAndWeights, - useTierConfigurations, -} from '../../api'; +import { useMinerPRs, useReposAndWeights } from '../../api'; import { useNavigate } from 'react-router-dom'; -import { TIER_COLORS } from '../../theme'; -import ExplorerFilterButton from './ExplorerFilterButton'; import SortableHeaderCell from './SortableHeaderCell'; import RankBadge from './RankBadge'; import EmptyStateMessage from './EmptyStateMessage'; @@ -36,51 +30,32 @@ import { tableContainerSx, } from './MinerRepositoriesTable.styles'; import { - type MinerTierFilter, - type QualificationFilter, type RepoSortField, type SortOrder, type RepoStats, buildRepoWeightsMap, - buildRepoTiersMap, - buildTierThresholdsMap, aggregatePRsByRepository, - filterMinerRepoStats, - filterByQualification, filterBySearch, sortMinerRepoStats, - computeTierCounts, - computeQualificationCounts, hasActiveFilters, getDisplayCount, - tierColorFor, } from '../../utils/ExplorerUtils'; interface MinerRepositoriesTableProps { githubId: string; - /** When set externally (e.g. from TierDetailsPage), overrides internal tier filter. */ - tierFilter?: string; } const PAGE_SIZE = 20; const MinerRepositoriesTable: React.FC = ({ githubId, - tierFilter: externalTierFilter, }) => { const theme = useTheme(); const navigate = useNavigate(); const { data: prs, isLoading: isLoadingPRs } = useMinerPRs(githubId); const { data: repos, isLoading: isLoadingRepos } = useReposAndWeights(); - const { data: tierConfig } = useTierConfigurations(); const [sortField, setSortField] = useState('score'); const [sortOrder, setSortOrder] = useState('desc'); - const [internalTierFilter, setTierFilter] = useState('all'); - const tierFilter: MinerTierFilter = - (externalTierFilter?.toLowerCase() as MinerTierFilter) || - internalTierFilter; - const [qualificationFilter, setQualificationFilter] = - useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(0); @@ -89,29 +64,17 @@ const MinerRepositoriesTable: React.FC = ({ // Build lookup maps from API data const repoWeights = useMemo(() => buildRepoWeightsMap(repos), [repos]); - const repoTiers = useMemo(() => buildRepoTiersMap(repos), [repos]); - const tierThresholds = useMemo( - () => buildTierThresholdsMap(tierConfig), - [tierConfig], - ); // Aggregate PRs by repository const repoStats = useMemo( - () => aggregatePRsByRepository(prs || [], repoWeights, repoTiers), - [prs, repoWeights, repoTiers], + () => aggregatePRsByRepository(prs || [], repoWeights), + [prs, repoWeights], ); // Filter and sort repository stats const filteredRepoStats = useMemo(() => { - let filtered = filterMinerRepoStats(repoStats, tierFilter); - filtered = filterByQualification( - filtered, - qualificationFilter, - tierThresholds, - ); - filtered = filterBySearch(filtered, searchQuery); - return filtered; - }, [repoStats, tierFilter, qualificationFilter, tierThresholds, searchQuery]); + return filterBySearch(repoStats, searchQuery); + }, [repoStats, searchQuery]); const sortedRepoStats = useMemo( () => sortMinerRepoStats(filteredRepoStats, sortField, sortOrder), @@ -125,13 +88,6 @@ const MinerRepositoriesTable: React.FC = ({ const totalPages = Math.ceil(sortedRepoStats.length / PAGE_SIZE); - const tierCounts = useMemo(() => computeTierCounts(repoStats), [repoStats]); - - const qualificationCounts = useMemo( - () => computeQualificationCounts(repoStats, tierThresholds), - [repoStats, tierThresholds], - ); - const handleSort = (field: RepoSortField) => { if (sortField === field) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); @@ -143,11 +99,7 @@ const MinerRepositoriesTable: React.FC = ({ const resetPage = () => setPage(0); - const isFiltered = hasActiveFilters( - tierFilter, - qualificationFilter, - searchQuery, - ); + const isFiltered = hasActiveFilters(searchQuery); const displayCount = getDisplayCount( sortedRepoStats.length, repoStats.length, @@ -227,94 +179,6 @@ const MinerRepositoriesTable: React.FC = ({ ({displayCount}) - - {/* Qualification Filter Buttons */} - - { - setQualificationFilter('all'); - resetPage(); - }} - /> - { - setQualificationFilter('qualified'); - resetPage(); - }} - /> - { - setQualificationFilter('unqualified'); - resetPage(); - }} - /> - - - {/* Tier Filter Buttons */} - - { - setTierFilter('all'); - resetPage(); - }} - /> - { - setTierFilter('gold'); - resetPage(); - }} - /> - { - setTierFilter('silver'); - resetPage(); - }} - /> - { - setTierFilter('bronze'); - resetPage(); - }} - /> - - {/* Search */} @@ -522,18 +386,6 @@ const RepoTableRow: React.FC = ({ backgroundColor: avatarBgColor, }} /> - {repo.tier && ( - - )} { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.neutral; - } -}; - interface MultiplierPillProps { label: string; value: number; @@ -133,15 +115,10 @@ const MultiplierPill: React.FC = ({ interface PrScoreRowProps { pr: CommitLog; - repoTier: string; onNavigateToPr: (repo: string, prNumber: number) => void; } -const PrScoreRow: React.FC = ({ - pr, - repoTier, - onNavigateToPr, -}) => { +const PrScoreRow: React.FC = ({ pr, onNavigateToPr }) => { const [expanded, setExpanded] = useState(false); // Fetch full PR details (with all multipliers) — cached by React Query @@ -219,7 +196,7 @@ const PrScoreRow: React.FC = ({ sx={{ fontFamily: '"JetBrains Mono", monospace', fontSize: '0.62rem', - color: tierColor(repoTier), + color: (t) => alpha(t.palette.text.primary, 0.5), }} > {repoName} @@ -311,13 +288,8 @@ const PrScoreRow: React.FC = ({ {Number(prDetails.credibilityMultiplier).toFixed(4)}× - Raw credibility:{' '} - {( - Number( - prDetails.rawCredibility ?? pr.rawCredibility ?? 0, - ) * 100 - ).toFixed(1)} - % + Based on your PR success rate, scaled to reward + consistency. } @@ -334,7 +306,7 @@ const PrScoreRow: React.FC = ({ {Number(prDetails.repoWeightMultiplier).toFixed(4)}× - Based on repository tier and activity. + Based on repository weight and activity. } @@ -356,26 +328,6 @@ const PrScoreRow: React.FC = ({ } /> )} - {prDetails?.repositoryUniquenessMultiplier != null && ( - - - Uniqueness{' '} - {Number( - prDetails.repositoryUniquenessMultiplier, - ).toFixed(4)} - × - - - Rewards contributing to diverse repos. - - - } - /> - )} {prDetails?.timeDecayMultiplier != null && ( = ({ }) => { const navigate = useNavigate(); const { data: prs, isLoading } = useMinerPRs(githubId); - const { data: repos } = useReposAndWeights(); const [page, setPage] = useState(0); const PAGE_SIZE = 10; @@ -571,16 +522,6 @@ const MinerScoreBreakdown: React.FC = ({ navigate(`/miners/pr?repo=${encodeURIComponent(repo)}&number=${prNumber}`); }; - const repoTierMap = useMemo(() => { - const map = new Map(); - if (Array.isArray(repos)) { - repos.forEach((r) => { - if (r?.fullName) map.set(r.fullName, r.tier || ''); - }); - } - return map; - }, [repos]); - const sortedPrs = useMemo(() => { if (!prs) return []; return [...prs].sort( @@ -588,28 +529,6 @@ const MinerScoreBreakdown: React.FC = ({ ); }, [prs]); - const tierDistribution = useMemo(() => { - if (!prs) return { bronze: 0, silver: 0, gold: 0, total: 0 }; - let bronze = 0; - let silver = 0; - let gold = 0; - let total = 0; - prs.forEach((pr) => { - if (!pr.mergedAt) return; - const score = parseFloat(pr.score || '0'); - total += score; - const tier = ( - pr.tier || - repoTierMap.get(pr.repository) || - '' - ).toLowerCase(); - if (tier === 'gold') gold += score; - else if (tier === 'silver') silver += score; - else bronze += score; - }); - return { bronze, silver, gold, total }; - }, [prs, repoTierMap]); - if (isLoading || !prs || prs.length === 0) return null; const totalPages = Math.ceil(sortedPrs.length / PAGE_SIZE); @@ -651,95 +570,12 @@ const MinerScoreBreakdown: React.FC = ({ - {/* Tier score distribution bar */} - {tierDistribution.total > 0 && ( - - - {tierDistribution.gold > 0 && ( - - Gold: {tierDistribution.gold.toFixed(2)} - - )} - {tierDistribution.silver > 0 && ( - - Silver: {tierDistribution.silver.toFixed(2)} - - )} - {tierDistribution.bronze > 0 && ( - - Bronze: {tierDistribution.bronze.toFixed(2)} - - )} - - - {tierDistribution.gold > 0 && ( - - )} - {tierDistribution.silver > 0 && ( - - )} - {tierDistribution.bronze > 0 && ( - - )} - - - )} - {/* PR list */} {displayPrs.map((pr, i) => ( ))} diff --git a/src/components/miners/MinerScoreCard.tsx b/src/components/miners/MinerScoreCard.tsx index 5d48b99..11a6bc8 100644 --- a/src/components/miners/MinerScoreCard.tsx +++ b/src/components/miners/MinerScoreCard.tsx @@ -28,7 +28,7 @@ import { useGeneralConfig, } from '../../api'; import { - TIER_COLORS, + RANK_COLORS, STATUS_COLORS, CREDIBILITY_COLORS, RISK_COLORS, @@ -54,19 +54,6 @@ const formatTimeAgo = (date: Date): string => { return `${diffDays} days ago`; }; -const tierColor = (tier: string | undefined) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.neutral; - } -}; - const credibilityColor = (cred: number) => { if (cred >= 0.9) return CREDIBILITY_COLORS.excellent; if (cred >= 0.7) return CREDIBILITY_COLORS.good; @@ -168,10 +155,10 @@ const StatTile: React.FC = ({ rank <= 3 ? alpha( rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : rank === 2 - ? TIER_COLORS.silver - : TIER_COLORS.bronze, + ? RANK_COLORS.second + : RANK_COLORS.third, 0.4, ) : 'border.light', @@ -185,11 +172,11 @@ const StatTile: React.FC = ({ fontWeight: 600, color: rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : rank === 2 - ? TIER_COLORS.silver + ? RANK_COLORS.second : rank === 3 - ? TIER_COLORS.bronze + ? RANK_COLORS.third : (t) => alpha(t.palette.text.primary, 0.6), }} > @@ -292,7 +279,14 @@ const MinerScoreCard: React.FC = ({ githubId }) => { const cred = parseNumber(minerStats.credibility); const openPrs = parseNumber(minerStats.totalOpenPrs); const collateral = parseNumber(minerStats.totalCollateralScore); - const tColor = tierColor(minerStats.currentTier); + const isEligible = minerStats.isEligible ?? false; + const isIssueEligible = minerStats.isIssueEligible ?? false; + const eligibilityColor = isEligible + ? STATUS_COLORS.success + : STATUS_COLORS.neutral; + const issueEligibilityColor = isIssueEligible + ? STATUS_COLORS.success + : STATUS_COLORS.neutral; return ( @@ -352,18 +346,52 @@ const MinerScoreCard: React.FC = ({ githubId }) => { > {githubData?.name || username} - + + + + + + = ({ githubId }) => { : 'No collateral' } color={openPrColor(openPrs, openPrThreshold)} - tooltip={`Open PRs have collateral deducted from score. Exceeding ${openPrThreshold} triggers a full penalty. Threshold scales with token score (+1 per 500).`} + tooltip={`Open PRs have collateral deducted from score. Exceeding ${openPrThreshold} triggers a full penalty. Threshold scales with token score (+1 per 300).`} /> diff --git a/src/components/miners/MinerTierPerformance.tsx b/src/components/miners/MinerTierPerformance.tsx deleted file mode 100644 index 91031f8..0000000 --- a/src/components/miners/MinerTierPerformance.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import React from 'react'; -import { - Box, - Typography, - Grid, - Card, - CircularProgress, - alpha, -} from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { - useMinerStats, - useTierConfigurations, - type TierConfig, -} from '../../api'; -import { TierCard } from './TierComponents'; -import { TIER_COLORS } from '../../theme'; - -const TIER_LEVELS: Record = { - bronze: 1, - silver: 2, - gold: 3, -}; - -const getTierLevel = (tier: string | undefined | null): number => { - if (!tier) return 0; // No tier yet - working towards bronze - return TIER_LEVELS[tier.toLowerCase()] || 0; -}; - -const getTierConfig = ( - tierName: string, - tierConfigs: TierConfig[] | undefined, -): TierConfig | undefined => - tierConfigs?.find((t) => t.name.toLowerCase() === tierName.toLowerCase()); - -const getPreviousTierName = (level: number): string => { - const tierNames = ['', 'Bronze', 'Silver', 'Gold']; - return tierNames[level - 1] || ''; -}; - -const getTooltipMessage = ( - tierName: string, - tierLevel: number, - isNextTier: boolean, - config: TierConfig | undefined, -): string => { - if (!config) { - if (isNextTier) { - return `${tierName} tier unlock in progress. Continue contributing to ${tierName} tier repos to unlock this tier.`; - } - const prevTier = getPreviousTierName(tierLevel); - return `${tierName} tier isn't unlocked yet, so contributions earn 0 points. It will only be eligible to unlock after ${prevTier} is unlocked.`; - } - - const reqQualifiedRepos = config.requiredQualifiedUniqueRepos; - const reqTokenScorePerRepo = config.requiredMinTokenScorePerRepo; - const reqCred = (config.requiredCredibility * 100).toFixed(0); - const reqTokenScore = config.requiredMinTokenScore; - - if (isNextTier) { - const tokenScoreReq = reqTokenScore - ? ` with ${reqTokenScore}+ total token score and` - : ''; - return `${tierName} tier unlock in progress. Requires${tokenScoreReq} ${reqQualifiedRepos} qualified repos (each with ${reqTokenScorePerRepo}+ token score) and ${reqCred}%+ credibility.`; - } - - const prevTier = getPreviousTierName(tierLevel); - return `${tierName} tier isn't unlocked yet, so contributions earn 0 points. It will only be eligible to unlock after ${prevTier} is unlocked.`; -}; - -interface MinerTierPerformanceProps { - githubId: string; -} - -const MinerTierPerformance: React.FC = ({ - githubId, -}) => { - const navigate = useNavigate(); - const { data: minerStats, isLoading, error } = useMinerStats(githubId); - const { data: tierConfigData } = useTierConfigurations(); - - const handleTierClick = (tierName: string) => { - navigate( - `/miners/tier-details?githubId=${encodeURIComponent(githubId)}&tier=${encodeURIComponent(tierName)}`, - { state: { backLabel: 'Back to Miner' } }, - ); - }; - - if (isLoading) { - return ( - - - - ); - } - - if (error || !minerStats) { - return null; // Don't show anything if no data - } - - const tierConfigs = tierConfigData?.tiers; - const currentTierLevel = getTierLevel(minerStats.currentTier); - - const tiers = [ - { - name: 'Bronze', - level: 1, - color: TIER_COLORS.bronze, - bgColor: alpha(TIER_COLORS.bronze, 0.05), - borderColor: alpha(TIER_COLORS.bronze, 0.2), - stats: { - score: minerStats.bronzeScore, - credibility: minerStats.bronzeCredibility, - merged: minerStats.bronzeMergedPrs, - closed: minerStats.bronzeClosedPrs, - total: minerStats.bronzeTotalPrs, - collateral: minerStats.bronzeCollateralScore, - uniqueRepos: minerStats.bronzeUniqueRepos, - qualifiedUniqueRepos: minerStats.bronzeQualifiedUniqueRepos, - tokenScore: minerStats.bronzeTokenScore, - }, - }, - { - name: 'Silver', - level: 2, - color: TIER_COLORS.silver, - bgColor: alpha(TIER_COLORS.silver, 0.05), - borderColor: alpha(TIER_COLORS.silver, 0.2), - stats: { - score: minerStats.silverScore, - credibility: minerStats.silverCredibility, - merged: minerStats.silverMergedPrs, - closed: minerStats.silverClosedPrs, - total: minerStats.silverTotalPrs, - collateral: minerStats.silverCollateralScore, - uniqueRepos: minerStats.silverUniqueRepos, - qualifiedUniqueRepos: minerStats.silverQualifiedUniqueRepos, - tokenScore: minerStats.silverTokenScore, - }, - }, - { - name: 'Gold', - level: 3, - color: TIER_COLORS.gold, - bgColor: alpha(TIER_COLORS.gold, 0.05), - borderColor: alpha(TIER_COLORS.gold, 0.2), - stats: { - score: minerStats.goldScore, - credibility: minerStats.goldCredibility, - merged: minerStats.goldMergedPrs, - closed: minerStats.goldClosedPrs, - total: minerStats.goldTotalPrs, - collateral: minerStats.goldCollateralScore, - uniqueRepos: minerStats.goldUniqueRepos, - qualifiedUniqueRepos: minerStats.goldQualifiedUniqueRepos, - tokenScore: minerStats.goldTokenScore, - }, - }, - ]; - - return ( - - - Tier Performance - - - - {tiers.map((tier) => { - const isLocked = tier.level > currentTierLevel; - const isNextTier = tier.level === currentTierLevel + 1; - const config = getTierConfig(tier.name, tierConfigs); - - // Calculate progress towards unlocking this tier - const tokenScore = tier.stats.tokenScore || 0; - const qualifiedReposCount = tier.stats.qualifiedUniqueRepos || 0; - const credibility = tier.stats.credibility || 0; - const requiredTokenScore = config?.requiredMinTokenScore ?? null; - const requiredQualifiedRepos = - config?.requiredQualifiedUniqueRepos || 3; - const requiredCredibility = config?.requiredCredibility || 0.7; - - const tokenScoreProgress = requiredTokenScore - ? Math.min((tokenScore / requiredTokenScore) * 100, 100) - : 100; - const qualifiedReposProgress = Math.min( - (qualifiedReposCount / requiredQualifiedRepos) * 100, - 100, - ); - const credibilityProgress = Math.min( - (credibility / requiredCredibility) * 100, - 100, - ); - - const unlockProgress = - (isNextTier || !isLocked) && config - ? { - tokenScore, - requiredTokenScore, - tokenScoreProgress, - qualifiedReposCount, - requiredQualifiedRepos, - qualifiedReposProgress, - credibility, - requiredCredibility, - credibilityProgress, - } - : undefined; - - return ( - - handleTierClick(tier.name)} - sx={{ - cursor: 'pointer', - height: '100%', - '&:hover': { opacity: 0.95 }, - }} - > - - - - ); - })} - - - ); -}; - -export default MinerTierPerformance; diff --git a/src/components/miners/RankBadge.tsx b/src/components/miners/RankBadge.tsx index 62b1d8b..b0b97cf 100644 --- a/src/components/miners/RankBadge.tsx +++ b/src/components/miners/RankBadge.tsx @@ -1,45 +1,45 @@ import React from 'react'; import { Box, Typography, alpha, useTheme } from '@mui/material'; -import { TIER_COLORS } from '../../theme'; +import { RANK_COLORS } from '../../theme'; interface RankBadgeProps { rank: number; displayNumber: number; } -const getRankTierColor = (rank: number): string | null => { +const getRankPodiumColor = (rank: number): string | null => { switch (rank) { case 0: - return TIER_COLORS.gold; + return RANK_COLORS.first; case 1: - return TIER_COLORS.silver; + return RANK_COLORS.second; case 2: - return TIER_COLORS.bronze; + return RANK_COLORS.third; default: return null; } }; const getRankBorderColor = (rank: number, fallbackColor: string): string => { - const tierColor = getRankTierColor(rank); - if (tierColor) { - return alpha(tierColor, 0.4); + const rankColor = getRankPodiumColor(rank); + if (rankColor) { + return alpha(rankColor, 0.4); } return fallbackColor; }; const getRankBoxShadow = (rank: number): string => { - const tierColor = getRankTierColor(rank); - if (tierColor) { - return `0 0 12px ${alpha(tierColor, 0.4)}, 0 0 4px ${alpha(tierColor, 0.2)}`; + const rankColor = getRankPodiumColor(rank); + if (rankColor) { + return `0 0 12px ${alpha(rankColor, 0.4)}, 0 0 4px ${alpha(rankColor, 0.2)}`; } return 'none'; }; const getRankTextColor = (rank: number, fallbackColor: string): string => { - const tierColor = getRankTierColor(rank); - if (tierColor) { - return tierColor; + const rankColor = getRankPodiumColor(rank); + if (rankColor) { + return rankColor; } return fallbackColor; }; diff --git a/src/components/miners/TierComponents.tsx b/src/components/miners/TierComponents.tsx deleted file mode 100644 index 1514dee..0000000 --- a/src/components/miners/TierComponents.tsx +++ /dev/null @@ -1,522 +0,0 @@ -import React from 'react'; -import { - Box, - Typography, - Tooltip, - type TooltipProps, - LinearProgress, - Stack, -} from '@mui/material'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { STATUS_COLORS } from '../../theme'; - -// Shared tooltip styling -const tooltipSlotProps: TooltipProps['slotProps'] = { - tooltip: { - sx: { - backgroundColor: 'surface.tooltip', - color: 'text.primary', - fontSize: '0.75rem', - fontFamily: '"JetBrains Mono", monospace', - padding: '8px 12px', - borderRadius: '6px', - border: '1px solid', - borderColor: 'border.light', - maxWidth: 240, - }, - }, - arrow: { - sx: { - color: 'surface.tooltip', - }, - }, -}; - -const largeTooltipSlotProps: TooltipProps['slotProps'] = { - tooltip: { - sx: { - backgroundColor: 'surface.tooltip', - color: 'text.primary', - fontSize: '0.8rem', - fontFamily: '"JetBrains Mono", monospace', - padding: '10px 14px', - borderRadius: '8px', - border: '1px solid', - borderColor: 'border.light', - maxWidth: 280, - }, - }, - arrow: { - sx: { - color: 'surface.tooltip', - }, - }, -}; - -// StyledTooltip component -interface StyledTooltipProps { - title: string; - children: React.ReactElement; - large?: boolean; - placement?: TooltipProps['placement']; -} - -export const StyledTooltip: React.FC = ({ - title, - children, - large = false, - placement = 'top', -}) => ( - - {children} - -); - -// TierStatItem component -interface TierStatItemProps { - label: string; - value: string; - tooltip?: string; - valueColor?: string; - large?: boolean; -} - -export const TierStatItem: React.FC = ({ - label, - value, - tooltip, - valueColor = 'text.primary', - large = false, -}) => { - const labelContent = ( - t.palette.text.secondary, - fontSize: '0.7rem', - fontFamily: '"JetBrains Mono", monospace', - textTransform: 'uppercase', - display: 'flex', - alignItems: 'center', - gap: 0.5, - cursor: tooltip ? 'pointer' : 'default', - }} - > - {label} - {tooltip && } - - ); - - return ( - - {tooltip ? ( - {labelContent} - ) : ( - labelContent - )} - - {value} - - - ); -}; - -// TierProgressBar component -interface TierProgressBarProps { - label: string; - current: number | string; - required: number | string; - progress: number; - tierColor: string; -} - -export const TierProgressBar: React.FC = ({ - label, - current, - required, - progress, - tierColor, -}) => { - const isComplete = progress >= 100; - - return ( - - - - {label} - - - {current}{' '} - t.palette.text.secondary }}> - (Req: {required}) - - - - - - ); -}; - -// TierPRActivity component -interface TierPRActivityProps { - merged: number; - opened: number; - closed: number; - borderColor: string; -} - -export const TierPRActivity: React.FC = ({ - merged, - opened, - closed, - borderColor, -}) => ( - - t.palette.text.secondary, - fontSize: '0.7rem', - fontFamily: '"JetBrains Mono", monospace', - mb: 0.5, - }} - > - PR Activity - - - - Merged: {merged} - - - Open: {opened} - - - Closed: {closed} - - - -); - -// TierUnlockProgress component -interface TierUnlockProgressProps { - tokenScore: number; - requiredTokenScore: number | null; - tokenScoreProgress: number; - qualifiedReposCount: number; - requiredQualifiedRepos: number; - qualifiedReposProgress: number; - credibility: number; - requiredCredibility: number; - credibilityProgress: number; - tierColor: string; - borderColor: string; - title?: string; -} - -export const TierUnlockProgress: React.FC = ({ - tokenScore, - requiredTokenScore, - tokenScoreProgress, - qualifiedReposCount, - requiredQualifiedRepos, - qualifiedReposProgress, - credibility, - requiredCredibility, - credibilityProgress, - tierColor, - borderColor, - title = 'Unlock Progress', -}) => ( - - t.palette.text.secondary, - fontSize: '0.7rem', - fontFamily: '"JetBrains Mono", monospace', - mb: 1, - textTransform: 'uppercase', - }} - > - {title} - - - {requiredTokenScore !== null && ( - - )} - - - - - -); - -// TierCard component -interface TierStats { - score?: number; - credibility?: number; - merged?: number; - closed?: number; - total?: number; - collateral?: number; - uniqueRepos?: number; - qualifiedUniqueRepos?: number; - tokenScore?: number; -} - -interface TierCardProps { - name: string; - color: string; - bgColor: string; - borderColor: string; - stats: TierStats; - isLocked: boolean; - isNextTier: boolean; - tooltipMessage?: string; - unlockProgress?: { - tokenScore: number; - requiredTokenScore: number | null; - tokenScoreProgress: number; - qualifiedReposCount: number; - requiredQualifiedRepos: number; - qualifiedReposProgress: number; - credibility: number; - requiredCredibility: number; - credibilityProgress: number; - }; -} - -export const TierCard: React.FC = ({ - name, - color, - bgColor, - borderColor, - stats, - isLocked, - isNextTier, - tooltipMessage, - unlockProgress, -}) => { - const opened = (stats.total || 0) - (stats.merged || 0) - (stats.closed || 0); - - const getFilterStyles = () => { - if (!isLocked) return { opacity: 1, filter: 'none' }; - if (isNextTier) return { opacity: 0.85, filter: 'grayscale(35%)' }; - return { opacity: 0.4, filter: 'grayscale(85%)' }; - }; - - const getHoverStyles = () => { - if (!isLocked) return {}; - if (isNextTier) return { opacity: 0.95, filter: 'grayscale(15%)' }; - return { opacity: 0.5, filter: 'grayscale(70%)' }; - }; - - const getBorderStyles = () => { - if (!isLocked) { - return { - border: `1.5px solid ${color}`, - boxShadow: `0 0 12px ${color}40, inset 0 0 8px ${color}15`, - }; - } - return { - border: `1px solid ${borderColor}`, - }; - }; - - const filterStyles = getFilterStyles(); - - const cardContent = ( - - {isLocked && ( - t.palette.text.secondary, - }} - > - - - )} - - {name} Tier - - - - - - - = 0.7 - ? STATUS_COLORS.success - : undefined - } - /> - - - - - - 0 ? opened : 0} - closed={stats.closed || 0} - borderColor={borderColor} - /> - - {unlockProgress && ( - - )} - - - ); - - if (isLocked && tooltipMessage) { - return ( - - {cardContent} - - ); - } - - return cardContent; -}; diff --git a/src/components/miners/index.ts b/src/components/miners/index.ts index dc74acb..3709a8e 100644 --- a/src/components/miners/index.ts +++ b/src/components/miners/index.ts @@ -1,5 +1,6 @@ export { default as CredibilityChart } from './CredibilityChart'; export { default as EmptyStateMessage } from './EmptyStateMessage'; +export { default as IssueDiscoveryScoreCard } from './IssueDiscoveryScoreCard'; export { default as ExplorerFilterButton } from './ExplorerFilterButton'; export { default as MinerActivity } from './MinerActivity'; export { default as MinerFocusCard } from './MinerFocusCard'; @@ -8,7 +9,6 @@ export { default as MinerPRsTable } from './MinerPRsTable'; export { default as MinerRepositoriesTable } from './MinerRepositoriesTable'; export { default as MinerScoreBreakdown } from './MinerScoreBreakdown'; export { default as MinerScoreCard } from './MinerScoreCard'; -export { default as MinerTierPerformance } from './MinerTierPerformance'; export { default as PerformanceRadar } from './PerformanceRadar'; export { default as RankBadge } from './RankBadge'; export { default as SortableHeaderCell } from './SortableHeaderCell'; diff --git a/src/components/onboard/GettingStarted.tsx b/src/components/onboard/GettingStarted.tsx index f84fe7e..cd64734 100644 --- a/src/components/onboard/GettingStarted.tsx +++ b/src/components/onboard/GettingStarted.tsx @@ -1,164 +1,596 @@ -import React from 'react'; -import { Box, Typography, Stack, Button } from '@mui/material'; +import React, { useState } from 'react'; +import { Box, Typography, Stack, Button, Tabs, Tab } from '@mui/material'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -export const GettingStarted: React.FC = () => ( - - - Miner Onboarding Process - - - - {/* Connecting Line (Desktop) */} +const MONO = '"JetBrains Mono", monospace'; + +const steps = [ + { + step: 1, + title: 'Create Wallet', + subtitle: 'Coldkey & Hotkey', + }, + { + step: 2, + title: 'Register', + subtitle: 'To Subnet', + }, + { + step: 3, + title: 'Create PAT', + subtitle: 'GitHub Token', + }, + { + step: 4, + title: 'Install CLI', + subtitle: 'gittensor tools', + }, + { + step: 5, + title: 'Broadcast', + subtitle: 'PAT to Validators', + }, + { + step: 6, + title: 'Verify', + subtitle: 'Check Status', + }, + { + step: 7, + title: 'Contribute', + subtitle: 'Earn Rewards', + active: true, + }, +]; + +const CodeBlock: React.FC<{ + children: string; + label?: string; +}> = ({ children, label }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(children.trim()); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + {label && ( + + {label} + + )} - - - {[ - { step: 1, title: 'Get Keys', subtitle: 'Coldkey & Hotkey' }, - { step: 2, title: 'Register', subtitle: 'To Subnet' }, - { step: 3, title: 'Authorize', subtitle: 'Create GitHub PAT' }, - { step: 4, title: 'Deploy', subtitle: 'Setup Miner' }, - { - step: 5, - title: 'Earn', - subtitle: 'Contribute & Get Paid', - active: true, - }, - ].map((item, index) => ( + + {children.trim()} + + + + + + + ); +}; + +const StepDetail: React.FC<{ step: number }> = ({ step }) => { + const [network, setNetwork] = useState<'mainnet' | 'testnet'>('mainnet'); + + switch (step) { + case 1: + return ( + + + Create a Bittensor wallet with a coldkey and hotkey. See the{' '} + + official Bittensor docs + {' '} + for creating or importing wallets. + + + ); + + case 2: + return ( + + + Register your hotkey to the Gittensor subnet. + + + {network === 'mainnet' ? ( + {`btcli subnet register --netuid 74 \\ + --wallet-name \\ + --hotkey `} + ) : ( + {`btcli subnet register --netuid 422 \\ + --wallet-name \\ + --hotkey \\ + --network test`} + )} + + ); + + case 3: + return ( + + + Create a fine-grained personal access token in GitHub: + + +
  • + Go to Settings →{' '} + Developer settings →{' '} + Personal access tokens →{' '} + Fine-grained tokens +
  • +
  • + Click Generate new token +
  • +
  • + Set Token name to gittensor,{' '} + Expiration to No Expiration, and{' '} + Repository access to{' '} + Public repositories (read-only) +
  • +
  • + Click Generate token and copy it +
  • +
    + + + Some GitHub organizations forbid fine-grained PATs with indefinite + lifetime. If so, create a PAT with an expiration and rotate it + periodically. + + +
    + ); + + case 4: + return ( + + + Install the Gittensor CLI tool. + + {`pip install uv +git clone git@github.com:entrius/gittensor.git +cd gittensor +uv venv && source .venv/bin/activate +uv pip install -e .`} + + ); + + case 5: + return ( + + + Broadcast your GitHub PAT to validators so they can score your + contributions. + + + {network === 'mainnet' ? ( + {`gitt miner post --pat \\ + --wallet \\ + --hotkey \\ + --netuid 74`} + ) : ( + {`gitt miner post --pat \\ + --wallet \\ + --hotkey \\ + --netuid 422 --network test`} + )} + + If you omit --pat, the CLI checks the GITTENSOR_MINER_PAT + environment variable, then prompts interactively. + + + ); + + case 6: + return ( + + + Confirm that validators received and validated your PAT. + + + {network === 'mainnet' ? ( + {`gitt miner check \\ + --wallet \\ + --hotkey \\ + --netuid 74`} + ) : ( + {`gitt miner check \\ + --wallet \\ + --hotkey \\ + --netuid 422 --network test`} + )} + + You should see a table showing which validators have your PAT stored + and whether it's valid. + + + ); + + case 7: + return ( + + + You're all set! Open PRs to recognized repositories and your scores + are calculated when PRs are merged. No miner process needs to be + running — the validator scoring round runs every 2 hours. + +
  • + Browse recognized repositories in the{' '} + Repositories tab +
  • +
  • + Eligibility requires 5 merged PRs with token score ≥ 5, 75% + credibility, and a 180-day-old GitHub account +
  • +
  • + See the Scoring tab for how rewards are + calculated +
  • +
    +
    + ); + + default: + return null; + } +}; + +const NetworkTabs: React.FC<{ + network: 'mainnet' | 'testnet'; + onChange: (v: 'mainnet' | 'testnet') => void; +}> = ({ network, onChange }) => ( + onChange(v)} + sx={{ + minHeight: 'auto', + mb: 2, + '& .MuiTab-root': { + minHeight: 'auto', + py: 0.5, + px: 2, + fontSize: '0.75rem', + fontFamily: MONO, + textTransform: 'none', + color: 'rgba(255,255,255,0.4)', + '&.Mui-selected': { color: '#fff' }, + }, + '& .MuiTabs-indicator': { backgroundColor: 'primary.main', height: 2 }, + }} + > + + + +); + +export const GettingStarted: React.FC = () => { + const [activeStep, setActiveStep] = useState(0); + + return ( + + + Miner Setup + + + {/* Step indicators */} + + {/* Connecting Line (Desktop) */} + + + + {steps.map((item, index) => ( setActiveStep(index)} sx={{ - width: 50, - height: 50, - borderRadius: '50%', - bgcolor: '#0b0b0b', // Darker background for contrast - border: '2px solid', - borderColor: item.active - ? 'secondary.main' - : 'rgba(255,255,255,0.1)', - color: item.active - ? 'secondary.main' - : 'rgba(255, 255, 255, 0.5)', display: 'flex', + flexDirection: { xs: 'row', md: 'column' }, alignItems: 'center', - justifyContent: 'center', - fontWeight: 'bold', - fontSize: '1.25rem', - boxShadow: item.active - ? '0 0 20px rgba(255, 215, 0, 0.15)' - : 'none', - transition: 'all 0.3s ease', - flexShrink: 0, + gap: { xs: 1.5, md: 1 }, + width: { xs: '100%', md: 'auto' }, + cursor: 'pointer', + '&:hover .step-circle': { + borderColor: 'rgba(255,255,255,0.3)', + }, }} > - {item.step} - - - - {item.title} - - - {item.subtitle} - + {item.step} + + + + {item.title} + + + {item.subtitle} + + - - ))} - - + ))} + +
    - - - Ready to Deploy? - - - Follow our comprehensive documentation to set up your environment and - start mining today. - - + + Full Documentation + + + For advanced configuration and troubleshooting, see the complete miner + guide. + + +
    -
    -); + ); +}; diff --git a/src/components/onboard/RoadmapContent.tsx b/src/components/onboard/RoadmapContent.tsx deleted file mode 100644 index 1234cb4..0000000 --- a/src/components/onboard/RoadmapContent.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React from 'react'; -import { Box, Stack, Typography, useTheme, useMediaQuery } from '@mui/material'; -import { - CurrencyExchange, - Speed, - SmartToy, - AutoAwesome, -} from '@mui/icons-material'; - -interface RoadmapItemProps { - title: string; - timeframe: string; - description: string; - icon: React.ReactNode; - isLast?: boolean; - index: number; -} - -const RoadmapItem: React.FC = ({ - title, - timeframe, - description, - icon, - isLast, - index, -}) => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const isEven = index % 2 === 0; - - return ( - - {/* Date/Timeframe - Desktop Only */} - {!isMobile && ( - - - {timeframe} - - - )} - - {/* Central Line & Dot */} - - - {icon} - - {!isLast && ( - - )} - - - {/* Content Card */} - - - {isMobile && ( - - {timeframe} - - )} - - {title} - - - {description} - - - - - ); -}; - -export const RoadmapContent: React.FC = () => { - const roadmapItems = [ - { - title: 'Issue Bounty Marketplace', - timeframe: 'Phase 1', - icon: , - description: - 'Users will be able to attach bounties to any GitHub issue through a secure smart contract interface. The platform will collect a small fee from each bounty, establishing a durable and scalable revenue model.', - }, - { - title: 'Custom Benchmark Suite', - timeframe: 'Phase 2', - icon: , - description: - 'Repository owners and organizations can upload proprietary benchmarks or evaluation criteria. Miners compete to optimize for any measurable objective, including accuracy, speed, cost efficiency, and reliability.', - }, - { - title: 'Code Review Agent', - timeframe: 'Phase 3', - icon: , - description: - 'A fully autonomous review system trained on hundreds of thousands of real merged and closed pull requests. The agent will evaluate contributions, make acceptance recommendations, and enable continuous improvement loops.', - }, - { - title: 'End to End Autonomy', - timeframe: 'Future', - icon: , - description: - 'The system will run itself: issues → autonomous PRs → autonomous review and merge → continuous self-improvement of real-world codebases.', - }, - ]; - - return ( - - - - {roadmapItems.map((item, index) => ( - - ))} - - - {/* Vision Section */} - - {/* Decorative Elements */} - - - - The Vision - - - - - We are creating the first permissionless marketplace for software - development, ushering in a new economic paradigm where any entity - can access a global talent pool. In this true meritocracy, - software development becomes a commodity, judged solely by value - and quality, abstracting away the distinction between human - developers and AI agents. - - - - By creating an open marketplace for coding tasks, we are - redefining how software is built. This decentralized approach - leverages collective intelligence to solve complex problems, - offering a scalable and community-owned foundation for the future - of software development. - - - - We are turning the concept of "autonomous agents" into operational - reality. Gittensor is not just a tool, it is a self-improving - ecosystem designed to maintain and evolve real-world applications - at a global scale. - - - - - - ); -}; diff --git a/src/components/onboard/Scoring.tsx b/src/components/onboard/Scoring.tsx index 5c65c8b..0238b9d 100644 --- a/src/components/onboard/Scoring.tsx +++ b/src/components/onboard/Scoring.tsx @@ -33,7 +33,7 @@ export const Scoring: React.FC = () => ( }, { title: 'Credibility', - desc: 'Keep your merge rate high. A strong ratio of merged vs. closed PRs increases your credibility and unlocks tier multipliers.', + desc: 'Keep your merge rate high. A strong ratio of merged vs. closed PRs increases your credibility. You need at least 75% credibility to become eligible for rewards.', }, ].map((item, index) => ( diff --git a/src/components/prs/PRComments.tsx b/src/components/prs/PRComments.tsx index 8e8a594..8812682 100644 --- a/src/components/prs/PRComments.tsx +++ b/src/components/prs/PRComments.tsx @@ -8,6 +8,9 @@ import { CircularProgress, Chip, } from '@mui/material'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; import { usePullRequestComments } from '../../api'; import { type PullRequestDetails } from '../../api/models/Dashboard'; import { STATUS_COLORS } from '../../theme'; @@ -56,7 +59,7 @@ const PRComments: React.FC = ({ avatarUrl: `https://avatars.githubusercontent.com/${prDetails.authorLogin}`, htmlUrl: `https://github.com/${prDetails.authorLogin}`, }, - body: prDetails.description || '*No description provided.*', + body: prDetails.description || 'No description provided.', createdAt: prDetails.createdAt, authorAssociation: 'OWNER', isDescription: true, @@ -94,7 +97,7 @@ const PRComments: React.FC = ({ flexDirection: 'column', gap: 3, pt: 2, - maxWidth: '960px', // Widen slightly for better code block readability + maxWidth: '960px', mx: 'auto', position: 'relative', }} @@ -278,15 +281,29 @@ const PRComments: React.FC = ({ }, '& h1': { fontSize: '2em' }, '& h2': { fontSize: '1.5em' }, + '& h3': { fontSize: '1.25em' }, + '& p': { mb: 2, mt: 0 }, '& a': { color: colors.accent.fg, textDecoration: 'none' }, '& a:hover': { textDecoration: 'underline' }, + '& ul, & ol': { + pl: '2em', + mb: 2, + listStyleType: 'disc', + }, + '& ol': { listStyleType: 'decimal' }, + '& li': { mb: 0.5 }, + '& li + li': { mt: '0.25em' }, '& blockquote': { padding: '0 1em', color: colors.fg.muted, borderLeft: `0.25em solid ${colors.border.default}`, my: 2, + mx: 0, + }, + '& input[type="checkbox"]': { + mr: 0.5, + verticalAlign: 'middle', }, - // Updated Code Block Styling '& code': { padding: '0.2em 0.4em', margin: 0, @@ -298,8 +315,36 @@ const PRComments: React.FC = ({ '& pre': { mt: 2, mb: 2, + p: 2, borderRadius: '6px', - overflow: 'hidden', // Let SyntaxHighlighter handle scroll + overflow: 'auto', + backgroundColor: '#161b22', + border: `1px solid ${colors.border.default}`, + '& code': { + backgroundColor: 'transparent', + p: 0, + fontSize: '100%', + }, + }, + '& table': { + borderCollapse: 'collapse', + width: '100%', + mb: 2, + overflowX: 'auto', + }, + '& th, & td': { + border: `1px solid ${colors.border.default}`, + padding: '6px 13px', + }, + '& th': { fontWeight: 600 }, + '& tr:nth-of-type(2n)': { + backgroundColor: '#161b22', + }, + '& hr': { + height: '0.25em', + my: 3, + backgroundColor: colors.border.default, + border: 0, }, '& img': { maxWidth: '100%', @@ -313,21 +358,16 @@ const PRComments: React.FC = ({ fontSize: '14px', lineHeight: 1.6, }, - '& .markdown-body pre': { - backgroundColor: '#161b22', // Distinct code block background - border: `1px solid ${colors.border.default}`, - borderRadius: '6px', - }, - '& .markdown-body code': { - fontFamily: '"JetBrains Mono", monospace', - }, }} > -
    +
    + + {item.body} + +
    diff --git a/src/components/prs/PRDetailsCard.tsx b/src/components/prs/PRDetailsCard.tsx index cc59c6c..176ffb0 100644 --- a/src/components/prs/PRDetailsCard.tsx +++ b/src/components/prs/PRDetailsCard.tsx @@ -6,14 +6,13 @@ import { CircularProgress, Avatar, Grid, - Chip, alpha, Tooltip, } from '@mui/material'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { usePullRequestDetails } from '../../api'; import { useNavigate } from 'react-router-dom'; -import theme, { TIER_COLORS, STATUS_COLORS } from '../../theme'; +import theme, { RANK_COLORS, STATUS_COLORS } from '../../theme'; interface PRDetailsCardProps { repository: string; @@ -73,19 +72,6 @@ const PRDetailsCard: React.FC = ({ const [owner] = repository.split('/'); - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.open; - } - }; - const isOpenPR = prDetails.prState === 'OPEN'; // Score/Collateral is now shown in header, so only show other stats here @@ -126,8 +112,6 @@ const PRDetailsCard: React.FC = ({ label: string; value: string; isCredibility?: boolean; - rawCredibility?: number; - credibilityScalar?: number; }> = isOpenPR ? [ { @@ -156,8 +140,6 @@ const PRDetailsCard: React.FC = ({ label: 'Credibility', value: `${parseFloat(prDetails.credibilityMultiplier ?? '0').toFixed(2)}x`, isCredibility: true, - rawCredibility: prDetails.rawCredibility, - credibilityScalar: prDetails.credibilityScalar, }, { label: 'Review Quality', @@ -167,10 +149,6 @@ const PRDetailsCard: React.FC = ({ label: 'Time Decay', value: `${parseFloat(prDetails.timeDecayMultiplier ?? '0').toFixed(2)}x`, }, - { - label: 'Repo Unique', - value: `${parseFloat(prDetails.repositoryUniquenessMultiplier ?? '0').toFixed(2)}x`, - }, ]; return ( @@ -299,16 +277,6 @@ const PRDetailsCard: React.FC = ({ > {repository} - {prDetails.tier && ( - - )} @@ -364,19 +332,19 @@ const PRDetailsCard: React.FC = ({ border: '1px solid', borderColor: item.rank === 1 - ? alpha(TIER_COLORS.gold, 0.4) + ? alpha(RANK_COLORS.first, 0.4) : item.rank === 2 - ? alpha(TIER_COLORS.silver, 0.4) + ? alpha(RANK_COLORS.second, 0.4) : item.rank === 3 - ? alpha(TIER_COLORS.bronze, 0.4) + ? alpha(RANK_COLORS.third, 0.4) : 'rgba(255, 255, 255, 0.15)', boxShadow: item.rank === 1 - ? `0 0 12px ${alpha(TIER_COLORS.gold, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.gold, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.first, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.first, 0.2)}` : item.rank === 2 - ? `0 0 12px ${alpha(TIER_COLORS.silver, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.silver, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.second, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.second, 0.2)}` : item.rank === 3 - ? `0 0 12px ${alpha(TIER_COLORS.bronze, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.bronze, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.third, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.third, 0.2)}` : 'none', }} > @@ -385,11 +353,11 @@ const PRDetailsCard: React.FC = ({ sx={{ color: item.rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : item.rank === 2 - ? TIER_COLORS.silver + ? RANK_COLORS.second : item.rank === 3 - ? TIER_COLORS.bronze + ? RANK_COLORS.third : 'rgba(255, 255, 255, 0.6)', fontFamily: '"JetBrains Mono", monospace', fontSize: '0.6rem', @@ -477,14 +445,6 @@ const PRDetailsCard: React.FC = ({ {multipliers.map((item, index) => { const isCredibilityItem = item.isCredibility === true; - const rawCred: number | undefined = - typeof item.rawCredibility === 'number' - ? item.rawCredibility - : undefined; - const scalar: number | undefined = - typeof item.credibilityScalar === 'number' - ? item.credibilityScalar - : undefined; const content = ( = ({ > {item.value} - {isCredibilityItem && rawCred !== undefined && ( - - from {(rawCred * 100).toFixed(1)}% - - )} ); @@ -551,56 +500,15 @@ const PRDetailsCard: React.FC = ({ > Credibility Multiplier - {rawCred !== undefined && scalar !== undefined ? ( - <> - - Your raw credibility ({(rawCred * 100).toFixed(1)} - %) is your PR success rate: merged PRs ÷ (merged + - closed) - - - It's then scaled using the tier's scalar ({scalar} - x) to reward consistency: - - - {(rawCred * 100).toFixed(1)}%{scalar} ={' '} - {(Math.pow(rawCred, scalar) * 100).toFixed(0)}% →{' '} - {item.value} - - - ) : ( - - This multiplier is based on your PR success rate - (raw credibility), exponentially scaled by the - tier's scalar to reward consistency. - - )} + + This multiplier is based on your PR success rate, + scaled to reward consistency. + } arrow diff --git a/src/components/prs/PRHeader.tsx b/src/components/prs/PRHeader.tsx index 3451463..710092d 100644 --- a/src/components/prs/PRHeader.tsx +++ b/src/components/prs/PRHeader.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Box, Typography, Avatar, Chip, Tooltip, alpha } from '@mui/material'; +import { Box, Typography, Avatar, Tooltip, alpha } from '@mui/material'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { useNavigate } from 'react-router-dom'; import { formatUsdEstimate } from '../../utils'; -import theme, { TIER_COLORS, STATUS_COLORS } from '../../theme'; +import theme, { STATUS_COLORS } from '../../theme'; interface PRHeaderProps { repository: string; pullRequestNumber: number; @@ -18,19 +18,6 @@ const PRHeader: React.FC = ({ const navigate = useNavigate(); const [owner] = repository.split('/'); - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.open; - } - }; - const isOpenPR = prDetails.prState === 'OPEN'; const isClosed = prDetails.prState === 'CLOSED'; const collateralScore = parseFloat(prDetails.collateralScore || '0'); @@ -148,16 +135,6 @@ const PRHeader: React.FC = ({ > {repository} - {prDetails.tier && ( - - )} diff --git a/src/components/repositories/RepositoryContributorsTable.tsx b/src/components/repositories/RepositoryContributorsTable.tsx index 6e68f25..5934f32 100644 --- a/src/components/repositories/RepositoryContributorsTable.tsx +++ b/src/components/repositories/RepositoryContributorsTable.tsx @@ -20,9 +20,9 @@ const RepositoryContributorsTable: React.FC< // State for how many items to show. Minimum 7. const [visibleCount, setVisibleCount] = useState(7); - // Build githubId -> miner rank/tier map + // Build githubId -> miner rank/eligibility map const minerDataMap = useMemo(() => { - const map = new Map(); + const map = new Map(); if (Array.isArray(allMinersStats)) { const sorted = [...allMinersStats].sort( (a, b) => Number(b.totalScore) - Number(a.totalScore), @@ -30,7 +30,7 @@ const RepositoryContributorsTable: React.FC< sorted.forEach((miner, index) => { map.set(miner.githubId, { rank: index + 1, - tier: miner.currentTier, + isEligible: miner.isEligible, }); }); } @@ -167,7 +167,7 @@ const RepositoryContributorsTable: React.FC< {displayedContributors.map((contributor, index) => { const minerData = minerDataMap.get(contributor.githubId); const minerRank = minerData?.rank; - const isInactive = !minerData?.tier; + const isInactive = !minerData?.isEligible; return ( = ({ // Fetch ALL PRs at once to enable client-side filtering and accurate counts // This avoids server roundtrips on filter change and provides instant UI feedback const { data: allMinerPRs, isLoading } = useAllPrs(); - const { data: allMinersStats } = useAllMiners(); - - // Create miner tier map for quick lookup - const minerTierMap = useMemo(() => { - const map = new Map(); - if (allMinersStats) { - allMinersStats.forEach((miner) => { - if (miner.githubId && miner.currentTier) { - map.set(miner.githubId, miner.currentTier); - } - }); - } - return map; - }, [allMinersStats]); const allPRs = useMemo(() => { if (!allMinerPRs) return []; @@ -355,10 +341,6 @@ const RepositoryPRsTable: React.FC = ({ display: 'flex', alignItems: 'center', gap: 1, - opacity: - pr.githubId && minerTierMap.has(pr.githubId) - ? 1 - : 0.5, }} > = ({ border: '1px solid', borderColor: item.rank === 1 - ? alpha(TIER_COLORS.gold, 0.4) + ? alpha(RANK_COLORS.first, 0.4) : item.rank === 2 - ? alpha(TIER_COLORS.silver, 0.4) + ? alpha(RANK_COLORS.second, 0.4) : item.rank === 3 - ? alpha(TIER_COLORS.bronze, 0.4) + ? alpha(RANK_COLORS.third, 0.4) : 'rgba(255, 255, 255, 0.15)', boxShadow: item.rank === 1 - ? `0 0 12px ${alpha(TIER_COLORS.gold, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.gold, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.first, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.first, 0.2)}` : item.rank === 2 - ? `0 0 12px ${alpha(TIER_COLORS.silver, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.silver, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.second, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.second, 0.2)}` : item.rank === 3 - ? `0 0 12px ${alpha(TIER_COLORS.bronze, 0.4)}, 0 0 4px ${alpha(TIER_COLORS.bronze, 0.2)}` + ? `0 0 12px ${alpha(RANK_COLORS.third, 0.4)}, 0 0 4px ${alpha(RANK_COLORS.third, 0.2)}` : 'none', }} > @@ -333,11 +333,11 @@ const RepositoryScoreCard: React.FC = ({ sx={{ color: item.rank === 1 - ? TIER_COLORS.gold + ? RANK_COLORS.first : item.rank === 2 - ? TIER_COLORS.silver + ? RANK_COLORS.second : item.rank === 3 - ? TIER_COLORS.bronze + ? RANK_COLORS.third : 'rgba(255, 255, 255, 0.6)', fontFamily: '"JetBrains Mono", monospace', fontSize: '0.6rem', diff --git a/src/components/repositories/RepositoryStats.tsx b/src/components/repositories/RepositoryStats.tsx index 80dbdea..15e92e0 100644 --- a/src/components/repositories/RepositoryStats.tsx +++ b/src/components/repositories/RepositoryStats.tsx @@ -1,12 +1,13 @@ import React, { useMemo } from 'react'; -import { Box, Typography, Skeleton, Divider } from '@mui/material'; +import { Box, Typography, Skeleton, Divider, Chip } from '@mui/material'; import { useReposAndWeights, useAllPrs, useRepositoryIssues, useRepoBountySummary, + useRepositoryConfig, } from '../../api'; -import { TIER_COLORS, STATUS_COLORS } from '../../theme'; +import { RANK_COLORS, STATUS_COLORS } from '../../theme'; interface RepositoryStatsProps { repositoryFullName: string; @@ -20,6 +21,7 @@ const RepositoryStats: React.FC = ({ const { data: issues, isLoading: isLoadingIssues } = useRepositoryIssues(repositoryFullName); const { data: bountySummary } = useRepoBountySummary(repositoryFullName); + const { data: repoConfig } = useRepositoryConfig(repositoryFullName); const repository = useMemo( () => @@ -79,19 +81,6 @@ const RepositoryStats: React.FC = ({ return null; } - const getTierColor = (tier: string) => { - switch (tier) { - case 'Gold': - return TIER_COLORS.gold; - case 'Silver': - return TIER_COLORS.silver; - case 'Bronze': - return TIER_COLORS.bronze; - default: - return STATUS_COLORS.open; - } - }; - return ( = ({ - {/* Tier */} - - - Tier - - - {repository.tier} - - - {/* Total Score */} @@ -252,14 +214,14 @@ const RepositoryStats: React.FC = ({ > Bounties = ({ )} )} + + {/* Additional Acceptable Branches */} + {repoConfig?.additionalAcceptableBranches && + repoConfig.additionalAcceptableBranches.length > 0 && ( + <> + + + Scorable Branches + + + {repoConfig.additionalAcceptableBranches.map((branch) => ( + + ))} + + + )} ); diff --git a/src/components/repositories/RepositoryWeightsTable.tsx b/src/components/repositories/RepositoryWeightsTable.tsx index 36d51a2..fab1026 100644 --- a/src/components/repositories/RepositoryWeightsTable.tsx +++ b/src/components/repositories/RepositoryWeightsTable.tsx @@ -22,8 +22,6 @@ import { MenuItem, FormControl, Avatar, - Chip, - Button, IconButton, Collapse, } from '@mui/material'; @@ -32,48 +30,19 @@ import BarChartIcon from '@mui/icons-material/BarChart'; import TableChartIcon from '@mui/icons-material/TableChart'; import ReactECharts from 'echarts-for-react'; import { useReposAndWeights } from '../../api'; -import { TIER_COLORS, STATUS_COLORS } from '../../theme'; import dayjs from 'dayjs'; -type SortField = 'owner' | 'name' | 'weight' | 'tier'; +type SortField = 'owner' | 'name' | 'weight'; type SortOrder = 'asc' | 'desc'; const baseGithubUrl = 'https://github.com/'; -const getTierColor = (tier: string): string => { - switch (tier?.toLowerCase()) { - case 'gold': - return TIER_COLORS.gold; - case 'silver': - return TIER_COLORS.silver; - case 'bronze': - return TIER_COLORS.bronze; - default: - return 'rgba(255, 255, 255, 0.4)'; - } -}; - -const getTierOrder = (tier: string): number => { - switch (tier?.toLowerCase()) { - case 'gold': - return 3; - case 'silver': - return 2; - case 'bronze': - return 1; - default: - return 0; - } -}; - const AnimatedWeightBar = ({ weight, maxWeight, - tier, }: { weight: number; maxWeight: number; - tier: string; }) => { const [width, setWidth] = useState(0); @@ -99,13 +68,7 @@ const AnimatedWeightBar = ({ width: `${width}%`, height: '100%', background: - tier?.toLowerCase() === 'gold' - ? 'linear-gradient(90deg, rgba(255, 215, 0, 0.9), rgba(255, 200, 0, 0.5))' - : tier?.toLowerCase() === 'silver' - ? 'linear-gradient(90deg, rgba(192, 192, 192, 0.9), rgba(170, 170, 170, 0.5))' - : tier?.toLowerCase() === 'bronze' - ? 'linear-gradient(90deg, rgba(205, 127, 50, 0.9), rgba(184, 115, 51, 0.5))' - : 'linear-gradient(90deg, rgba(139, 148, 158, 0.8), rgba(100, 108, 118, 0.4))', + 'linear-gradient(90deg, rgba(139, 148, 158, 0.8), rgba(100, 108, 118, 0.4))', borderRadius: '2px', transition: 'width 1s cubic-bezier(0.4, 0, 0.2, 1)', }} @@ -119,9 +82,6 @@ const RepositoryWeightsTable: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [sortField, setSortField] = useState('weight'); const [sortOrder, setSortOrder] = useState('desc'); - const [tierFilter, setTierFilter] = useState< - 'all' | 'gold' | 'silver' | 'bronze' - >('all'); const [showChart, setShowChart] = useState(false); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); @@ -165,14 +125,10 @@ const RepositoryWeightsTable: React.FC = () => { const filtered = reposWithParts.filter((repo) => { const searchLower = searchQuery.toLowerCase(); - const matchesSearch = + return ( repo.owner.toLowerCase().includes(searchLower) || - repo.name.toLowerCase().includes(searchLower); - - const matchesTier = - tierFilter === 'all' || repo.tier?.toLowerCase() === tierFilter; - - return matchesSearch && matchesTier; + repo.name.toLowerCase().includes(searchLower) + ); }); filtered.sort((a, b) => { @@ -185,10 +141,6 @@ const RepositoryWeightsTable: React.FC = () => { } else if (sortField === 'name') { aValue = a.name; bValue = b.name; - } else if (sortField === 'tier') { - const aOrder = getTierOrder(a.tier); - const bOrder = getTierOrder(b.tier); - return sortOrder === 'asc' ? aOrder - bOrder : bOrder - aOrder; } else { aValue = a.weight; bValue = b.weight; @@ -212,7 +164,7 @@ const RepositoryWeightsTable: React.FC = () => { }); return filtered; - }, [data, searchQuery, sortField, sortOrder, tierFilter]); + }, [data, searchQuery, sortField, sortOrder]); const maxWeight = useMemo(() => { if (filteredAndSortedRepos.length === 0) return 1; @@ -222,62 +174,9 @@ const RepositoryWeightsTable: React.FC = () => { return weights.length > 0 ? Math.max(...weights) : 1; }, [filteredAndSortedRepos]); - const tierCounts = useMemo(() => { - if (!data) return { all: 0, gold: 0, silver: 0, bronze: 0 }; - return { - all: data.length, - gold: data.filter((r) => r.tier?.toLowerCase() === 'gold').length, - silver: data.filter((r) => r.tier?.toLowerCase() === 'silver').length, - bronze: data.filter((r) => r.tier?.toLowerCase() === 'bronze').length, - }; - }, [data]); - - const TierFilterButton = ({ - label, - value, - count, - color, - }: { - label: string; - value: typeof tierFilter; - count: number; - color: string; - }) => ( - - ); - const getChartOption = () => { - // Show all data to visualize the full spread const chartData = filteredAndSortedRepos; - // Calculate min/max from the entire filtered set (the tier) to scale Y-axis correctly const weights = filteredAndSortedRepos .map((r) => parseFloat(r.weight as string)) .filter((w) => !isNaN(w)); @@ -287,59 +186,6 @@ const RepositoryWeightsTable: React.FC = () => { const textColor = 'rgba(255, 255, 255, 0.85)'; const gridColor = 'rgba(255, 255, 255, 0.08)'; - const getTierColorGradient = (tier: string) => { - switch (tier?.toLowerCase()) { - case 'gold': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(255, 215, 0, 0.9)' }, - { offset: 1, color: 'rgba(255, 200, 0, 0.5)' }, - ], - }; - case 'silver': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(192, 192, 192, 0.9)' }, - { offset: 1, color: 'rgba(170, 170, 170, 0.5)' }, - ], - }; - case 'bronze': - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(205, 127, 50, 0.9)' }, - { offset: 1, color: 'rgba(184, 115, 51, 0.5)' }, - ], - }; - default: - return { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(139, 148, 158, 0.8)' }, - { offset: 1, color: 'rgba(100, 108, 118, 0.4)' }, - ], - }; - } - }; - const xAxisData = chartData.map((item) => item.name); const seriesData = chartData.map((item) => ({ @@ -347,9 +193,18 @@ const RepositoryWeightsTable: React.FC = () => { name: item.name, fullName: item.fullName, owner: item.owner, - tier: item.tier, itemStyle: { - color: getTierColorGradient(item.tier), + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(139, 148, 158, 0.8)' }, + { offset: 1, color: 'rgba(100, 108, 118, 0.4)' }, + ], + }, borderRadius: [4, 4, 0, 0], }, })); @@ -386,14 +241,13 @@ const RepositoryWeightsTable: React.FC = () => { return `
    -
    ${data.fullName}
    Weight: ${data.value}
    -
    Tier: ${data.tier || 'N/A'}
    `; }, @@ -477,39 +331,12 @@ const RepositoryWeightsTable: React.FC = () => { px: 2, pb: 2, display: 'flex', - justifyContent: 'space-between', + justifyContent: 'flex-end', alignItems: 'center', gap: 2, flexWrap: 'wrap', }} > - - - - - - - { > {showChart && filteredAndSortedRepos.length > 0 && ( { height: '56px', py: 1.5, boxSizing: 'border-box', - width: '20%', + width: '25%', }} > { height: '56px', py: 1.5, boxSizing: 'border-box', - width: '40%', + width: '50%', }} > { Repository - - handleSort('tier')} - sx={{ - '&:hover': { - color: 'secondary.main', - }, - '&.Mui-active': { - color: 'secondary.main', - }, - }} - > - Tier - - { height: '56px', py: 1.5, boxSizing: 'border-box', - width: '15%', + width: '25%', }} > { { sx={{ display: 'flex', flexDirection: 'column', - alignItems: 'center', + alignItems: 'flex-end', gap: 1, - width: '100%', }} > - + > + {repo.weight} + {!isMobile && ( )} - - - {repo.weight} - - ); diff --git a/src/pages/DiscoveriesPage.tsx b/src/pages/DiscoveriesPage.tsx new file mode 100644 index 0000000..7d83f07 --- /dev/null +++ b/src/pages/DiscoveriesPage.tsx @@ -0,0 +1,151 @@ +import React, { useMemo } from 'react'; +import { useMediaQuery, Box, Typography, alpha } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { Page } from '../components/layout'; +import { TopMinersTable, LeaderboardSidebar, SEO } from '../components'; +import { useAllMiners } from '../api'; +import theme from '../theme'; + +const DiscoveriesPage: React.FC = () => { + const navigate = useNavigate(); + + const allMinerStatsQuery = useAllMiners(); + const allMinersStats = allMinerStatsQuery?.data; + const isLoadingMinerStats = allMinerStatsQuery?.isLoading; + + const handleSelectMiner = (githubId: string) => { + navigate(`/discoveries/details?githubId=${githubId}`, { + state: { backLabel: 'Back to Discoveries' }, + }); + }; + + // Process miner stats for TopMinersTable, using issue discovery fields + const minerStats = useMemo(() => { + if (!Array.isArray(allMinersStats)) return []; + return allMinersStats.map((stat) => ({ + githubId: stat.githubId || '', + author: stat.githubUsername || undefined, + totalScore: Number(stat.issueDiscoveryScore) || 0, + baseTotalScore: Number(stat.baseTotalScore) || 0, + totalPRs: + (Number(stat.totalSolvedIssues) || 0) + + (Number(stat.totalClosedIssues) || 0), + linesChanged: Number(stat.totalNodesScored) || 0, + linesAdded: Number(stat.totalAdditions) || 0, + linesDeleted: Number(stat.totalDeletions) || 0, + hotkey: stat.hotkey || 'N/A', + uniqueReposCount: Number(stat.uniqueReposCount) || 0, + credibility: Number(stat.issueCredibility) || 0, + isEligible: stat.isIssueEligible ?? false, + usdPerDay: Number(stat.usdPerDay) || 0, + // Issue counts mapped to PR status fields + totalMergedPrs: Number(stat.totalSolvedIssues) || 0, + totalOpenPrs: Number(stat.totalOpenIssues) || 0, + totalClosedPrs: Number(stat.totalClosedIssues) || 0, + })); + }, [allMinersStats]); + + // Sort miners by issue discovery score + const sortedMinerStats = useMemo( + () => [...minerStats].sort((a, b) => b.totalScore - a.totalScore), + [minerStats], + ); + + // Dashboard-like responsive logic + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const isTablet = useMediaQuery(theme.breakpoints.between('sm', 'md')); + const isLargeScreen = useMediaQuery(theme.breakpoints.up('xl')); + const showSidebarRight = useMediaQuery(theme.breakpoints.up('xl')); + + // Dynamic sidebar width based on screen size (matching DashboardPage) + const sidebarWidth = + isMobile || isTablet ? '100%' : isLargeScreen ? '340px' : '300px'; + + return ( + + + + {/* Main Content Area */} + + alpha(t.palette.text.primary, 0.5), + lineHeight: 1.6, + }} + > + Miners earn discovery rewards by filing quality issues that others + solve via merged PRs. Rewarded separately from OSS contributions. + + + + + + + {/* Right Sidebar */} + + + + + + ); +}; + +export default DiscoveriesPage; diff --git a/src/pages/DiscoveryMinerDetailsPage.tsx b/src/pages/DiscoveryMinerDetailsPage.tsx new file mode 100644 index 0000000..650a312 --- /dev/null +++ b/src/pages/DiscoveryMinerDetailsPage.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Navigate, useSearchParams } from 'react-router-dom'; +import { Box, Tab, Tabs } from '@mui/material'; +import { Page } from '../components/layout'; +import { + BackButton, + MinerActivity, + MinerInsightsCard, + MinerPRsTable, + MinerRepositoriesTable, + MinerScoreBreakdown, + MinerScoreCard, + IssueDiscoveryScoreCard, + SEO, +} from '../components'; + +const TAB_NAMES = [ + 'overview', + 'activity', + 'pull-requests', + 'repositories', +] as const; +type MinerDetailsTab = (typeof TAB_NAMES)[number]; + +const DiscoveryMinerDetailsPage: React.FC = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const githubId = searchParams.get('githubId'); + + const tabParam = searchParams.get('tab'); + const activeTab: MinerDetailsTab = + tabParam && TAB_NAMES.includes(tabParam as MinerDetailsTab) + ? (tabParam as MinerDetailsTab) + : 'overview'; + + const handleTabChange = ( + _event: React.SyntheticEvent, + newValue: MinerDetailsTab, + ) => { + const newParams = new URLSearchParams(searchParams); + newParams.set('tab', newValue); + setSearchParams(newParams); + }; + + if (!githubId) { + return ; + } + + return ( + + + + + + + + + + + t.palette.text.secondary, + fontFamily: '"JetBrains Mono", monospace', + textTransform: 'none', + fontSize: '0.83rem', + fontWeight: 500, + '&.Mui-selected': { + color: 'primary.main', + }, + }, + }} + > + + + + + + + + + {activeTab === 'overview' && ( + <> + + + + )} + + {activeTab === 'activity' && } + {activeTab === 'pull-requests' && ( + + )} + {activeTab === 'repositories' && ( + + )} + + + + + ); +}; + +export default DiscoveryMinerDetailsPage; diff --git a/src/pages/FAQPage.tsx b/src/pages/FAQPage.tsx index 950a163..5ddcf75 100644 --- a/src/pages/FAQPage.tsx +++ b/src/pages/FAQPage.tsx @@ -128,10 +128,9 @@ export const FAQContent: React.FC = () => ( answer={ <> You must contribute to an incentivized repository listed in our - master list. Repositories are organized into three tiers (Bronze, - Silver, Gold), and you must unlock each tier by meeting specific - merged PR count and credibility requirements to earn rewards from - it. Check the{' '} + master list. To become eligible for rewards, you need at least 5 + merged PRs with a token score of 5 or higher, 75% credibility, and a + GitHub account that is at least 180 days old. Check the{' '} ( > Scoring documentation {' '} - for details on unlocking tiers. + for full eligibility details. } /> diff --git a/src/pages/MinerDetailsPage.tsx b/src/pages/MinerDetailsPage.tsx index f47e159..96f68ae 100644 --- a/src/pages/MinerDetailsPage.tsx +++ b/src/pages/MinerDetailsPage.tsx @@ -10,7 +10,6 @@ import { MinerRepositoriesTable, MinerScoreBreakdown, MinerScoreCard, - MinerTierPerformance, SEO, } from '../components'; @@ -49,7 +48,7 @@ const MinerDetailsPage: React.FC = () => { { {activeTab === 'overview' && ( <> - diff --git a/src/pages/OnboardPage.tsx b/src/pages/OnboardPage.tsx index f83aaec..2186fec 100644 --- a/src/pages/OnboardPage.tsx +++ b/src/pages/OnboardPage.tsx @@ -3,9 +3,7 @@ import { Box, Tabs, Tab, Card, CardContent } from '@mui/material'; import { Page } from '../components/layout'; import { SEO } from '../components'; import { useSearchParams } from 'react-router-dom'; -import { RoadmapContent } from '../components/onboard/RoadmapContent'; import { AboutContent } from './AboutPage'; -import { FAQContent } from './FAQPage'; import { GettingStarted } from '../components/onboard/GettingStarted'; import { Scoring } from '../components/onboard/Scoring'; @@ -25,8 +23,6 @@ const OnboardPage: React.FC = () => { scoring: 2, repositories: 3, languages: 4, - roadmap: 5, - faq: 6, }; const indexToTabName: Record = { @@ -35,8 +31,6 @@ const OnboardPage: React.FC = () => { 2: 'scoring', 3: 'repositories', 4: 'languages', - 5: 'roadmap', - 6: 'faq', }; const activeTab = @@ -98,8 +92,6 @@ const OnboardPage: React.FC = () => { - - @@ -157,8 +149,6 @@ const OnboardPage: React.FC = () => { )} - {activeTab === 5 && } - {activeTab === 6 && } diff --git a/src/pages/RepositoriesPage.tsx b/src/pages/RepositoriesPage.tsx index f97c4af..9a057ad 100644 --- a/src/pages/RepositoriesPage.tsx +++ b/src/pages/RepositoriesPage.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { Avatar, Box, Card, Tooltip, Typography } from '@mui/material'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Page } from '../components/layout'; import { TopRepositoriesTable, SEO } from '../components'; import { useAllPrs, useReposAndWeights } from '../api'; @@ -106,7 +106,6 @@ const cardSx = { // ── Page ──────────────────────────────────────────────────────────────────── const RepositoriesPage: React.FC = () => { const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const formatRelativeTime = (date: Date) => { const now = new Date(); @@ -122,11 +121,6 @@ const RepositoriesPage: React.FC = () => { if (days < 30) return `${days}d ${hrs % 24}h ago`; return `${days}d ago`; }; - const initialTierFilter = searchParams.get('tier') as - | 'Gold' - | 'Silver' - | 'Bronze' - | null; const { data: allPRs, isLoading: isLoadingPRs } = useAllPrs(); const { data: reposWithWeights, isLoading: isLoadingRepos } = @@ -178,7 +172,6 @@ const RepositoriesPage: React.FC = () => { totalPRs: s?.totalPRs || 0, uniqueMiners: s?.uniqueMiners || new Set(), weight: repo.weight ? parseFloat(String(repo.weight)) : 0, - tier: repo.tier || '', inactiveAt: repo.inactiveAt, }; }) @@ -227,7 +220,6 @@ const RepositoriesPage: React.FC = () => { ) .map(([name, s]) => ({ name, - tier: repoMap.get(name)?.tier || '', recentScore: s.recentScore, priorScore: s.priorScore, pctIncrease: (s.recentScore / s.priorScore) * 100, @@ -267,7 +259,6 @@ const RepositoriesPage: React.FC = () => { return Array.from(repoCollateral.entries()) .map(([name, data]) => ({ name, - tier: repoMap.get(name)?.tier || '', collateral: data.totalCollateral, openPRs: data.openPRs, })) @@ -308,7 +299,6 @@ const RepositoriesPage: React.FC = () => { .slice(0, 5) .map((pr) => ({ name: pr.repository, - tier: repoMap.get(pr.repository)?.tier || '', title: pr.pullRequestTitle, createdAt: new Date(pr.mergedAt || new Date()), number: pr.pullRequestNumber, @@ -575,7 +565,6 @@ const RepositoriesPage: React.FC = () => { repositories={repoStats} isLoading={isLoading} onSelectRepository={handleSelectRepository} - initialTierFilter={initialTierFilter || undefined} /> diff --git a/src/pages/RepositoryDetailsPage.tsx b/src/pages/RepositoryDetailsPage.tsx index 4c101b9..5859743 100644 --- a/src/pages/RepositoryDetailsPage.tsx +++ b/src/pages/RepositoryDetailsPage.tsx @@ -21,7 +21,7 @@ import MergeTypeIcon from '@mui/icons-material/MergeType'; import ArticleIcon from '@mui/icons-material/Article'; import VolunteerActivismIcon from '@mui/icons-material/VolunteerActivism'; import FactCheckIcon from '@mui/icons-material/FactCheck'; -import { TIER_COLORS, STATUS_COLORS } from '../theme'; +import { RANK_COLORS, STATUS_COLORS } from '../theme'; import { Page } from '../components/layout'; import { useReposAndWeights, useRepoBountySummary } from '../api'; import { @@ -291,9 +291,9 @@ const RepositoryDetailsPage: React.FC = () => { = ({ label, value }) => ( - - - {label} - - - {value} - - -); - -const TierDetailsPage: React.FC = () => { - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - const githubId = searchParams.get('githubId'); - const tierParam = searchParams.get('tier') || ''; - const tier = - VALID_TIERS.find((t) => t.toLowerCase() === tierParam.toLowerCase()) || - 'Bronze'; - - if (!githubId) { - navigate('/top-miners'); - return null; - } - - const tierKey = tier.toLowerCase() as 'bronze' | 'silver' | 'gold'; - const color = TIER_COLORS[tierKey]; - const bgColor = alpha(color, 0.08); - const borderColor = alpha(color, 0.25); - - return ( - - - - - - - - - - Repositories in {tier} Tier - - - - - Pull Requests in {tier} Tier - - - - - - ); -}; - -interface TierSummaryCardProps { - githubId: string; - tier: string; - color: string; - bgColor: string; - borderColor: string; -} - -const TierSummaryCard: React.FC = ({ - githubId, - tier, - color, - bgColor, - borderColor, -}) => { - const { data: minerStats, isLoading } = useMinerStats(githubId); - - const tierKey = tier.toLowerCase() as 'bronze' | 'silver' | 'gold'; - const score = - tierKey === 'bronze' - ? minerStats?.bronzeScore - : tierKey === 'silver' - ? minerStats?.silverScore - : minerStats?.goldScore; - const credibility = - tierKey === 'bronze' - ? minerStats?.bronzeCredibility - : tierKey === 'silver' - ? minerStats?.silverCredibility - : minerStats?.goldCredibility; - const merged = - tierKey === 'bronze' - ? minerStats?.bronzeMergedPrs - : tierKey === 'silver' - ? minerStats?.silverMergedPrs - : minerStats?.goldMergedPrs; - const closed = - tierKey === 'bronze' - ? minerStats?.bronzeClosedPrs - : tierKey === 'silver' - ? minerStats?.silverClosedPrs - : minerStats?.goldClosedPrs; - const total = - tierKey === 'bronze' - ? minerStats?.bronzeTotalPrs - : tierKey === 'silver' - ? minerStats?.silverTotalPrs - : minerStats?.goldTotalPrs; - const opened = (total ?? 0) - (merged ?? 0) - (closed ?? 0); - const uniqueRepos = - tierKey === 'bronze' - ? minerStats?.bronzeUniqueRepos - : tierKey === 'silver' - ? minerStats?.silverUniqueRepos - : minerStats?.goldUniqueRepos; - - if (isLoading) { - return null; - } - - return ( - - - {tier} Tier Summary - - *': { minWidth: 0 }, - '& .MuiTypography-root': { - border: 'none', - borderBottom: 'none', - textDecoration: 'none', - margin: 0, - padding: 0, - }, - }} - > - - - - - - - - - ); -}; - -export default TierDetailsPage; diff --git a/src/pages/TopMinersPage.tsx b/src/pages/TopMinersPage.tsx index ff49923..fc6be64 100644 --- a/src/pages/TopMinersPage.tsx +++ b/src/pages/TopMinersPage.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { useMediaQuery, Box } from '@mui/material'; +import { useMediaQuery, Box, Typography, alpha } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { Page } from '../components/layout'; import { TopMinersTable, LeaderboardSidebar, SEO } from '../components'; @@ -34,7 +34,7 @@ const TopMinersPage: React.FC = () => { hotkey: stat.hotkey || 'N/A', uniqueReposCount: Number(stat.uniqueReposCount) || 0, credibility: Number(stat.credibility) || 0, - currentTier: stat.currentTier, + isEligible: stat.isEligible ?? false, usdPerDay: Number(stat.usdPerDay) || 0, // PR status counts for credibility donut totalMergedPrs: Number(stat.totalMergedPrs) || 0, @@ -103,6 +103,18 @@ const TopMinersPage: React.FC = () => { }, }} > + alpha(t.palette.text.primary, 0.5), + lineHeight: 1.6, + }} + > + Miners earn OSS contribution rewards by getting pull requests merged + into recognized repositories. Scored on code quality via AST token + analysis. Rewarded separately from issue discovery. + [] = [ minWidth: 260, }, }, - { - key: 'tier', - header: 'Tier', - width: '12%', - renderCell: (miner: MinerSearchData) => - miner.currentTier ? : null, - }, { key: 'credibility', header: 'Credibility', diff --git a/src/pages/search/RepositoryTab.tsx b/src/pages/search/RepositoryTab.tsx index 90fb884..af95528 100644 --- a/src/pages/search/RepositoryTab.tsx +++ b/src/pages/search/RepositoryTab.tsx @@ -8,7 +8,6 @@ import SearchResultsTable, { } from './SearchResultsTable'; import { SearchAvatarContentCell, - SearchTierBadge, SearchTruncatedText, } from './SearchTableCells'; import { type RepoSearchData } from './searchData'; @@ -75,25 +74,14 @@ const repositoryColumns: SearchResultsTableColumn[] = [ avatarAlt={repo.owner} avatarSrc={getGithubAvatarSrc(repo.owner)} > - - ({ - flex: 1, - color: theme.palette.text.primary, - fontWeight: 600, - })} - text={repo.fullName} - /> - - + ({ + color: theme.palette.text.primary, + fontWeight: 600, + })} + text={repo.fullName} + /> ), }, diff --git a/src/pages/search/SearchTableCells.tsx b/src/pages/search/SearchTableCells.tsx index 017e12c..9e645c8 100644 --- a/src/pages/search/SearchTableCells.tsx +++ b/src/pages/search/SearchTableCells.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Avatar, Box, Chip, Tooltip, Typography } from '@mui/material'; import { type Theme } from '@mui/material/styles'; import { type SystemStyleObject } from '@mui/system'; -import { getTierColors } from '../../components/leaderboard/types'; type CellSx = | SystemStyleObject @@ -97,41 +96,6 @@ const SearchTruncatedText: React.FC = ({ ); }; -type TierBadgeProps = { - label?: string; - tier?: string; -}; - -const SearchTierBadge: React.FC = ({ label, tier }) => { - const resolvedLabel = tier || label; - if (!resolvedLabel) return null; - - return ( - { - const tierColors = getTierColors(tier); - - return { - display: 'inline-flex', - alignItems: 'center', - border: '1px solid', - borderRadius: 1.25, - px: 1, - py: 0.25, - ...theme.typography.monoSmall, - lineHeight: 1, - color: tierColors.text, - borderColor: tierColors.border, - backgroundColor: tierColors.bg, - }; - }} - > - {resolvedLabel} - - ); -}; - type StatusChipProps = { backgroundColor: ThemeColorValue; borderColor: ThemeColorValue; @@ -162,9 +126,4 @@ const SearchStatusChip: React.FC = ({ /> ); -export { - SearchAvatarContentCell, - SearchStatusChip, - SearchTierBadge, - SearchTruncatedText, -}; +export { SearchAvatarContentCell, SearchStatusChip, SearchTruncatedText }; diff --git a/src/pages/search/searchData.ts b/src/pages/search/searchData.ts index ba5aff9..d46d07e 100644 --- a/src/pages/search/searchData.ts +++ b/src/pages/search/searchData.ts @@ -20,7 +20,6 @@ export type MinerSearchData = { githubId: string; githubUsername: string; hotkey: string; - currentTier: string; credibility: number; leaderboardRank: number; totalPrs: number; @@ -31,7 +30,6 @@ export type MinerSearchData = { export type RepoSearchData = { fullName: string; owner: string; - tier: string; weight: number; rank: number; contributors: number; @@ -91,7 +89,6 @@ const buildMinerSearchData = (miners: MinerEvaluation[]): MinerSearchData[] => { githubId: miner.githubId, githubUsername: miner.githubUsername || '', hotkey: miner.hotkey || '', - currentTier: miner.currentTier || '', credibility: parseNumber(miner.credibility), leaderboardRank: 0, totalPrs: parseNumber(miner.totalPrs), @@ -120,10 +117,7 @@ const getMinerSearchResults = ( const results = sortByMatchThenTiebreaker( miners, query, - (miner) => - matchMode === 'quick' - ? [miner.githubId, miner.githubUsername] - : [miner.githubId, miner.githubUsername, miner.currentTier], + (miner) => [miner.githubId, miner.githubUsername], (miner) => miner.totalScore, ); @@ -172,7 +166,6 @@ const buildRepoSearchData = ( return { fullName: repo.fullName, owner: repo.owner, - tier: repo.tier || '', weight: parseNumber(repo.weight), totalScore: stats?.totalScore || 0, totalPRs: stats?.totalPRs || 0, @@ -198,10 +191,7 @@ const getRepositorySearchResults = ( const results = sortByMatchThenTiebreaker( repositories, query, - (repo) => - matchMode === 'quick' - ? [repo.fullName] - : [repo.fullName, repo.owner, repo.tier], + (repo) => [repo.fullName, repo.owner], (repo) => repo.weight, ); diff --git a/src/routes.tsx b/src/routes.tsx index 170ed29..52f4929 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -18,11 +18,14 @@ const IssueDetailsPage = React.lazy(() => import('./pages/IssueDetailsPage')); const TopMinersPage = React.lazy(() => import('./pages/TopMinersPage')); const RepositoriesPage = React.lazy(() => import('./pages/RepositoriesPage')); const MinerDetailsPage = React.lazy(() => import('./pages/MinerDetailsPage')); -const TierDetailsPage = React.lazy(() => import('./pages/TierDetailsPage')); const RepositoryDetailsPage = React.lazy( () => import('./pages/RepositoryDetailsPage'), ); const PRDetailsPage = React.lazy(() => import('./pages/PRDetailsPage')); +const DiscoveriesPage = React.lazy(() => import('./pages/DiscoveriesPage')); +const DiscoveryMinerDetailsPage = React.lazy( + () => import('./pages/DiscoveryMinerDetailsPage'), +); const OnboardPage = React.lazy(() => import('./pages/OnboardPage')); // 404 page @@ -48,7 +51,23 @@ const routesArray: AppRoute[] = [ showGlobalSearch: true, }, { name: 'search', path: '/search', element: }, - { name: 'top-miners', path: '/top-miners', element: }, + { + name: 'discoveries', + path: '/discoveries', + element: , + showGlobalSearch: true, + }, + { + name: 'discovery-miner-details', + path: '/discoveries/details', + element: , + }, + { + name: 'top-miners', + path: '/top-miners', + element: , + showGlobalSearch: true, + }, { name: 'repositories', path: '/repositories', @@ -59,11 +78,6 @@ const routesArray: AppRoute[] = [ path: '/miners/details', element: , }, - { - name: 'tier-details', - path: '/miners/tier-details', - element: , - }, { name: 'repository-details', path: '/miners/repository', diff --git a/src/tests/ExplorerUtils.test.ts b/src/tests/ExplorerUtils.test.ts index 23eb5b2..2b17ff4 100644 --- a/src/tests/ExplorerUtils.test.ts +++ b/src/tests/ExplorerUtils.test.ts @@ -1,27 +1,14 @@ import { describe, it, expect } from 'vitest'; import { parseNumber, - getTierLevel, calculateDynamicOpenPrThreshold, normalizeMinerEvaluations, normalizeCommitLogs, - formatTierLabel, - tierColorFor, - getTierFilterValue, - filterMinerRepoStats, sortMinerRepoStats, - countPrTiers, - filterPrsByTier, buildRepoWeightsMap, - buildRepoTiersMap, - buildTierThresholdsMap, - isRepoQualified, aggregatePRsByRepository, - computeTierCounts, - computeQualificationCounts, hasActiveFilters, getDisplayCount, - filterByQualification, filterBySearch, type RepoStats, } from '../utils/ExplorerUtils'; @@ -54,146 +41,47 @@ describe('parseNumber', () => { }); }); -describe('getTierLevel', () => { - it('returns correct level for valid tiers', () => { - expect(getTierLevel('bronze')).toBe(1); - expect(getTierLevel('silver')).toBe(2); - expect(getTierLevel('gold')).toBe(3); - }); - - it('handles case insensitivity', () => { - expect(getTierLevel('GOLD')).toBe(3); - expect(getTierLevel('Silver')).toBe(2); - expect(getTierLevel('BRONZE')).toBe(1); - }); - - it('returns 0 for invalid or missing tiers', () => { - expect(getTierLevel(null)).toBe(0); - expect(getTierLevel(undefined)).toBe(0); - expect(getTierLevel('')).toBe(0); - expect(getTierLevel('platinum')).toBe(0); - }); -}); - -describe('formatTierLabel', () => { - it('capitalizes tier names', () => { - expect(formatTierLabel('gold')).toBe('Gold'); - expect(formatTierLabel('silver')).toBe('Silver'); - expect(formatTierLabel('bronze')).toBe('Bronze'); - }); - - it('handles mixed case input', () => { - expect(formatTierLabel('GOLD')).toBe('Gold'); - expect(formatTierLabel('SiLvEr')).toBe('Silver'); - }); - - it('returns Unknown for invalid input', () => { - expect(formatTierLabel(null)).toBe('Unknown'); - expect(formatTierLabel(undefined)).toBe('Unknown'); - expect(formatTierLabel('')).toBe('Unknown'); - }); -}); - -describe('tierColorFor', () => { - const tierColors = { - gold: '#FFD700', - silver: '#C0C0C0', - bronze: '#CD7F32', - }; - - it('returns correct color for tiers', () => { - expect(tierColorFor('gold', tierColors)).toBe('#FFD700'); - expect(tierColorFor('silver', tierColors)).toBe('#C0C0C0'); - expect(tierColorFor('bronze', tierColors)).toBe('#CD7F32'); - }); - - it('handles case insensitivity', () => { - expect(tierColorFor('GOLD', tierColors)).toBe('#FFD700'); - expect(tierColorFor('Silver', tierColors)).toBe('#C0C0C0'); - }); - - it('returns transparent for invalid tiers', () => { - expect(tierColorFor(null, tierColors)).toBe('transparent'); - expect(tierColorFor(undefined, tierColors)).toBe('transparent'); - expect(tierColorFor('platinum', tierColors)).toBe('transparent'); - }); -}); - -describe('getTierFilterValue', () => { - it('returns correct filter value for valid tiers', () => { - expect(getTierFilterValue('gold')).toBe('gold'); - expect(getTierFilterValue('silver')).toBe('silver'); - expect(getTierFilterValue('bronze')).toBe('bronze'); - }); - - it('handles case insensitivity', () => { - expect(getTierFilterValue('GOLD')).toBe('gold'); - expect(getTierFilterValue('Silver')).toBe('silver'); - }); - - it('returns all for invalid tiers', () => { - expect(getTierFilterValue(null)).toBe('all'); - expect(getTierFilterValue(undefined)).toBe('all'); - expect(getTierFilterValue('platinum')).toBe('all'); - }); -}); - -describe('filterMinerRepoStats', () => { - const mockStats: RepoStats[] = [ - { - repository: 'repo1', - prs: 5, - score: 100, - tokenScore: 80, - weight: 0.5, - tier: 'gold', - }, - { - repository: 'repo2', - prs: 3, - score: 50, - tokenScore: 40, - weight: 0.3, - tier: 'silver', - }, - { - repository: 'repo3', - prs: 2, - score: 25, - tokenScore: 20, - weight: 0.2, - tier: 'bronze', - }, - { - repository: 'repo4', - prs: 4, - score: 75, - tokenScore: 60, - weight: 0.4, - tier: 'Gold', - }, - ]; +describe('calculateDynamicOpenPrThreshold', () => { + const baseMiner = { + githubId: 'test', + totalTokenScore: 600, + } as any; - it('returns all stats when filter is all', () => { - expect(filterMinerRepoStats(mockStats, 'all')).toEqual(mockStats); + it('returns default threshold with bonus when no scoring config', () => { + // defaults: base=10, tokenScorePer=300, max=30; 600/300=2 bonus → 12 + expect(calculateDynamicOpenPrThreshold(baseMiner, undefined)).toBe(12); }); - it('filters by gold tier', () => { - const result = filterMinerRepoStats(mockStats, 'gold'); - expect(result).toHaveLength(2); - expect(result.every((s) => s.tier.toLowerCase() === 'gold')).toBe(true); + it('calculates bonus from token score', () => { + const scoring = { + excessivePrPenaltyThreshold: 10, + openPrThresholdTokenScore: 300, + maxOpenPrThreshold: 30, + } as any; + // 600 / 300 = 2 bonus → 10 + 2 = 12 + expect(calculateDynamicOpenPrThreshold(baseMiner, scoring)).toBe(12); }); - it('filters by silver tier', () => { - const result = filterMinerRepoStats(mockStats, 'silver'); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('repo2'); + it('caps at max threshold', () => { + const highMiner = { + ...baseMiner, + totalTokenScore: 15000, + } as any; + const scoring = { + excessivePrPenaltyThreshold: 10, + openPrThresholdTokenScore: 300, + maxOpenPrThreshold: 30, + } as any; + expect(calculateDynamicOpenPrThreshold(highMiner, scoring)).toBe(30); }); - it('filters by bronze tier', () => { - const result = filterMinerRepoStats(mockStats, 'bronze'); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('repo3'); + it('returns base when tokenScorePer is 0', () => { + const scoring = { + excessivePrPenaltyThreshold: 8, + openPrThresholdTokenScore: 0, + maxOpenPrThreshold: 30, + } as any; + expect(calculateDynamicOpenPrThreshold(baseMiner, scoring)).toBe(8); }); }); @@ -205,7 +93,6 @@ describe('sortMinerRepoStats', () => { score: 100, tokenScore: 80, weight: 0.5, - tier: 'gold', }, { repository: 'alpha', @@ -213,7 +100,6 @@ describe('sortMinerRepoStats', () => { score: 50, tokenScore: 40, weight: 0.3, - tier: 'silver', }, { repository: 'gamma', @@ -221,7 +107,6 @@ describe('sortMinerRepoStats', () => { score: 75, tokenScore: 60, weight: 0.2, - tier: 'bronze', }, ]; @@ -263,133 +148,6 @@ describe('sortMinerRepoStats', () => { }); }); -describe('countPrTiers', () => { - const repoTiers = new Map([ - ['repo1', 'gold'], - ['repo2', 'silver'], - ['repo3', 'bronze'], - ]); - - it('counts PRs by tier from PR tier property', () => { - const prs = [ - { repository: 'repo1', tier: 'gold' }, - { repository: 'repo2', tier: 'gold' }, - { repository: 'repo3', tier: 'silver' }, - ]; - const counts = countPrTiers(prs, new Map()); - expect(counts.all).toBe(3); - expect(counts.gold).toBe(2); - expect(counts.silver).toBe(1); - expect(counts.bronze).toBe(0); - }); - - it('falls back to repo tier when PR tier is missing', () => { - const prs = [ - { repository: 'repo1', tier: null }, - { repository: 'repo2', tier: undefined }, - { repository: 'repo3' }, - ]; - const counts = countPrTiers(prs, repoTiers); - expect(counts.all).toBe(3); - expect(counts.gold).toBe(1); - expect(counts.silver).toBe(1); - expect(counts.bronze).toBe(1); - }); - - it('handles empty array', () => { - const counts = countPrTiers([], repoTiers); - expect(counts.all).toBe(0); - expect(counts.gold).toBe(0); - expect(counts.silver).toBe(0); - expect(counts.bronze).toBe(0); - }); -}); - -describe('filterPrsByTier', () => { - const repoTiers = new Map([ - ['repo1', 'gold'], - ['repo2', 'silver'], - ]); - - const prs = [ - { repository: 'repo1', tier: 'gold' }, - { repository: 'repo2', tier: null }, - { repository: 'repo3', tier: 'bronze' }, - ]; - - it('returns all PRs when filter is all', () => { - expect(filterPrsByTier(prs, 'all', repoTiers)).toEqual(prs); - }); - - it('filters by PR tier property', () => { - const result = filterPrsByTier(prs, 'gold', repoTiers); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('repo1'); - }); - - it('falls back to repo tier when PR tier is missing', () => { - const result = filterPrsByTier(prs, 'silver', repoTiers); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('repo2'); - }); - - it('filters bronze tier', () => { - const result = filterPrsByTier(prs, 'bronze', repoTiers); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('repo3'); - }); -}); - -describe('calculateDynamicOpenPrThreshold', () => { - const baseMiner = { - githubId: 'test', - currentTier: 'bronze', - bronzeTokenScore: 600, - silverTokenScore: 0, - goldTokenScore: 0, - } as any; - - it('returns default threshold with bonus when no scoring config', () => { - // defaults: base=10, tokenScorePer=500, max=30; 600/500=1 bonus → 11 - expect(calculateDynamicOpenPrThreshold(baseMiner, undefined)).toBe(11); - }); - - it('calculates bonus from token score', () => { - const scoring = { - excessivePrPenaltyThreshold: 10, - openPrThresholdTokenScore: 500, - maxOpenPrThreshold: 30, - } as any; - // 600 / 500 = 1 bonus → 10 + 1 = 11 - expect(calculateDynamicOpenPrThreshold(baseMiner, scoring)).toBe(11); - }); - - it('caps at max threshold', () => { - const highMiner = { - ...baseMiner, - currentTier: 'gold', - bronzeTokenScore: 5000, - silverTokenScore: 5000, - goldTokenScore: 5000, - } as any; - const scoring = { - excessivePrPenaltyThreshold: 10, - openPrThresholdTokenScore: 500, - maxOpenPrThreshold: 30, - } as any; - expect(calculateDynamicOpenPrThreshold(highMiner, scoring)).toBe(30); - }); - - it('returns base when tokenScorePer is 0', () => { - const scoring = { - excessivePrPenaltyThreshold: 8, - openPrThresholdTokenScore: 0, - maxOpenPrThreshold: 30, - } as any; - expect(calculateDynamicOpenPrThreshold(baseMiner, scoring)).toBe(8); - }); -}); - describe('normalizeMinerEvaluations', () => { it('returns array directly if items have githubId', () => { const arr = [{ githubId: 'a' }, { githubId: 'b' }]; @@ -458,14 +216,12 @@ describe('buildRepoWeightsMap', () => { owner: 'org', name: 'repo1', weight: '0.5', - tier: 'gold', }, { fullName: 'org/repo2', owner: 'org', name: 'repo2', weight: '0.3', - tier: 'silver', }, ]; const map = buildRepoWeightsMap(repos); @@ -474,115 +230,17 @@ describe('buildRepoWeightsMap', () => { }); it('skips entries with missing fullName', () => { - const repos = [ - { fullName: '', owner: '', name: '', weight: '0.5', tier: 'gold' }, - ]; + const repos = [{ fullName: '', owner: '', name: '', weight: '0.5' }]; const map = buildRepoWeightsMap(repos); expect(map.size).toBe(0); }); }); -describe('buildRepoTiersMap', () => { - it('returns empty map for undefined', () => { - expect(buildRepoTiersMap(undefined).size).toBe(0); - }); - - it('builds map from valid repos', () => { - const repos = [ - { - fullName: 'org/repo1', - owner: 'org', - name: 'repo1', - weight: '0.5', - tier: 'gold', - }, - ]; - const map = buildRepoTiersMap(repos); - expect(map.get('org/repo1')).toBe('gold'); - }); -}); - -describe('buildTierThresholdsMap', () => { - it('returns empty map for undefined', () => { - expect(buildTierThresholdsMap(undefined).size).toBe(0); - }); - - it('builds map from tier config', () => { - const config = { - tiers: [ - { name: 'Gold', requiredMinTokenScorePerRepo: 100 } as any, - { name: 'Silver', requiredMinTokenScorePerRepo: 50 } as any, - ], - tierOrder: ['Gold', 'Silver'], - }; - const map = buildTierThresholdsMap(config); - expect(map.get('gold')).toBe(100); - expect(map.get('silver')).toBe(50); - }); -}); - -describe('isRepoQualified', () => { - const thresholds = new Map([ - ['gold', 100], - ['silver', 50], - ['bronze', 25], - ]); - - it('returns true when tokenScore meets threshold', () => { - const repo: RepoStats = { - repository: 'r', - prs: 1, - score: 10, - tokenScore: 100, - weight: 0.5, - tier: 'gold', - }; - expect(isRepoQualified(repo, thresholds)).toBe(true); - }); - - it('returns true when tokenScore exceeds threshold', () => { - const repo: RepoStats = { - repository: 'r', - prs: 1, - score: 10, - tokenScore: 200, - weight: 0.5, - tier: 'gold', - }; - expect(isRepoQualified(repo, thresholds)).toBe(true); - }); - - it('returns false when tokenScore is below threshold', () => { - const repo: RepoStats = { - repository: 'r', - prs: 1, - score: 10, - tokenScore: 99, - weight: 0.5, - tier: 'gold', - }; - expect(isRepoQualified(repo, thresholds)).toBe(false); - }); - - it('returns false when tier is not in thresholds', () => { - const repo: RepoStats = { - repository: 'r', - prs: 1, - score: 10, - tokenScore: 999, - weight: 0.5, - tier: 'platinum', - }; - expect(isRepoQualified(repo, thresholds)).toBe(false); - }); -}); - describe('aggregatePRsByRepository', () => { const weights = new Map([['org/repo1', 0.5]]); - const tiers = new Map([['org/repo1', 'gold']]); it('returns empty array for empty prs', () => { - expect(aggregatePRsByRepository([], weights, tiers)).toEqual([]); + expect(aggregatePRsByRepository([], weights)).toEqual([]); }); it('aggregates PRs into repo stats', () => { @@ -595,13 +253,12 @@ describe('aggregatePRsByRepository', () => { }, { repository: 'org/repo1', score: '5', prState: 'OPEN', tokenScore: 30 }, ] as any[]; - const result = aggregatePRsByRepository(prs, weights, tiers); + const result = aggregatePRsByRepository(prs, weights); expect(result).toHaveLength(1); expect(result[0].prs).toBe(2); expect(result[0].score).toBe(15); expect(result[0].tokenScore).toBe(50); // only MERGED PR counted expect(result[0].weight).toBe(0.5); - expect(result[0].tier).toBe('gold'); }); it('creates separate entries for different repos', () => { @@ -619,116 +276,19 @@ describe('aggregatePRsByRepository', () => { tokenScore: 30, }, ] as any[]; - const result = aggregatePRsByRepository(prs, weights, tiers); + const result = aggregatePRsByRepository(prs, weights); expect(result).toHaveLength(2); }); }); -describe('computeTierCounts', () => { - it('counts tiers correctly', () => { - const stats: RepoStats[] = [ - { - repository: 'r1', - prs: 1, - score: 10, - tokenScore: 50, - weight: 0.5, - tier: 'gold', - }, - { - repository: 'r2', - prs: 1, - score: 10, - tokenScore: 50, - weight: 0.5, - tier: 'Gold', - }, - { - repository: 'r3', - prs: 1, - score: 10, - tokenScore: 50, - weight: 0.5, - tier: 'silver', - }, - { - repository: 'r4', - prs: 1, - score: 10, - tokenScore: 50, - weight: 0.5, - tier: 'bronze', - }, - ]; - const counts = computeTierCounts(stats); - expect(counts.all).toBe(4); - expect(counts.gold).toBe(2); - expect(counts.silver).toBe(1); - expect(counts.bronze).toBe(1); - }); - - it('returns zeros for empty array', () => { - const counts = computeTierCounts([]); - expect(counts).toEqual({ all: 0, gold: 0, silver: 0, bronze: 0 }); - }); -}); - -describe('computeQualificationCounts', () => { - const thresholds = new Map([ - ['gold', 100], - ['silver', 50], - ]); - - it('counts qualified and unqualified repos', () => { - const stats: RepoStats[] = [ - { - repository: 'r1', - prs: 1, - score: 10, - tokenScore: 150, - weight: 0.5, - tier: 'gold', - }, - { - repository: 'r2', - prs: 1, - score: 10, - tokenScore: 30, - weight: 0.5, - tier: 'silver', - }, - { - repository: 'r3', - prs: 1, - score: 10, - tokenScore: 80, - weight: 0.5, - tier: 'gold', - }, - ]; - const counts = computeQualificationCounts(stats, thresholds); - expect(counts.all).toBe(3); - expect(counts.qualified).toBe(1); - expect(counts.unqualified).toBe(2); - }); -}); - describe('hasActiveFilters', () => { - it('returns false when all filters are default', () => { - expect(hasActiveFilters('all', 'all', '')).toBe(false); - expect(hasActiveFilters('all', 'all', ' ')).toBe(false); - }); - - it('returns true when tier filter is active', () => { - expect(hasActiveFilters('gold', 'all', '')).toBe(true); - }); - - it('returns true when qualification filter is active', () => { - expect(hasActiveFilters('all', 'qualified', '')).toBe(true); + it('returns false when search is empty', () => { + expect(hasActiveFilters('')).toBe(false); + expect(hasActiveFilters(' ')).toBe(false); }); it('returns true when search query is active', () => { - expect(hasActiveFilters('all', 'all', 'repo')).toBe(true); + expect(hasActiveFilters('repo')).toBe(true); }); }); @@ -742,44 +302,6 @@ describe('getDisplayCount', () => { }); }); -describe('filterByQualification', () => { - const thresholds = new Map([['gold', 100]]); - const stats: RepoStats[] = [ - { - repository: 'r1', - prs: 1, - score: 10, - tokenScore: 150, - weight: 0.5, - tier: 'gold', - }, - { - repository: 'r2', - prs: 1, - score: 10, - tokenScore: 50, - weight: 0.5, - tier: 'gold', - }, - ]; - - it('returns all when filter is all', () => { - expect(filterByQualification(stats, 'all', thresholds)).toEqual(stats); - }); - - it('returns only qualified repos', () => { - const result = filterByQualification(stats, 'qualified', thresholds); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('r1'); - }); - - it('returns only unqualified repos', () => { - const result = filterByQualification(stats, 'unqualified', thresholds); - expect(result).toHaveLength(1); - expect(result[0].repository).toBe('r2'); - }); -}); - describe('filterBySearch', () => { const stats: RepoStats[] = [ { @@ -788,7 +310,6 @@ describe('filterBySearch', () => { score: 10, tokenScore: 50, weight: 0.5, - tier: 'gold', }, { repository: 'org/beta', @@ -796,7 +317,6 @@ describe('filterBySearch', () => { score: 10, tokenScore: 50, weight: 0.5, - tier: 'silver', }, ]; diff --git a/src/theme.ts b/src/theme.ts index d97ad56..ea03a06 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,10 +1,10 @@ import { createTheme } from '@mui/material/styles'; // Shared Color Constants (exported for use outside MUI components) -export const TIER_COLORS = { - gold: '#FFD700', - silver: '#C0C0C0', - bronze: '#CD7F32', +export const RANK_COLORS = { + first: '#FFD700', + second: '#C0C0C0', + third: '#CD7F32', } as const; export const STATUS_COLORS = { @@ -84,10 +84,10 @@ declare module '@mui/material/styles' { } interface Palette { - tier: { - gold: string; - silver: string; - bronze: string; + rank: { + first: string; + second: string; + third: string; }; status: { merged: string; @@ -132,10 +132,10 @@ declare module '@mui/material/styles' { } interface PaletteOptions { - tier?: { - gold: string; - silver: string; - bronze: string; + rank?: { + first: string; + second: string; + third: string; }; status?: { merged: string; @@ -209,7 +209,6 @@ declare module '@mui/material/Card' { declare module '@mui/material/Chip' { interface ChipPropsVariantOverrides { - tier: true; status: true; info: true; filter: true; @@ -234,11 +233,11 @@ const theme = createTheme({ secondary: '#7d7d7d', }, divider: '#ffffff', - // Custom tier colors - tier: { - gold: TIER_COLORS.gold, - silver: TIER_COLORS.silver, - bronze: TIER_COLORS.bronze, + // Rank podium colors (1st/2nd/3rd) + rank: { + first: RANK_COLORS.first, + second: RANK_COLORS.second, + third: RANK_COLORS.third, }, // Custom status colors status: { @@ -476,15 +475,6 @@ const theme = createTheme({ }, }, variants: [ - // Tier variant - for Gold/Silver/Bronze badges - { - props: { variant: 'tier' }, - style: { - backgroundColor: 'transparent', - border: '1px solid', - borderRadius: '6px', - }, - }, // Status variant - for merged/open/closed states { props: { variant: 'status' }, diff --git a/src/utils/ExplorerUtils.ts b/src/utils/ExplorerUtils.ts index 658e4f4..f108cc6 100644 --- a/src/utils/ExplorerUtils.ts +++ b/src/utils/ExplorerUtils.ts @@ -3,15 +3,8 @@ import { type MinerEvaluation, type Repository, type RepositoryPrScoring, - type TierConfigResponse, } from '../api'; -export const TIER_LEVELS: Record = { - bronze: 1, - silver: 2, - gold: 3, -}; - export const getGithubAvatarSrc = (username?: string | null) => { if (username) { return `https://avatars.githubusercontent.com/${username}`; @@ -31,36 +24,20 @@ export const parseNumber = (value: unknown, fallback = 0): number => { return fallback; }; -export const getTierLevel = (tier: string | undefined | null): number => { - if (!tier) return 0; - return TIER_LEVELS[tier.toLowerCase()] || 0; -}; - -const getUnlockedTierTokenScore = (minerStats: MinerEvaluation): number => { - const tierLevel = getTierLevel(minerStats.currentTier); - let total = 0; - - if (tierLevel >= 1) total += parseNumber(minerStats.bronzeTokenScore); - if (tierLevel >= 2) total += parseNumber(minerStats.silverTokenScore); - if (tierLevel >= 3) total += parseNumber(minerStats.goldTokenScore); - - return total; -}; - export const calculateDynamicOpenPrThreshold = ( minerStats: MinerEvaluation, prScoring: RepositoryPrScoring | undefined, ): number => { const baseThreshold = parseNumber(prScoring?.excessivePrPenaltyThreshold, 10); - const tokenScorePer = parseNumber(prScoring?.openPrThresholdTokenScore, 500); + const tokenScorePer = parseNumber(prScoring?.openPrThresholdTokenScore, 300); const maxThreshold = parseNumber(prScoring?.maxOpenPrThreshold, 30); if (tokenScorePer <= 0) { return Math.min(baseThreshold, maxThreshold); } - const unlockedTokenScore = getUnlockedTierTokenScore(minerStats); - const bonus = Math.floor(unlockedTokenScore / tokenScorePer); + const tokenScore = parseNumber(minerStats.totalTokenScore); + const bonus = Math.floor(tokenScore / tokenScorePer); return Math.min(baseThreshold + bonus, maxThreshold); }; @@ -125,67 +102,16 @@ export const normalizeCommitLogs = (payload: unknown): CommitLog[] => { return []; }; -export type MinerTierFilter = 'all' | 'gold' | 'silver' | 'bronze'; export type MinerStatusFilter = 'all' | 'open' | 'merged' | 'closed'; -export const formatTierLabel = (tier: string | undefined | null): string => { - if (!tier) return 'Unknown'; - const normalized = tier.toLowerCase(); - return normalized.charAt(0).toUpperCase() + normalized.slice(1); -}; - -export const tierColorFor = ( - tier: string | undefined | null, - tierColors: { gold: string; silver: string; bronze: string }, -): string => { - if (!tier) return 'transparent'; - const normalized = tier.toLowerCase(); - switch (normalized) { - case 'gold': - return tierColors.gold; - case 'silver': - return tierColors.silver; - case 'bronze': - return tierColors.bronze; - default: - return 'transparent'; - } -}; - -export const getTierFilterValue = ( - tier: string | undefined | null, -): MinerTierFilter => { - if (!tier) return 'all'; - const normalized = tier.toLowerCase(); - if ( - normalized === 'gold' || - normalized === 'silver' || - normalized === 'bronze' - ) { - return normalized; - } - return 'all'; -}; - export interface RepoStats { repository: string; prs: number; score: number; tokenScore: number; weight: number; - tier: string; } -export const filterMinerRepoStats = ( - stats: RepoStats[], - tierFilter: MinerTierFilter, -): RepoStats[] => { - if (tierFilter === 'all') return stats; - return stats.filter( - (repo) => repo.tier.toLowerCase() === tierFilter.toLowerCase(), - ); -}; - export type RepoSortField = | 'rank' | 'repository' @@ -228,51 +154,6 @@ export const sortMinerRepoStats = ( return sorted; }; -export interface PrTierCounts { - all: number; - gold: number; - silver: number; - bronze: number; -} - -export const countPrTiers = < - T extends { tier?: string | null; repository: string }, ->( - prs: T[], - repoTiers: Map, -): PrTierCounts => { - const counts: PrTierCounts = { all: 0, gold: 0, silver: 0, bronze: 0 }; - for (const pr of prs) { - counts.all++; - const tier = pr.tier || repoTiers.get(pr.repository) || ''; - const normalized = tier.toLowerCase(); - if (normalized === 'gold') counts.gold++; - else if (normalized === 'silver') counts.silver++; - else if (normalized === 'bronze') counts.bronze++; - } - return counts; -}; - -export const filterPrsByTier = < - T extends { tier?: string | null; repository: string }, ->( - prs: T[], - tierFilter: MinerTierFilter, - repoTiers: Map, -): T[] => { - if (tierFilter === 'all') return prs; - return prs.filter((pr) => { - const tier = pr.tier || repoTiers.get(pr.repository) || ''; - return tier.toLowerCase() === tierFilter.toLowerCase(); - }); -}; - -// --------------------------------------------------------------------------- -// Qualification filter -// --------------------------------------------------------------------------- - -export type QualificationFilter = 'all' | 'qualified' | 'unqualified'; - // --------------------------------------------------------------------------- // Map builders – extract lookup maps from API data // --------------------------------------------------------------------------- @@ -290,66 +171,6 @@ export const buildRepoWeightsMap = ( return map; }; -export const buildRepoTiersMap = ( - repos: Repository[] | undefined, -): Map => { - const map = new Map(); - if (!Array.isArray(repos)) return map; - for (const repo of repos) { - if (repo && repo.fullName) { - map.set(repo.fullName, repo.tier || ''); - } - } - return map; -}; - -export const buildTierThresholdsMap = ( - tierConfig: TierConfigResponse | undefined, -): Map => { - const map = new Map(); - if (!tierConfig?.tiers) return map; - for (const t of tierConfig.tiers) { - map.set(t.name.toLowerCase(), t.requiredMinTokenScorePerRepo); - } - return map; -}; - -// --------------------------------------------------------------------------- -// Qualification helpers -// --------------------------------------------------------------------------- - -export const isRepoQualified = ( - repo: RepoStats, - tierThresholds: Map, -): boolean => { - const tier = repo.tier.toLowerCase(); - const threshold = tierThresholds.get(tier); - if (threshold == null) return false; - return repo.tokenScore >= threshold; -}; - -export interface QualificationCounts { - all: number; - qualified: number; - unqualified: number; -} - -export const computeQualificationCounts = ( - repoStats: RepoStats[], - tierThresholds: Map, -): QualificationCounts => { - let qualified = 0; - let unqualified = 0; - for (const repo of repoStats) { - if (isRepoQualified(repo, tierThresholds)) { - qualified++; - } else { - unqualified++; - } - } - return { all: repoStats.length, qualified, unqualified }; -}; - // --------------------------------------------------------------------------- // PR aggregation – builds per-repository stats from commit logs // --------------------------------------------------------------------------- @@ -357,7 +178,6 @@ export const computeQualificationCounts = ( export const aggregatePRsByRepository = ( prs: CommitLog[], repoWeights: Map, - repoTiers: Map, ): RepoStats[] => { if (!prs || prs.length === 0) return []; @@ -370,7 +190,6 @@ export const aggregatePRsByRepository = ( score: 0, tokenScore: 0, weight: repoWeights.get(pr.repository) || 0, - tier: repoTiers.get(pr.repository) || '', }; existing.prs += 1; existing.score += parseFloat(pr.score || '0'); @@ -383,51 +202,12 @@ export const aggregatePRsByRepository = ( return Array.from(statsMap.values()); }; -// --------------------------------------------------------------------------- -// Tier counts -// --------------------------------------------------------------------------- - -export interface TierCounts { - all: number; - gold: number; - silver: number; - bronze: number; -} - -export const computeTierCounts = (repoStats: RepoStats[]): TierCounts => { - const counts: TierCounts = { - all: repoStats.length, - gold: 0, - silver: 0, - bronze: 0, - }; - for (const repo of repoStats) { - const tier = repo.tier.toLowerCase(); - if (tier === 'gold') { - counts.gold++; - } else if (tier === 'silver') { - counts.silver++; - } else if (tier === 'bronze') { - counts.bronze++; - } - } - return counts; -}; - // --------------------------------------------------------------------------- // Filter / display helpers // --------------------------------------------------------------------------- -export const hasActiveFilters = ( - tierFilter: MinerTierFilter, - qualificationFilter: QualificationFilter, - searchQuery: string, -): boolean => { - return ( - tierFilter !== 'all' || - qualificationFilter !== 'all' || - !!searchQuery.trim() - ); +export const hasActiveFilters = (searchQuery: string): boolean => { + return !!searchQuery.trim(); }; export const getDisplayCount = ( @@ -441,26 +221,6 @@ export const getDisplayCount = ( return String(filteredCount); }; -// --------------------------------------------------------------------------- -// Qualification-based filtering -// --------------------------------------------------------------------------- - -export const filterByQualification = ( - stats: RepoStats[], - qualificationFilter: QualificationFilter, - tierThresholds: Map, -): RepoStats[] => { - switch (qualificationFilter) { - case 'qualified': - return stats.filter((r) => isRepoQualified(r, tierThresholds)); - case 'unqualified': - return stats.filter((r) => !isRepoQualified(r, tierThresholds)); - case 'all': - default: - return stats; - } -}; - export const filterBySearch = ( stats: RepoStats[], searchQuery: string,