import { Spinner } from './Spinner' import React, { useState, memo, useRef } from 'react' import debounce from 'debounce' // const usersCache = new Map() type AccountDetails = { user: string fullname: string avatarUrl: string followed_by: Set // list of usernames followers_count: number details: string } async function accountFollows( handle: string, limit: number, logError: (x: string) => void ): Promise> { let nextPage: | string | null = `https://huggingface.co/api/users/${handle}/following` let data: Array = [] while (nextPage && data.length <= limit) { console.log(`Get page: ${nextPage}`) let response let page try { response = await fetch(nextPage) if (response.status !== 200) { throw new Error('HTTP request failed') } page = await response.json() } catch (e) { logError(`Error while retrieving follows for ${handle}.`) break } if (!page.map) { break } // const newData = await Promise.all( // page.map(async (account) => { // const user = account.user // if (!usersCache.has(user)) { // const details = await accountDetails(user, logError) // // const followers_count = await accountFollowersCount(user, logError) // usersCache.set(user, { ...account, details }) // } // return usersCache.get(user) // }) // ) // data = [...data, ...newData] data = [...data, ...page] nextPage = getNextPage(response.headers.get('Link')) } return data } async function organizationMembers( organization: string, logError: (x: string) => void ): Promise> { let nextPage: | string | null = `https://huggingface.co/api/organizations/${organization}/members` let members: Array = [] while (nextPage) { console.log(`Get page: ${nextPage}`) let response let page try { response = await fetch(nextPage) if (response.status !== 200) { throw new Error('HTTP request failed') } page = await response.json() } catch (e) { logError(`Error while retrieving members for ${organization}.`) break } if (!page.map) { break } members = [...members, ...page.map(({ user }) => user)] nextPage = getNextPage(response.headers.get('Link')) } return members } // async function accountFollowersCount( // handle: string, // logError: (x: string) => void // ): Promise { // let nextPage: // | string // | null = `https://huggingface.co/api/users/${handle}/followers` // let count = 0 // while (nextPage) { // console.log(`Get page: ${nextPage}`) // let response // let page // try { // response = await fetch(nextPage) // if (response.status !== 200) { // throw new Error('HTTP request failed') // } // page = await response.json() // } catch (e) { // logError(`Error while retrieving followers for ${handle}.`) // break // } // if (!page.map) { // break // } // count += page.length // nextPage = getNextPage(response.headers.get('Link')) // } // return count // } // async function accountDetails( // handle: string, // logError: (x: string) => void // ): Promise { // let page // try { // let response = await fetch( // `https://huggingface.co/api/users/${handle}/overview` // ) // if (response.status !== 200) { // throw new Error('HTTP request failed') // } // let page = await response.json() // return page?.details ?? '' // } catch (e) { // logError(`Error while retrieving details for ${handle}.`) // } // return '' // } async function accountFofs( handle: string, setProgress: (x: Array) => void, setFollows: (x: Array) => void, logError: (x: string) => void ): Promise { const hfMembers = await organizationMembers('huggingface', logError) const directFollows = await accountFollows(handle, 2000, logError) setProgress([0, directFollows.length]) let progress = 0 const directFollowIds = new Set([ handle, ...directFollows.map(({ user }) => user), ...hfMembers, ]) const indirectFollowLists: Array> = [] const updateList = debounce(() => { let indirectFollows: Array = [].concat( [], ...indirectFollowLists ) const indirectFollowMap = new Map() indirectFollows .filter( // exclude direct follows ({ user }) => !directFollowIds.has(user) ) .map((account) => { const acct = account.user if (indirectFollowMap.has(acct)) { const otherAccount = indirectFollowMap.get(acct) account.followed_by = new Set([ ...Array.from(account.followed_by.values()), ...otherAccount.followed_by, ]) } indirectFollowMap.set(acct, account) }) const list = Array.from(indirectFollowMap.values()).sort((a, b) => { if (a.followed_by.size != b.followed_by.size) { return b.followed_by.size - a.followed_by.size } return b.followers_count - a.followers_count }) setFollows(list) }, 2000) await Promise.all( directFollows.map(async ({ user }) => { const follows = await accountFollows(user, 200, logError) progress++ setProgress([progress, directFollows.length]) indirectFollowLists.push( follows.map((account) => ({ ...account, followed_by: new Set([user]) })) ) updateList() }) ) updateList.flush() } function getNextPage(linkHeader: string | null): string | null { if (!linkHeader) { return null } // Example header: // Link: ; rel="next", ; rel="prev" const match = linkHeader.match(/<(.+)>; rel="next"/) if (match && match.length > 0) { return match[1] } return null } function matchesSearch(account: AccountDetails, search: string): boolean { if (/^\s*$/.test(search)) { return true } const sanitizedSearch = search.replace(/^\s+|\s+$/, '').toLocaleLowerCase() if (account.user.toLocaleLowerCase().includes(sanitizedSearch)) { return true } if (account.fullname.toLocaleLowerCase().includes(sanitizedSearch)) { return true } return false } export function Content({}) { const [handle, setHandle] = useState('') const [follows, setFollows] = useState>([]) const [isLoading, setLoading] = useState(false) const [isDone, setDone] = useState(false) const [[numLoaded, totalToLoad], setProgress] = useState>([ 0, 0, ]) const [errors, setErrors] = useState>([]) async function search(handle: string) { setErrors([]) setLoading(true) setDone(false) setFollows([]) setProgress([0, 0]) await accountFofs(handle, setProgress, setFollows, (error) => setErrors((e) => [...e, error]) ) setLoading(false) setDone(true) } return (
{ search(handle) e.preventDefault() return false }} >
setHandle(e.target.value)} className="form-control block w-80 px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-gray-200 dark:focus:bg-gray-900 dark:focus:text-gray-200 " id="huggingFaceHandle" aria-describedby="huggingFaceHandleHelp" placeholder="merve" /> {isLoading ? (

Loaded {numLoaded} of {totalToLoad}...

) : null} {isDone && follows.length === 0 ? (
Info
No results found. Please double check for typos in the username, and ensure that you follow at least a few people to seed the search. Otherwise, try again later as Hugging Face may throttle requests.
) : null}
{isDone || follows.length > 0 ? : null}
) } const AccountDetails = memo(({ account }: { account: AccountDetails }) => { const { avatarUrl, fullname, user, followed_by } = account const [expandedFollowers, setExpandedFollowers] = useState(false) const hasAvatar = avatarUrl && !avatarUrl.endsWith('.svg') return (
  • {hasAvatar ? ( {`${fullname}'s ) : (

    {fullname}

    Followed by{' '} {followed_by.size < 9 || expandedFollowers ? ( Array.from(followed_by.values()).map((handle, idx) => ( {handle.replace(/@.+/, '')} {idx === followed_by.size - 1 ? '.' : ', '} )) ) : ( <> . )}
  • ) }) AccountDetails.displayName = 'AccountDetails' function ErrorLog({ errors }: { errors: Array }) { const [expanded, setExpanded] = useState(false) return ( <> {errors.length > 0 ? (
    Found{' '} {expanded ? ':' : '.'} {expanded ? errors.map((err) => (

    {err}

    )) : null}
    ) : null} ) } function Results({ follows }: { follows: Array }) { let [search, setSearch] = useState('') const [isLoading, setLoading] = useState(false) const updateSearch = useRef( debounce((s: string) => { setLoading(false) setSearch(s) }, 1500) ).current follows = follows.filter((acc) => matchesSearch(acc, search)).slice(0, 500) return (
    {follows.length === 0 ? (

    No results found.

    ) : null}
      {follows.map((account) => ( ))}
    ) } function SearchInput({ onChange }: { onChange: (s: string) => void }) { let [search, setSearchInputValue] = useState('') return ( { setSearchInputValue(e.target.value) onChange(e.target.value) }} className=" form-control block w-80 px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-900 focus:bg-white focus:border-green-600 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-gray-200 dark:focus:bg-gray-900 dark:focus:text-gray-200" /> ) }