Vokturz commited on
Commit
6ebf2fd
·
1 Parent(s): 59a1fe9

Refactor model selection and info display

Browse files

- Introduced ModelInfo component to encapsulate model details and stats.
- Updated App component to utilize ModelInfo and streamline model fetching logic.
- Enhanced ModelSelector with sorting functionality and improved UI using Headless UI Listbox.
- Implemented PipelineSelector with a similar UI enhancement.
- Removed redundant model info fetching from ZeroShotClassification.
- Updated ModelContext to manage selected quantization state.
- Enhanced getModelInfo function to include compatibility checks and supported quantizations.
- Updated types to reflect new model info structure and quantization types.

package.json CHANGED
@@ -3,6 +3,7 @@
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
 
6
  "@huggingface/transformers": "^3.6.1",
7
  "@testing-library/dom": "^10.4.0",
8
  "@testing-library/jest-dom": "^6.6.3",
 
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
6
+ "@headlessui/react": "^2.2.4",
7
  "@huggingface/transformers": "^3.6.1",
8
  "@testing-library/dom": "^10.4.0",
9
  "@testing-library/jest-dom": "^6.6.3",
pnpm-lock.yaml CHANGED
@@ -8,6 +8,9 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  '@huggingface/transformers':
12
  specifier: ^3.6.1
13
  version: 3.6.1
@@ -917,6 +920,34 @@ packages:
917
  resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
918
  engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
919
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  '@huggingface/[email protected]':
921
  resolution: {integrity: sha512-3WXbMFaPkk03LRCM0z0sylmn8ddDm4ubjU7X+Hg4M2GOuMklwoGAFXp9V2keq7vltoB/c7McE5aHUVVddAewsw==}
922
  engines: {node: '>=18'}
@@ -1233,6 +1264,43 @@ packages:
1233
  '@protobufjs/[email protected]':
1234
  resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
1235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1236
  '@rollup/[email protected]':
1237
  resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
1238
  engines: {node: '>= 10.0.0'}
@@ -1335,6 +1403,18 @@ packages:
1335
  resolution: {integrity: sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==}
1336
  engines: {node: '>=10'}
1337
 
 
 
 
 
 
 
 
 
 
 
 
 
1338
  '@testing-library/[email protected]':
1339
  resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
1340
  engines: {node: '>=18'}
@@ -2076,6 +2156,10 @@ packages:
2076
2077
  resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
2078
 
 
 
 
 
2079
2080
  resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
2081
  engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -5377,6 +5461,9 @@ packages:
5377
5378
  resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
5379
 
 
 
 
5380
5381
  resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
5382
  engines: {node: '>=14.0.0'}
@@ -5619,6 +5706,11 @@ packages:
5619
5620
  resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
5621
 
 
 
 
 
 
5622
5623
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
5624
 
@@ -6951,6 +7043,41 @@ snapshots:
6951
 
6952
  '@eslint/[email protected]': {}
6953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6954
  '@huggingface/[email protected]': {}
6955
 
6956
  '@huggingface/[email protected]':
@@ -7331,6 +7458,55 @@ snapshots:
7331
 
7332
  '@protobufjs/[email protected]': {}
7333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7334
7335
  dependencies:
7336
  '@babel/core': 7.28.0
@@ -7453,6 +7629,18 @@ snapshots:
7453
  transitivePeerDependencies:
7454
  - supports-color
7455
 
 
 
 
 
 
 
 
 
 
 
 
 
7456
  '@testing-library/[email protected]':
7457
  dependencies:
7458
  '@babel/code-frame': 7.27.1
@@ -8376,6 +8564,8 @@ snapshots:
8376
  strip-ansi: 6.0.1
8377
  wrap-ansi: 7.0.0
8378
 
 
 
8379
8380
 
8381
@@ -12287,6 +12477,8 @@ snapshots:
12287
 
12288
12289
 
 
 
12290
12291
  dependencies:
12292
  '@alloc/quick-lru': 5.2.0
@@ -12539,6 +12731,10 @@ snapshots:
12539
  querystringify: 2.2.0
12540
  requires-port: 1.0.0
12541
 
 
 
 
 
12542
12543
 
12544
 
8
 
9
  .:
10
  dependencies:
11
+ '@headlessui/react':
12
+ specifier: ^2.2.4
13
14
  '@huggingface/transformers':
15
  specifier: ^3.6.1
16
  version: 3.6.1
 
920
  resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
921
  engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
922
 
923
+ '@floating-ui/[email protected]':
924
+ resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
925
+
926
+ '@floating-ui/[email protected]':
927
+ resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
928
+
929
+ '@floating-ui/[email protected]':
930
+ resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==}
931
+ peerDependencies:
932
+ react: '>=16.8.0'
933
+ react-dom: '>=16.8.0'
934
+
935
+ '@floating-ui/[email protected]':
936
+ resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==}
937
+ peerDependencies:
938
+ react: '>=16.8.0'
939
+ react-dom: '>=16.8.0'
940
+
941
+ '@floating-ui/[email protected]':
942
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
943
+
944
+ '@headlessui/[email protected]':
945
+ resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==}
946
+ engines: {node: '>=10'}
947
+ peerDependencies:
948
+ react: ^18 || ^19 || ^19.0.0-rc
949
+ react-dom: ^18 || ^19 || ^19.0.0-rc
950
+
951
  '@huggingface/[email protected]':
952
  resolution: {integrity: sha512-3WXbMFaPkk03LRCM0z0sylmn8ddDm4ubjU7X+Hg4M2GOuMklwoGAFXp9V2keq7vltoB/c7McE5aHUVVddAewsw==}
953
  engines: {node: '>=18'}
 
1264
  '@protobufjs/[email protected]':
1265
  resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
1266
 
1267
+ '@react-aria/[email protected]':
1268
+ resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==}
1269
+ peerDependencies:
1270
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1271
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1272
+
1273
+ '@react-aria/[email protected]':
1274
+ resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==}
1275
+ peerDependencies:
1276
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1277
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1278
+
1279
+ '@react-aria/[email protected]':
1280
+ resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==}
1281
+ engines: {node: '>= 12'}
1282
+ peerDependencies:
1283
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1284
+
1285
+ '@react-aria/[email protected]':
1286
+ resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==}
1287
+ peerDependencies:
1288
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1289
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1290
+
1291
+ '@react-stately/[email protected]':
1292
+ resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==}
1293
+
1294
+ '@react-stately/[email protected]':
1295
+ resolution: {integrity: sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==}
1296
+ peerDependencies:
1297
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1298
+
1299
+ '@react-types/[email protected]':
1300
+ resolution: {integrity: sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==}
1301
+ peerDependencies:
1302
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
1303
+
1304
  '@rollup/[email protected]':
1305
  resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
1306
  engines: {node: '>= 10.0.0'}
 
1403
  resolution: {integrity: sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==}
1404
  engines: {node: '>=10'}
1405
 
1406
+ '@swc/[email protected]':
1407
+ resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
1408
+
1409
+ '@tanstack/[email protected]':
1410
+ resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
1411
+ peerDependencies:
1412
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
1413
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
1414
+
1415
+ '@tanstack/[email protected]':
1416
+ resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
1417
+
1418
  '@testing-library/[email protected]':
1419
  resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
1420
  engines: {node: '>=18'}
 
2156
2157
  resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
2158
 
2159
2160
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
2161
+ engines: {node: '>=6'}
2162
+
2163
2164
  resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
2165
  engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
 
5461
5462
  resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
5463
 
5464
5465
+ resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
5466
+
5467
5468
  resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
5469
  engines: {node: '>=14.0.0'}
 
5706
5707
  resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
5708
 
5709
5710
+ resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
5711
+ peerDependencies:
5712
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
5713
+
5714
5715
  resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
5716
 
 
7043
 
7044
  '@eslint/[email protected]': {}
7045
 
7046
+ '@floating-ui/[email protected]':
7047
+ dependencies:
7048
+ '@floating-ui/utils': 0.2.10
7049
+
7050
+ '@floating-ui/[email protected]':
7051
+ dependencies:
7052
+ '@floating-ui/core': 1.7.2
7053
+ '@floating-ui/utils': 0.2.10
7054
+
7055
7056
+ dependencies:
7057
+ '@floating-ui/dom': 1.7.2
7058
+ react: 19.1.0
7059
+ react-dom: 19.1.0([email protected])
7060
+
7061
7062
+ dependencies:
7063
+ '@floating-ui/react-dom': 2.1.4([email protected]([email protected]))([email protected])
7064
+ '@floating-ui/utils': 0.2.10
7065
+ react: 19.1.0
7066
+ react-dom: 19.1.0([email protected])
7067
+ tabbable: 6.2.0
7068
+
7069
+ '@floating-ui/[email protected]': {}
7070
+
7071
7072
+ dependencies:
7073
+ '@floating-ui/react': 0.26.28([email protected]([email protected]))([email protected])
7074
+ '@react-aria/focus': 3.20.5([email protected]([email protected]))([email protected])
7075
+ '@react-aria/interactions': 3.25.3([email protected]([email protected]))([email protected])
7076
+ '@tanstack/react-virtual': 3.13.12([email protected]([email protected]))([email protected])
7077
+ react: 19.1.0
7078
+ react-dom: 19.1.0([email protected])
7079
+ use-sync-external-store: 1.5.0([email protected])
7080
+
7081
  '@huggingface/[email protected]': {}
7082
 
7083
  '@huggingface/[email protected]':
 
7458
 
7459
  '@protobufjs/[email protected]': {}
7460
 
7461
7462
+ dependencies:
7463
+ '@react-aria/interactions': 3.25.3([email protected]([email protected]))([email protected])
7464
+ '@react-aria/utils': 3.29.1([email protected]([email protected]))([email protected])
7465
+ '@react-types/shared': 3.30.0([email protected])
7466
+ '@swc/helpers': 0.5.17
7467
+ clsx: 2.1.1
7468
+ react: 19.1.0
7469
+ react-dom: 19.1.0([email protected])
7470
+
7471
7472
+ dependencies:
7473
+ '@react-aria/ssr': 3.9.9([email protected])
7474
+ '@react-aria/utils': 3.29.1([email protected]([email protected]))([email protected])
7475
+ '@react-stately/flags': 3.1.2
7476
+ '@react-types/shared': 3.30.0([email protected])
7477
+ '@swc/helpers': 0.5.17
7478
+ react: 19.1.0
7479
+ react-dom: 19.1.0([email protected])
7480
+
7481
7482
+ dependencies:
7483
+ '@swc/helpers': 0.5.17
7484
+ react: 19.1.0
7485
+
7486
7487
+ dependencies:
7488
+ '@react-aria/ssr': 3.9.9([email protected])
7489
+ '@react-stately/flags': 3.1.2
7490
+ '@react-stately/utils': 3.10.7([email protected])
7491
+ '@react-types/shared': 3.30.0([email protected])
7492
+ '@swc/helpers': 0.5.17
7493
+ clsx: 2.1.1
7494
+ react: 19.1.0
7495
+ react-dom: 19.1.0([email protected])
7496
+
7497
+ '@react-stately/[email protected]':
7498
+ dependencies:
7499
+ '@swc/helpers': 0.5.17
7500
+
7501
+ '@react-stately/[email protected]([email protected])':
7502
+ dependencies:
7503
+ '@swc/helpers': 0.5.17
7504
+ react: 19.1.0
7505
+
7506
7507
+ dependencies:
7508
+ react: 19.1.0
7509
+
7510
7511
  dependencies:
7512
  '@babel/core': 7.28.0
 
7629
  transitivePeerDependencies:
7630
  - supports-color
7631
 
7632
+ '@swc/[email protected]':
7633
+ dependencies:
7634
+ tslib: 2.8.1
7635
+
7636
7637
+ dependencies:
7638
+ '@tanstack/virtual-core': 3.13.12
7639
+ react: 19.1.0
7640
+ react-dom: 19.1.0([email protected])
7641
+
7642
+ '@tanstack/[email protected]': {}
7643
+
7644
  '@testing-library/[email protected]':
7645
  dependencies:
7646
  '@babel/code-frame': 7.27.1
 
8564
  strip-ansi: 6.0.1
8565
  wrap-ansi: 7.0.0
8566
 
8567
8568
+
8569
8570
 
8571
 
12477
 
12478
12479
 
12480
12481
+
12482
12483
  dependencies:
12484
  '@alloc/quick-lru': 5.2.0
 
12731
  querystringify: 2.2.0
12732
  requires-port: 1.0.0
12733
 
12734
12735
+ dependencies:
12736
+ react: 19.1.0
12737
+
12738
12739
 
12740
src/App.tsx CHANGED
@@ -5,31 +5,22 @@ import TextClassification from './components/TextClassification'
5
  import Header from './Header'
6
  import Footer from './Footer'
7
  import { useModel } from './contexts/ModelContext'
8
- import { Bot, Heart, Download, Cpu, DatabaseIcon } from 'lucide-react'
9
- import { getModelsByPipeline, getModelSize } from './lib/huggingface'
10
  import ModelSelector from './components/ModelSelector'
 
11
 
12
  function App() {
13
- const { pipeline, setPipeline, progress, status, modelInfo, setModels } = useModel()
 
14
 
15
- const formatNumber = (num: number) => {
16
- if (num >= 1000000000) {
17
- return (num / 1000000000).toFixed(1) + 'B'
18
- } else if (num >= 1000000) {
19
- return (num / 1000000).toFixed(1) + 'M'
20
- } else if (num >= 1000) {
21
- return (num / 1000).toFixed(1) + 'K'
22
  }
23
- return num.toString()
24
- }
25
-
26
- useEffect(() => {
27
- const fetchModels = async () => {
28
- const fetchedModels = await getModelsByPipeline(pipeline);
29
- setModels(fetchedModels);
30
- };
31
- fetchModels();
32
- }, [setModels, pipeline]);
33
 
34
  return (
35
  <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
@@ -40,66 +31,27 @@ function App() {
40
  <div className="mb-8">
41
  <div className="bg-white rounded-lg shadow-sm border p-6">
42
  <div className="flex items-center justify-between mb-4">
43
- {/* Model Info Display */}
44
- {modelInfo.name && (
45
- <div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-3 rounded-lg border border-blue-200 space-y-2">
46
- {/* Model Name Row */}
47
- <div className="flex items-center space-x-2">
48
- <Bot className="w-4 h-4 text-blue-600" />
49
- <span
50
- className="text-sm font-medium text-gray-700 truncate max-w-100"
51
- title={modelInfo.name}
52
- >
53
- {modelInfo.name.split('/').pop()}
54
- </span>
55
- </div>
56
-
57
- {/* Stats Row */}
58
- <div className="flex items-center justify-self-end space-x-4 text-xs text-gray-600">
59
- {modelInfo.likes > 0 && (
60
- <div className="flex items-center space-x-1">
61
- <Heart className="w-3 h-3 text-red-500" />
62
- <span>{formatNumber(modelInfo.likes)}</span>
63
- </div>
64
- )}
65
-
66
- {modelInfo.downloads > 0 && (
67
- <div className="flex items-center space-x-1">
68
- <Download className="w-3 h-3 text-green-500" />
69
- <span>{formatNumber(modelInfo.downloads)}</span>
70
- </div>
71
- )}
72
-
73
- {modelInfo.parameters > 0 && (
74
- <div className="flex items-center space-x-1">
75
- <Cpu className="w-3 h-3 text-purple-500" />
76
- <span>{formatNumber(modelInfo.parameters)}</span>
77
- </div>
78
- )}
79
-
80
- {modelInfo.parameters > 0 && (
81
- <div className="flex items-center space-x-1">
82
- <DatabaseIcon className="w-3 h-3 text-purple-500" />
83
- <span>
84
- {`~${getModelSize(
85
- modelInfo.parameters,
86
- 'INT8'
87
- ).toFixed(1)}MB`}
88
- </span>
89
- </div>
90
- )}
91
- </div>
92
- </div>
93
- )}
94
  </div>
95
 
96
- <div className="flex flex-row items-center space-x-4">
97
- <span className="text-lg font-semibold text-gray-900">
98
- Choose a Pipeline
99
- </span>
100
- <PipelineSelector pipeline={pipeline} setPipeline={setPipeline} />
 
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
- <ModelSelector />
103
 
104
  {/* Model Loading Progress */}
105
  {status === 'progress' && (
 
5
  import Header from './Header'
6
  import Footer from './Footer'
7
  import { useModel } from './contexts/ModelContext'
8
+ import { getModelsByPipeline } from './lib/huggingface'
 
9
  import ModelSelector from './components/ModelSelector'
10
+ import ModelInfo from './components/ModelInfo'
11
 
12
  function App() {
13
+ const { pipeline, setPipeline, progress, status, modelInfo, setModels } =
14
+ useModel()
15
 
16
+ useEffect(() => {
17
+ const fetchModels = async () => {
18
+ const fetchedModels = await getModelsByPipeline(pipeline)
19
+ setModels(fetchedModels)
20
+ console.log(fetchedModels)
 
 
21
  }
22
+ fetchModels()
23
+ }, [setModels, pipeline])
 
 
 
 
 
 
 
 
24
 
25
  return (
26
  <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
 
31
  <div className="mb-8">
32
  <div className="bg-white rounded-lg shadow-sm border p-6">
33
  <div className="flex items-center justify-between mb-4">
34
+ <ModelInfo />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  </div>
36
 
37
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-4 items-start">
38
+ <div className="space-y-2">
39
+ <span className="text-lg font-semibold text-gray-900 block">
40
+ Choose a Pipeline
41
+ </span>
42
+ <PipelineSelector
43
+ pipeline={pipeline}
44
+ setPipeline={setPipeline}
45
+ />
46
+ </div>
47
+
48
+ <div className="space-y-2">
49
+ <span className="text-lg font-semibold text-gray-900 block">
50
+ Select Model
51
+ </span>
52
+ <ModelSelector />
53
+ </div>
54
  </div>
 
55
 
56
  {/* Model Loading Progress */}
57
  {status === 'progress' && (
src/components/ModelInfo.tsx ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Bot, Heart, Download, Cpu, DatabaseIcon, CheckCircle, XCircle, ExternalLink, ChevronDown } from 'lucide-react'
2
+ import { getModelSize } from '../lib/huggingface'
3
+ import { useModel } from '../contexts/ModelContext'
4
+ import { useEffect } from 'react'
5
+ import { QuantizationType } from '../types'
6
+
7
+ const ModelInfo = () => {
8
+ const formatNumber = (num: number) => {
9
+ if (num >= 1000000000) {
10
+ return (num / 1000000000).toFixed(1) + 'B'
11
+ } else if (num >= 1000000) {
12
+ return (num / 1000000).toFixed(1) + 'M'
13
+ } else if (num >= 1000) {
14
+ return (num / 1000).toFixed(1) + 'K'
15
+ }
16
+ return num.toString()
17
+ }
18
+
19
+ const { modelInfo, selectedQuantization, setSelectedQuantization } = useModel()
20
+
21
+ // Set default quantization when model changes
22
+ useEffect(() => {
23
+ if (modelInfo.isCompatible && modelInfo.supportedQuantizations.length > 0) {
24
+ const quantizations = modelInfo.supportedQuantizations
25
+ let defaultQuant: QuantizationType = 'fp32'
26
+
27
+ if (quantizations.includes('int8')) {
28
+ defaultQuant = 'int8'
29
+ } else if (quantizations.includes('q8')) {
30
+ defaultQuant = 'q8'
31
+ } else if (quantizations.includes('q4')) {
32
+ defaultQuant = 'q4'
33
+ }
34
+
35
+ setSelectedQuantization(defaultQuant)
36
+ }
37
+ }, [modelInfo.supportedQuantizations, modelInfo.isCompatible, setSelectedQuantization])
38
+
39
+ if (!modelInfo.name) {
40
+ return null
41
+ }
42
+
43
+ return (
44
+ <div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-3 rounded-lg border border-blue-200 space-y-3">
45
+ {/* Model Name Row */}
46
+ <div className="flex items-center space-x-2">
47
+ <Bot className="w-4 h-4 text-blue-600" />
48
+ <a
49
+ href={`https://huggingface.co/${modelInfo.name}`}
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ className="text-sm font-medium text-gray-700 truncate max-w-100 hover:underline"
53
+ title={modelInfo.name}
54
+ >
55
+ <ExternalLink className="w-3 h-3 inline-block mr-1" />
56
+ {modelInfo.name}
57
+ </a>
58
+ {/* Compatibility Status */}
59
+ {typeof modelInfo.isCompatible === 'boolean' && (
60
+ <div className="flex items-center space-x-1">
61
+ {modelInfo.isCompatible ? (
62
+ <>
63
+ <CheckCircle className="w-4 h-4 text-green-500" />
64
+ </>
65
+ ) : (
66
+ <>
67
+ <XCircle className="w-4 h-4 text-red-500" />
68
+ </>
69
+ )}
70
+ </div>
71
+ )}
72
+ </div>
73
+
74
+ {/* Base Model Link */}
75
+ {modelInfo.baseId && (
76
+ <div className="flex items-center space-x-2 ml-6">
77
+ <a
78
+ href={`https://huggingface.co/${modelInfo.baseId}`}
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ className="text-xs text-gray-600 truncate max-w-100 hover:underline"
82
+ title={`Base model: ${modelInfo.baseId}`}
83
+ >
84
+ <ExternalLink className="w-3 h-3 inline-block mr-1" />
85
+ {modelInfo.baseId}
86
+ </a>
87
+ </div>
88
+ )}
89
+
90
+
91
+ {/* Stats Row */}
92
+ <div className="flex items-center justify-self-end space-x-4 text-xs text-gray-600">
93
+ {modelInfo.likes > 0 && (
94
+ <div className="flex items-center space-x-1">
95
+ <Heart className="w-3 h-3 text-red-500" />
96
+ <span>{formatNumber(modelInfo.likes)}</span>
97
+ </div>
98
+ )}
99
+
100
+ {modelInfo.downloads > 0 && (
101
+ <div className="flex items-center space-x-1">
102
+ <Download className="w-3 h-3 text-green-500" />
103
+ <span>{formatNumber(modelInfo.downloads)}</span>
104
+ </div>
105
+ )}
106
+
107
+ {modelInfo.parameters > 0 && (
108
+ <div className="flex items-center space-x-1">
109
+ <Cpu className="w-3 h-3 text-purple-500" />
110
+ <span>{formatNumber(modelInfo.parameters)}</span>
111
+ </div>
112
+ )}
113
+
114
+ {modelInfo.parameters > 0 && (
115
+ <div className="flex items-center space-x-1">
116
+ <DatabaseIcon className="w-3 h-3 text-purple-500" />
117
+ <span>
118
+ {`~${getModelSize(modelInfo.parameters, selectedQuantization).toFixed(1)}MB`}
119
+ </span>
120
+ </div>
121
+ )}
122
+ </div>
123
+
124
+ {/* Separator */}
125
+ {modelInfo.isCompatible && modelInfo.supportedQuantizations.length > 0 && (
126
+ <hr className="border-gray-200" />
127
+ )}
128
+
129
+ {/* Quantization Dropdown */}
130
+ {modelInfo.isCompatible && modelInfo.supportedQuantizations.length > 0 && (
131
+ <div className="flex items-center space-x-2">
132
+ <span className="text-xs text-gray-600 font-medium">Quantization:</span>
133
+ <div className="relative">
134
+ <select
135
+ value={selectedQuantization || ''}
136
+ onChange={(e) => setSelectedQuantization(e.target.value as QuantizationType)}
137
+ className="appearance-none bg-white border border-gray-300 rounded-md px-3 py-1 pr-8 text-xs text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
138
+ >
139
+ <option value="">Select quantization</option>
140
+ {modelInfo.supportedQuantizations.map((quant) => (
141
+ <option key={quant} value={quant}>
142
+ {quant}
143
+ </option>
144
+ ))}
145
+ </select>
146
+ <ChevronDown className="absolute right-2 top-1/2 transform -translate-y-1/2 w-3 h-3 text-gray-400 pointer-events-none" />
147
+ </div>
148
+ </div>
149
+ )}
150
+
151
+ {/* Incompatibility Message */}
152
+ {modelInfo.isCompatible === false && modelInfo.incompatibilityReason && (
153
+ <div className="bg-red-50 border border-red-200 rounded-md px-3 py-2">
154
+ <p className="text-sm text-red-700">
155
+ <span className="font-medium">Incompatible:</span>{' '}
156
+ {modelInfo.incompatibilityReason}
157
+ </p>
158
+ </div>
159
+ )}
160
+ </div>
161
+ )
162
+ }
163
+
164
+ export default ModelInfo
src/components/ModelSelector.tsx CHANGED
@@ -1,14 +1,15 @@
1
  import React, { useEffect, useState } from 'react'
 
2
  import { useModel } from '../contexts/ModelContext'
3
  import { getModelInfo } from '../lib/huggingface'
4
- import { Heart, Download, ChevronDown } from 'lucide-react'
 
 
5
 
6
  const ModelSelector: React.FC = () => {
7
- const { models, setModelInfo, modelInfo } = useModel()
8
- const [isOpen, setIsOpen] = useState(false)
9
- const [modelStats, setModelStats] = useState<
10
- Record<string, { likes: number; downloads: number; createdAt: string }>
11
- >({})
12
 
13
  const formatNumber = (num: number) => {
14
  if (num >= 1000000000) {
@@ -21,39 +22,49 @@ const ModelSelector: React.FC = () => {
21
  return num.toString()
22
  }
23
 
24
- // Separate function to fetch only stats without updating selected model
25
- const fetchModelStats = async (modelId: string) => {
26
- try {
27
- const modelInfoResponse = await getModelInfo(modelId)
28
 
29
- setModelStats((prev) => ({
30
- ...prev,
31
- [modelId]: {
32
- likes: modelInfoResponse.likes || 0,
33
- downloads: modelInfoResponse.downloads || 0,
34
- createdAt: modelInfoResponse.createdAt || ''
35
- }
36
- }))
37
- } catch (error) {
38
- console.error('Error fetching model stats:', error)
39
- }
40
- }
 
 
 
 
 
41
 
42
- // Function to fetch full model info and set as selected
43
- const fetchModelAndSetInfo = async (modelId: string) => {
 
 
 
 
44
  try {
45
  const modelInfoResponse = await getModelInfo(modelId)
 
46
  let parameters = 0
47
  if (modelInfoResponse.safetensors) {
48
  const safetensors = modelInfoResponse.safetensors
49
  parameters =
 
50
  safetensors.parameters.F16 ||
51
  safetensors.parameters.F32 ||
52
  safetensors.parameters.total ||
53
  0
54
  }
55
 
56
- // Transform ModelInfoResponse to ModelInfo
57
  const modelInfo = {
58
  id: modelId,
59
  name: modelInfoResponse.id || modelId,
@@ -61,20 +72,14 @@ const ModelSelector: React.FC = () => {
61
  parameters,
62
  likes: modelInfoResponse.likes || 0,
63
  downloads: modelInfoResponse.downloads || 0,
64
- createdAt: modelInfoResponse.createdAt || ''
 
 
 
 
65
  }
66
 
67
- // Also update stats
68
- setModelStats((prev) => ({
69
- ...prev,
70
- [modelId]: {
71
- likes: modelInfoResponse.likes || 0,
72
- downloads: modelInfoResponse.downloads || 0,
73
- createdAt: modelInfoResponse.createdAt || ''
74
- }
75
- }))
76
-
77
- console.log(modelInfoResponse)
78
 
79
  setModelInfo(modelInfo)
80
  } catch (error) {
@@ -82,97 +87,175 @@ const ModelSelector: React.FC = () => {
82
  }
83
  }
84
 
85
- // Fetch stats for all models when component mounts (without setting as selected)
86
  useEffect(() => {
87
- models.forEach((model) => {
88
- if (!modelStats[model.id]) {
89
- fetchModelStats(model.id)
90
- }
91
- })
92
- }, [models])
93
-
94
- // Only fetch full info when a model is actually selected
95
- useEffect(() => {
96
- if (!modelInfo.id) return
97
- // Only fetch if we don't already have the full info
98
- if (!modelStats[modelInfo.id]) {
99
- fetchModelAndSetInfo(modelInfo.id)
100
  }
101
- }, [modelInfo.id])
102
 
103
  const handleModelSelect = (modelId: string) => {
104
- fetchModelAndSetInfo(modelId)
105
- setIsOpen(false)
 
 
 
 
 
 
 
 
106
  }
107
 
 
 
108
  return (
109
  <div className="relative">
110
- {/* Custom Dropdown Button */}
111
- <button
112
- onClick={() => setIsOpen(!isOpen)}
113
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white text-left flex items-center justify-between"
114
- >
115
- <span className="truncate">{modelInfo.id || 'Select a model'}</span>
116
- <ChevronDown
117
- className={`w-4 h-4 transition-transform ${
118
- isOpen ? 'rotate-180' : ''
119
- }`}
120
- />
121
- </button>
122
-
123
- {/* Custom Dropdown Options */}
124
- {isOpen && (
125
- <div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto">
126
- {models.map((model) => (
127
- <div
128
- key={model.id}
129
- onClick={() => handleModelSelect(model.id)}
130
- className="px-3 py-2 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0"
131
- >
132
- <div className="flex items-center justify-between">
133
- <span className="text-sm font-medium truncate flex-1 mr-2">
134
- {model.id}
135
- </span>
136
-
137
- {/* Stats Display */}
138
- {modelStats[model.id] &&
139
- (modelStats[model.id].likes > 0 ||
140
- modelStats[model.id].downloads > 0) && (
141
- <div className="flex items-center space-x-3 text-xs text-gray-500 flex-shrink-0">
142
- {modelStats[model.id].likes > 0 && (
143
- <div className="flex items-center space-x-1">
144
- <Heart className="w-3 h-3 text-red-500" />
145
- <span>
146
- {formatNumber(modelStats[model.id].likes)}
147
- </span>
148
- </div>
149
- )}
150
 
151
- {modelStats[model.id].downloads > 0 && (
152
- <div className="flex items-center space-x-1">
153
- <Download className="w-3 h-3 text-green-500" />
154
- <span>
155
- {formatNumber(modelStats[model.id].downloads)}
156
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  </div>
158
  )}
159
- {modelStats[model.id].createdAt !== '' && (
160
- <span className="text-xs text-gray-400">
161
- {modelStats[model.id].createdAt.split('T')[0]}
162
- </span>
163
- )}
164
- </div>
165
- )}
166
  </div>
167
- </div>
168
- ))}
169
  </div>
170
- )}
171
-
172
- {/* Click outside to close */}
173
- {isOpen && (
174
- <div className="fixed inset-0 z-0" onClick={() => setIsOpen(false)} />
175
- )}
176
  </div>
177
  )
178
  }
 
1
  import React, { useEffect, useState } from 'react'
2
+ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
3
  import { useModel } from '../contexts/ModelContext'
4
  import { getModelInfo } from '../lib/huggingface'
5
+ import { Heart, Download, ChevronDown, Check, ArrowUpDown } from 'lucide-react'
6
+
7
+ type SortOption = 'likes' | 'downloads' | 'createdAt' | 'name'
8
 
9
  const ModelSelector: React.FC = () => {
10
+ const { models, setModelInfo, modelInfo, pipeline } = useModel()
11
+ const [sortBy, setSortBy] = useState<SortOption>('likes')
12
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
 
 
13
 
14
  const formatNumber = (num: number) => {
15
  if (num >= 1000000000) {
 
22
  return num.toString()
23
  }
24
 
25
+ // Sort models based on current sort criteria
26
+ const sortedModels = React.useMemo(() => {
27
+ return [...models].sort((a, b) => {
28
+ let comparison = 0
29
 
30
+ switch (sortBy) {
31
+ case 'downloads':
32
+ comparison = (a.downloads || 0) - (b.downloads || 0)
33
+ break
34
+ case 'createdAt':
35
+ const dateA = new Date(a.createdAt || '').getTime()
36
+ const dateB = new Date(b.createdAt || '').getTime()
37
+ comparison = dateA - dateB
38
+ break
39
+ case 'name':
40
+ comparison = a.id.localeCompare(b.id)
41
+ break
42
+ case 'likes':
43
+ default:
44
+ comparison = (a.likes || 0) - (b.likes || 0)
45
+ break
46
+ }
47
 
48
+ return sortOrder === 'desc' ? -comparison : comparison
49
+ })
50
+ }, [models, sortBy, sortOrder])
51
+
52
+ // Function to fetch detailed model info and set as selected
53
+ const fetchAndSetModelInfo = async (modelId: string) => {
54
  try {
55
  const modelInfoResponse = await getModelInfo(modelId)
56
+
57
  let parameters = 0
58
  if (modelInfoResponse.safetensors) {
59
  const safetensors = modelInfoResponse.safetensors
60
  parameters =
61
+ safetensors.parameters.BF16 ||
62
  safetensors.parameters.F16 ||
63
  safetensors.parameters.F32 ||
64
  safetensors.parameters.total ||
65
  0
66
  }
67
 
 
68
  const modelInfo = {
69
  id: modelId,
70
  name: modelInfoResponse.id || modelId,
 
72
  parameters,
73
  likes: modelInfoResponse.likes || 0,
74
  downloads: modelInfoResponse.downloads || 0,
75
+ createdAt: modelInfoResponse.createdAt || '',
76
+ isCompatible: modelInfoResponse.isCompatible,
77
+ incompatibilityReason: modelInfoResponse.incompatibilityReason,
78
+ supportedQuantizations: modelInfoResponse.supportedQuantizations,
79
+ baseId: modelInfoResponse.baseId
80
  }
81
 
82
+ console.log('Fetched model info:', modelInfoResponse)
 
 
 
 
 
 
 
 
 
 
83
 
84
  setModelInfo(modelInfo)
85
  } catch (error) {
 
87
  }
88
  }
89
 
90
+ // Update modelInfo to first model when pipeline changes
91
  useEffect(() => {
92
+ if (models.length > 0) {
93
+ const firstModel = models[0]
94
+ fetchAndSetModelInfo(firstModel.id)
 
 
 
 
 
 
 
 
 
 
95
  }
96
+ }, [pipeline, models])
97
 
98
  const handleModelSelect = (modelId: string) => {
99
+ fetchAndSetModelInfo(modelId)
100
+ }
101
+
102
+ const handleSortChange = (newSortBy: SortOption) => {
103
+ if (sortBy === newSortBy) {
104
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
105
+ } else {
106
+ setSortBy(newSortBy)
107
+ setSortOrder('desc')
108
+ }
109
  }
110
 
111
+ const selectedModel = models.find(model => model.id === modelInfo.id) || models[0]
112
+
113
  return (
114
  <div className="relative">
115
+ <Listbox value={selectedModel} onChange={(model) => handleModelSelect(model.id)}>
116
+ <div className="relative">
117
+ <ListboxButton className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white text-left flex items-center justify-between">
118
+ <div className="flex items-center justify-between w-full">
119
+ <div className="flex flex-col flex-1 min-w-0">
120
+ <span className="truncate font-medium">{modelInfo.id || 'Select a model'}</span>
121
+ </div>
122
+
123
+ <div className="flex items-center space-x-3">
124
+ {selectedModel && (selectedModel.likes > 0 || selectedModel.downloads > 0) && (
125
+ <div className="flex items-center space-x-3 text-xs text-gray-500">
126
+ {selectedModel.likes > 0 && (
127
+ <div className="flex items-center space-x-1">
128
+ <Heart className="w-3 h-3 text-red-500" />
129
+ <span>{formatNumber(selectedModel.likes)}</span>
130
+ </div>
131
+ )}
132
+ {selectedModel.downloads > 0 && (
133
+ <div className="flex items-center space-x-1">
134
+ <Download className="w-3 h-3 text-green-500" />
135
+ <span>{formatNumber(selectedModel.downloads)}</span>
136
+ </div>
137
+ )}
138
+ </div>
139
+ )}
140
+ <ChevronDown className="w-4 h-4 ui-open:rotate-180 transition-transform flex-shrink-0" />
141
+ </div>
142
+ </div>
143
+ </ListboxButton>
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ <Transition
146
+ enter="transition duration-100 ease-out"
147
+ enterFrom="transform scale-95 opacity-0"
148
+ enterTo="transform scale-100 opacity-100"
149
+ leave="transition duration-75 ease-out"
150
+ leaveFrom="transform scale-100 opacity-100"
151
+ leaveTo="transform scale-95 opacity-0"
152
+ >
153
+ <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">
154
+ {/* Sort Controls - Always Visible */}
155
+ <div className="px-3 py-2 border-b border-gray-200 bg-gray-50 sticky top-0 z-10">
156
+ <div className="flex items-center space-x-2 text-xs">
157
+ <span className="text-gray-600 font-medium">Sort by:</span>
158
+ <button
159
+ onClick={() => handleSortChange('name')}
160
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
161
+ sortBy === 'name' ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'
162
+ }`}
163
+ >
164
+ <span>Name</span>
165
+ {sortBy === 'name' && <ArrowUpDown className="w-3 h-3" />}
166
+ </button>
167
+ <button
168
+ onClick={() => handleSortChange('likes')}
169
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
170
+ sortBy === 'likes' ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'
171
+ }`}
172
+ >
173
+ <Heart className="w-3 h-3" />
174
+ <span>Likes</span>
175
+ {sortBy === 'likes' && <ArrowUpDown className="w-3 h-3" />}
176
+ </button>
177
+ <button
178
+ onClick={() => handleSortChange('downloads')}
179
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
180
+ sortBy === 'downloads' ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'
181
+ }`}
182
+ >
183
+ <Download className="w-3 h-3" />
184
+ <span>Downloads</span>
185
+ {sortBy === 'downloads' && <ArrowUpDown className="w-3 h-3" />}
186
+ </button>
187
+ <button
188
+ onClick={() => handleSortChange('createdAt')}
189
+ className={`px-2 py-1 rounded flex items-center space-x-1 ${
190
+ sortBy === 'createdAt' ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'
191
+ }`}
192
+ >
193
+ <span>Date</span>
194
+ {sortBy === 'createdAt' && <ArrowUpDown className="w-3 h-3" />}
195
+ </button>
196
+ </div>
197
+ </div>
198
+
199
+ {/* Model Options - Scrollable */}
200
+ <div className="overflow-auto max-h-48">
201
+ {sortedModels.map((model) => {
202
+ const hasStats = model.likes > 0 || model.downloads > 0
203
+
204
+ return (
205
+ <ListboxOption
206
+ key={model.id}
207
+ value={model}
208
+ className={({ active, selected }) =>
209
+ `px-3 py-2 cursor-pointer border-b border-gray-100 last:border-b-0 ${
210
+ active ? 'bg-gray-50' : ''
211
+ } ${selected ? 'bg-blue-50' : ''}`
212
+ }
213
+ >
214
+ {({ selected }) => (
215
+ <div className="flex items-center justify-between">
216
+ <div className="flex items-center flex-1 mr-2">
217
+ <span className="text-sm font-medium truncate">
218
+ {model.id}
219
+ </span>
220
+ {selected && (
221
+ <Check className="w-4 h-4 text-blue-600 ml-2 flex-shrink-0" />
222
+ )}
223
+ </div>
224
+
225
+ {/* Stats Display */}
226
+ {hasStats && (
227
+ <div className="flex items-center space-x-3 text-xs text-gray-500 flex-shrink-0">
228
+ {model.likes > 0 && (
229
+ <div className="flex items-center space-x-1">
230
+ <Heart className="w-3 h-3 text-red-500" />
231
+ <span>{formatNumber(model.likes)}</span>
232
+ </div>
233
+ )}
234
+
235
+ {model.downloads > 0 && (
236
+ <div className="flex items-center space-x-1">
237
+ <Download className="w-3 h-3 text-green-500" />
238
+ <span>{formatNumber(model.downloads)}</span>
239
+ </div>
240
+ )}
241
+
242
+ {model.createdAt && (
243
+ <span className="text-xs text-gray-400">
244
+ {model.createdAt.split('T')[0]}
245
+ </span>
246
+ )}
247
+ </div>
248
+ )}
249
  </div>
250
  )}
251
+ </ListboxOption>
252
+ )
253
+ })}
 
 
 
 
254
  </div>
255
+ </ListboxOptions>
256
+ </Transition>
257
  </div>
258
+ </Listbox>
 
 
 
 
 
259
  </div>
260
  )
261
  }
src/components/PipelineSelector.tsx CHANGED
@@ -1,4 +1,12 @@
1
  import React from 'react';
 
 
 
 
 
 
 
 
2
 
3
  const pipelines = [
4
  'zero-shot-classification',
@@ -21,14 +29,73 @@ const PipelineSelector: React.FC<PipelineSelectorProps> = ({
21
  pipeline,
22
  setPipeline
23
  }) => {
 
 
 
 
 
 
 
 
 
24
  return (
25
- <select value={pipeline} onChange={(e) => setPipeline(e.target.value)}>
26
- {pipelines.map((p) => (
27
- <option key={p} value={p}>
28
- {p}
29
- </option>
30
- ))}
31
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  );
33
  };
34
 
 
1
  import React from 'react';
2
+ import {
3
+ Listbox,
4
+ ListboxOption,
5
+ ListboxButton,
6
+ ListboxOptions,
7
+ Transition
8
+ } from '@headlessui/react'
9
+ import { ChevronDown, Check } from 'lucide-react';
10
 
11
  const pipelines = [
12
  'zero-shot-classification',
 
29
  pipeline,
30
  setPipeline
31
  }) => {
32
+ const selectedPipeline = pipeline;
33
+
34
+ const formatPipelineName = (pipelineId: string) => {
35
+ return pipelineId
36
+ .split('-')
37
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
38
+ .join(' ');
39
+ };
40
+
41
  return (
42
+ <div className="relative">
43
+ <Listbox value={selectedPipeline} onChange={setPipeline}>
44
+ <div className="relative">
45
+ <ListboxButton className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm border border-gray-300">
46
+ <span className="block truncate font-medium">
47
+ {formatPipelineName(selectedPipeline)}
48
+ </span>
49
+ <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
50
+ <ChevronDown
51
+ className="h-5 w-5 text-gray-400 ui-open:rotate-180 transition-transform"
52
+ aria-hidden="true"
53
+ />
54
+ </span>
55
+ </ListboxButton>
56
+
57
+ <Transition
58
+ enter="transition duration-100 ease-out"
59
+ enterFrom="transform scale-95 opacity-0"
60
+ enterTo="transform scale-100 opacity-100"
61
+ leave="transition duration-75 ease-out"
62
+ leaveFrom="transform scale-100 opacity-100"
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 shadow-lg 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 }) =>
70
+ `relative cursor-default select-none py-2 px-4 ${
71
+ active ? 'bg-amber-100 text-amber-900' : 'text-gray-900'
72
+ }`
73
+ }
74
+ value={p}
75
+ >
76
+ {({ selected }) => (
77
+ <div className="flex items-center justify-between">
78
+ <span
79
+ className={`block truncate ${
80
+ selected ? 'font-medium' : 'font-normal'
81
+ }`}
82
+ >
83
+ {formatPipelineName(p)}
84
+ </span>
85
+ {selected && (
86
+ <span className="flex items-center text-amber-600">
87
+ <Check className="h-5 w-5" aria-hidden="true" />
88
+ </span>
89
+ )}
90
+ </div>
91
+ )}
92
+ </ListboxOption>
93
+ ))}
94
+ </ListboxOptions>
95
+ </Transition>
96
+ </div>
97
+ </Listbox>
98
+ </div>
99
  );
100
  };
101
 
src/components/ZeroShotClassification.tsx CHANGED
@@ -52,37 +52,6 @@ function ZeroShotClassification() {
52
  )
53
 
54
  const { setProgress, status, setStatus, modelInfo, setModelInfo } = useModel()
55
- useEffect(() => {
56
- const modelName = 'lxyuan/distilbert-base-multilingual-cased-sentiments-student'
57
- const fetchModelInfo = async () => {
58
- try {
59
- const modelInfoResponse = await getModelInfo(modelName)
60
- console.log(modelInfoResponse)
61
- let parameters = 0
62
- if (modelInfoResponse.safetensors) {
63
- const safetensors = modelInfoResponse.safetensors
64
- parameters =
65
- safetensors.parameters.F16 ||
66
- safetensors.parameters.F32 ||
67
- safetensors.parameters.total ||
68
- 0
69
- }
70
- setModelInfo({
71
- id: modelInfoResponse.id,
72
- name: modelName,
73
- architecture: modelInfoResponse.config?.architectures[0] ?? '',
74
- parameters,
75
- likes: modelInfoResponse.likes,
76
- downloads: modelInfoResponse.downloads,
77
- createdAt: modelInfoResponse.createdAt,
78
- })
79
- } catch (error) {
80
- console.error('Error fetching model info:', error)
81
- }
82
- }
83
-
84
- fetchModelInfo()
85
- }, [setModelInfo])
86
 
87
  // Create a reference to the worker object.
88
  const worker = useRef<Worker | null>(null)
 
52
  )
53
 
54
  const { setProgress, status, setStatus, modelInfo, setModelInfo } = useModel()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  // Create a reference to the worker object.
57
  const worker = useRef<Worker | null>(null)
src/contexts/ModelContext.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { createContext, useContext, useEffect, useState } from 'react'
2
- import { ModelInfo, ModelInfoResponse } from '../types'
3
 
4
  interface ModelContextType {
5
  progress: number
@@ -12,6 +12,8 @@ interface ModelContextType {
12
  setPipeline: (pipeline: string) => void
13
  models: ModelInfoResponse[]
14
  setModels: (models: ModelInfoResponse[]) => void
 
 
15
  }
16
 
17
  const ModelContext = createContext<ModelContextType | undefined>(undefined)
@@ -22,6 +24,7 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
22
  const [modelInfo, setModelInfo] = useState<ModelInfo>({} as ModelInfo)
23
  const [models, setModels] = useState<ModelInfoResponse[]>([] as ModelInfoResponse[])
24
  const [pipeline, setPipeline] = useState<string>('zero-shot-classification')
 
25
 
26
  // set progress to 0 when model is changed
27
  useEffect(() => {
@@ -41,6 +44,8 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
41
  setModels,
42
  pipeline,
43
  setPipeline,
 
 
44
  }}
45
  >
46
  {children}
 
1
  import React, { createContext, useContext, useEffect, useState } from 'react'
2
+ import { ModelInfo, ModelInfoResponse, QuantizationType } from '../types'
3
 
4
  interface ModelContextType {
5
  progress: number
 
12
  setPipeline: (pipeline: string) => void
13
  models: ModelInfoResponse[]
14
  setModels: (models: ModelInfoResponse[]) => void
15
+ selectedQuantization: QuantizationType
16
+ setSelectedQuantization: (quantization: QuantizationType) => void
17
  }
18
 
19
  const ModelContext = createContext<ModelContextType | undefined>(undefined)
 
24
  const [modelInfo, setModelInfo] = useState<ModelInfo>({} as ModelInfo)
25
  const [models, setModels] = useState<ModelInfoResponse[]>([] as ModelInfoResponse[])
26
  const [pipeline, setPipeline] = useState<string>('zero-shot-classification')
27
+ const [selectedQuantization, setSelectedQuantization] = useState<QuantizationType>('int8')
28
 
29
  // set progress to 0 when model is changed
30
  useEffect(() => {
 
44
  setModels,
45
  pipeline,
46
  setPipeline,
47
+ selectedQuantization,
48
+ setSelectedQuantization,
49
  }}
50
  >
51
  {children}
src/lib/huggingface.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Mode } from "fs"
2
- import { ModelInfoResponse } from "../types"
3
 
4
  const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
5
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
@@ -23,7 +22,70 @@ const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
23
  if (!response.ok) {
24
  throw new Error(`Failed to fetch model info: ${response.statusText}`)
25
  }
26
- return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  const getModelsByPipeline = async (
@@ -57,8 +119,6 @@ const getModelsByPipeline = async (
57
  return models.slice(0, 10)
58
  }
59
 
60
- // Define the possible quantization types for clarity and type safety
61
- type QuantizationType = 'FP32' | 'FP16' | 'INT8' | 'Q4'
62
  function getModelSize(
63
  parameters: number,
64
  quantization: QuantizationType
@@ -66,20 +126,23 @@ function getModelSize(
66
  let bytesPerParameter: number
67
 
68
  switch (quantization) {
69
- case 'FP32':
70
  // 32-bit floating point uses 4 bytes
71
  bytesPerParameter = 4
72
  break
73
- case 'FP16':
74
  bytesPerParameter = 2
75
  break
76
- case 'INT8':
 
 
 
77
  bytesPerParameter = 1
78
  break
79
- case 'Q4':
 
80
  bytesPerParameter = 0.5
81
- const theoreticalSize = (parameters * bytesPerParameter) / (1024 * 1024)
82
- return theoreticalSize
83
  }
84
 
85
  // There are 1,024 * 1,024 bytes in a megabyte
@@ -91,4 +154,3 @@ function getModelSize(
91
 
92
 
93
  export { getModelInfo, getModelSize, getModelsByPipeline }
94
-
 
1
+ import { ModelInfoResponse, QuantizationType } from "../types"
 
2
 
3
  const getModelInfo = async (modelName: string): Promise<ModelInfoResponse> => {
4
  const token = process.env.REACT_APP_HUGGINGFACE_TOKEN
 
22
  if (!response.ok) {
23
  throw new Error(`Failed to fetch model info: ${response.statusText}`)
24
  }
25
+
26
+ const modelData: ModelInfoResponse = await response.json()
27
+
28
+ const requiredFiles = [
29
+ 'config.json',
30
+ 'tokenizer.json',
31
+ 'tokenizer_config.json',
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])
46
+ .filter((q) => q !== 'quantized')
47
+ const uniqueSupportedQuantizations = Array.from(new Set(supportedQuantizations))
48
+ uniqueSupportedQuantizations.sort((a, b) => {
49
+ const getNumericValue = (str: string) => {
50
+ const match = str.match(/(\d+)/)
51
+ return match ? parseInt(match[1]) : Infinity
52
+ }
53
+ return getNumericValue(a) - getNumericValue(b)
54
+ })
55
+
56
+ // If there's a base model, fetch its info and merge with compatibility data
57
+ const baseModel = modelData.cardData?.base_model ?? modelData.modelId
58
+ if (baseModel && !modelData.safetensors) {
59
+ const baseModelResponse = await fetch(
60
+ `https://huggingface.co/api/models/${baseModel}`,
61
+ {
62
+ method: 'GET',
63
+ headers: {
64
+ Authorization: `Bearer ${token}`
65
+ }
66
+ }
67
+ )
68
+
69
+ if (baseModelResponse.ok) {
70
+ const baseModelData: ModelInfoResponse = await baseModelResponse.json()
71
+
72
+ return {
73
+ ...baseModelData,
74
+ id: modelData.id,
75
+ baseId: baseModel,
76
+ isCompatible,
77
+ incompatibilityReason,
78
+ supportedQuantizations: uniqueSupportedQuantizations as QuantizationType[]
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ ...modelData,
85
+ isCompatible,
86
+ incompatibilityReason,
87
+ supportedQuantizations: uniqueSupportedQuantizations as QuantizationType[]
88
+ }
89
  }
90
 
91
  const getModelsByPipeline = async (
 
119
  return models.slice(0, 10)
120
  }
121
 
 
 
122
  function getModelSize(
123
  parameters: number,
124
  quantization: QuantizationType
 
126
  let bytesPerParameter: number
127
 
128
  switch (quantization) {
129
+ case 'fp32':
130
  // 32-bit floating point uses 4 bytes
131
  bytesPerParameter = 4
132
  break
133
+ case 'fp16':
134
  bytesPerParameter = 2
135
  break
136
+ case 'int8':
137
+ case 'bnb8':
138
+ case 'uint8':
139
+ case 'q8':
140
  bytesPerParameter = 1
141
  break
142
+ case 'bnb4':
143
+ case 'q4':
144
  bytesPerParameter = 0.5
145
+ break
 
146
  }
147
 
148
  // There are 1,024 * 1,024 bytes in a megabyte
 
154
 
155
 
156
  export { getModelInfo, getModelSize, getModelsByPipeline }
 
src/types.ts CHANGED
@@ -27,6 +27,14 @@ export interface TextClassificationWorkerInput {
27
 
28
  export type AppStatus = 'idle' | 'loading' | 'processing'
29
 
 
 
 
 
 
 
 
 
30
  export interface ModelInfo {
31
  id: string
32
  name: string
@@ -35,6 +43,10 @@ export interface ModelInfo {
35
  likes: number
36
  downloads: number
37
  createdAt: string
 
 
 
 
38
  }
39
 
40
 
@@ -48,6 +60,10 @@ export interface ModelInfoResponse {
48
  lastModified: string
49
  pipeline_tag: string
50
  tags: string[]
 
 
 
 
51
  transformersInfo: {
52
  pipeline_tag: string
53
  auto_model: string
@@ -55,11 +71,19 @@ export interface ModelInfoResponse {
55
  }
56
  safetensors?: {
57
  parameters: {
 
58
  F16?: number
59
  F32?: number
60
  total?: number
61
  }
62
  }
 
 
 
 
 
 
 
63
  likes: number
64
  downloads: number
65
  }
 
27
 
28
  export type AppStatus = 'idle' | 'loading' | 'processing'
29
 
30
+ type q8 = 'q8' | 'int8' | 'bnb8' | 'uint8'
31
+ type q4 = 'q4' | 'bnb4'
32
+ type fp16 = 'fp16'
33
+ type fp32 = 'fp32'
34
+
35
+ export type QuantizationType = q8 | q4 | fp16 | fp32
36
+
37
+
38
  export interface ModelInfo {
39
  id: string
40
  name: string
 
43
  likes: number
44
  downloads: number
45
  createdAt: string
46
+ isCompatible?: boolean
47
+ incompatibilityReason?: string
48
+ supportedQuantizations: QuantizationType[]
49
+ baseId?: string
50
  }
51
 
52
 
 
60
  lastModified: string
61
  pipeline_tag: string
62
  tags: string[]
63
+ cardData?: {
64
+ base_model: string
65
+ }
66
+ baseId?: string
67
  transformersInfo: {
68
  pipeline_tag: string
69
  auto_model: string
 
71
  }
72
  safetensors?: {
73
  parameters: {
74
+ BF16?: number
75
  F16?: number
76
  F32?: number
77
  total?: number
78
  }
79
  }
80
+ siblings?: {
81
+ rfilename: string
82
+ }[]
83
+ modelId?: string
84
+ isCompatible: boolean
85
+ incompatibilityReason?: string
86
+ supportedQuantizations: QuantizationType[]
87
  likes: number
88
  downloads: number
89
  }