Commit
•
eda28cd
1
Parent(s):
22aa376
remove requirement for name, get user full name and avatar url
Browse files- src/components/Heatmap.tsx +34 -16
- src/pages/[author]/index.tsx +24 -25
- src/pages/index.tsx +47 -53
- src/types/heatmap.ts +4 -2
- src/utils/authors.ts +68 -0
- src/utils/calendar.ts +10 -10
src/components/Heatmap.tsx
CHANGED
@@ -1,17 +1,36 @@
|
|
1 |
import React from "react";
|
2 |
import ActivityCalendar from "react-activity-calendar";
|
3 |
-
import { Tooltip } from "@mui/material";
|
4 |
import Link from "next/link";
|
5 |
|
6 |
type HeatmapProps = {
|
7 |
data: Array<{ date: string; count: number; level: number }>;
|
8 |
color: string;
|
9 |
providerName: string;
|
|
|
|
|
10 |
};
|
11 |
|
12 |
-
const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName }) => {
|
13 |
return (
|
14 |
-
<div className="flex flex-col items-center">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
<div className="w-full overflow-x-auto flex justify-center">
|
16 |
<ActivityCalendar
|
17 |
data={data}
|
@@ -30,19 +49,18 @@ const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName }) => {
|
|
30 |
)}
|
31 |
/>
|
32 |
</div>
|
33 |
-
<
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
</div>
|
46 |
</div>
|
47 |
);
|
48 |
};
|
|
|
1 |
import React from "react";
|
2 |
import ActivityCalendar from "react-activity-calendar";
|
3 |
+
import { Tooltip, Avatar } from "@mui/material";
|
4 |
import Link from "next/link";
|
5 |
|
6 |
type HeatmapProps = {
|
7 |
data: Array<{ date: string; count: number; level: number }>;
|
8 |
color: string;
|
9 |
providerName: string;
|
10 |
+
fullName: string;
|
11 |
+
avatarUrl: string;
|
12 |
};
|
13 |
|
14 |
+
const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName, fullName, avatarUrl }) => {
|
15 |
return (
|
16 |
+
<div className="flex flex-col items-center w-full mx-auto">
|
17 |
+
<div className="flex flex-col sm:flex-row items-center mb-4 w-full justify-center">
|
18 |
+
{avatarUrl && (
|
19 |
+
<Avatar src={avatarUrl} alt={fullName} className="mb-2 sm:mb-0 sm:mr-4" sx={{ width: 48, height: 48 }} />
|
20 |
+
)}
|
21 |
+
<div className="text-center sm:text-left">
|
22 |
+
<h2 className="text-lg font-semibold">
|
23 |
+
<Link
|
24 |
+
href={`https://huggingface.co/${providerName}`}
|
25 |
+
target="_blank"
|
26 |
+
rel="noopener noreferrer"
|
27 |
+
className="hover:text-blue-500 hover:underline"
|
28 |
+
>
|
29 |
+
{fullName}
|
30 |
+
</Link>
|
31 |
+
</h2>
|
32 |
+
</div>
|
33 |
+
</div>
|
34 |
<div className="w-full overflow-x-auto flex justify-center">
|
35 |
<ActivityCalendar
|
36 |
data={data}
|
|
|
49 |
)}
|
50 |
/>
|
51 |
</div>
|
52 |
+
<p className="text-xs italic text-slate-500 mt-2 text-center">
|
53 |
+
Models, Datasets, and Spaces created on {" "}
|
54 |
+
<Link
|
55 |
+
href="https://huggingface.co"
|
56 |
+
target="_blank"
|
57 |
+
rel="noopener noreferrer"
|
58 |
+
className="hover:underline"
|
59 |
+
>
|
60 |
+
Hugging Face
|
61 |
+
</Link>
|
62 |
+
.
|
63 |
+
</p>
|
|
|
64 |
</div>
|
65 |
);
|
66 |
};
|
src/pages/[author]/index.tsx
CHANGED
@@ -3,6 +3,7 @@ import { GetServerSidePropsContext } from "next";
|
|
3 |
import { OpenSourceHeatmapProps } from "../../types/heatmap";
|
4 |
import { generateCalendarData } from "../../utils/calendar";
|
5 |
import Heatmap from "../../components/Heatmap";
|
|
|
6 |
|
7 |
const DEFAULT_COLOR = "#FF9D00";
|
8 |
|
@@ -30,12 +31,14 @@ const OpenSourceHeatmap: React.FC<OpenSourceHeatmapProps> = ({
|
|
30 |
calendarData[keyB].reduce((sum, day) => sum + day.count, 0) -
|
31 |
calendarData[keyA].reduce((sum, day) => sum + day.count, 0)
|
32 |
)
|
33 |
-
.map(([providerName, { color }]) => (
|
34 |
<Heatmap
|
35 |
key={providerName}
|
36 |
data={calendarData[providerName]}
|
37 |
color={color}
|
38 |
providerName={providerName}
|
|
|
|
|
39 |
/>
|
40 |
))}
|
41 |
</div>
|
@@ -49,31 +52,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
49 |
|
50 |
const authorColor = color || DEFAULT_COLOR;
|
51 |
|
52 |
-
const providers = {
|
53 |
-
[author as string]: {
|
54 |
-
color: authorColor as string,
|
55 |
-
authors: [author as string],
|
56 |
-
},
|
57 |
-
};
|
58 |
-
|
59 |
try {
|
60 |
-
const
|
61 |
-
const allData = await Promise.all(
|
62 |
-
entityTypes.map(async (type) => {
|
63 |
-
const response = await fetch(
|
64 |
-
`https://huggingface.co/api/${type}?author=${author}&sort=createdAt&direction=-1`,
|
65 |
-
);
|
66 |
-
const data = await response.json();
|
67 |
-
return data.map((item: any) => ({
|
68 |
-
createdAt: item.createdAt,
|
69 |
-
id: item.id,
|
70 |
-
type: type,
|
71 |
-
}));
|
72 |
-
}),
|
73 |
-
);
|
74 |
|
75 |
-
const
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
return {
|
79 |
props: {
|
@@ -88,7 +80,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
88 |
props: {
|
89 |
calendarData: {},
|
90 |
color: authorColor,
|
91 |
-
providers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
},
|
93 |
};
|
94 |
}
|
|
|
3 |
import { OpenSourceHeatmapProps } from "../../types/heatmap";
|
4 |
import { generateCalendarData } from "../../utils/calendar";
|
5 |
import Heatmap from "../../components/Heatmap";
|
6 |
+
import { fetchUserData, fetchAuthorData } from "../../utils/authors";
|
7 |
|
8 |
const DEFAULT_COLOR = "#FF9D00";
|
9 |
|
|
|
31 |
calendarData[keyB].reduce((sum, day) => sum + day.count, 0) -
|
32 |
calendarData[keyA].reduce((sum, day) => sum + day.count, 0)
|
33 |
)
|
34 |
+
.map(([providerName, { color, fullName, avatarUrl }]) => (
|
35 |
<Heatmap
|
36 |
key={providerName}
|
37 |
data={calendarData[providerName]}
|
38 |
color={color}
|
39 |
providerName={providerName}
|
40 |
+
fullName={fullName ?? providerName}
|
41 |
+
avatarUrl={avatarUrl ?? ''}
|
42 |
/>
|
43 |
))}
|
44 |
</div>
|
|
|
52 |
|
53 |
const authorColor = color || DEFAULT_COLOR;
|
54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
try {
|
56 |
+
const { fullName, avatarUrl } = await fetchUserData([author as string]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
+
const providers = {
|
59 |
+
[author as string]: {
|
60 |
+
color: authorColor as string,
|
61 |
+
authors: [author as string],
|
62 |
+
fullName,
|
63 |
+
avatarUrl,
|
64 |
+
},
|
65 |
+
};
|
66 |
+
|
67 |
+
const flatData = await fetchAuthorData(author as string);
|
68 |
+
const calendarData = generateCalendarData(flatData, [providers[author as string]]);
|
69 |
|
70 |
return {
|
71 |
props: {
|
|
|
80 |
props: {
|
81 |
calendarData: {},
|
82 |
color: authorColor,
|
83 |
+
providers: {
|
84 |
+
[author as string]: {
|
85 |
+
color: authorColor as string,
|
86 |
+
authors: [author as string],
|
87 |
+
fullName: author,
|
88 |
+
avatarUrl: null,
|
89 |
+
},
|
90 |
+
},
|
91 |
},
|
92 |
};
|
93 |
}
|
src/pages/index.tsx
CHANGED
@@ -6,55 +6,39 @@ import {
|
|
6 |
ModelData,
|
7 |
} from "../types/heatmap";
|
8 |
import Heatmap from "../components/Heatmap";
|
|
|
9 |
|
10 |
-
const
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
|
27 |
export async function getStaticProps() {
|
28 |
try {
|
29 |
-
const allAuthors =
|
30 |
-
({ authors }) => authors,
|
31 |
-
);
|
32 |
const uniqueAuthors = Array.from(new Set(allAuthors));
|
33 |
|
34 |
-
const
|
35 |
-
const
|
36 |
-
uniqueAuthors.flatMap((author) =>
|
37 |
-
entityTypes.map(async (type) => {
|
38 |
-
const response = await fetch(
|
39 |
-
`https://huggingface.co/api/${type}?author=${author}&sort=createdAt&direction=-1`,
|
40 |
-
);
|
41 |
-
const data = await response.json();
|
42 |
-
return data.map((item: any) => ({
|
43 |
-
createdAt: item.createdAt,
|
44 |
-
id: item.id,
|
45 |
-
type: type,
|
46 |
-
}));
|
47 |
-
}),
|
48 |
-
),
|
49 |
-
);
|
50 |
|
51 |
-
const
|
52 |
-
const calendarData = generateCalendarData(flatData, PROVIDERS_MAP);
|
53 |
|
54 |
return {
|
55 |
props: {
|
56 |
calendarData,
|
57 |
-
providers:
|
58 |
},
|
59 |
revalidate: 3600, // regenerate every hour
|
60 |
};
|
@@ -63,7 +47,7 @@ export async function getStaticProps() {
|
|
63 |
return {
|
64 |
props: {
|
65 |
calendarData: {},
|
66 |
-
providers:
|
67 |
},
|
68 |
revalidate: 60, // retry after 1 minute if there was an error
|
69 |
};
|
@@ -104,21 +88,31 @@ const OpenSourceHeatmap: React.FC<OpenSourceHeatmapProps> = ({
|
|
104 |
<p className="text-center">Loading...</p>
|
105 |
) : (
|
106 |
<div className="space-y-16">
|
107 |
-
{
|
108 |
-
.sort(
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
112 |
)
|
113 |
-
.map((
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
122 |
</div>
|
123 |
)}
|
124 |
</div>
|
|
|
6 |
ModelData,
|
7 |
} from "../types/heatmap";
|
8 |
import Heatmap from "../components/Heatmap";
|
9 |
+
import { fetchAllProvidersData, fetchAllAuthorsData } from "../utils/authors";
|
10 |
|
11 |
+
const PROVIDERS: ProviderInfo[] = [
|
12 |
+
{ color: "#ff7000", authors: ["mistralai"] },
|
13 |
+
{ color: "#1877F2", authors: ["meta-llama", "facebook", ] },
|
14 |
+
{ color: "#10A37F", authors: ["openai"] },
|
15 |
+
{ color: "#cc785c", authors: ["Anthropic"] },
|
16 |
+
{ color: "#DB4437", authors: ["google"] },
|
17 |
+
{ color: "#5E35B1", authors: ["allenai"] },
|
18 |
+
{ color: "#0088cc", authors: ["apple"] },
|
19 |
+
{ color: "#FEB800", authors: ["microsoft"] },
|
20 |
+
{ color: "#76B900", authors: ["nvidia"] },
|
21 |
+
{ color: "#0088cc", authors: ["deepseek-ai"] },
|
22 |
+
{ color: "#0088cc", authors: ["Qwen"] },
|
23 |
+
{ color: "#4C6EE6", authors: ["CohereForAI"] },
|
24 |
+
{ color: "#4C6EE6", authors: ["ibm-granite"] },
|
25 |
+
{ color: "#A020F0", authors: ["stabilityai"] },
|
26 |
+
];
|
27 |
|
28 |
export async function getStaticProps() {
|
29 |
try {
|
30 |
+
const allAuthors = PROVIDERS.flatMap(({ authors }) => authors);
|
|
|
|
|
31 |
const uniqueAuthors = Array.from(new Set(allAuthors));
|
32 |
|
33 |
+
const flatData: ModelData[] = await fetchAllAuthorsData(uniqueAuthors);
|
34 |
+
const updatedProviders = await fetchAllProvidersData(PROVIDERS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
+
const calendarData = generateCalendarData(flatData, updatedProviders);
|
|
|
37 |
|
38 |
return {
|
39 |
props: {
|
40 |
calendarData,
|
41 |
+
providers: updatedProviders,
|
42 |
},
|
43 |
revalidate: 3600, // regenerate every hour
|
44 |
};
|
|
|
47 |
return {
|
48 |
props: {
|
49 |
calendarData: {},
|
50 |
+
providers: PROVIDERS,
|
51 |
},
|
52 |
revalidate: 60, // retry after 1 minute if there was an error
|
53 |
};
|
|
|
88 |
<p className="text-center">Loading...</p>
|
89 |
) : (
|
90 |
<div className="space-y-16">
|
91 |
+
{providers
|
92 |
+
.sort((a, b) =>
|
93 |
+
calendarData[b.fullName || b.authors[0]].reduce(
|
94 |
+
(sum, day) => sum + day.count,
|
95 |
+
0
|
96 |
+
) -
|
97 |
+
calendarData[a.fullName || a.authors[0]].reduce(
|
98 |
+
(sum, day) => sum + day.count,
|
99 |
+
0
|
100 |
+
)
|
101 |
)
|
102 |
+
.map((provider) => {
|
103 |
+
const providerName = provider.fullName || provider.authors[0];
|
104 |
+
return (
|
105 |
+
<div key={providerName} className="flex flex-col items-center">
|
106 |
+
<Heatmap
|
107 |
+
data={calendarData[providerName]}
|
108 |
+
color={provider.color}
|
109 |
+
providerName={providerName}
|
110 |
+
fullName={provider.fullName ?? providerName}
|
111 |
+
avatarUrl={provider.avatarUrl ?? ''}
|
112 |
+
/>
|
113 |
+
</div>
|
114 |
+
);
|
115 |
+
})}
|
116 |
</div>
|
117 |
)}
|
118 |
</div>
|
src/types/heatmap.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
export interface ProviderInfo {
|
2 |
color: string;
|
3 |
authors: string[];
|
|
|
|
|
4 |
}
|
5 |
|
6 |
export interface ModelData {
|
@@ -22,5 +24,5 @@ export interface OpenSourceHeatmapProps {
|
|
22 |
calendarData: CalendarData;
|
23 |
author: string;
|
24 |
color: string;
|
25 |
-
providers:
|
26 |
-
}
|
|
|
1 |
export interface ProviderInfo {
|
2 |
color: string;
|
3 |
authors: string[];
|
4 |
+
fullName?: string;
|
5 |
+
avatarUrl?: string | null;
|
6 |
}
|
7 |
|
8 |
export interface ModelData {
|
|
|
24 |
calendarData: CalendarData;
|
25 |
author: string;
|
26 |
color: string;
|
27 |
+
providers: ProviderInfo[];
|
28 |
+
}
|
src/utils/authors.ts
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ProviderInfo, ModelData } from "../types/heatmap";
|
2 |
+
|
3 |
+
export async function fetchUserData(authors: string[]) {
|
4 |
+
const primaryAuthor = authors[0];
|
5 |
+
try {
|
6 |
+
const response = await fetch(`https://huggingface.co/api/users/${primaryAuthor}/overview`);
|
7 |
+
const data = await response.json();
|
8 |
+
return {
|
9 |
+
fullName: data.fullname || primaryAuthor,
|
10 |
+
avatarUrl: data.avatarUrl || null,
|
11 |
+
};
|
12 |
+
} catch (error) {
|
13 |
+
console.error(`Error fetching user data for ${primaryAuthor}:`, error);
|
14 |
+
return {
|
15 |
+
fullName: primaryAuthor,
|
16 |
+
avatarUrl: null,
|
17 |
+
};
|
18 |
+
}
|
19 |
+
}
|
20 |
+
|
21 |
+
export async function fetchAllProvidersData(providers: ProviderInfo[]): Promise<ProviderInfo[]> {
|
22 |
+
return Promise.all(providers.map(async (providerInfo) => {
|
23 |
+
const { fullName, avatarUrl } = await fetchUserData(providerInfo.authors);
|
24 |
+
return {
|
25 |
+
...providerInfo,
|
26 |
+
fullName,
|
27 |
+
avatarUrl: avatarUrl || null
|
28 |
+
};
|
29 |
+
}));
|
30 |
+
}
|
31 |
+
|
32 |
+
export async function fetchAuthorData(author: string): Promise<ModelData[]> {
|
33 |
+
const entityTypes = ["models", "datasets", "spaces"] as const;
|
34 |
+
try {
|
35 |
+
const allData = await Promise.all(
|
36 |
+
entityTypes.map(async (type) => {
|
37 |
+
const response = await fetch(
|
38 |
+
`https://huggingface.co/api/${type}?author=${author}&sort=createdAt&direction=-1`
|
39 |
+
);
|
40 |
+
if (!response.ok) {
|
41 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
42 |
+
}
|
43 |
+
const data = await response.json();
|
44 |
+
return data.map((item: any): ModelData => ({
|
45 |
+
createdAt: item.createdAt,
|
46 |
+
id: item.id,
|
47 |
+
}));
|
48 |
+
})
|
49 |
+
);
|
50 |
+
|
51 |
+
return allData.flat();
|
52 |
+
} catch (error) {
|
53 |
+
console.error(`Error fetching data for author ${author}:`, error);
|
54 |
+
return [];
|
55 |
+
}
|
56 |
+
}
|
57 |
+
|
58 |
+
export async function fetchAllAuthorsData(authors: string[]): Promise<ModelData[]> {
|
59 |
+
try {
|
60 |
+
const allData = await Promise.all(
|
61 |
+
authors.map(async (author) => await fetchAuthorData(author))
|
62 |
+
);
|
63 |
+
return allData.flat();
|
64 |
+
} catch (error) {
|
65 |
+
console.error("Error fetching data for all authors:", error);
|
66 |
+
return [];
|
67 |
+
}
|
68 |
+
}
|
src/utils/calendar.ts
CHANGED
@@ -7,10 +7,10 @@ import {
|
|
7 |
|
8 |
export const generateCalendarData = (
|
9 |
modelData: ModelData[],
|
10 |
-
providers:
|
11 |
): CalendarData => {
|
12 |
const data: Record<string, Activity[]> = Object.fromEntries(
|
13 |
-
|
14 |
);
|
15 |
|
16 |
const today = new Date();
|
@@ -23,11 +23,11 @@ export const generateCalendarData = (
|
|
23 |
|
24 |
modelData.forEach((item) => {
|
25 |
const dateString = item.createdAt.split("T")[0];
|
26 |
-
|
27 |
if (authors.some((author) => item.id.startsWith(author))) {
|
28 |
-
countMap[
|
29 |
-
countMap[
|
30 |
-
(countMap[
|
31 |
}
|
32 |
});
|
33 |
});
|
@@ -36,9 +36,9 @@ export const generateCalendarData = (
|
|
36 |
for (let d = new Date(startDate); d <= today; d.setDate(d.getDate() + 1)) {
|
37 |
const dateString = d.toISOString().split("T")[0];
|
38 |
|
39 |
-
|
40 |
-
const count = countMap[
|
41 |
-
data[
|
42 |
});
|
43 |
}
|
44 |
|
@@ -68,4 +68,4 @@ export const generateCalendarData = (
|
|
68 |
});
|
69 |
|
70 |
return data;
|
71 |
-
};
|
|
|
7 |
|
8 |
export const generateCalendarData = (
|
9 |
modelData: ModelData[],
|
10 |
+
providers: ProviderInfo[]
|
11 |
): CalendarData => {
|
12 |
const data: Record<string, Activity[]> = Object.fromEntries(
|
13 |
+
providers.map((provider) => [provider.authors[0], []]),
|
14 |
);
|
15 |
|
16 |
const today = new Date();
|
|
|
23 |
|
24 |
modelData.forEach((item) => {
|
25 |
const dateString = item.createdAt.split("T")[0];
|
26 |
+
providers.forEach(({ authors }) => {
|
27 |
if (authors.some((author) => item.id.startsWith(author))) {
|
28 |
+
countMap[authors[0]] = countMap[authors[0]] || {};
|
29 |
+
countMap[authors[0]][dateString] =
|
30 |
+
(countMap[authors[0]][dateString] || 0) + 1;
|
31 |
}
|
32 |
});
|
33 |
});
|
|
|
36 |
for (let d = new Date(startDate); d <= today; d.setDate(d.getDate() + 1)) {
|
37 |
const dateString = d.toISOString().split("T")[0];
|
38 |
|
39 |
+
providers.forEach(({ authors }) => {
|
40 |
+
const count = countMap[authors[0]]?.[dateString] || 0;
|
41 |
+
data[authors[0]].push({ date: dateString, count, level: 0 });
|
42 |
});
|
43 |
}
|
44 |
|
|
|
68 |
});
|
69 |
|
70 |
return data;
|
71 |
+
};
|