Spaces:
Running
Running
| import { useEffect, useState } from "react"; | |
| import chartXkcd from "chart.xkcd"; | |
| function transformLikesData(likesData) { | |
| // Step 1: Sort by likedAt to ensure chronological order and easily find the first like date. | |
| likesData.sort((a, b) => new Date(a.likedAt) - new Date(b.likedAt)); | |
| if (likesData.length === 0) { | |
| return []; | |
| } | |
| // Determine the start date for this specific project (first like date) | |
| const startDate = new Date(likesData[0].likedAt); | |
| // Set time to 00:00:00.000 UTC for accurate day calculation | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| const cumulativeLikesByDay = {}; | |
| let cumulativeCount = 0; | |
| // Step 2 & 3: Calculate cumulative likes and days since start | |
| likesData.forEach(like => { | |
| cumulativeCount++; | |
| const currentDate = new Date(like.likedAt); | |
| // Set time to 00:00:00.000 UTC for current date too | |
| currentDate.setUTCHours(0, 0, 0, 0); | |
| const timeDiff = currentDate.getTime() - startDate.getTime(); | |
| // Calculate days since startDate. Math.floor ensures we get full days. | |
| const daysSinceStart = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); | |
| // Store the latest cumulative count for this specific day | |
| cumulativeLikesByDay[daysSinceStart] = cumulativeCount; | |
| }); | |
| // Step 4: Convert the object back into an array of { x: days, y: likes } | |
| // Ensure the keys (days) are sorted numerically for the chart | |
| const transformedData = Object.keys(cumulativeLikesByDay) | |
| .sort((a, b) => parseInt(a) - parseInt(b)) | |
| .map(day => ({ | |
| x: parseInt(day), // x-axis will be numbers (days since first like) | |
| y: cumulativeLikesByDay[day].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: 'Days since first like', // Changed xLabel to reflect days | |
| yLabel: 'Likes', | |
| data: { | |
| datasets, | |
| }, | |
| options: { | |
| // unxkcdify: true, | |
| xTickCount: 3, | |
| yTickCount: 4, | |
| legendPosition: chartXkcd.config.positionType.upLeft, | |
| showLine: true, | |
| // Removed timeFormat as x-axis is now numerical days | |
| 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() { | |
| setIsLoading(true); | |
| const newDatasets = []; // Create a temporary array to store new datasets | |
| for (const project of projects) { | |
| let projectPath = project.startsWith('spaces/') || project.startsWith('datasets/') ? project : `models/${project}` | |
| const likeHistory = await getLikeHistory(projectPath); | |
| newDatasets.push({ // Add to the temporary array | |
| label: project, | |
| data: likeHistory, | |
| }) | |
| } | |
| setDatasets(newDatasets); // Set state once after all fetches are done | |
| 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" }}>🤗 forked from like-history.ai</span> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; |