like-history / src /App.js
timqian's picture
set autoCapitalize to none and build
7c246aa
import { useEffect, useState } from "react";
import chartXkcd from "chart.xkcd";
function transformLikesData(likesData) {
// Step 1
likesData.sort((a, b) => new Date(a.likedAt) - new Date(b.likedAt));
// Step 2
const cumulativeLikes = {};
let cumulativeCount = 0;
// Step 3
likesData.forEach(like => {
const date = like.likedAt
cumulativeCount++;
cumulativeLikes[date] = cumulativeCount;
});
// Step 4
const transformedData = Object.keys(cumulativeLikes).map(date => ({
x: date,
y: cumulativeLikes[date].toString()
}));
return transformedData;
}
function getProjectsFromHash() {
let hash = window.location.hash;
console.log('hash', hash)
const projects = hash.replace("#", "").split('&').filter(project => project !== '');
return projects;
}
const initProjects = getProjectsFromHash();
function App() {
const [projectType, setProjectType] = useState("models");
const [projectName, setProjectName] = useState("");
const [hasGraph, setHasGraph] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [datasets, setDatasets] = useState([]);
function setHash() {
const hashes = datasets.map(dataset => dataset.label).join('&');
if (window.parent && window.parent.postMessage) {
window.parent.postMessage({
hash: hashes,
}, "*");
}
window.location.hash = hashes
}
async function getLikeHistory(projectPath) {
const res = await fetch(`https://huggingface.co/api/${projectPath}/likers?expand[]=likeAt`)
/**
* Format:
* [{"user": "timqian", "likedAt": "2021-07-01T00:00:00.000Z"}, {"user": "yy", "likedAt": "2021-07-02T00:00:00.000Z"}]
*/
const likers = await res.json()
let likeHistory = transformLikesData(likers)
if (likeHistory.length > 40) {
// sample 20 points
const sampledLikeHistory = []
const step = Math.floor(likeHistory.length / 20)
for (let i = 0; i < likeHistory.length; i += step) {
sampledLikeHistory.push(likeHistory[i])
}
// Add the last point if it's not included
if (sampledLikeHistory[sampledLikeHistory.length - 1].x !== likeHistory[likeHistory.length - 1].x) {
sampledLikeHistory.push(likeHistory[likeHistory.length - 1])
}
likeHistory = sampledLikeHistory
}
return likeHistory;
}
const onSubmit = async () => {
setIsLoading(true)
const likeHistory = await getLikeHistory(`${projectType}/${projectName}`);
// if likeHistory is empty, show error message
if (likeHistory.length === 0) {
setIsLoading(false)
alert("No like history found")
return
}
setDatasets([...datasets, {
label: `${projectType !== 'models' ? `${projectType}/` : ''}${projectName}`,
data: likeHistory,
}])
setHasGraph(true)
setIsLoading(false)
setProjectName("")
}
useEffect(() => {
const svg = document.querySelector('.line-chart')
if (datasets.length === 0) {
svg.innerHTML = ''
setHash()
return
}
// draw chart in next tick
new chartXkcd.XY(svg, {
title: 'Like History',
xLabel: 'Time',
yLabel: 'Likes',
data: {
datasets,
},
options: {
// unxkcdify: true,
xTickCount: 3,
yTickCount: 4,
legendPosition: chartXkcd.config.positionType.upLeft,
showLine: true,
timeFormat: 'MM/DD/YYYY',
dotSize: 0.5,
dataColors: [
"#FBBF24", // Warm Yellow
"#60A5FA", // Light Blue
"#14B8A6", // Teal
"#A78BFA", // Soft Purple
"#FF8C00", // Orange
"#64748B", // Slate Gray
"#FB7185", // Coral Pink
"#6EE7B7", // Mint Green
"#2563EB", // Deep Blue
"#374151" // Charcoal
]
},
});
setHash()
}, [datasets])
useEffect(() => {
function handleReceiveMessage(event) {
// You might want to check event.origin here for security if needed
// and ensure that event.data contains the properties you expect
if (event.data && typeof event.data === 'object' && 'hash' in event.data) {
// Update the hash of the parent window's URL
window.location.hash = event.data.hash;
console.log('hash')
console.log(window.location.hash)
}
}
// Add event listener for 'message' events
window.addEventListener('message', handleReceiveMessage);
// Clean up the event listener on component unmount
return () => {
window.removeEventListener('message', handleReceiveMessage);
};
}, []);
useEffect(() => {
const projects = initProjects;
if (projects.length <= 0) return;
async function getLikeHistoryAndDisplay() {
console.log('hi')
setIsLoading(true);
for (const project of projects) {
let projectPath = project.startsWith('spaces/') || project.startsWith('datasets/') ? project : `models/${project}`
const likeHistory = await getLikeHistory(projectPath);
setDatasets(prevDatasets => [...prevDatasets, {
label: project,
data: likeHistory,
}])
}
setIsLoading(false);
}
getLikeHistoryAndDisplay()
}, [])
return (
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-16">
<div className="mx-auto max-w-3xl">
<h1 className="text-sm font-light right-0 text-right text-gray-600">
View the like history of a project on <span className="font-semibold">huggingface</span> <span className="text-lg">🤗</span>
</h1>
<div className="mb-12">
<div className="relative mt-2 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center">
<label htmlFor="projectType" className="sr-only">
ProjectType
</label>
<select
id="projectType"
name="projectType"
autoComplete="projectType"
className="h-full rounded-md border-0 bg-transparent py-0 pl-3 pr-7 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm"
onChange={(e) => setProjectType(e.target.value)}
>
<option value="models">Model</option>
<option value="datasets">Dataset</option>
<option value="spaces">Space</option>
</select>
</div>
<input
type="text"
name="phone-number"
id="phone-number"
autoCapitalize="none"
className="block w-full rounded-md border-0 py-1.5 pl-24 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="openai/whisper-large"
value={projectName}
onChange={(e) => setProjectName(e.target.value.trim())}
onFocus={(e) => e.target.select()}
onKeyDown={async (e) => {
if (e.key === "Enter") {
try {
await onSubmit();
} catch (err) {
setIsLoading(false);
alert(`No like history found for ${projectName}, please check the name and try again`);
}
}
}}
disabled={isLoading}
/>
{
isLoading &&
<div className="absolute inset-y-0 right-0 flex items-center">
<svg className="animate-spin h-5 w-5 mr-3 text-gray-400" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z">
</path>
</svg>
</div>
}
</div>
</div>
<div className="relative min-w-sm">
{datasets.length > 0 &&
<div className="my-4 flex justify-end gap-1 flex-wrap">
{datasets.map(dataset =>
<button
key={dataset.label}
className="flex items-center justify-center gap-x-1 rounded-md px-2 py-1 text-xs font-medium text-gray-900 ring-1 ring-inset ring-gray-200 hover:bg-gray-100"
onClick={() => {
setDatasets(datasets.filter(ds => ds.label !== dataset.label));
}}
>
<span>{dataset.label}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>)
}
</div>
}
<svg className="line-chart"></svg>
{
hasGraph &&
<span className="text-slate-500 absolute bottom-0 right-8" style={{ fontFamily: "xkcd" }}>🤗 like-history.ai</span>
}
</div>
<a
className={`${!hasGraph ? "mt-64" : "mt-12"} flex gap-x-2 text-slate-600 justify-end items-center text-xl`}
href="https://chromewebstore.google.com/detail/like-history/ockfibaidgopelphgdgcnfijdnhnmpek"
target="_blank" rel="noreferrer"
>
<img className="w-6 inline" src="/extension.svg" /> Install the chrome extension
</a>
</div>
</div>
);
}
export default App;