File size: 9,774 Bytes
2408763
574b4e5
3e887f8
362fadb
 
 
574b4e5
362fadb
 
 
 
 
 
888a56b
362fadb
 
 
 
 
 
 
 
 
 
 
 
97f2719
2408763
c2d4e9c
6cc6e46
2408763
 
 
 
6cc6e46
 
09ab9c0
362fadb
97f2719
 
 
2408763
574b4e5
2408763
 
3e887f8
2408763
 
 
 
 
 
 
 
 
 
362fadb
 
 
 
 
 
 
 
 
 
 
 
888a56b
362fadb
 
 
97f2719
 
 
 
362fadb
 
 
2408763
 
 
 
 
 
 
362fadb
 
97f2719
362fadb
 
 
 
2408763
 
97f2719
2408763
 
 
 
 
 
97f2719
2408763
 
6cc6e46
2408763
 
 
 
97f2719
362fadb
97f2719
362fadb
 
574b4e5
97f2719
362fadb
 
97f2719
362fadb
 
 
 
 
 
97f2719
 
 
 
 
 
 
 
 
 
 
362fadb
574b4e5
 
2408763
 
 
 
e050f32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2408763
6cc6e46
2408763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09ab9c0
9054fbb
 
3e887f8
97f2719
1465ed6
2408763
3e887f8
 
2408763
 
3e887f8
 
2408763
 
 
3e887f8
574b4e5
3e887f8
362fadb
 
 
3e887f8
 
 
 
 
 
7c246aa
3e887f8
2ce75b3
574b4e5
c6b2cb1
362fadb
 
574b4e5
362fadb
 
 
97f2719
362fadb
 
574b4e5
 
97f2719
3e887f8
9be25dc
97f2719
 
 
 
 
 
 
 
 
 
 
 
 
3e887f8
 
9be25dc
 
 
2408763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574b4e5
97f2719
 
 
 
574b4e5
9be25dc
2408763
 
 
 
 
9be25dc
 
9054fbb
09ab9c0
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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;