Spaces:
Running
Running
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; | |