chore: allow users to import hf repo (#5103)

* chore: allow users to input HF repo id / url

* chore: allow users to search HuggingFace models

* chore: normalize input

* chore: normalize input from FE

* chore: clean up

* chore: clean up

* fix: conflict

* fix: model name from metada instead id

* chore: enable ryhype raw for desc card hub

* fix: broken link

* chore: remove log

---------

Co-authored-by: Faisal Amir <urmauur@gmail.com>
This commit is contained in:
Louis 2025-05-26 15:13:22 +07:00 committed by GitHub
parent b8de48c9e9
commit 64f5703461
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 11 deletions

View File

@ -36,7 +36,7 @@ export function CardItem({
)} )}
> >
<div className="space-y-1.5"> <div className="space-y-1.5">
<h1 className="font-medium">{title}</h1> <h1 className="font-medium line-clamp-1">{title}</h1>
{description && ( {description && (
<span className="text-main-view-fg/70 leading-normal"> <span className="text-main-view-fg/70 leading-normal">
{description} {description}

View File

@ -14,15 +14,18 @@ import { cn } from '@/lib/utils'
import { useCodeblock } from '@/hooks/useCodeblock' import { useCodeblock } from '@/hooks/useCodeblock'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import { IconCopy, IconCopyCheck } from '@tabler/icons-react' import { IconCopy, IconCopyCheck } from '@tabler/icons-react'
import rehypeRaw from 'rehype-raw'
interface MarkdownProps { interface MarkdownProps {
content: string content: string
className?: string className?: string
components?: Components components?: Components
enableRawHtml?: boolean
} }
function RenderMarkdownComponent({ function RenderMarkdownComponent({
content, content,
enableRawHtml,
className, className,
components, components,
}: MarkdownProps) { }: MarkdownProps) {
@ -147,7 +150,9 @@ function RenderMarkdownComponent({
}, []) }, [])
// Memoize the rehypePlugins to prevent unnecessary re-renders // Memoize the rehypePlugins to prevent unnecessary re-renders
const rehypePlugins = useMemo(() => [rehypeKatex], []) const rehypePlugins = useMemo(() => {
return enableRawHtml ? [rehypeKatex, rehypeRaw] : [rehypeKatex]
}, [enableRawHtml])
// Merge custom components with default components // Merge custom components with default components
const mergedComponents = useMemo( const mergedComponents = useMemo(

View File

@ -2,7 +2,14 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { route } from '@/constants/routes' import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources' import { useModelSources } from '@/hooks/useModelSources'
import { cn, fuzzySearch, toGigabytes } from '@/lib/utils' import { cn, fuzzySearch, toGigabytes } from '@/lib/utils'
import { useState, useMemo, useEffect, ChangeEvent, useCallback } from 'react' import {
useState,
useMemo,
useEffect,
ChangeEvent,
useCallback,
useRef,
} from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useModelProvider } from '@/hooks/useModelProvider' import { useModelProvider } from '@/hooks/useModelProvider'
import { Card, CardItem } from '@/containers/Card' import { Card, CardItem } from '@/containers/Card'
@ -16,10 +23,11 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { downloadModel } from '@/services/models' import { addModelSource, downloadModel } from '@/services/models'
import { useDownloadStore } from '@/hooks/useDownloadStore' import { useDownloadStore } from '@/hooks/useDownloadStore'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
import { Loader } from 'lucide-react'
type ModelProps = { type ModelProps = {
model: { model: {
@ -47,6 +55,10 @@ function Hub() {
const [expandedModels, setExpandedModels] = useState<Record<string, boolean>>( const [expandedModels, setExpandedModels] = useState<Record<string, boolean>>(
{} {}
) )
const [isSearching, setIsSearching] = useState(false)
const addModelSourceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null
)
const toggleModelExpansion = (modelId: string) => { const toggleModelExpansion = (modelId: string) => {
setExpandedModels((prev) => ({ setExpandedModels((prev) => ({
@ -87,7 +99,26 @@ function Hub() {
}, [fetchSources]) }, [fetchSources])
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setIsSearching(false)
setSearchValue(e.target.value) setSearchValue(e.target.value)
if (addModelSourceTimeoutRef.current) {
clearTimeout(addModelSourceTimeoutRef.current)
}
if (
e.target.value.length &&
(e.target.value.includes('/') || e.target.value.startsWith('http'))
) {
setIsSearching(true)
addModelSourceTimeoutRef.current = setTimeout(() => {
addModelSource(e.target.value)
.then(() => {
fetchSources()
})
.finally(() => {
setIsSearching(false)
})
}, 500)
}
} }
const { downloads } = useDownloadStore() const { downloads } = useDownloadStore()
@ -163,10 +194,14 @@ function Hub() {
<div className="flex flex-col h-full w-full"> <div className="flex flex-col h-full w-full">
<HeaderPage> <HeaderPage>
<div className="pr-4 py-3 border-b border-main-view-fg/5 h-10 w-full flex items-center justify-between relative z-20 "> <div className="pr-4 py-3 border-b border-main-view-fg/5 h-10 w-full flex items-center justify-between relative z-20 ">
<div className="flex items-center gap-2 w-full pr-4"> <div className="flex items-center gap-2 w-full">
<IconSearch className="text-main-view-fg/60" size={14} /> {isSearching ? (
<Loader className="size-4 animate-spin text-main-view-fg/60" />
) : (
<IconSearch className="text-main-view-fg/60" size={14} />
)}
<input <input
placeholder="Search models..." placeholder="Search for models on Hugging Face..."
value={searchValue} value={searchValue}
onChange={handleSearchChange} onChange={handleSearchChange}
className="w-full focus:outline-none" className="w-full focus:outline-none"
@ -223,11 +258,13 @@ function Hub() {
header={ header={
<div className="flex items-center justify-between gap-x-2"> <div className="flex items-center justify-between gap-x-2">
<Link <Link
to={`https://huggingface.co/${model.id}` as string} to={
`https://huggingface.co/${model.metadata?.id}` as string
}
target="_blank" target="_blank"
> >
<h1 className="text-main-view-fg font-medium text-base capitalize truncate"> <h1 className="text-main-view-fg font-medium text-base capitalize truncate">
{extractModelName(model.id) || ''} {extractModelName(model.metadata?.id) || ''}
</h1> </h1>
</Link> </Link>
<div className="shrink-0 space-x-3 flex items-center"> <div className="shrink-0 space-x-3 flex items-center">
@ -241,6 +278,8 @@ function Hub() {
> >
<div className="line-clamp-2 mt-3 text-main-view-fg/60"> <div className="line-clamp-2 mt-3 text-main-view-fg/60">
<RenderMarkdown <RenderMarkdown
enableRawHtml={true}
className="select-none"
components={{ components={{
a: ({ ...props }) => ( a: ({ ...props }) => (
<a <a
@ -251,7 +290,8 @@ function Hub() {
), ),
}} }}
content={ content={
extractDescription(model.metadata.description) || '' extractDescription(model.metadata?.description) ||
''
} }
/> />
</div> </div>
@ -303,7 +343,6 @@ function Hub() {
title={variant.id} title={variant.id}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* {defaultVariant && <>test</>} */}
<p className="text-main-view-fg/70 font-medium text-xs"> <p className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(variant.size)} {toGigabytes(variant.size)}
</p> </p>