Vokturz commited on
Commit
322c234
·
1 Parent(s): ce51997

Add lodash and its types, enhance model loading with custom model support, and improve pipeline handling

Browse files
package.json CHANGED
@@ -15,6 +15,7 @@
15
  "@types/react-dom": "^19.1.6",
16
  "build": "^0.1.4",
17
  "dotenv": "^17.0.1",
 
18
  "lucide-react": "^0.525.0",
19
  "path-browserify": "^1.0.1",
20
  "react": "^19.1.0",
@@ -51,6 +52,7 @@
51
  ]
52
  },
53
  "devDependencies": {
 
54
  "@types/react-syntax-highlighter": "^15.5.13",
55
  "tailwindcss": "3"
56
  }
 
15
  "@types/react-dom": "^19.1.6",
16
  "build": "^0.1.4",
17
  "dotenv": "^17.0.1",
18
+ "lodash": "^4.17.21",
19
  "lucide-react": "^0.525.0",
20
  "path-browserify": "^1.0.1",
21
  "react": "^19.1.0",
 
52
  ]
53
  },
54
  "devDependencies": {
55
+ "@types/lodash": "^4.17.20",
56
  "@types/react-syntax-highlighter": "^15.5.13",
57
  "tailwindcss": "3"
58
  }
pnpm-lock.yaml CHANGED
@@ -44,6 +44,9 @@ importers:
44
  dotenv:
45
  specifier: ^17.0.1
46
  version: 17.0.1
 
 
 
47
  lucide-react:
48
  specifier: ^0.525.0
49
  version: 0.525.0(react@19.1.0)
@@ -75,6 +78,9 @@ importers:
75
  specifier: ^2.1.4
76
  version: 2.1.4
77
  devDependencies:
 
 
 
78
  '@types/react-syntax-highlighter':
79
  specifier: ^15.5.13
80
  version: 15.5.13
@@ -1557,6 +1563,9 @@ packages:
1557
  '@types/json5@0.0.29':
1558
  resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
1559
 
 
 
 
1560
  '@types/mdast@4.0.4':
1561
  resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
1562
 
@@ -8192,6 +8201,8 @@ snapshots:
8192
 
8193
  '@types/json5@0.0.29': {}
8194
 
 
 
8195
  '@types/mdast@4.0.4':
8196
  dependencies:
8197
  '@types/unist': 3.0.3
 
44
  dotenv:
45
  specifier: ^17.0.1
46
  version: 17.0.1
47
+ lodash:
48
+ specifier: ^4.17.21
49
+ version: 4.17.21
50
  lucide-react:
51
  specifier: ^0.525.0
52
  version: 0.525.0(react@19.1.0)
 
78
  specifier: ^2.1.4
79
  version: 2.1.4
80
  devDependencies:
81
+ '@types/lodash':
82
+ specifier: ^4.17.20
83
+ version: 4.17.20
84
  '@types/react-syntax-highlighter':
85
  specifier: ^15.5.13
86
  version: 15.5.13
 
1563
  '@types/json5@0.0.29':
1564
  resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
1565
 
1566
+ '@types/lodash@4.17.20':
1567
+ resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
1568
+
1569
  '@types/mdast@4.0.4':
1570
  resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
1571
 
 
8201
 
8202
  '@types/json5@0.0.29': {}
8203
 
8204
+ '@types/lodash@4.17.20': {}
8205
+
8206
  '@types/mdast@4.0.4':
8207
  dependencies:
8208
  '@types/unist': 3.0.3
src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react'
2
  import PipelineSelector from './components/PipelineSelector'
3
  import ZeroShotClassification from './components/ZeroShotClassification'
4
  import TextClassification from './components/TextClassification'
@@ -11,15 +11,18 @@ import ModelReadme from './components/ModelReadme'
11
 
12
  function App() {
13
  const { pipeline, setPipeline, setModels, setModelInfo, modelInfo } = useModel()
 
14
 
15
  useEffect(() => {
16
  setModelInfo(null)
17
  const fetchModels = async () => {
 
18
  const fetchedModels = await getModelsByPipeline(pipeline)
19
  setModels(fetchedModels)
 
20
  }
21
  fetchModels()
22
- }, [setModels, pipeline])
23
 
24
  return (
25
  <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
@@ -44,12 +47,12 @@ function App() {
44
  <span className="text-lg font-semibold text-gray-900 block">
45
  Select Model
46
  </span>
47
- <ModelSelector />
48
  </div>
49
  </div>
50
 
51
  <div className="ml-6">
52
- <ModelInfo />
53
  </div>
54
  </div>
55
 
 
1
+ import { useEffect, useState } from 'react'
2
  import PipelineSelector from './components/PipelineSelector'
3
  import ZeroShotClassification from './components/ZeroShotClassification'
4
  import TextClassification from './components/TextClassification'
 
11
 
12
  function App() {
13
  const { pipeline, setPipeline, setModels, setModelInfo, modelInfo } = useModel()
14
+ const [isFetching, setIsFetching] = useState(false)
15
 
16
  useEffect(() => {
17
  setModelInfo(null)
18
  const fetchModels = async () => {
19
+ setIsFetching(true)
20
  const fetchedModels = await getModelsByPipeline(pipeline)
21
  setModels(fetchedModels)
22
+ setIsFetching(false)
23
  }
24
  fetchModels()
25
+ }, [setModels, setModelInfo, pipeline])
26
 
27
  return (
28
  <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
 
47
  <span className="text-lg font-semibold text-gray-900 block">
48
  Select Model
49
  </span>
50
+ <ModelSelector isFetching={isFetching} />
51
  </div>
52
  </div>
53
 
54
  <div className="ml-6">
55
+ <ModelInfo isFetching={isFetching} />
56
  </div>
57
  </div>
58
 
src/components/ModelInfo.tsx CHANGED
@@ -12,7 +12,7 @@ import { getModelSize } from '../lib/huggingface'
12
  import { useModel } from '../contexts/ModelContext'
13
  import ModelLoader from './ModelLoader'
14
 
15
- const ModelInfo = () => {
16
  const formatNumber = (num: number) => {
17
  if (num >= 1000000000) {
18
  return (num / 1000000000).toFixed(1) + 'B'
@@ -64,10 +64,11 @@ const ModelInfo = () => {
64
  </div>
65
  )
66
 
67
- if (!modelInfo) {
68
  return <ModelInfoSkeleton />
69
  }
70
 
 
71
  return (
72
  <div className="mt-5 bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-3 rounded-lg border border-blue-200 space-y-3 h-full min-h-[150px]">
73
  {/* Model Name Row */}
@@ -77,7 +78,7 @@ const ModelInfo = () => {
77
  href={`https://huggingface.co/${modelInfo.name}`}
78
  target="_blank"
79
  rel="noopener noreferrer"
80
- className="text-sm font-medium text-gray-700 truncate max-w-100 hover:underline"
81
  title={modelInfo.name}
82
  >
83
  <ExternalLink className="w-3 h-3 inline-block mr-1" />
@@ -160,8 +161,7 @@ const ModelInfo = () => {
160
  {/* Incompatibility Message */}
161
  {modelInfo.isCompatible === false && modelInfo.incompatibilityReason && (
162
  <div className="bg-red-50 border border-red-200 rounded-md px-3 py-2">
163
- <p className="text-sm text-red-700">
164
- <span className="font-medium">Incompatible:</span>{' '}
165
  {modelInfo.incompatibilityReason}
166
  </p>
167
  </div>
 
12
  import { useModel } from '../contexts/ModelContext'
13
  import ModelLoader from './ModelLoader'
14
 
15
+ const ModelInfo = ({ isFetching }: { isFetching: boolean }) => {
16
  const formatNumber = (num: number) => {
17
  if (num >= 1000000000) {
18
  return (num / 1000000000).toFixed(1) + 'B'
 
64
  </div>
65
  )
66
 
67
+ if (!modelInfo || isFetching) {
68
  return <ModelInfoSkeleton />
69
  }
70
 
71
+
72
  return (
73
  <div className="mt-5 bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-3 rounded-lg border border-blue-200 space-y-3 h-full min-h-[150px]">
74
  {/* Model Name Row */}
 
78
  href={`https://huggingface.co/${modelInfo.name}`}
79
  target="_blank"
80
  rel="noopener noreferrer"
81
+ className="text-sm font-medium text-gray-700 truncate max-w-80 hover:underline"
82
  title={modelInfo.name}
83
  >
84
  <ExternalLink className="w-3 h-3 inline-block mr-1" />
 
161
  {/* Incompatibility Message */}
162
  {modelInfo.isCompatible === false && modelInfo.incompatibilityReason && (
163
  <div className="bg-red-50 border border-red-200 rounded-md px-3 py-2">
164
+ <p className="text-sm text-red-700 whitespace-break-spaces">
 
165
  {modelInfo.incompatibilityReason}
166
  </p>
167
  </div>
src/components/ModelReadme.tsx CHANGED
@@ -13,31 +13,23 @@ const ModelReadme = ({ readme, pipeline, modelName}: ModelReadmeProps) => {
13
  return (
14
  <div className="mt-2">
15
  <Disclosure>
16
- {({ open }) => (
17
- <>
18
- <DisclosureButton className="flex justify-between items-center w-full px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors">
19
- <div className="flex items-center text-sm text-gray-600">
20
- <FileText className="w-4 h-4 mr-2" />
21
- README.md
22
- </div>
23
- <div className="flex items-center space-x-2">
24
- <div className="text-xs text-gray-400">
25
- <i>{pipeline}</i> {modelName}
26
- </div>
27
- <ChevronDown
28
- className={`w-4 h-4 text-gray-400 transition-transform ${
29
- open ? 'rotate-180' : ''
30
- }`}
31
- />
32
- </div>
33
- </DisclosureButton>
34
- <DisclosurePanel className="px-4 py-5 bg-gray-50 rounded-b-lg border-l border-r border-b border-gray-200 max-h-[600px] overflow-y-auto">
35
- <div className="text-sm text-gray-800">
36
- <MarkdownRenderer content={readme} />
37
- </div>
38
- </DisclosurePanel>
39
- </>
40
- )}
41
  </Disclosure>
42
  </div>
43
  )
 
13
  return (
14
  <div className="mt-2">
15
  <Disclosure>
16
+ <DisclosureButton className="flex justify-between items-center w-full px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors">
17
+ <div className="flex items-center text-sm text-gray-600">
18
+ <FileText className="w-4 h-4 mr-2" />
19
+ README.md
20
+ </div>
21
+ <div className="flex items-center space-x-2">
22
+ <div className="text-xs text-gray-400">
23
+ <i>{pipeline}</i> {modelName}
24
+ </div>
25
+ <ChevronDown className="w-4 h-4 text-gray-400 transition-transform ui-open:rotate-180" />
26
+ </div>
27
+ </DisclosureButton>
28
+ <DisclosurePanel className="px-4 py-5 bg-gray-50 rounded-b-lg border-l border-r border-b border-gray-200 max-h-[600px] overflow-y-auto">
29
+ <div className="text-sm text-gray-800">
30
+ <MarkdownRenderer content={readme} />
31
+ </div>
32
+ </DisclosurePanel>
 
 
 
 
 
 
 
 
33
  </Disclosure>
34
  </div>
35
  )
src/components/ModelSelector.tsx CHANGED
@@ -8,14 +8,29 @@ import {
8
  } from '@headlessui/react'
9
  import { useModel } from '../contexts/ModelContext'
10
  import { getModelInfo } from '../lib/huggingface'
11
- import { Heart, Download, ChevronDown, Check, ArrowDown, ArrowUp } from 'lucide-react'
 
 
 
 
 
 
 
 
 
 
12
 
13
  type SortOption = 'likes' | 'downloads' | 'createdAt' | 'name'
14
 
15
- const ModelSelector: React.FC = () => {
16
  const { models, setModelInfo, modelInfo, pipeline } = useModel()
17
  const [sortBy, setSortBy] = useState<SortOption>('downloads')
18
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
 
 
 
 
 
19
 
20
  const formatNumber = (num: number) => {
21
  if (num >= 1000000000) {
@@ -57,9 +72,9 @@ const ModelSelector: React.FC = () => {
57
 
58
  // Function to fetch detailed model info and set as selected
59
  const fetchAndSetModelInfo = useCallback(
60
- async (modelId: string) => {
61
  try {
62
- const modelInfoResponse = await getModelInfo(modelId)
63
 
64
  let parameters = 0
65
  if (modelInfoResponse.safetensors) {
@@ -87,26 +102,28 @@ const ModelSelector: React.FC = () => {
87
  baseId: modelInfoResponse.baseId,
88
  readme: modelInfoResponse.readme
89
  }
90
-
91
-
92
  setModelInfo(modelInfo)
 
93
  } catch (error) {
94
  console.error('Error fetching model info:', error)
 
95
  }
96
  },
97
- [setModelInfo]
98
  )
99
 
100
  // Update modelInfo to first model when pipeline changes
101
  useEffect(() => {
102
- if (models.length > 0) {
 
 
103
  const firstModel = models[0]
104
- fetchAndSetModelInfo(firstModel.id)
105
  }
106
- }, [pipeline, models, fetchAndSetModelInfo])
107
 
108
  const handleModelSelect = (modelId: string) => {
109
- fetchAndSetModelInfo(modelId)
110
  }
111
 
112
  const handleSortChange = (newSortBy: SortOption) => {
@@ -118,6 +135,46 @@ const ModelSelector: React.FC = () => {
118
  }
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  const selectedModel =
122
  models.find((model) => model.id === modelInfo?.id) || models[0]
123
 
@@ -129,6 +186,72 @@ const ModelSelector: React.FC = () => {
129
  )
130
  }
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  return (
133
  <div className="relative">
134
  <Listbox
@@ -176,119 +299,187 @@ const ModelSelector: React.FC = () => {
176
  leaveTo="transform scale-95 opacity-0"
177
  >
178
  <ListboxOptions className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-hidden focus:outline-none">
179
- {/* Sort Controls - Always Visible */}
180
- <div className="px-3 py-2 border-b border-gray-200 bg-gray-50 sticky top-0 z-10">
181
- <div className="flex items-center space-x-2 text-xs">
182
- <span className="text-gray-600 font-medium">Sort by:</span>
183
- <button
184
- onClick={() => handleSortChange('name')}
185
- className={`px-2 py-1 rounded flex items-center space-x-1 ${
186
- sortBy === 'name'
187
- ? 'bg-blue-100 text-blue-700'
188
- : 'text-gray-600 hover:bg-gray-100'
189
- }`}
190
- >
191
- <span>Name</span>
192
- {sortBy === 'name' && <SortIcon sortOrder={sortOrder} />}
193
- </button>
194
- <button
195
- onClick={() => handleSortChange('likes')}
196
- className={`px-2 py-1 rounded flex items-center space-x-1 ${
197
- sortBy === 'likes'
198
- ? 'bg-blue-100 text-blue-700'
199
- : 'text-gray-600 hover:bg-gray-100'
200
- }`}
201
- >
202
- <Heart className="w-3 h-3" />
203
- <span>Likes</span>
204
- {sortBy === 'likes' && <SortIcon sortOrder={sortOrder} />}
205
- </button>
206
- <button
207
- onClick={() => handleSortChange('downloads')}
208
- className={`px-2 py-1 rounded flex items-center space-x-1 ${
209
- sortBy === 'downloads'
210
- ? 'bg-blue-100 text-blue-700'
211
- : 'text-gray-600 hover:bg-gray-100'
212
- }`}
213
- >
214
- <Download className="w-3 h-3" />
215
- <span>Downloads</span>
216
- {sortBy === 'downloads' && (
217
- <SortIcon sortOrder={sortOrder} />
218
- )}
219
- </button>
220
- <button
221
- onClick={() => handleSortChange('createdAt')}
222
- className={`px-2 py-1 rounded flex items-center space-x-1 ${
223
- sortBy === 'createdAt'
224
- ? 'bg-blue-100 text-blue-700'
225
- : 'text-gray-600 hover:bg-gray-100'
226
- }`}
227
- >
228
- <span>Date</span>
229
- {sortBy === 'createdAt' && (
230
- <SortIcon sortOrder={sortOrder} />
231
  )}
232
- </button>
 
 
 
233
  </div>
234
- </div>
235
-
236
- {/* Model Options - Scrollable */}
237
- <div className="overflow-auto max-h-48">
238
- {sortedModels.map((model) => {
239
- const hasStats = model.likes > 0 || model.downloads > 0
240
-
241
- return (
242
- <ListboxOption
243
- key={model.id}
244
- value={model}
245
- className={({ active, selected }) =>
246
- `px-3 py-2 cursor-pointer border-b border-gray-100 last:border-b-0 ${
247
- active ? 'bg-gray-50' : ''
248
- } ${selected ? 'bg-blue-50' : ''}`
249
- }
250
  >
251
- {({ selected }) => (
252
- <div className="flex items-center justify-between">
253
- <div className="flex items-center flex-1 mr-2">
254
- <span className="text-sm font-medium truncate">
255
- {model.id}
256
- </span>
257
- {selected && (
258
- <Check className="w-4 h-4 text-blue-600 ml-2 flex-shrink-0" />
259
- )}
260
- </div>
261
 
262
- {/* Stats Display */}
263
- {hasStats && (
264
- <div className="flex items-center space-x-3 text-xs text-gray-500 flex-shrink-0">
265
- {model.likes > 0 && (
266
- <div className="flex items-center space-x-1">
267
- <Heart className="w-3 h-3 text-red-500" />
268
- <span>{formatNumber(model.likes)}</span>
269
- </div>
270
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- {model.downloads > 0 && (
273
- <div className="flex items-center space-x-1">
274
- <Download className="w-3 h-3 text-green-500" />
275
- <span>{formatNumber(model.downloads)}</span>
276
- </div>
277
- )}
278
 
279
- {model.createdAt && (
280
- <span className="text-xs text-gray-400">
281
- {model.createdAt.split('T')[0]}
282
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  )}
284
  </div>
285
- )}
286
- </div>
287
- )}
288
- </ListboxOption>
289
- )
290
- })}
291
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  </ListboxOptions>
293
  </Transition>
294
  </div>
 
8
  } from '@headlessui/react'
9
  import { useModel } from '../contexts/ModelContext'
10
  import { getModelInfo } from '../lib/huggingface'
11
+ import {
12
+ Heart,
13
+ Download,
14
+ ChevronDown,
15
+ Check,
16
+ ArrowDown,
17
+ ArrowUp,
18
+ Plus,
19
+ Search,
20
+ X
21
+ } from 'lucide-react'
22
 
23
  type SortOption = 'likes' | 'downloads' | 'createdAt' | 'name'
24
 
25
+ function ModelSelector({ isFetching }: { isFetching: boolean }) {
26
  const { models, setModelInfo, modelInfo, pipeline } = useModel()
27
  const [sortBy, setSortBy] = useState<SortOption>('downloads')
28
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
29
+ const [showCustomInput, setShowCustomInput] = useState(false)
30
+ const [customModelName, setCustomModelName] = useState('')
31
+ const [isLoadingCustomModel, setIsLoadingCustomModel] = useState(false)
32
+ const [customModelError, setCustomModelError] = useState('')
33
+ const [isCustomModel, setIsCustomModel] = useState(false)
34
 
35
  const formatNumber = (num: number) => {
36
  if (num >= 1000000000) {
 
72
 
73
  // Function to fetch detailed model info and set as selected
74
  const fetchAndSetModelInfo = useCallback(
75
+ async (modelId: string, isCustom: boolean = false) => {
76
  try {
77
+ const modelInfoResponse = await getModelInfo(modelId, pipeline)
78
 
79
  let parameters = 0
80
  if (modelInfoResponse.safetensors) {
 
102
  baseId: modelInfoResponse.baseId,
103
  readme: modelInfoResponse.readme
104
  }
 
 
105
  setModelInfo(modelInfo)
106
+ setIsCustomModel(isCustom)
107
  } catch (error) {
108
  console.error('Error fetching model info:', error)
109
+ throw error
110
  }
111
  },
112
+ [setModelInfo, pipeline]
113
  )
114
 
115
  // Update modelInfo to first model when pipeline changes
116
  useEffect(() => {
117
+ if (isFetching) return
118
+
119
+ if (models.length > 0 && !isCustomModel) {
120
  const firstModel = models[0]
121
+ fetchAndSetModelInfo(firstModel.id, false)
122
  }
123
+ }, [pipeline, models, fetchAndSetModelInfo, isCustomModel, isFetching])
124
 
125
  const handleModelSelect = (modelId: string) => {
126
+ fetchAndSetModelInfo(modelId, false)
127
  }
128
 
129
  const handleSortChange = (newSortBy: SortOption) => {
 
135
  }
136
  }
137
 
138
+ const handleCustomModelLoad = async () => {
139
+ if (!customModelName.trim()) {
140
+ setCustomModelError('Please enter a model name')
141
+ return
142
+ }
143
+
144
+ setIsLoadingCustomModel(true)
145
+ setCustomModelError('')
146
+
147
+ try {
148
+ await fetchAndSetModelInfo(customModelName.trim(), true)
149
+ setShowCustomInput(false)
150
+ setCustomModelName('')
151
+ } catch (error) {
152
+ setCustomModelError(
153
+ 'Failed to load model. Please check the model name and try again.'
154
+ )
155
+ } finally {
156
+ setIsLoadingCustomModel(false)
157
+ }
158
+ }
159
+
160
+ const handleRemoveCustomModel = () => {
161
+ setIsCustomModel(false)
162
+ // Load the first model from the list
163
+ if (models.length > 0) {
164
+ fetchAndSetModelInfo(models[0].id, false)
165
+ }
166
+ }
167
+
168
+ const handleCustomInputKeyPress = (e: React.KeyboardEvent) => {
169
+ if (e.key === 'Enter') {
170
+ handleCustomModelLoad()
171
+ } else if (e.key === 'Escape') {
172
+ setShowCustomInput(false)
173
+ setCustomModelName('')
174
+ setCustomModelError('')
175
+ }
176
+ }
177
+
178
  const selectedModel =
179
  models.find((model) => model.id === modelInfo?.id) || models[0]
180
 
 
186
  )
187
  }
188
 
189
+ if (isCustomModel) {
190
+ return (
191
+ <div className="relative">
192
+ <div className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white flex items-center justify-between">
193
+ <div className="flex flex-col flex-1 min-w-0">
194
+ <span className="truncate font-medium">
195
+ {modelInfo?.id || 'Custom model'}
196
+ </span>
197
+ </div>
198
+
199
+ <div className="flex items-center space-x-3">
200
+ {modelInfo && (modelInfo.likes > 0 || modelInfo.downloads > 0) && (
201
+ <div className="flex items-center space-x-3 text-xs text-gray-500">
202
+ {modelInfo.likes > 0 && (
203
+ <div className="flex items-center space-x-1">
204
+ <Heart className="w-3 h-3 text-red-500" />
205
+ <span>{formatNumber(modelInfo.likes)}</span>
206
+ </div>
207
+ )}
208
+ {modelInfo.downloads > 0 && (
209
+ <div className="flex items-center space-x-1">
210
+ <Download className="w-3 h-3 text-green-500" />
211
+ <span>{formatNumber(modelInfo.downloads)}</span>
212
+ </div>
213
+ )}
214
+ </div>
215
+ )}
216
+ <button
217
+ onClick={handleRemoveCustomModel}
218
+ className="p-1 text-gray-400 hover:text-red-500 transition-colors"
219
+ title="Remove custom model"
220
+ >
221
+ <X className="w-4 h-4" />
222
+ </button>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ if (isFetching) {
230
+ return (
231
+ <div className="relative">
232
+ <div className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white flex items-center justify-between animate-pulse h-10">
233
+ <div className="flex flex-col flex-1 min-w-0 space-y-2">
234
+ <div className="h-4 bg-gray-200 rounded w-3/4"></div>
235
+ </div>
236
+
237
+ <div className="flex items-center space-x-3">
238
+ <div className="flex items-center space-x-3">
239
+ <div className="flex items-center space-x-1">
240
+ <Heart className="w-3 h-3 text-red-500" />
241
+ <div className="h-3 bg-gray-200 rounded w-8"></div>
242
+ </div>
243
+ <div className="flex items-center space-x-1">
244
+ <Download className="w-3 h-3 text-green-500" />
245
+ <div className="h-3 bg-gray-200 rounded w-8"></div>
246
+ </div>
247
+ </div>
248
+ <div className="w-4 h-4 bg-gray-200 rounded"></div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ )
253
+ }
254
+
255
  return (
256
  <div className="relative">
257
  <Listbox
 
299
  leaveTo="transform scale-95 opacity-0"
300
  >
301
  <ListboxOptions className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-hidden focus:outline-none">
302
+ {/* Custom Model Input */}
303
+ {showCustomInput ? (
304
+ <div className="px-3 py-3 border-b border-gray-200 bg-gray-50 sticky top-0 z-10">
305
+ <div className="space-y-2">
306
+ <div className="flex items-center space-x-2">
307
+ <input
308
+ type="text"
309
+ value={customModelName}
310
+ onChange={(e) => setCustomModelName(e.target.value)}
311
+ onKeyDown={handleCustomInputKeyPress}
312
+ placeholder="Enter model name (e.g., Qwen/Qwen3-0.6B)"
313
+ className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
314
+ autoFocus
315
+ />
316
+ <button
317
+ onClick={handleCustomModelLoad}
318
+ disabled={isLoadingCustomModel}
319
+ className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
320
+ >
321
+ {isLoadingCustomModel ? (
322
+ <div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin" />
323
+ ) : (
324
+ <Search className="w-3 h-3" />
325
+ )}
326
+ <span>Load</span>
327
+ </button>
328
+ <button
329
+ onClick={() => {
330
+ setShowCustomInput(false)
331
+ setCustomModelName('')
332
+ setCustomModelError('')
333
+ }}
334
+ className="px-2 py-1 text-sm text-gray-600 hover:text-gray-800"
335
+ >
336
+ Cancel
337
+ </button>
338
+ </div>
339
+ {customModelError && (
340
+ <p className="text-xs text-red-600">{customModelError}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  )}
342
+ <p className="text-xs text-gray-500">
343
+ Press Enter to load or Escape to cancel
344
+ </p>
345
+ </div>
346
  </div>
347
+ ) : (
348
+ <>
349
+ {/* Load Custom Model Button */}
350
+ <div className="px-3 py-2 border-b border-gray-200 bg-gray-50 sticky top-0 z-10">
351
+ <button
352
+ onClick={() => setShowCustomInput(true)}
353
+ className="w-full flex items-center justify-center space-x-2 px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded transition-colors"
 
 
 
 
 
 
 
 
 
354
  >
355
+ <Plus className="w-4 h-4" />
356
+ <span>Load Custom Model</span>
357
+ </button>
358
+ </div>
 
 
 
 
 
 
359
 
360
+ {/* Sort Controls */}
361
+ <div className="px-3 py-2 border-b border-gray-200 bg-gray-50 sticky top-0 z-10">
362
+ <div className="flex items-center space-x-2 text-xs">
363
+ <span className="text-gray-600 font-medium">
364
+ Sort by:
365
+ </span>
366
+ <button
367
+ onClick={() => handleSortChange('name')}
368
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
369
+ sortBy === 'name'
370
+ ? 'bg-blue-100 text-blue-700'
371
+ : 'text-gray-600 hover:bg-gray-100'
372
+ }`}
373
+ >
374
+ <span>Name</span>
375
+ {sortBy === 'name' && (
376
+ <SortIcon sortOrder={sortOrder} />
377
+ )}
378
+ </button>
379
+ <button
380
+ onClick={() => handleSortChange('likes')}
381
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
382
+ sortBy === 'likes'
383
+ ? 'bg-blue-100 text-blue-700'
384
+ : 'text-gray-600 hover:bg-gray-100'
385
+ }`}
386
+ >
387
+ <Heart className="w-3 h-3" />
388
+ <span>Likes</span>
389
+ {sortBy === 'likes' && (
390
+ <SortIcon sortOrder={sortOrder} />
391
+ )}
392
+ </button>
393
+ <button
394
+ onClick={() => handleSortChange('downloads')}
395
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
396
+ sortBy === 'downloads'
397
+ ? 'bg-blue-100 text-blue-700'
398
+ : 'text-gray-600 hover:bg-gray-100'
399
+ }`}
400
+ >
401
+ <Download className="w-3 h-3" />
402
+ <span>Downloads</span>
403
+ {sortBy === 'downloads' && (
404
+ <SortIcon sortOrder={sortOrder} />
405
+ )}
406
+ </button>
407
+ <button
408
+ onClick={() => handleSortChange('createdAt')}
409
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
410
+ sortBy === 'createdAt'
411
+ ? 'bg-blue-100 text-blue-700'
412
+ : 'text-gray-600 hover:bg-gray-100'
413
+ }`}
414
+ >
415
+ <span>Date</span>
416
+ {sortBy === 'createdAt' && (
417
+ <SortIcon sortOrder={sortOrder} />
418
+ )}
419
+ </button>
420
+ </div>
421
+ </div>
422
+ </>
423
+ )}
424
 
425
+ {/* Model Options - Scrollable */}
426
+ {!showCustomInput && (
427
+ <div className="overflow-auto max-h-48">
428
+ {sortedModels.map((model) => {
429
+ const hasStats = model.likes > 0 || model.downloads > 0
 
430
 
431
+ return (
432
+ <ListboxOption
433
+ key={model.id}
434
+ value={model}
435
+ className={({ active, selected }) =>
436
+ `px-3 py-2 cursor-pointer border-b border-gray-100 last:border-b-0 ${
437
+ active ? 'bg-gray-50' : ''
438
+ } ${selected ? 'bg-blue-50' : ''}`
439
+ }
440
+ >
441
+ {({ selected }) => (
442
+ <div className="flex items-center justify-between">
443
+ <div className="flex items-center flex-1 mr-2">
444
+ <span className="text-sm font-medium truncate">
445
+ {model.id}
446
+ </span>
447
+ {selected && (
448
+ <Check className="w-4 h-4 text-blue-600 ml-2 flex-shrink-0" />
449
  )}
450
  </div>
451
+
452
+ {/* Stats Display */}
453
+ {hasStats && (
454
+ <div className="flex items-center space-x-3 text-xs text-gray-500 flex-shrink-0">
455
+ {model.likes > 0 && (
456
+ <div className="flex items-center space-x-1">
457
+ <Heart className="w-3 h-3 text-red-500" />
458
+ <span>{formatNumber(model.likes)}</span>
459
+ </div>
460
+ )}
461
+
462
+ {model.downloads > 0 && (
463
+ <div className="flex items-center space-x-1">
464
+ <Download className="w-3 h-3 text-green-500" />
465
+ <span>{formatNumber(model.downloads)}</span>
466
+ </div>
467
+ )}
468
+
469
+ {model.createdAt && (
470
+ <span className="text-xs text-gray-400">
471
+ {model.createdAt.split('T')[0]}
472
+ </span>
473
+ )}
474
+ </div>
475
+ )}
476
+ </div>
477
+ )}
478
+ </ListboxOption>
479
+ )
480
+ })}
481
+ </div>
482
+ )}
483
  </ListboxOptions>
484
  </Transition>
485
  </div>
src/components/PipelineSelector.tsx CHANGED
@@ -8,7 +8,7 @@ import {
8
  } from '@headlessui/react'
9
  import { ChevronDown, Check } from 'lucide-react';
10
 
11
- const pipelines = [
12
  'text-classification',
13
  'zero-shot-classification',
14
  'text-generation',
@@ -63,7 +63,7 @@ const PipelineSelector: React.FC<PipelineSelectorProps> = ({
63
  leaveTo="transform scale-95 opacity-0"
64
  >
65
  <ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
66
- {pipelines.map((p) => (
67
  <ListboxOption
68
  key={p}
69
  className={({ active }) =>
 
8
  } from '@headlessui/react'
9
  import { ChevronDown, Check } from 'lucide-react';
10
 
11
+ export const supportedPipelines = [
12
  'text-classification',
13
  'zero-shot-classification',
14
  'text-generation',
 
63
  leaveTo="transform scale-95 opacity-0"
64
  >
65
  <ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
66
+ {supportedPipelines.map((p) => (
67
  <ListboxOption
68
  key={p}
69
  className={({ active }) =>
src/lib/huggingface.ts CHANGED
@@ -1,6 +1,7 @@
 
1
  import { ModelInfoResponse, QuantizationType } from "../types"
2
 
3
- const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
4
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
5
 
6
  if (!token) {
@@ -32,14 +33,21 @@ const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
32
  ]
33
 
34
  const siblingFiles = modelData.siblings?.map(s => s.rfilename) || []
35
- const isCompatible =
36
- requiredFiles.every((file) => siblingFiles.includes(file)) &&
37
- siblingFiles.some((file) => file.endsWith('.onnx') && file.startsWith('onnx/'))
38
- const incompatibilityReason = isCompatible
39
- ? ''
40
- : `Missing required files: ${requiredFiles
41
- .filter(file => !siblingFiles.includes(file))
42
- .join(', ')}`
 
 
 
 
 
 
 
43
  const supportedQuantizations = siblingFiles
44
  .filter((file) => file.endsWith('.onnx') && file.includes('_'))
45
  .map((file) => file.split('/')[1].split('_')[1].split('.')[0])
@@ -106,7 +114,7 @@ const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
106
  }
107
 
108
  const getModelsByPipeline = async (
109
- pipeline_tag: string
110
  ): Promise<ModelInfoResponse[]> => {
111
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
112
 
@@ -116,8 +124,71 @@ const getModelsByPipeline = async (
116
  )
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  const response = await fetch(
120
- `https://huggingface.co/api/models?filter=${pipeline_tag}&filter=transformers.js&sort=downloads`,
121
  {
122
  method: 'GET',
123
  headers: {
@@ -126,12 +197,14 @@ const getModelsByPipeline = async (
126
  }
127
  )
128
 
129
- if (!response.ok) {
130
  throw new Error(`Failed to fetch models for pipeline: ${response.statusText}`)
131
  }
132
  const models = await response.json()
133
- if (pipeline_tag === 'text-classification') {
134
- return models
 
 
135
  .filter(
136
  (model: ModelInfoResponse) =>
137
  !model.tags.includes('reranker') &&
@@ -139,10 +212,10 @@ const getModelsByPipeline = async (
139
  !model.id.includes('ms-marco') &&
140
  !model.id.includes('MiniLM')
141
  )
142
- .slice(0, 10)
143
  }
144
 
145
- return models.slice(0, 10)
146
  }
147
 
148
  function getModelSize(
@@ -178,4 +251,4 @@ function getModelSize(
178
  }
179
 
180
 
181
- export { getModelInfo, getModelSize, getModelsByPipeline }
 
1
+ import { supportedPipelines } from "../components/PipelineSelector"
2
  import { ModelInfoResponse, QuantizationType } from "../types"
3
 
4
+ const getModelInfo = async (modelName: string, pipeline: string): Promise<ModelInfoResponse> => {
5
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
6
 
7
  if (!token) {
 
33
  ]
34
 
35
  const siblingFiles = modelData.siblings?.map(s => s.rfilename) || []
36
+ const missingFiles = requiredFiles.filter(file => !siblingFiles.includes(file))
37
+ const hasOnnxFolder = siblingFiles.some((file) => file.endsWith('.onnx') && file.startsWith('onnx/'))
38
+
39
+ const isCompatible = missingFiles.length === 0 && hasOnnxFolder && modelData.tags.includes(pipeline)
40
+
41
+
42
+ let incompatibilityReason = ''
43
+ if (!modelData.tags.includes(pipeline)) {
44
+ const expectedPipelines = modelData.tags.filter(tag => supportedPipelines.includes(tag)).join(', ')
45
+ incompatibilityReason = expectedPipelines ? `- Model can be used with ${expectedPipelines} pipelines only\n` : `- Pipeline ${pipeline} not supported by the model\n`
46
+ } if (missingFiles.length > 0) {
47
+ incompatibilityReason += `- Missing required files: ${missingFiles.join(', ')}\n`
48
+ } else if (!hasOnnxFolder) {
49
+ incompatibilityReason += '- Folder onnx/ is missing\n'
50
+ }
51
  const supportedQuantizations = siblingFiles
52
  .filter((file) => file.endsWith('.onnx') && file.includes('_'))
53
  .map((file) => file.split('/')[1].split('_')[1].split('.')[0])
 
114
  }
115
 
116
  const getModelsByPipeline = async (
117
+ pipelineTag: string
118
  ): Promise<ModelInfoResponse[]> => {
119
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
120
 
 
124
  )
125
  }
126
 
127
+ // First search with filter=onnx
128
+ const response1 = await fetch(
129
+ `https://huggingface.co/api/models?filter=${pipelineTag}&filter=onnx&sort=downloads&limit=50`,
130
+ {
131
+ method: 'GET',
132
+ headers: {
133
+ Authorization: `Bearer ${token}`
134
+ }
135
+ }
136
+ )
137
+ if (!response1.ok) {
138
+ throw new Error(`Failed to fetch models for pipeline: ${response1.statusText}`)
139
+ }
140
+ const models1 = await response1.json()
141
+
142
+ // Second search with search=onnx
143
+ const response2 = await fetch(
144
+ `https://huggingface.co/api/models?filter=${pipelineTag}&search=onnx&sort=downloads&limit=50`,
145
+ {
146
+ method: 'GET',
147
+ headers: {
148
+ Authorization: `Bearer ${token}`
149
+ }
150
+ }
151
+ )
152
+ if (!response2.ok) {
153
+ throw new Error(`Failed to fetch models for pipeline: ${response2.statusText}`)
154
+ }
155
+ const models2 = await response2.json()
156
+
157
+ // Combine and deduplicate models based on id
158
+ const combinedModels = [...models1, ...models2].filter((m: ModelInfoResponse) => m.createdAt > '2022/02/03')
159
+ const uniqueModels = combinedModels.filter((model, index, self) =>
160
+ index === self.findIndex(m => m.id === model.id)
161
+ )
162
+
163
+ if (pipelineTag === 'text-classification') {
164
+ return uniqueModels
165
+ .filter(
166
+ (model: ModelInfoResponse) =>
167
+ !model.tags.includes('reranker') &&
168
+ !model.id.includes('reranker') &&
169
+ !model.id.includes('ms-marco') &&
170
+ !model.id.includes('MiniLM')
171
+ )
172
+ .slice(0, 20)
173
+ }
174
+
175
+ return uniqueModels.slice(0, 20)
176
+ }
177
+
178
+
179
+ const getModelsByPipelineCustom = async (
180
+ searchString: string,
181
+ pipelineTag: string
182
+ ): Promise<ModelInfoResponse[]> => {
183
+ const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
184
+
185
+ if (!token) {
186
+ throw new Error(
187
+ 'Hugging Face token not found. Please set REACT_APP_HUGGINGFACE_TOKEN in your .env file'
188
+ )
189
+ }
190
  const response = await fetch(
191
+ `https://huggingface.co/api/models?filter=${pipelineTag}&search=${searchString}&sort=downloads&limit=50`,
192
  {
193
  method: 'GET',
194
  headers: {
 
197
  }
198
  )
199
 
200
+ if (!response.ok) {
201
  throw new Error(`Failed to fetch models for pipeline: ${response.statusText}`)
202
  }
203
  const models = await response.json()
204
+
205
+ const uniqueModels = models.filter((m: ModelInfoResponse) => m.createdAt > '2022/02/03')
206
+ if (pipelineTag === 'text-classification') {
207
+ return uniqueModels
208
  .filter(
209
  (model: ModelInfoResponse) =>
210
  !model.tags.includes('reranker') &&
 
212
  !model.id.includes('ms-marco') &&
213
  !model.id.includes('MiniLM')
214
  )
215
+ .slice(0, 20)
216
  }
217
 
218
+ return uniqueModels.slice(0, 20)
219
  }
220
 
221
  function getModelSize(
 
251
  }
252
 
253
 
254
+ export { getModelInfo, getModelSize, getModelsByPipeline, getModelsByPipelineCustom }