Thomas G. Lopes victor HF Staff commited on
Commit
97c4991
·
unverified ·
1 Parent(s): 91d01ad

Custom endpoints (#72)

Browse files

Co-authored-by: Victor Muštar (aider) <[email protected]>

package.json CHANGED
@@ -37,7 +37,8 @@
37
  "globals": "^16.0.0",
38
  "highlight.js": "^11.10.0",
39
  "jiti": "^2.4.2",
40
- "melt": "^0.21.0",
 
41
  "postcss": "^8.4.38",
42
  "prettier": "^3.1.1",
43
  "prettier-plugin-svelte": "^3.2.6",
 
37
  "globals": "^16.0.0",
38
  "highlight.js": "^11.10.0",
39
  "jiti": "^2.4.2",
40
+ "melt": "^0.28.0",
41
+ "openai": "^4.90.0",
42
  "postcss": "^8.4.38",
43
  "prettier": "^3.1.1",
44
  "prettier-plugin-svelte": "^3.2.6",
pnpm-lock.yaml CHANGED
@@ -41,22 +41,22 @@ importers:
41
  version: 1.2.15
42
  '@ryoppippi/unplugin-typia':
43
  specifier: ^1.0.0
44
45
  '@samchon/openapi':
46
  specifier: ^3.0.0
47
  version: 3.0.0
48
  '@sveltejs/adapter-auto':
49
  specifier: ^3.2.2
50
51
  '@sveltejs/adapter-node':
52
  specifier: ^5.2.0
53
54
  '@sveltejs/kit':
55
  specifier: ^2.5.27
56
57
  '@sveltejs/vite-plugin-svelte':
58
  specifier: ^4.0.0
59
60
  '@tailwindcss/container-queries':
61
  specifier: ^0.1.1
62
  version: 0.1.1([email protected])
@@ -85,8 +85,11 @@ importers:
85
  specifier: ^2.4.2
86
  version: 2.4.2
87
  melt:
88
- specifier: ^0.21.0
89
- version: 0.21.0(@floating-ui/[email protected])([email protected])
 
 
 
90
  postcss:
91
  specifier: ^8.4.38
92
  version: 8.5.3
@@ -131,7 +134,7 @@ importers:
131
  version: 22.1.0([email protected])
132
  vite:
133
  specifier: ^5.4.4
134
- version: 5.4.14([email protected])
135
 
136
  packages:
137
 
@@ -840,6 +843,12 @@ packages:
840
  '@types/[email protected]':
841
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
842
 
 
 
 
 
 
 
843
  '@types/[email protected]':
844
  resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
845
 
@@ -890,6 +899,10 @@ packages:
890
  resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==}
891
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
892
 
 
 
 
 
893
894
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
895
  peerDependencies:
@@ -900,6 +913,10 @@ packages:
900
  engines: {node: '>=0.4.0'}
901
  hasBin: true
902
 
 
 
 
 
903
904
  resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
905
 
@@ -929,6 +946,9 @@ packages:
929
930
  resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
931
 
 
 
 
932
933
  resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==}
934
  engines: {node: '>=4'}
@@ -959,6 +979,10 @@ packages:
959
960
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
961
 
 
 
 
 
962
963
  resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
964
  engines: {node: '>=6'}
@@ -1001,6 +1025,10 @@ packages:
1001
1002
  resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
1003
 
 
 
 
 
1004
1005
  resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
1006
  engines: {node: '>=14'}
@@ -1066,6 +1094,10 @@ packages:
1066
1067
  resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
1068
 
 
 
 
 
1069
1070
  resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
1071
  engines: {node: '>=0.10'}
@@ -1085,6 +1117,10 @@ packages:
1085
  resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
1086
  engines: {node: '>=4'}
1087
 
 
 
 
 
1088
1089
  resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
1090
 
@@ -1092,6 +1128,22 @@ packages:
1092
  resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
1093
  engines: {node: '>=10.13.0'}
1094
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
1096
  resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
1097
  engines: {node: '>=12'}
@@ -1202,6 +1254,10 @@ packages:
1202
  resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
1203
  engines: {node: '>=0.10.0'}
1204
 
 
 
 
 
1205
1206
  resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==}
1207
 
@@ -1267,6 +1323,17 @@ packages:
1267
1268
  resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
1269
 
 
 
 
 
 
 
 
 
 
 
 
1270
1271
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1272
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1275,6 +1342,14 @@ packages:
1275
1276
  resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
1277
 
 
 
 
 
 
 
 
 
1278
1279
  resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
1280
  engines: {node: '>= 6'}
@@ -1299,6 +1374,10 @@ packages:
1299
  resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==}
1300
  engines: {node: '>=18'}
1301
 
 
 
 
 
1302
1303
  resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1304
 
@@ -1313,6 +1392,14 @@ packages:
1313
  resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
1314
  engines: {node: '>=8'}
1315
 
 
 
 
 
 
 
 
 
1316
1317
  resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1318
  engines: {node: '>= 0.4'}
@@ -1321,6 +1408,9 @@ packages:
1321
  resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
1322
  engines: {node: '>=12.0.0'}
1323
 
 
 
 
1324
1325
  resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
1326
  engines: {node: '>=0.10.0'}
@@ -1548,8 +1638,12 @@ packages:
1548
1549
  resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
1550
 
1551
- melt@0.21.0:
1552
- resolution: {integrity: sha512-eD0gGaer3CDB8vklec8KWx9X8Gi5BqufZo4eEvRc3GNPpBvAi3i0ZCuziG+/C1jkL+a+Mi6tSKqiBMfyOdJskg==}
 
 
 
 
1553
  peerDependencies:
1554
  '@floating-ui/dom': ^1.6.0
1555
  svelte: ^5.0.0
@@ -1562,6 +1656,14 @@ packages:
1562
  resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
1563
  engines: {node: '>=8.6'}
1564
 
 
 
 
 
 
 
 
 
1565
1566
  resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
1567
  engines: {node: '>=6'}
@@ -1606,10 +1708,35 @@ packages:
1606
1607
  resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1609
1610
  resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
1611
  engines: {node: '>=6'}
1612
 
 
 
 
 
 
 
 
 
 
 
 
 
1613
1614
  resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1615
  engines: {node: '>= 0.8.0'}
@@ -1985,6 +2112,9 @@ packages:
1985
  resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
1986
  engines: {node: '>=6'}
1987
 
 
 
 
1988
1989
  resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
1990
  engines: {node: '>=18.12'}
@@ -2044,6 +2174,9 @@ packages:
2044
2045
  resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
2046
 
 
 
 
2047
2048
  resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==}
2049
  peerDependencies:
@@ -2163,9 +2296,19 @@ packages:
2163
2164
  resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
2165
 
 
 
 
 
 
 
 
2166
2167
  resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
2168
 
 
 
 
2169
2170
  resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
2171
  engines: {node: '>= 8'}
@@ -2597,7 +2740,7 @@ snapshots:
2597
  '@rollup/[email protected]':
2598
  optional: true
2599
 
2600
2601
  dependencies:
2602
  '@rollup/pluginutils': 5.1.4([email protected])
2603
  consola: 3.4.0
@@ -2611,7 +2754,7 @@ snapshots:
2611
  typescript: 5.6.3
2612
  typia: 7.6.4(@samchon/[email protected])([email protected])
2613
  unplugin: 1.16.1
2614
2615
  transitivePeerDependencies:
2616
  - '@samchon/openapi'
2617
  - '@types/node'
@@ -2635,22 +2778,22 @@ snapshots:
2635
  dependencies:
2636
  acorn: 8.14.0
2637
 
2638
2639
  dependencies:
2640
2641
  import-meta-resolve: 4.1.0
2642
 
2643
2644
  dependencies:
2645
  '@rollup/plugin-commonjs': 28.0.2([email protected])
2646
  '@rollup/plugin-json': 6.1.0([email protected])
2647
  '@rollup/plugin-node-resolve': 16.0.0([email protected])
2648
2649
  rollup: 4.34.9
2650
 
2651
2652
  dependencies:
2653
- '@sveltejs/vite-plugin-svelte': 4.0.4([email protected])([email protected]([email protected]))
2654
  '@types/cookie': 0.6.0
2655
  cookie: 0.6.0
2656
  devalue: 5.1.1
@@ -2663,27 +2806,27 @@ snapshots:
2663
  set-cookie-parser: 2.7.1
2664
  sirv: 3.0.1
2665
  svelte: 5.23.0
2666
- vite: 5.4.14([email protected])
2667
 
2668
2669
  dependencies:
2670
- '@sveltejs/vite-plugin-svelte': 4.0.4([email protected])([email protected]([email protected]))
2671
  debug: 4.4.0
2672
  svelte: 5.23.0
2673
- vite: 5.4.14([email protected])
2674
  transitivePeerDependencies:
2675
  - supports-color
2676
 
2677
2678
  dependencies:
2679
2680
  debug: 4.4.0
2681
  deepmerge: 4.3.1
2682
  kleur: 4.1.5
2683
  magic-string: 0.30.17
2684
  svelte: 5.23.0
2685
- vite: 5.4.14([email protected])
2686
2687
  transitivePeerDependencies:
2688
  - supports-color
2689
 
@@ -2759,6 +2902,15 @@ snapshots:
2759
 
2760
  '@types/[email protected]': {}
2761
 
 
 
 
 
 
 
 
 
 
2762
  '@types/[email protected]': {}
2763
 
2764
@@ -2838,12 +2990,20 @@ snapshots:
2838
  '@typescript-eslint/types': 8.26.1
2839
  eslint-visitor-keys: 4.2.0
2840
 
 
 
 
 
2841
2842
  dependencies:
2843
  acorn: 8.14.0
2844
 
2845
2846
 
 
 
 
 
2847
2848
  dependencies:
2849
  fast-deep-equal: 3.1.3
@@ -2869,6 +3029,8 @@ snapshots:
2869
 
2870
2871
 
 
 
2872
2873
 
2874
@@ -2901,6 +3063,11 @@ snapshots:
2901
  base64-js: 1.5.1
2902
  ieee754: 1.2.1
2903
 
 
 
 
 
 
2904
2905
 
2906
@@ -2932,6 +3099,10 @@ snapshots:
2932
 
2933
2934
 
 
 
 
 
2935
2936
 
2937
@@ -2980,6 +3151,8 @@ snapshots:
2980
 
2981
2982
 
 
 
2983
2984
 
2985
@@ -2990,6 +3163,12 @@ snapshots:
2990
 
2991
2992
 
 
 
 
 
 
 
2993
2994
 
2995
@@ -2997,6 +3176,21 @@ snapshots:
2997
  graceful-fs: 4.2.11
2998
  tapable: 2.2.1
2999
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3000
3001
  optionalDependencies:
3002
  '@esbuild/aix-ppc64': 0.21.5
@@ -3170,6 +3364,8 @@ snapshots:
3170
 
3171
3172
 
 
 
3173
3174
 
3175
@@ -3236,11 +3432,43 @@ snapshots:
3236
 
3237
3238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3239
3240
  optional: true
3241
 
3242
3243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3244
3245
  dependencies:
3246
  is-glob: 4.0.3
@@ -3261,6 +3489,8 @@ snapshots:
3261
 
3262
3263
 
 
 
3264
3265
 
3266
@@ -3269,12 +3499,22 @@ snapshots:
3269
 
3270
3271
 
 
 
 
 
 
 
3272
3273
  dependencies:
3274
  function-bind: 1.1.2
3275
 
3276
3277
 
 
 
 
 
3278
3279
  dependencies:
3280
  safer-buffer: 2.1.2
@@ -3474,7 +3714,9 @@ snapshots:
3474
  dependencies:
3475
  '@jridgewell/sourcemap-codec': 1.5.0
3476
 
3477
- [email protected](@floating-ui/dom@1.6.13)([email protected].0):
 
 
3478
  dependencies:
3479
  '@floating-ui/dom': 1.6.13
3480
  jest-axe: 9.0.0
@@ -3489,6 +3731,12 @@ snapshots:
3489
  braces: 3.0.3
3490
  picomatch: 2.3.1
3491
 
 
 
 
 
 
 
3492
3493
 
3494
@@ -3522,10 +3770,28 @@ snapshots:
3522
 
3523
3524
 
 
 
 
 
 
 
3525
3526
  dependencies:
3527
  mimic-fn: 2.1.0
3528
 
 
 
 
 
 
 
 
 
 
 
 
 
3529
3530
  dependencies:
3531
  deep-is: 0.1.4
@@ -3858,6 +4124,8 @@ snapshots:
3858
 
3859
3860
 
 
 
3861
3862
  dependencies:
3863
  typescript: 5.8.2
@@ -3917,6 +4185,8 @@ snapshots:
3917
 
3918
3919
 
 
 
3920
3921
  dependencies:
3922
  '@antfu/install-pkg': 1.0.0
@@ -3945,36 +4215,47 @@ snapshots:
3945
 
3946
3947
 
3948
3949
  dependencies:
3950
  esbuild: 0.21.5
3951
  postcss: 8.5.3
3952
  rollup: 4.34.9
3953
  optionalDependencies:
 
3954
  fsevents: 2.3.3
3955
  lightningcss: 1.29.1
3956
 
3957
3958
  dependencies:
3959
  esbuild: 0.25.1
3960
  postcss: 8.5.3
3961
  rollup: 4.34.9
3962
  optionalDependencies:
 
3963
  fsevents: 2.3.3
3964
  jiti: 2.4.2
3965
  lightningcss: 1.29.1
3966
  yaml: 2.7.0
3967
 
3968
3969
  optionalDependencies:
3970
- vite: 5.4.14([email protected])
3971
 
3972
3973
  dependencies:
3974
  defaults: 1.0.4
3975
 
 
 
 
 
3976
3977
 
 
 
 
 
 
3978
3979
  dependencies:
3980
  isexe: 2.0.0
 
41
  version: 1.2.15
42
  '@ryoppippi/unplugin-typia':
43
  specifier: ^1.0.0
44
45
  '@samchon/openapi':
46
  specifier: ^3.0.0
47
  version: 3.0.0
48
  '@sveltejs/adapter-auto':
49
  specifier: ^3.2.2
50
51
  '@sveltejs/adapter-node':
52
  specifier: ^5.2.0
53
54
  '@sveltejs/kit':
55
  specifier: ^2.5.27
56
57
  '@sveltejs/vite-plugin-svelte':
58
  specifier: ^4.0.0
59
60
  '@tailwindcss/container-queries':
61
  specifier: ^0.1.1
62
  version: 0.1.1([email protected])
 
85
  specifier: ^2.4.2
86
  version: 2.4.2
87
  melt:
88
+ specifier: ^0.28.0
89
+ version: 0.28.0(@floating-ui/[email protected])([email protected])
90
+ openai:
91
+ specifier: ^4.90.0
92
+ version: 4.90.0
93
  postcss:
94
  specifier: ^8.4.38
95
  version: 8.5.3
 
134
  version: 22.1.0([email protected])
135
  vite:
136
  specifier: ^5.4.4
137
+ version: 5.4.14(@types/[email protected])([email protected])
138
 
139
  packages:
140
 
 
843
  '@types/[email protected]':
844
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
845
 
846
+ '@types/[email protected]':
847
+ resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
848
+
849
+ '@types/[email protected]':
850
+ resolution: {integrity: sha512-ACYy2HGcZPHxEeWTqowTF7dhXN+JU1o7Gr4b41klnn6pj2LD6rsiGqSZojMdk1Jh2ys3m76ap+ae1vvE4+5+vg==}
851
+
852
  '@types/[email protected]':
853
  resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
854
 
 
899
  resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==}
900
  engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
901
 
902
903
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
904
+ engines: {node: '>=6.5'}
905
+
906
907
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
908
  peerDependencies:
 
913
  engines: {node: '>=0.4.0'}
914
  hasBin: true
915
 
916
917
+ resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
918
+ engines: {node: '>= 8.0.0'}
919
+
920
921
  resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
922
 
 
946
947
  resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
948
 
949
950
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
951
+
952
953
  resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==}
954
  engines: {node: '>=4'}
 
979
980
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
981
 
982
983
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
984
+ engines: {node: '>= 0.4'}
985
+
986
987
  resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
988
  engines: {node: '>=6'}
 
1025
1026
  resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
1027
 
1028
1029
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
1030
+ engines: {node: '>= 0.8'}
1031
+
1032
1033
  resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
1034
  engines: {node: '>=14'}
 
1094
1095
  resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
1096
 
1097
1098
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
1099
+ engines: {node: '>=0.4.0'}
1100
+
1101
1102
  resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
1103
  engines: {node: '>=0.10'}
 
1117
  resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
1118
  engines: {node: '>=4'}
1119
 
1120
1121
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
1122
+ engines: {node: '>= 0.4'}
1123
+
1124
1125
  resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
1126
 
 
1128
  resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
1129
  engines: {node: '>=10.13.0'}
1130
 
1131
1132
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
1133
+ engines: {node: '>= 0.4'}
1134
+
1135
1136
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
1137
+ engines: {node: '>= 0.4'}
1138
+
1139
1140
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
1141
+ engines: {node: '>= 0.4'}
1142
+
1143
1144
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
1145
+ engines: {node: '>= 0.4'}
1146
+
1147
1148
  resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
1149
  engines: {node: '>=12'}
 
1254
  resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
1255
  engines: {node: '>=0.10.0'}
1256
 
1257
1258
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
1259
+ engines: {node: '>=6'}
1260
+
1261
1262
  resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==}
1263
 
 
1323
1324
  resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
1325
 
1326
1327
+ resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
1328
+
1329
1330
+ resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
1331
+ engines: {node: '>= 6'}
1332
+
1333
1334
+ resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
1335
+ engines: {node: '>= 12.20'}
1336
+
1337
1338
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1339
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
 
1342
1343
  resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
1344
 
1345
1346
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
1347
+ engines: {node: '>= 0.4'}
1348
+
1349
1350
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
1351
+ engines: {node: '>= 0.4'}
1352
+
1353
1354
  resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
1355
  engines: {node: '>= 6'}
 
1374
  resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==}
1375
  engines: {node: '>=18'}
1376
 
1377
1378
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
1379
+ engines: {node: '>= 0.4'}
1380
+
1381
1382
  resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1383
 
 
1392
  resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
1393
  engines: {node: '>=8'}
1394
 
1395
1396
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
1397
+ engines: {node: '>= 0.4'}
1398
+
1399
1400
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
1401
+ engines: {node: '>= 0.4'}
1402
+
1403
1404
  resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1405
  engines: {node: '>= 0.4'}
 
1408
  resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
1409
  engines: {node: '>=12.0.0'}
1410
 
1411
1412
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
1413
+
1414
1415
  resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
1416
  engines: {node: '>=0.10.0'}
 
1638
1639
  resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
1640
 
1641
+ math-intrinsics@1.1.0:
1642
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
1643
+ engines: {node: '>= 0.4'}
1644
+
1645
1646
+ resolution: {integrity: sha512-kiqaTgNB/IkADmUfJZKROqQ3z+isal8LjLhckQANqjfjggIosHM8M7RO3Og7IQ12zK06nLnwanL80SuTPhblrw==}
1647
  peerDependencies:
1648
  '@floating-ui/dom': ^1.6.0
1649
  svelte: ^5.0.0
 
1656
  resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
1657
  engines: {node: '>=8.6'}
1658
 
1659
1660
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
1661
+ engines: {node: '>= 0.6'}
1662
+
1663
1664
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
1665
+ engines: {node: '>= 0.6'}
1666
+
1667
1668
  resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
1669
  engines: {node: '>=6'}
 
1708
1709
  resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1710
 
1711
1712
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
1713
+ engines: {node: '>=10.5.0'}
1714
+
1715
1716
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
1717
+ engines: {node: 4.x || >=6.0.0}
1718
+ peerDependencies:
1719
+ encoding: ^0.1.0
1720
+ peerDependenciesMeta:
1721
+ encoding:
1722
+ optional: true
1723
+
1724
1725
  resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
1726
  engines: {node: '>=6'}
1727
 
1728
1729
+ resolution: {integrity: sha512-YCuHMMycqtCg1B8G9ezkOF0j8UnBWD3Al/zYaelpuXwU1yhCEv+Y4n9G20MnyGy6cH4GsFwOMrgstQ+bgG1PtA==}
1730
+ hasBin: true
1731
+ peerDependencies:
1732
+ ws: ^8.18.0
1733
+ zod: ^3.23.8
1734
+ peerDependenciesMeta:
1735
+ ws:
1736
+ optional: true
1737
+ zod:
1738
+ optional: true
1739
+
1740
1741
  resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1742
  engines: {node: '>= 0.8.0'}
 
2112
  resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
2113
  engines: {node: '>=6'}
2114
 
2115
2116
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
2117
+
2118
2119
  resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
2120
  engines: {node: '>=18.12'}
 
2174
2175
  resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
2176
 
2177
2178
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
2179
+
2180
2181
  resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==}
2182
  peerDependencies:
 
2296
2297
  resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
2298
 
2299
2300
+ resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
2301
+ engines: {node: '>= 14'}
2302
+
2303
2304
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
2305
+
2306
2307
  resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
2308
 
2309
2310
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
2311
+
2312
2313
  resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
2314
  engines: {node: '>= 8'}
 
2740
  '@rollup/[email protected]':
2741
  optional: true
2742
 
2743
2744
  dependencies:
2745
  '@rollup/pluginutils': 5.1.4([email protected])
2746
  consola: 3.4.0
 
2754
  typescript: 5.6.3
2755
  typia: 7.6.4(@samchon/[email protected])([email protected])
2756
  unplugin: 1.16.1
2757
2758
  transitivePeerDependencies:
2759
  - '@samchon/openapi'
2760
  - '@types/node'
 
2778
  dependencies:
2779
  acorn: 8.14.0
2780
 
2781
2782
  dependencies:
2783
2784
  import-meta-resolve: 4.1.0
2785
 
2786
2787
  dependencies:
2788
  '@rollup/plugin-commonjs': 28.0.2([email protected])
2789
  '@rollup/plugin-json': 6.1.0([email protected])
2790
  '@rollup/plugin-node-resolve': 16.0.0([email protected])
2791
2792
  rollup: 4.34.9
2793
 
2794
2795
  dependencies:
2796
+ '@sveltejs/vite-plugin-svelte': 4.0.4([email protected])([email protected](@types/[email protected])([email protected]))
2797
  '@types/cookie': 0.6.0
2798
  cookie: 0.6.0
2799
  devalue: 5.1.1
 
2806
  set-cookie-parser: 2.7.1
2807
  sirv: 3.0.1
2808
  svelte: 5.23.0
2809
+ vite: 5.4.14(@types/[email protected])([email protected])
2810
 
2811
2812
  dependencies:
2813
+ '@sveltejs/vite-plugin-svelte': 4.0.4([email protected])([email protected](@types/[email protected])([email protected]))
2814
  debug: 4.4.0
2815
  svelte: 5.23.0
2816
+ vite: 5.4.14(@types/[email protected])([email protected])
2817
  transitivePeerDependencies:
2818
  - supports-color
2819
 
2820
2821
  dependencies:
2822
2823
  debug: 4.4.0
2824
  deepmerge: 4.3.1
2825
  kleur: 4.1.5
2826
  magic-string: 0.30.17
2827
  svelte: 5.23.0
2828
+ vite: 5.4.14(@types/[email protected])([email protected])
2829
2830
  transitivePeerDependencies:
2831
  - supports-color
2832
 
 
2902
 
2903
  '@types/[email protected]': {}
2904
 
2905
+ '@types/[email protected]':
2906
+ dependencies:
2907
+ '@types/node': 18.19.84
2908
+ form-data: 4.0.2
2909
+
2910
+ '@types/[email protected]':
2911
+ dependencies:
2912
+ undici-types: 5.26.5
2913
+
2914
  '@types/[email protected]': {}
2915
 
2916
 
2990
  '@typescript-eslint/types': 8.26.1
2991
  eslint-visitor-keys: 4.2.0
2992
 
2993
2994
+ dependencies:
2995
+ event-target-shim: 5.0.1
2996
+
2997
2998
  dependencies:
2999
  acorn: 8.14.0
3000
 
3001
3002
 
3003
3004
+ dependencies:
3005
+ humanize-ms: 1.2.1
3006
+
3007
3008
  dependencies:
3009
  fast-deep-equal: 3.1.3
 
3029
 
3030
3031
 
3032
3033
+
3034
3035
 
3036
 
3063
  base64-js: 1.5.1
3064
  ieee754: 1.2.1
3065
 
3066
3067
+ dependencies:
3068
+ es-errors: 1.3.0
3069
+ function-bind: 1.1.2
3070
+
3071
3072
 
3073
 
3099
 
3100
3101
 
3102
3103
+ dependencies:
3104
+ delayed-stream: 1.0.0
3105
+
3106
3107
 
3108
 
3151
 
3152
3153
 
3154
3155
+
3156
3157
 
3158
 
3163
 
3164
3165
 
3166
3167
+ dependencies:
3168
+ call-bind-apply-helpers: 1.0.2
3169
+ es-errors: 1.3.0
3170
+ gopd: 1.2.0
3171
+
3172
3173
 
3174
 
3176
  graceful-fs: 4.2.11
3177
  tapable: 2.2.1
3178
 
3179
3180
+
3181
3182
+
3183
3184
+ dependencies:
3185
+ es-errors: 1.3.0
3186
+
3187
3188
+ dependencies:
3189
+ es-errors: 1.3.0
3190
+ get-intrinsic: 1.3.0
3191
+ has-tostringtag: 1.0.2
3192
+ hasown: 2.0.2
3193
+
3194
3195
  optionalDependencies:
3196
  '@esbuild/aix-ppc64': 0.21.5
 
3364
 
3365
3366
 
3367
3368
+
3369
3370
 
3371
 
3432
 
3433
3434
 
3435
3436
+
3437
3438
+ dependencies:
3439
+ asynckit: 0.4.0
3440
+ combined-stream: 1.0.8
3441
+ es-set-tostringtag: 2.1.0
3442
+ mime-types: 2.1.35
3443
+
3444
3445
+ dependencies:
3446
+ node-domexception: 1.0.0
3447
+ web-streams-polyfill: 4.0.0-beta.3
3448
+
3449
3450
  optional: true
3451
 
3452
3453
 
3454
3455
+ dependencies:
3456
+ call-bind-apply-helpers: 1.0.2
3457
+ es-define-property: 1.0.1
3458
+ es-errors: 1.3.0
3459
+ es-object-atoms: 1.1.1
3460
+ function-bind: 1.1.2
3461
+ get-proto: 1.0.1
3462
+ gopd: 1.2.0
3463
+ has-symbols: 1.1.0
3464
+ hasown: 2.0.2
3465
+ math-intrinsics: 1.1.0
3466
+
3467
3468
+ dependencies:
3469
+ dunder-proto: 1.0.1
3470
+ es-object-atoms: 1.1.1
3471
+
3472
3473
  dependencies:
3474
  is-glob: 4.0.3
 
3489
 
3490
3491
 
3492
3493
+
3494
3495
 
3496
 
3499
 
3500
3501
 
3502
3503
+
3504
3505
+ dependencies:
3506
+ has-symbols: 1.1.0
3507
+
3508
3509
  dependencies:
3510
  function-bind: 1.1.2
3511
 
3512
3513
 
3514
3515
+ dependencies:
3516
+ ms: 2.1.3
3517
+
3518
3519
  dependencies:
3520
  safer-buffer: 2.1.2
 
3714
  dependencies:
3715
  '@jridgewell/sourcemap-codec': 1.5.0
3716
 
3717
+ math-intrinsics@1.1.0: {}
3718
+
3719
3720
  dependencies:
3721
  '@floating-ui/dom': 1.6.13
3722
  jest-axe: 9.0.0
 
3731
  braces: 3.0.3
3732
  picomatch: 2.3.1
3733
 
3734
3735
+
3736
3737
+ dependencies:
3738
+ mime-db: 1.52.0
3739
+
3740
3741
 
3742
 
3770
 
3771
3772
 
3773
3774
+
3775
3776
+ dependencies:
3777
+ whatwg-url: 5.0.0
3778
+
3779
3780
  dependencies:
3781
  mimic-fn: 2.1.0
3782
 
3783
3784
+ dependencies:
3785
+ '@types/node': 18.19.84
3786
+ '@types/node-fetch': 2.6.12
3787
+ abort-controller: 3.0.0
3788
+ agentkeepalive: 4.6.0
3789
+ form-data-encoder: 1.7.2
3790
+ formdata-node: 4.4.1
3791
+ node-fetch: 2.7.0
3792
+ transitivePeerDependencies:
3793
+ - encoding
3794
+
3795
3796
  dependencies:
3797
  deep-is: 0.1.4
 
4124
 
4125
4126
 
4127
4128
+
4129
4130
  dependencies:
4131
  typescript: 5.8.2
 
4185
 
4186
4187
 
4188
4189
+
4190
4191
  dependencies:
4192
  '@antfu/install-pkg': 1.0.0
 
4215
 
4216
4217
 
4218
4219
  dependencies:
4220
  esbuild: 0.21.5
4221
  postcss: 8.5.3
4222
  rollup: 4.34.9
4223
  optionalDependencies:
4224
+ '@types/node': 18.19.84
4225
  fsevents: 2.3.3
4226
  lightningcss: 1.29.1
4227
 
4228
4229
  dependencies:
4230
  esbuild: 0.25.1
4231
  postcss: 8.5.3
4232
  rollup: 4.34.9
4233
  optionalDependencies:
4234
+ '@types/node': 18.19.84
4235
  fsevents: 2.3.3
4236
  jiti: 2.4.2
4237
  lightningcss: 1.29.1
4238
  yaml: 2.7.0
4239
 
4240
4241
  optionalDependencies:
4242
+ vite: 5.4.14(@types/[email protected])([email protected])
4243
 
4244
4245
  dependencies:
4246
  defaults: 1.0.4
4247
 
4248
4249
+
4250
4251
+
4252
4253
 
4254
4255
+ dependencies:
4256
+ tr46: 0.0.3
4257
+ webidl-conversions: 3.0.1
4258
+
4259
4260
  dependencies:
4261
  isexe: 2.0.0
src/app.css CHANGED
@@ -60,6 +60,20 @@
60
  @apply flex h-[39px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:ring-4 focus:ring-gray-100 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700;
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  /* Elements & Classes */
64
  html {
65
  font-size: 15px;
 
60
  @apply flex h-[39px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:ring-4 focus:ring-gray-100 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700;
61
  }
62
 
63
+ @utility custom-outline {
64
+ @apply outline-hidden;
65
+ @apply border-blue-500 ring ring-blue-500;
66
+ }
67
+
68
+ @utility focus-outline {
69
+ @apply focus-visible:custom-outline;
70
+ }
71
+
72
+ @utility input {
73
+ @apply rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400;
74
+ @apply focus-outline;
75
+ }
76
+
77
  /* Elements & Classes */
78
  html {
79
  font-size: 15px;
src/lib/actions/click-outside.ts CHANGED
@@ -8,6 +8,10 @@ export const clickOutside: Action<HTMLElement, () => void> = (node, callback) =>
8
  }
9
 
10
  function handleClick(event: MouseEvent) {
 
 
 
 
11
  if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
12
  _callback();
13
  }
 
8
  }
9
 
10
  function handleClick(event: MouseEvent) {
11
+ if (window.getSelection()?.toString()) {
12
+ // Don't close if text is selected
13
+ return;
14
+ }
15
  if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
16
  _callback();
17
  }
src/lib/components/avatar.svelte CHANGED
@@ -1,12 +1,18 @@
1
  <script lang="ts">
 
 
 
2
  interface Props {
3
- orgName: string | undefined;
 
4
  size?: "sm" | "md";
5
  }
6
 
7
- let { orgName, size = "md" }: Props = $props();
8
 
9
  let sizeClass = $derived(size === "sm" ? "size-3" : "size-4");
 
 
10
 
11
  async function getAvatarUrl(orgName?: string) {
12
  if (!orgName) return;
@@ -22,14 +28,22 @@
22
  }
23
  </script>
24
 
25
- {#await getAvatarUrl(orgName)}
26
- <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
27
- {:then avatarUrl}
28
- {#if avatarUrl}
29
- <img class="{sizeClass} flex-none rounded-sm bg-gray-200 object-cover" src={avatarUrl} alt="{orgName} avatar" />
30
- {:else}
 
 
 
 
 
 
 
 
 
 
31
  <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
32
- {/if}
33
- {:catch}
34
- <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
35
- {/await}
 
1
  <script lang="ts">
2
+ import { isCustomModel, type CustomModel, type Model } from "$lib/types.js";
3
+ import IconCube from "~icons/carbon/cube";
4
+
5
  interface Props {
6
+ model: Model | CustomModel;
7
+ orgName?: string | undefined;
8
  size?: "sm" | "md";
9
  }
10
 
11
+ let { model, orgName = undefined, size = "md" }: Props = $props();
12
 
13
  let sizeClass = $derived(size === "sm" ? "size-3" : "size-4");
14
+ let isCustom = $derived(isCustomModel(model));
15
+ let _orgName = $derived(orgName ?? (!isCustom ? model.id.split("/")[0] : undefined));
16
 
17
  async function getAvatarUrl(orgName?: string) {
18
  if (!orgName) return;
 
28
  }
29
  </script>
30
 
31
+ {#if isCustom}
32
+ <div
33
+ class="{sizeClass} grid place-items-center rounded-sm bg-gray-500/10 text-gray-500 dark:bg-gray-500/20 dark:text-gray-300"
34
+ >
35
+ <IconCube class="size-full p-0.5" />
36
+ </div>
37
+ {:else}
38
+ {#await getAvatarUrl(_orgName)}
39
+ <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
40
+ {:then avatarUrl}
41
+ {#if avatarUrl}
42
+ <img class="{sizeClass} flex-none rounded-sm bg-gray-200 object-cover" src={avatarUrl} alt="{_orgName} avatar" />
43
+ {:else}
44
+ <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
45
+ {/if}
46
+ {:catch}
47
  <div class="{sizeClass} flex-none rounded-sm bg-gray-200"></div>
48
+ {/await}
49
+ {/if}
 
 
src/lib/components/debug-menu.svelte CHANGED
@@ -10,6 +10,7 @@
10
  import { addToast } from "./toaster.svelte.js";
11
  import { models } from "$lib/state/models.svelte";
12
  import { last } from "$lib/utils/array.js";
 
13
 
14
  let innerWidth = $state<number>();
15
  let innerHeight = $state<number>();
@@ -95,6 +96,17 @@
95
  addToast(toastData[Math.floor(Math.random() * toastData.length)]!);
96
  },
97
  },
 
 
 
 
 
 
 
 
 
 
 
98
  ].toSorted((a, b) => compareStr(a.label, b.label));
99
  </script>
100
 
 
10
  import { addToast } from "./toaster.svelte.js";
11
  import { models } from "$lib/state/models.svelte";
12
  import { last } from "$lib/utils/array.js";
13
+ import { openCustomModelConfig } from "./inference-playground/custom-model-config.svelte";
14
 
15
  let innerWidth = $state<number>();
16
  let innerHeight = $state<number>();
 
96
  addToast(toastData[Math.floor(Math.random() * toastData.length)]!);
97
  },
98
  },
99
+ {
100
+ label: "Pre-filled custom model config",
101
+ cb: () => {
102
+ openCustomModelConfig({
103
+ model: {
104
+ id: "google/gemini-2.0-flash-001",
105
+ endpointUrl: "https://openrouter.ai/api",
106
+ },
107
+ });
108
+ },
109
+ },
110
  ].toSorted((a, b) => compareStr(a.label, b.label));
111
  </script>
112
 
src/lib/components/icon-provider.svelte CHANGED
@@ -30,7 +30,7 @@
30
  fill="#EE7624"
31
  ></path></svg
32
  >
33
- {:else if provider === "fal"}
34
  <svg
35
  class="text-lg"
36
  xmlns="http://www.w3.org/2000/svg"
@@ -298,6 +298,24 @@
298
  fill="#814D00"
299
  ></path></svg
300
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  {:else if children}{@render children()}{:else}
302
  <div class="size-4 flex-none rounded-sm bg-gray-200"></div>
303
  {/if}
 
30
  fill="#EE7624"
31
  ></path></svg
32
  >
33
+ {:else if provider === "fal" || provider === "fal-ai"}
34
  <svg
35
  class="text-lg"
36
  xmlns="http://www.w3.org/2000/svg"
 
298
  fill="#814D00"
299
  ></path></svg
300
  >
301
+ {:else if provider === "openai"}
302
+ <svg
303
+ class="text-lg"
304
+ aria-hidden="true"
305
+ focusable="false"
306
+ width="1em"
307
+ height="1em"
308
+ viewBox="0 0 24 24"
309
+ role="img"
310
+ xmlns="http://www.w3.org/2000/svg"
311
+ preserveAspectRatio="xMidYMid meet"
312
+ fill="currentColor"
313
+ >
314
+ <title>OpenAI icon</title>
315
+ <path
316
+ d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
317
+ />
318
+ </svg>
319
  {:else if children}{@render children()}{:else}
320
  <div class="size-4 flex-none rounded-sm bg-gray-200"></div>
321
  {/if}
src/lib/components/inference-playground/code-snippets.svelte CHANGED
@@ -1,17 +1,18 @@
1
  <script lang="ts">
2
- import type { Conversation } from "$lib/types.js";
3
-
 
 
 
 
4
  import hljs from "highlight.js/lib/core";
5
  import http from "highlight.js/lib/languages/http";
6
  import javascript from "highlight.js/lib/languages/javascript";
7
  import python from "highlight.js/lib/languages/python";
8
  import { createEventDispatcher } from "svelte";
9
-
10
- import { token } from "$lib/state/token.svelte.js";
11
- import { entries, fromEntries, keys } from "$lib/utils/object.js";
12
- import type { InferenceProvider } from "@huggingface/inference";
13
  import IconExternal from "~icons/carbon/arrow-up-right";
14
- import IconCopyCode from "~icons/carbon/copy";
 
15
  import { getInferenceSnippet, type GetInferenceSnippetReturn, type InferenceSnippetLanguage } from "./utils.js";
16
 
17
  hljs.registerLanguage("javascript", javascript);
@@ -42,7 +43,40 @@
42
  lang: InferenceSnippetLanguage;
43
  };
44
  function getSnippet({ tokenStr, conversation, lang }: GetSnippetArgs) {
45
- return getInferenceSnippet(conversation.model, conversation.provider as InferenceProvider, lang, tokenStr, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  messages: conversation.messages,
47
  streaming: conversation.streaming,
48
  max_tokens: conversation.config.max_tokens,
@@ -66,78 +100,55 @@
66
  docs: string;
67
  };
68
 
69
- function getTokenStr(showToken: boolean) {
70
- if (token.value && showToken) {
71
- return token.value;
72
- }
73
- return "YOUR_HF_TOKEN";
74
- }
75
-
76
  function highlight(code?: string, language?: InferenceSnippetLanguage) {
 
77
  if (!code || !language) return "";
78
  return hljs.highlight(code, { language: language === "curl" ? "http" : language }).value;
79
  }
80
 
81
- function copy(el: HTMLElement, _content?: string) {
82
- let timeout: ReturnType<typeof setTimeout>;
83
- let content = _content ?? "";
84
 
85
- function update(_content?: string) {
86
- content = _content ?? "";
87
  }
88
 
89
- function onClick() {
90
- el.classList.add("text-green-500");
91
- navigator.clipboard.writeText(content);
92
- clearTimeout(timeout);
93
- timeout = setTimeout(() => {
94
- el.classList.remove("text-green-500");
95
- }, 400);
96
- }
97
- el.addEventListener("click", onClick);
98
-
99
- return {
100
- update,
101
- destroy() {
102
- clearTimeout(timeout);
103
- el.removeEventListener("click", onClick);
104
- },
105
- };
106
- }
107
- let tokenStr = $derived(getTokenStr(showToken));
108
- let snippetsByLang = $derived({
109
  javascript: getSnippet({ lang: "js", tokenStr, conversation }),
110
  python: getSnippet({ lang: "python", tokenStr, conversation }),
111
  http: getSnippet({ lang: "curl", tokenStr, conversation }),
112
  } as Record<Language, GetInferenceSnippetReturn>);
113
- let selectedSnippet = $derived(snippetsByLang[lang][selectedSnippetIdxByLang[lang]]);
114
- let installInstructions = $derived(
115
- (function getInstallInstructions(): InstallInstructions | undefined {
116
- if (lang === "javascript") {
117
- const isHugging = selectedSnippet?.client.includes("hugging");
118
- const toInstall = isHugging ? "@huggingface/inference" : "openai";
119
- const docs = isHugging
120
- ? "https://huggingface.co/docs/huggingface.js/inference/README"
121
- : "https://platform.openai.com/docs/libraries";
122
- return {
123
- title: `Install ${toInstall}`,
124
- content: `npm install --save ${toInstall}`,
125
- docs,
126
- };
127
- } else if (lang === "python") {
128
- const isHugging = selectedSnippet?.client.includes("hugging");
129
- const toInstall = isHugging ? "huggingface_hub" : "openai";
130
- const docs = isHugging
131
- ? "https://huggingface.co/docs/huggingface_hub/guides/inference"
132
- : "https://platform.openai.com/docs/libraries";
133
- return {
134
- title: `Install the latest`,
135
- content: `pip install --upgrade ${toInstall}`,
136
- docs,
137
- };
138
- }
139
- })()
140
- );
141
  </script>
142
 
143
  <div class="px-2 pt-2">
@@ -198,12 +209,21 @@
198
  </a>
199
  </h2>
200
  <div class="flex items-center gap-x-4">
201
- <button
202
- class="flex items-center gap-x-2 rounded-md border bg-white px-1.5 py-0.5 text-sm shadow-xs transition dark:border-gray-800 dark:bg-gray-800"
203
- use:copy={installInstructions.content}
204
- >
205
- <IconCopyCode class="text-2xs" /> Copy code
206
- </button>
 
 
 
 
 
 
 
 
 
207
  </div>
208
  </div>
209
  <pre
@@ -224,12 +244,21 @@
224
  <input type="checkbox" bind:checked={showToken} />
225
  <p class="leading-none">With token</p>
226
  </label>
227
- <button
228
- class="flex items-center gap-x-2 rounded-md border bg-white px-1.5 py-0.5 text-sm shadow-xs transition dark:border-gray-800 dark:bg-gray-800"
229
- use:copy={selectedSnippet?.content}
230
- >
231
- <IconCopyCode class="text-2xs" /> Copy code
232
- </button>
 
 
 
 
 
 
 
 
 
233
  </div>
234
  </div>
235
  <pre
 
1
  <script lang="ts">
2
+ import { emptyModel } from "$lib/state/session.svelte.js";
3
+ import { token } from "$lib/state/token.svelte.js";
4
+ import { isConversationWithCustomModel, isCustomModel, PipelineTag, type Conversation } from "$lib/types.js";
5
+ import { copyToClipboard } from "$lib/utils/copy.js";
6
+ import { entries, fromEntries, keys } from "$lib/utils/object.js";
7
+ import type { InferenceProvider } from "@huggingface/inference";
8
  import hljs from "highlight.js/lib/core";
9
  import http from "highlight.js/lib/languages/http";
10
  import javascript from "highlight.js/lib/languages/javascript";
11
  import python from "highlight.js/lib/languages/python";
12
  import { createEventDispatcher } from "svelte";
 
 
 
 
13
  import IconExternal from "~icons/carbon/arrow-up-right";
14
+ import IconCopy from "~icons/carbon/copy";
15
+ import LocalToasts from "../local-toasts.svelte";
16
  import { getInferenceSnippet, type GetInferenceSnippetReturn, type InferenceSnippetLanguage } from "./utils.js";
17
 
18
  hljs.registerLanguage("javascript", javascript);
 
43
  lang: InferenceSnippetLanguage;
44
  };
45
  function getSnippet({ tokenStr, conversation, lang }: GetSnippetArgs) {
46
+ const model = conversation.model;
47
+ if (isCustomModel(model)) {
48
+ const snippets = getInferenceSnippet(
49
+ {
50
+ ...emptyModel,
51
+ _id: model._id,
52
+ id: model.id,
53
+ pipeline_tag: PipelineTag.TextGeneration,
54
+ tags: ["conversational"],
55
+ },
56
+ "hf-inference",
57
+ lang,
58
+ tokenStr,
59
+ {
60
+ messages: conversation.messages,
61
+ streaming: conversation.streaming,
62
+ max_tokens: conversation.config.max_tokens,
63
+ temperature: conversation.config.temperature,
64
+ top_p: conversation.config.top_p,
65
+ }
66
+ );
67
+ return snippets
68
+ .filter(s => s.client.startsWith("open") || lang === "curl")
69
+ .map(s => {
70
+ return {
71
+ ...s,
72
+ content: s.content
73
+ .replaceAll("https://router.huggingface.co/hf-inference/v1", model.endpointUrl)
74
+ .replaceAll(`https://router.huggingface.co/hf-inference/models/${model.id}/v1`, model.endpointUrl),
75
+ };
76
+ });
77
+ }
78
+
79
+ return getInferenceSnippet(model, conversation.provider as InferenceProvider, lang, tokenStr, {
80
  messages: conversation.messages,
81
  streaming: conversation.streaming,
82
  max_tokens: conversation.config.max_tokens,
 
100
  docs: string;
101
  };
102
 
 
 
 
 
 
 
 
103
  function highlight(code?: string, language?: InferenceSnippetLanguage) {
104
+ console.log({ code, language });
105
  if (!code || !language) return "";
106
  return hljs.highlight(code, { language: language === "curl" ? "http" : language }).value;
107
  }
108
 
109
+ const tokenStr = $derived.by(() => {
110
+ if (isConversationWithCustomModel(conversation)) {
111
+ const t = conversation.model.accessToken;
112
 
113
+ return t && showToken ? t : "YOUR_ACCESS_TOKEN";
 
114
  }
115
 
116
+ return token.value && showToken ? token.value : "YOUR_HF_TOKEN";
117
+ });
118
+
119
+ const snippetsByLang = $derived({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  javascript: getSnippet({ lang: "js", tokenStr, conversation }),
121
  python: getSnippet({ lang: "python", tokenStr, conversation }),
122
  http: getSnippet({ lang: "curl", tokenStr, conversation }),
123
  } as Record<Language, GetInferenceSnippetReturn>);
124
+
125
+ const selectedSnippet = $derived(snippetsByLang[lang][selectedSnippetIdxByLang[lang]]);
126
+
127
+ const installInstructions = $derived.by(function getInstallInstructions(): InstallInstructions | undefined {
128
+ if (lang === "javascript") {
129
+ const isHugging = selectedSnippet?.client.includes("hugging");
130
+ const toInstall = isHugging ? "@huggingface/inference" : "openai";
131
+ const docs = isHugging
132
+ ? "https://huggingface.co/docs/huggingface.js/inference/README"
133
+ : "https://platform.openai.com/docs/libraries";
134
+ return {
135
+ title: `Install ${toInstall}`,
136
+ content: `npm install --save ${toInstall}`,
137
+ docs,
138
+ };
139
+ } else if (lang === "python") {
140
+ const isHugging = selectedSnippet?.client.includes("hugging");
141
+ const toInstall = isHugging ? "huggingface_hub" : "openai";
142
+ const docs = isHugging
143
+ ? "https://huggingface.co/docs/huggingface_hub/guides/inference"
144
+ : "https://platform.openai.com/docs/libraries";
145
+ return {
146
+ title: `Install the latest`,
147
+ content: `pip install --upgrade ${toInstall}`,
148
+ docs,
149
+ };
150
+ }
151
+ });
152
  </script>
153
 
154
  <div class="px-2 pt-2">
 
209
  </a>
210
  </h2>
211
  <div class="flex items-center gap-x-4">
212
+ <LocalToasts>
213
+ {#snippet children({ addToast, trigger })}
214
+ <button
215
+ {...trigger}
216
+ class="btn flex h-auto items-center gap-2 px-2 py-1.5 text-xs"
217
+ onclick={() => {
218
+ copyToClipboard(installInstructions.content);
219
+ addToast({ data: { content: "Copied to clipboard", variant: "info" } });
220
+ }}
221
+ >
222
+ <IconCopy />
223
+ Copy code
224
+ </button>
225
+ {/snippet}
226
+ </LocalToasts>
227
  </div>
228
  </div>
229
  <pre
 
244
  <input type="checkbox" bind:checked={showToken} />
245
  <p class="leading-none">With token</p>
246
  </label>
247
+ <LocalToasts>
248
+ {#snippet children({ addToast, trigger })}
249
+ <button
250
+ {...trigger}
251
+ class="btn flex h-auto items-center gap-2 px-2 py-1.5 text-xs"
252
+ onclick={() => {
253
+ copyToClipboard(selectedSnippet?.content ?? "");
254
+ addToast({ data: { content: "Copied to clipboard", variant: "info" } });
255
+ }}
256
+ >
257
+ <IconCopy />
258
+ Copy code
259
+ </button>
260
+ {/snippet}
261
+ </LocalToasts>
262
  </div>
263
  </div>
264
  <pre
src/lib/components/inference-playground/conversation-header.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import type { Conversation, ModelWithTokenizer } from "$lib/types.js";
3
 
4
  import { createEventDispatcher } from "svelte";
5
 
@@ -42,9 +42,10 @@
42
  ? 'mr-4 max-sm:ml-4'
43
  : 'mx-4'} flex h-11 flex-none items-center gap-2 rounded-lg border border-gray-200/80 bg-white pr-2 pl-3 text-sm leading-none whitespace-nowrap shadow-xs *:flex-none max-sm:mt-4 dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
44
  >
45
- <Avatar orgName={nameSpace} size="md" />
46
- <button class="flex-1! self-stretch text-left hover:underline" onclick={() => (modelSelectorOpen = true)}
47
- >{conversation.model.id}</button
 
48
  >
49
  <button
50
  class="borderdark:border-white/5 flex size-6 items-center justify-center rounded-sm bg-gray-50 text-xs hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600"
@@ -63,13 +64,15 @@
63
  </button>
64
  </div>
65
 
66
- <div
67
- class="{conversationIdx === 0
68
- ? 'mr-4 max-sm:ml-4'
69
- : 'mx-4'} mt-2 h-11 text-sm leading-none whitespace-nowrap max-sm:mt-4"
70
- >
71
- <ProviderSelect
72
- bind:conversation
73
- class="rounded-lg border border-gray-200/80 bg-white dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
74
- />
75
- </div>
 
 
 
1
  <script lang="ts">
2
+ import { isConversationWithHFModel, type Conversation, type ModelWithTokenizer } from "$lib/types.js";
3
 
4
  import { createEventDispatcher } from "svelte";
5
 
 
42
  ? 'mr-4 max-sm:ml-4'
43
  : 'mx-4'} flex h-11 flex-none items-center gap-2 rounded-lg border border-gray-200/80 bg-white pr-2 pl-3 text-sm leading-none whitespace-nowrap shadow-xs *:flex-none max-sm:mt-4 dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
44
  >
45
+ <Avatar model={conversation.model} orgName={nameSpace} size="md" />
46
+ <button
47
+ class="focus-outline flex-1! self-stretch text-left hover:underline"
48
+ onclick={() => (modelSelectorOpen = true)}>{conversation.model.id}</button
49
  >
50
  <button
51
  class="borderdark:border-white/5 flex size-6 items-center justify-center rounded-sm bg-gray-50 text-xs hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600"
 
64
  </button>
65
  </div>
66
 
67
+ {#if isConversationWithHFModel(conversation)}
68
+ <div
69
+ class="{conversationIdx === 0
70
+ ? 'mr-4 max-sm:ml-4'
71
+ : 'mx-4'} mt-2 h-11 text-sm leading-none whitespace-nowrap max-sm:mt-4"
72
+ >
73
+ <ProviderSelect
74
+ bind:conversation
75
+ class="rounded-lg border border-gray-200/80 bg-white dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
76
+ />
77
+ </div>
78
+ {/if}
src/lib/components/inference-playground/conversation.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import type { Conversation } from "$lib/types.js";
3
 
4
  import { ScrollState } from "$lib/spells/scroll-state.svelte";
5
  import { watch } from "runed";
 
1
  <script lang="ts">
2
+ import { type Conversation } from "$lib/types.js";
3
 
4
  import { ScrollState } from "$lib/spells/scroll-state.svelte";
5
  import { watch } from "runed";
src/lib/components/inference-playground/custom-model-config.svelte ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" module>
2
+ let model = $state<Partial<CustomModel> | undefined>(undefined);
3
+ let onSubmit = $state<OpenCustomModelConfigArgs["onSubmit"]>();
4
+
5
+ type OpenCustomModelConfigArgs = {
6
+ model?: typeof model;
7
+ onSubmit?: (model: CustomModel) => void;
8
+ onDelete?: () => void;
9
+ };
10
+
11
+ export function openCustomModelConfig(args?: OpenCustomModelConfigArgs) {
12
+ model = $state.snapshot(args?.model ?? {});
13
+ onSubmit = args?.onSubmit;
14
+ }
15
+
16
+ function close() {
17
+ model = undefined;
18
+ onSubmit = undefined;
19
+ }
20
+ </script>
21
+
22
+ <script lang="ts">
23
+ import { autofocus } from "$lib/actions/autofocus.js";
24
+
25
+ import { clickOutside } from "$lib/actions/click-outside.js";
26
+ import { models } from "$lib/state/models.svelte";
27
+ import { type Conversation, type CustomModel } from "$lib/types.js";
28
+ import type { HTMLFormAttributes } from "svelte/elements";
29
+ import { fade, scale } from "svelte/transition";
30
+ import IconCross from "~icons/carbon/close";
31
+ import typia from "typia";
32
+ import { handleNonStreamingResponse } from "./utils.js";
33
+
34
+ let dialog: HTMLDialogElement | undefined = $state();
35
+ const exists = $derived(!!models.custom.find(m => m._id === model?._id));
36
+
37
+ $effect(() => {
38
+ if (model !== undefined) {
39
+ dialog?.showModal();
40
+ } else {
41
+ setTimeout(() => {
42
+ dialog?.close();
43
+ clear();
44
+ }, 250);
45
+ }
46
+ });
47
+
48
+ type Message = {
49
+ type: "error" | "success";
50
+ content: string;
51
+ };
52
+ let message = $state<Message | null>(null);
53
+
54
+ const error = (content: string) => (message = { type: "error", content }) satisfies Message;
55
+ const success = (content: string) => (message = { type: "success", content }) satisfies Message;
56
+ const clear = () => (message = null);
57
+
58
+ const onsubmit: HTMLFormAttributes["onsubmit"] = async e => {
59
+ e.preventDefault();
60
+ clear();
61
+ const isTest = e.submitter?.dataset.form === "test";
62
+ if (isTest) {
63
+ testing = true;
64
+
65
+ const conv: Conversation = {
66
+ model: {
67
+ ...model,
68
+ _id: "",
69
+ /** These will never be empty, because of our form validation */
70
+ id: model?.id ?? "",
71
+ endpointUrl: model?.endpointUrl ?? "",
72
+ },
73
+ messages: [
74
+ {
75
+ role: "user",
76
+ content: "test",
77
+ },
78
+ ],
79
+ config: {},
80
+ systemMessage: { role: "system", content: "" },
81
+ streaming: false,
82
+ };
83
+ try {
84
+ await handleNonStreamingResponse(conv);
85
+ success("Test successful!");
86
+ } catch (err) {
87
+ if (err instanceof Error) {
88
+ error(`Test failed: ${err.message}`);
89
+ } else {
90
+ error(`An unknown error occurred during testing.`);
91
+ }
92
+ } finally {
93
+ testing = false;
94
+ }
95
+ } else {
96
+ const withUUID = { _id: crypto.randomUUID(), ...model };
97
+ if (!typia.is<CustomModel>(withUUID)) return;
98
+ models.upsertCustom(withUUID);
99
+ onSubmit?.(withUUID);
100
+ model = undefined;
101
+ }
102
+ };
103
+
104
+ let testing = $state(false);
105
+ </script>
106
+
107
+ <dialog class="backdrop:bg-transparent" bind:this={dialog} onclose={() => close()}>
108
+ {#if model !== undefined}
109
+ <!-- Backdrop -->
110
+ <div
111
+ class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-black/50 backdrop-blur-sm"
112
+ transition:fade={{ duration: 150 }}
113
+ >
114
+ <!-- Content -->
115
+ <form
116
+ class="relative w-xl rounded-xl bg-white shadow-sm dark:bg-gray-900"
117
+ use:clickOutside={() => close()}
118
+ transition:scale={{ start: 0.975, duration: 250 }}
119
+ {onsubmit}
120
+ >
121
+ <div class="flex items-center justify-between rounded-t border-b p-4 md:px-5 md:py-4 dark:border-gray-800">
122
+ <h2 class="flex items-center gap-2.5 text-lg font-semibold text-gray-900 dark:text-white">
123
+ Configure custom endpoint
124
+ </h2>
125
+ <button
126
+ type="button"
127
+ class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
128
+ onclick={close}
129
+ >
130
+ <div class="text-xl">
131
+ <IconCross />
132
+ </div>
133
+ <span class="sr-only">Close modal</span>
134
+ </button>
135
+ </div>
136
+ <!-- Modal body -->
137
+ <div class="flex flex-col gap-3 p-4 md:p-5">
138
+ <label class="flex flex-col gap-2">
139
+ <p class="block text-sm font-medium text-gray-900 dark:text-white">
140
+ Model ID <span class="text-red-800 dark:text-red-300">*</span>
141
+ </p>
142
+ <input
143
+ use:autofocus
144
+ bind:value={model.id}
145
+ required
146
+ placeholder="e.g. mistralai/mistral-large-2407"
147
+ type="text"
148
+ class="input block w-full"
149
+ />
150
+ </label>
151
+ <label class="flex flex-col gap-2">
152
+ <p class="block text-sm font-medium text-gray-900 dark:text-white">
153
+ Endpoint URL <span class="text-red-800 dark:text-red-300">*</span>
154
+ </p>
155
+ <input
156
+ bind:value={model.endpointUrl}
157
+ placeholder="e.g. https://your-provider.com/api/v1"
158
+ required
159
+ type="text"
160
+ class="input block w-full"
161
+ />
162
+ </label>
163
+ <label class="flex flex-col gap-2">
164
+ <p class="block text-sm font-medium text-gray-900 dark:text-white">Access Token</p>
165
+ <input
166
+ bind:value={model.accessToken}
167
+ placeholder="XXXXXXXXXXXXXXXXXXXX"
168
+ type="text"
169
+ class="input block w-full"
170
+ />
171
+ <p class="text-sm text-gray-500">Stored locally - not sent to our server</p>
172
+ </label>
173
+
174
+ {#if message}
175
+ <div
176
+ class={[
177
+ "mt-2 rounded-lg border p-3 text-sm",
178
+ message.type === "error" &&
179
+ "border-red-400 bg-red-100 text-red-700 dark:border-red-600 dark:bg-red-900/30 dark:text-red-300",
180
+ message.type === "success" &&
181
+ "border-green-400 bg-green-100 text-green-700 dark:border-green-600 dark:bg-green-900/30 dark:text-green-300",
182
+ ]}
183
+ role="alert"
184
+ >
185
+ {message.content}
186
+ </div>
187
+ {/if}
188
+ </div>
189
+
190
+ <!-- Modal footer -->
191
+ <div class="flex gap-2 rounded-b border-t border-gray-200 p-4 md:p-5 dark:border-gray-800">
192
+ {#if exists}
193
+ <button
194
+ type="button"
195
+ class="rounded-lg bg-red-100 px-5 py-2.5 text-sm font-medium text-red-700 hover:bg-red-200 focus:ring-4 focus:ring-red-300 focus:outline-none dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/40 dark:focus:ring-red-900"
196
+ onclick={() => {
197
+ if (model?._id) models.removeCustom(model._id);
198
+ close();
199
+ }}
200
+ disabled={testing}
201
+ >
202
+ Delete
203
+ </button>
204
+ {/if}
205
+ <!-- Reverse flex so that submit is the button called on enter -->
206
+ <div class="ml-auto flex flex-row-reverse items-center gap-2">
207
+ <button
208
+ data-form="submit"
209
+ type="submit"
210
+ class="rounded-lg bg-black px-5 py-2.5 text-sm font-medium text-white
211
+ hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 focus:outline-none
212
+ disabled:!bg-black dark:border-gray-700
213
+ dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:disabled:!bg-gray-800"
214
+ disabled={testing}
215
+ >
216
+ Submit
217
+ </button>
218
+ <button
219
+ data-form="test"
220
+ type="submit"
221
+ class="flex items-center rounded-lg bg-black px-5 py-2.5 text-sm font-medium text-white
222
+ hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 focus:outline-none
223
+ disabled:!bg-black dark:border-gray-700
224
+ dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:disabled:!bg-gray-800"
225
+ disabled={testing}
226
+ >
227
+ {#if testing}
228
+ <svg
229
+ class="mr-2 h-4 w-4 animate-spin text-white"
230
+ xmlns="http://www.w3.org/2000/svg"
231
+ fill="none"
232
+ viewBox="0 0 24 24"
233
+ >
234
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
235
+ <path
236
+ class="opacity-75"
237
+ fill="currentColor"
238
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
239
+ ></path>
240
+ </svg>
241
+ Testing...
242
+ {:else}
243
+ Test
244
+ {/if}
245
+ </button>
246
+ </div>
247
+ </div>
248
+ </form>
249
+ </div>
250
+ {/if}
251
+ </dialog>
src/lib/components/inference-playground/custom-provider-select.svelte ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { randomPick } from "$lib/utils/array.js";
3
+ import { cn } from "$lib/utils/cn.js";
4
+ import { INFERENCE_PROVIDERS, type InferenceProvider } from "@huggingface/inference";
5
+ import { Select } from "melt/builders";
6
+ import { onMount } from "svelte";
7
+ import IconCaret from "~icons/carbon/chevron-down";
8
+ import IconProvider from "../icon-provider.svelte";
9
+
10
+ const providers = [...INFERENCE_PROVIDERS];
11
+
12
+ interface Props {
13
+ provider?: InferenceProvider;
14
+ class?: string;
15
+ }
16
+
17
+ let { provider = $bindable(), class: classes = undefined }: Props = $props();
18
+
19
+ function reset() {
20
+ if (provider !== undefined) return;
21
+ provider = randomPick(providers);
22
+ }
23
+
24
+ onMount(reset);
25
+
26
+ const select = new Select<InferenceProvider, false>({
27
+ value: () => provider,
28
+ onValueChange(v) {
29
+ provider = v;
30
+ },
31
+ });
32
+
33
+ const nameMap: Record<InferenceProvider, string> = {
34
+ "sambanova": "SambaNova",
35
+ "fal-ai": "fal",
36
+ "cerebras": "Cerebras",
37
+ "replicate": "Replicate",
38
+ "black-forest-labs": "Black Forest Labs",
39
+ "fireworks-ai": "Fireworks",
40
+ "together": "Together AI",
41
+ "nebius": "Nebius AI Studio",
42
+ "hyperbolic": "Hyperbolic",
43
+ "novita": "Novita",
44
+ "cohere": "Nohere",
45
+ "hf-inference": "HF Inference API",
46
+ "openai": "OpenAI Compatible",
47
+ };
48
+ const UPPERCASE_WORDS = ["hf", "ai"];
49
+
50
+ function formatName(provider: InferenceProvider) {
51
+ if (provider in nameMap) return nameMap[provider];
52
+
53
+ const words = provider
54
+ .toLowerCase()
55
+ .split("-")
56
+ .map(word => {
57
+ if (UPPERCASE_WORDS.includes(word)) {
58
+ return word.toUpperCase();
59
+ } else {
60
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
61
+ }
62
+ });
63
+
64
+ return words.join(" ");
65
+ }
66
+ </script>
67
+
68
+ <button
69
+ {...select.trigger}
70
+ class={cn(
71
+ "focus-outline relative flex items-center justify-between gap-6 overflow-hidden rounded-lg border bg-gray-100/80 px-3 py-1.5 leading-tight whitespace-nowrap shadow-sm",
72
+ "hover:brightness-95 dark:border-gray-700 dark:bg-gray-800 dark:hover:brightness-110",
73
+ select.open && "!custom-outline",
74
+ classes
75
+ )}
76
+ type="button"
77
+ >
78
+ <div class="flex items-center gap-1 text-sm">
79
+ <IconProvider {provider} />
80
+ {provider && formatName(provider)}
81
+ </div>
82
+ <div
83
+ class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
84
+ >
85
+ <IconCaret />
86
+ </div>
87
+ </button>
88
+
89
+ <div {...select.content} class="rounded-lg border bg-gray-100 outline-hidden dark:border-gray-700 dark:bg-gray-800">
90
+ {#each providers as p}
91
+ <button {...select.getOption(p)} class="group block w-full p-1 text-sm dark:text-white" type="button">
92
+ <div
93
+ class="flex items-center gap-2 rounded-md px-2 py-1.5 group-data-[highlighted]:bg-gray-200 dark:group-data-[highlighted]:bg-gray-700"
94
+ >
95
+ <IconProvider provider={p} />
96
+ {formatName(p)}
97
+ </div>
98
+ </button>
99
+ {/each}
100
+ </div>
src/lib/components/inference-playground/generation-config.svelte CHANGED
@@ -11,9 +11,7 @@
11
 
12
  let { conversation = $bindable(), classNames = "" }: Props = $props();
13
 
14
- let modelMaxLength = $derived(
15
- customMaxTokens[conversation.model.id] ?? conversation.model.tokenizerConfig.model_max_length
16
- );
17
  let maxTokens = $derived(Math.min(modelMaxLength ?? GENERATION_CONFIG_SETTINGS["max_tokens"].max, 64_000));
18
  </script>
19
 
 
11
 
12
  let { conversation = $bindable(), classNames = "" }: Props = $props();
13
 
14
+ let modelMaxLength = $derived(customMaxTokens[conversation.model.id] ?? 100000);
 
 
15
  let maxTokens = $derived(Math.min(modelMaxLength ?? GENERATION_CONFIG_SETTINGS["max_tokens"].max, 64_000));
16
  </script>
17
 
src/lib/components/inference-playground/hf-token-modal.svelte CHANGED
@@ -81,9 +81,9 @@
81
  ><br /> Your token is kept safe by only being used from your browser.
82
  </p>
83
  <div>
84
- <label for="hf-token" class="text-smd mb-3 block font-medium text-gray-900 dark:text-white"
85
- >Hugging Face Token</label
86
- >
87
  <input
88
  use:autofocus
89
  required
 
81
  ><br /> Your token is kept safe by only being used from your browser.
82
  </p>
83
  <div>
84
+ <label for="hf-token" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">
85
+ Hugging Face Token
86
+ </label>
87
  <input
88
  use:autofocus
89
  required
src/lib/components/inference-playground/message.svelte CHANGED
@@ -26,7 +26,9 @@
26
  });
27
 
28
  const canUploadImgs = $derived(
29
- message.role === "user" && conversation.model.pipeline_tag === PipelineTag.ImageTextToText
 
 
30
  );
31
  const fileUpload = new FileUpload({
32
  accept: "image/*",
 
26
  });
27
 
28
  const canUploadImgs = $derived(
29
+ message.role === "user" &&
30
+ "pipeline_tag" in conversation.model &&
31
+ conversation.model.pipeline_tag === PipelineTag.ImageTextToText
32
  );
33
  const fileUpload = new FileUpload({
34
  accept: "image/*",
src/lib/components/inference-playground/model-selector-modal.svelte CHANGED
@@ -1,15 +1,21 @@
1
  <script lang="ts">
2
- import type { Conversation, ModelWithTokenizer } from "$lib/types.js";
3
-
4
- import { tick } from "svelte";
5
-
6
  import { autofocus } from "$lib/actions/autofocus.js";
7
  import { models } from "$lib/state/models.svelte.js";
 
 
8
  import fuzzysearch from "$lib/utils/search.js";
9
- import { watch } from "runed";
 
 
 
 
 
 
10
  import IconSearch from "~icons/carbon/search";
11
  import IconStar from "~icons/carbon/star";
12
  import IconEye from "~icons/carbon/view";
 
 
13
 
14
  interface Props {
15
  onModelSelect?: (model: string) => void;
@@ -19,71 +25,35 @@
19
 
20
  let { onModelSelect, onClose, conversation }: Props = $props();
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  let backdropEl = $state<HTMLDivElement>();
23
- let highlightIdx = $state(-1);
24
- let ignoreCursorHighlight = $state(false);
25
- let containerEl = $state<HTMLDivElement>();
26
  let query = $state("");
27
 
28
  const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
29
  const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
30
- const queried = $derived(trending.concat(other));
31
- function getModelIdx(model: ModelWithTokenizer) {
32
- return queried.findIndex(m => m.id === model.id);
33
- }
34
- const highlighted = $derived(queried[highlightIdx]);
35
-
36
- watch(
37
- () => queried,
38
- (curr, prev) => {
39
- const prevModel = prev?.[highlightIdx];
40
- if (prevModel) {
41
- // maintain model selection
42
- highlightIdx = Math.max(
43
- 0,
44
- curr.findIndex(model => model.id === prevModel?.id)
45
- );
46
- } else {
47
- highlightIdx = curr.findIndex(model => model.id === conversation.model.id);
48
- }
49
- scrollToResult();
50
- }
51
- );
52
-
53
- function selectModel(model: ModelWithTokenizer) {
54
- onModelSelect?.(model.id);
55
- onClose?.();
56
- }
57
-
58
- function handleKeydown(e: KeyboardEvent) {
59
- if (e.key === "Escape") {
60
- onClose?.();
61
- } else if (e.key === "Enter") {
62
- if (highlighted) selectModel(highlighted);
63
- } else if (e.key === "ArrowUp") {
64
- if (highlightIdx > 0) highlightIdx--;
65
- ignoreCursorHighlight = true;
66
- } else if (e.key === "ArrowDown") {
67
- if (highlightIdx < queried.length - 1) highlightIdx++;
68
- ignoreCursorHighlight = true;
69
- } else {
70
- return;
71
- }
72
- e.preventDefault();
73
-
74
- scrollToResult();
75
- }
76
-
77
- async function scrollToResult() {
78
- await tick();
79
- const highlightedEl = document.querySelector("[data-model][data-highlighted]");
80
- highlightedEl?.scrollIntoView({ block: "nearest" });
81
- }
82
-
83
- function highlightRow(idx: number) {
84
- if (ignoreCursorHighlight) return;
85
- highlightIdx = idx;
86
- }
87
 
88
  function handleBackdropClick(event: MouseEvent) {
89
  event.stopPropagation();
@@ -94,9 +64,9 @@
94
  onClose?.();
95
  }
96
  }
97
- </script>
98
 
99
- <svelte:window onkeydown={handleKeydown} onmousemove={() => (ignoreCursorHighlight = false)} />
 
100
 
101
  <!-- svelte-ignore a11y_no_static_element_interactions -->
102
  <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -108,50 +78,93 @@
108
  <div class="flex w-full max-w-[600px] items-start justify-center overflow-hidden p-10 text-left whitespace-nowrap">
109
  <div
110
  class="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-white text-gray-900 shadow-md dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
111
- bind:this={containerEl}
112
  >
113
  <div class="flex items-center border-b px-3 dark:border-gray-800">
114
  <div class="mr-2 text-sm">
115
  <IconSearch />
116
  </div>
117
  <input
 
118
  use:autofocus
119
  class="flex h-10 w-full rounded-md bg-transparent py-3 text-sm placeholder-gray-400 outline-hidden"
120
  placeholder="Search models ..."
121
  bind:value={query}
122
  />
123
  </div>
124
- <div class="max-h-[300px] overflow-x-hidden overflow-y-auto">
125
- {#snippet modelEntry(model: ModelWithTokenizer, trending?: boolean)}
126
- {@const idx = getModelIdx(model)}
127
  {@const [nameSpace, modelName] = model.id.split("/")}
128
  <button
129
  class="flex w-full cursor-pointer items-center px-2 py-1.5 text-sm
130
  data-[highlighted]:bg-gray-100 data-[highlighted]:dark:bg-gray-800"
131
- data-highlighted={highlightIdx === idx ? true : undefined}
132
  data-model
133
- onmouseenter={() => highlightRow(idx)}
134
- onclick={() => {
135
- onModelSelect?.(model.id);
136
- onClose?.();
137
- }}
138
  >
139
  {#if trending}
140
  <div class=" mr-1.5 size-4 text-yellow-400">
141
  <IconStar />
142
  </div>
143
  {/if}
144
- <span class="inline-flex items-center"
145
- ><span class="text-gray-500 dark:text-gray-400">{nameSpace}</span><span
146
- class="mx-1 text-gray-300 dark:text-gray-700">/</span
147
- ><span class="text-black dark:text-white">{modelName}</span></span
148
- >
149
- {#if model.pipeline_tag === "image-text-to-text"}
150
- <div
151
- class="ml-2 grid size-5 place-items-center rounded bg-gray-500/10 text-gray-500 dark:bg-gray-500/20 dark:text-gray-300"
152
- >
153
- <IconEye class="size-3.5" />
154
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  {/if}
156
  </button>
157
  {/snippet}
@@ -161,6 +174,26 @@
161
  {@render modelEntry(model, true)}
162
  {/each}
163
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  {#if other.length > 0}
165
  <div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other models</div>
166
  {#each other as model}
 
1
  <script lang="ts">
 
 
 
 
2
  import { autofocus } from "$lib/actions/autofocus.js";
3
  import { models } from "$lib/state/models.svelte.js";
4
+ import type { Conversation, CustomModel, ModelWithTokenizer } from "$lib/types.js";
5
+ import { noop } from "$lib/utils/noop.js";
6
  import fuzzysearch from "$lib/utils/search.js";
7
+ import { sleep } from "$lib/utils/sleep.js";
8
+ import { Combobox } from "melt/builders";
9
+ import { untrack } from "svelte";
10
+ import typia from "typia";
11
+ import IconAdd from "~icons/carbon/add";
12
+ import IconCube from "~icons/carbon/cube";
13
+ import IconEdit from "~icons/carbon/edit";
14
  import IconSearch from "~icons/carbon/search";
15
  import IconStar from "~icons/carbon/star";
16
  import IconEye from "~icons/carbon/view";
17
+ import Tooltip from "../tooltip.svelte";
18
+ import { openCustomModelConfig } from "./custom-model-config.svelte";
19
 
20
  interface Props {
21
  onModelSelect?: (model: string) => void;
 
25
 
26
  let { onModelSelect, onClose, conversation }: Props = $props();
27
 
28
+ const combobox = new Combobox({
29
+ onOpenChange(o) {
30
+ if (!o) onClose?.();
31
+ },
32
+ floatingConfig: {
33
+ onCompute: noop,
34
+ },
35
+ sameWidth: false,
36
+ value: () => undefined,
37
+ onValueChange(modelId) {
38
+ if (!modelId) return;
39
+ onModelSelect?.(modelId);
40
+ onClose?.();
41
+ },
42
+ });
43
+ $effect(() => {
44
+ untrack(() => combobox.highlight(conversation.model.id));
45
+ // Workaround while this component does not use a <dialog />
46
+ sleep(10).then(() => {
47
+ combobox.open = true;
48
+ });
49
+ });
50
+
51
  let backdropEl = $state<HTMLDivElement>();
 
 
 
52
  let query = $state("");
53
 
54
  const trending = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
55
  const other = $derived(fuzzysearch({ needle: query, haystack: models.nonTrending, property: "id" }));
56
+ const custom = $derived(fuzzysearch({ needle: query, haystack: models.custom, property: "id" }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  function handleBackdropClick(event: MouseEvent) {
59
  event.stopPropagation();
 
64
  onClose?.();
65
  }
66
  }
 
67
 
68
+ const isCustom = typia.createIs<CustomModel>();
69
+ </script>
70
 
71
  <!-- svelte-ignore a11y_no_static_element_interactions -->
72
  <!-- svelte-ignore a11y_click_events_have_key_events -->
 
78
  <div class="flex w-full max-w-[600px] items-start justify-center overflow-hidden p-10 text-left whitespace-nowrap">
79
  <div
80
  class="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-white text-gray-900 shadow-md dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
 
81
  >
82
  <div class="flex items-center border-b px-3 dark:border-gray-800">
83
  <div class="mr-2 text-sm">
84
  <IconSearch />
85
  </div>
86
  <input
87
+ {...combobox.input}
88
  use:autofocus
89
  class="flex h-10 w-full rounded-md bg-transparent py-3 text-sm placeholder-gray-400 outline-hidden"
90
  placeholder="Search models ..."
91
  bind:value={query}
92
  />
93
  </div>
94
+ <div class="max-h-[300px] overflow-x-hidden overflow-y-auto" {...combobox.content} popover={undefined}>
95
+ {#snippet modelEntry(model: ModelWithTokenizer | CustomModel, trending?: boolean)}
 
96
  {@const [nameSpace, modelName] = model.id.split("/")}
97
  <button
98
  class="flex w-full cursor-pointer items-center px-2 py-1.5 text-sm
99
  data-[highlighted]:bg-gray-100 data-[highlighted]:dark:bg-gray-800"
 
100
  data-model
101
+ {...combobox.getOption(model.id)}
 
 
 
 
102
  >
103
  {#if trending}
104
  <div class=" mr-1.5 size-4 text-yellow-400">
105
  <IconStar />
106
  </div>
107
  {/if}
108
+
109
+ {#if modelName}
110
+ <span class="inline-flex items-center">
111
+ <span class="text-gray-500 dark:text-gray-400">{nameSpace}</span>
112
+ <span class="mx-1 text-gray-300 dark:text-gray-700">/</span>
113
+ <span class="text-black dark:text-white">{modelName}</span>
114
+ </span>
115
+ {:else}
116
+ <span class="text-black dark:text-white">{nameSpace}</span>
117
+ {/if}
118
+
119
+ {#if "pipeline_tag" in model && model.pipeline_tag === "image-text-to-text"}
120
+ <Tooltip openDelay={100}>
121
+ {#snippet trigger(tooltip)}
122
+ <div
123
+ class="ml-2 grid size-5 place-items-center rounded bg-gray-500/10 text-gray-500 dark:bg-gray-500/20 dark:text-gray-300"
124
+ {...tooltip.trigger}
125
+ >
126
+ <IconEye class="size-3.5" />
127
+ </div>
128
+ {/snippet}
129
+ Image text-to-text
130
+ </Tooltip>
131
+ {/if}
132
+
133
+ {#if isCustom(model)}
134
+ <Tooltip openDelay={100}>
135
+ {#snippet trigger(tooltip)}
136
+ <div
137
+ class="ml-2 grid size-5 place-items-center rounded bg-gray-500/10 text-gray-500 dark:bg-gray-500/20 dark:text-gray-300"
138
+ {...tooltip.trigger}
139
+ >
140
+ <IconCube class="size-3.5" />
141
+ </div>
142
+ {/snippet}
143
+ Custom Model
144
+ </Tooltip>
145
+ <Tooltip>
146
+ {#snippet trigger(tooltip)}
147
+ <button
148
+ class="mr-1 ml-auto grid size-4.5 place-items-center rounded-sm bg-gray-100 text-xs
149
+ hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500"
150
+ aria-label="Add custom model"
151
+ {...tooltip.trigger}
152
+ onclick={e => {
153
+ e.stopPropagation();
154
+ onClose?.();
155
+ openCustomModelConfig({
156
+ model,
157
+ onSubmit: model => {
158
+ onModelSelect?.(model.id);
159
+ },
160
+ });
161
+ }}
162
+ >
163
+ <IconEdit class="size-3" />
164
+ </button>
165
+ {/snippet}
166
+ <span class="text-sm">Edit</span>
167
+ </Tooltip>
168
  {/if}
169
  </button>
170
  {/snippet}
 
174
  {@render modelEntry(model, true)}
175
  {/each}
176
  {/if}
177
+ <div class="px-2 py-1.5 text-xs font-medium text-gray-500">Custom endpoints</div>
178
+ {#if custom.length > 0}
179
+ {#each custom as model}
180
+ {@render modelEntry(model, false)}
181
+ {/each}
182
+ {/if}
183
+ <button
184
+ class="flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-sm text-gray-500 data-[highlighted]:bg-blue-500/15 data-[highlighted]:text-blue-600 dark:text-gray-400 dark:data-[highlighted]:text-blue-300"
185
+ {...combobox.getOption("__custom__", () => {
186
+ onClose?.();
187
+ openCustomModelConfig({
188
+ onSubmit: model => {
189
+ onModelSelect?.(model.id);
190
+ },
191
+ });
192
+ })}
193
+ >
194
+ <IconAdd class="rounded bg-blue-500/10 text-blue-600" />
195
+ Add a custom endpoint
196
+ </button>
197
  {#if other.length > 0}
198
  <div class="px-2 py-1.5 text-xs font-medium text-gray-500">Other models</div>
199
  {#each other as model}
src/lib/components/inference-playground/model-selector.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import type { Conversation, ModelWithTokenizer } from "$lib/types.js";
3
 
4
  import { models } from "$lib/state/models.svelte.js";
5
  import IconCaret from "~icons/carbon/chevron-down";
@@ -27,27 +27,28 @@
27
  conversation.provider = undefined;
28
  }
29
 
30
- let nameSpace = $derived(conversation.model.id.split("/")[0] ?? "");
31
- let modelName = $derived(conversation.model.id.split("/")[1] ?? "");
32
- const id = crypto.randomUUID();
 
 
33
  </script>
34
 
35
  <div class="flex flex-col gap-2">
36
  <label for={id} class="flex items-baseline gap-2 text-sm font-medium text-gray-900 dark:text-white">
37
  Models<span class="text-xs font-normal text-gray-400">{models.all.length}</span>
38
  </label>
39
-
40
  <button
41
  {id}
42
- class="relative flex items-center justify-between gap-6 overflow-hidden rounded-lg border bg-gray-100/80 px-3 py-1.5 leading-tight whitespace-nowrap shadow-sm hover:brightness-95 dark:border-gray-700 dark:bg-gray-800 dark:hover:brightness-110"
43
  onclick={() => (showModelPickerModal = true)}
44
  >
45
- <div class="flex flex-col items-start">
46
  <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
47
- <Avatar orgName={nameSpace} size="sm" />
48
  {nameSpace}
49
  </div>
50
- <div>{modelName}</div>
51
  </div>
52
  <div
53
  class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
@@ -61,4 +62,6 @@
61
  <ModelSelectorModal {conversation} onModelSelect={changeModel} onClose={() => (showModelPickerModal = false)} />
62
  {/if}
63
 
64
- <ProviderSelect bind:conversation />
 
 
 
1
  <script lang="ts">
2
+ import { isConversationWithHFModel, isCustomModel, type Conversation, type ModelWithTokenizer } from "$lib/types.js";
3
 
4
  import { models } from "$lib/state/models.svelte.js";
5
  import IconCaret from "~icons/carbon/chevron-down";
 
27
  conversation.provider = undefined;
28
  }
29
 
30
+ const model = $derived(conversation.model);
31
+ const isCustom = $derived(isCustomModel(model));
32
+ const nameSpace = $derived(isCustom ? "Custom endpoint" : (model.id.split("/")[0] ?? ""));
33
+ const modelName = $derived(isCustom ? model.id : (model.id.split("/")[1] ?? ""));
34
+ const id = $props.id();
35
  </script>
36
 
37
  <div class="flex flex-col gap-2">
38
  <label for={id} class="flex items-baseline gap-2 text-sm font-medium text-gray-900 dark:text-white">
39
  Models<span class="text-xs font-normal text-gray-400">{models.all.length}</span>
40
  </label>
 
41
  <button
42
  {id}
43
+ class="focus-outline relative flex items-center justify-between gap-6 overflow-hidden rounded-lg border bg-gray-100/80 px-3 py-1.5 leading-tight whitespace-nowrap shadow-sm hover:brightness-95 dark:border-gray-700 dark:bg-gray-800 dark:hover:brightness-110"
44
  onclick={() => (showModelPickerModal = true)}
45
  >
46
+ <div class="overflow-hidden text-start">
47
  <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
48
+ <Avatar model={conversation.model} orgName={nameSpace} size="sm" />
49
  {nameSpace}
50
  </div>
51
+ <div class="truncate">{modelName}</div>
52
  </div>
53
  <div
54
  class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
 
62
  <ModelSelectorModal {conversation} onModelSelect={changeModel} onClose={() => (showModelPickerModal = false)} />
63
  {/if}
64
 
65
+ {#if isConversationWithHFModel(conversation)}
66
+ <ProviderSelect bind:conversation />
67
+ {/if}
src/lib/components/inference-playground/playground.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import type { ConversationMessage, ModelWithTokenizer, Project } from "$lib/types.js";
3
 
4
  import { handleNonStreamingResponse, handleStreamingResponse, isSystemPromptSupported } from "./utils.js";
5
 
@@ -8,7 +8,6 @@
8
  import { session } from "$lib/state/session.svelte.js";
9
  import { token } from "$lib/state/token.svelte.js";
10
  import { isMac } from "$lib/utils/platform.js";
11
- import { HfInference } from "@huggingface/inference";
12
  import typia from "typia";
13
  import IconExternal from "~icons/carbon/arrow-up-right";
14
  import IconCode from "~icons/carbon/code";
@@ -21,6 +20,7 @@
21
  import { showShareModal } from "../share-modal.svelte";
22
  import Toaster from "../toaster.svelte";
23
  import { addToast } from "../toaster.svelte.js";
 
24
  import PlaygroundConversationHeader from "./conversation-header.svelte";
25
  import PlaygroundConversation from "./conversation.svelte";
26
  import GenerationConfig from "./generation-config.svelte";
@@ -69,14 +69,12 @@
69
  if (!conversation) return;
70
 
71
  const startTime = performance.now();
72
- const hf = new HfInference(token.value);
73
 
74
  if (conversation.streaming) {
75
  let addedMessage = false;
76
  let streamingMessage = $state({ role: "assistant", content: "" });
77
 
78
  await handleStreamingResponse(
79
- hf,
80
  conversation,
81
  content => {
82
  if (!streamingMessage) return;
@@ -92,10 +90,7 @@
92
  abortManager.createController()
93
  );
94
  } else {
95
- const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
96
- hf,
97
- conversation
98
- );
99
  conversation.messages = [...conversation.messages, newMessage];
100
  const c = generationStats[conversationIdx];
101
  if (c) c.generatedTokensCount += newTokensCount;
@@ -286,9 +281,14 @@
286
  {!viewSettings ? "Settings" : "Hide Settings"}
287
  </button>
288
  {/if}
289
- <button type="button" onclick={reset} class="btn size-[39px]">
290
- <IconDelete />
291
- </button>
 
 
 
 
 
292
  </div>
293
  <div class="flex flex-1 shrink-0 items-center justify-center gap-x-8 text-center text-sm text-gray-500">
294
  {#each generationStats as { latency, generatedTokensCount }}
@@ -298,8 +298,8 @@
298
  <div class="flex flex-1 justify-end gap-x-2">
299
  <button type="button" onclick={() => (viewCode = !viewCode)} class="btn">
300
  <IconCode />
301
- {!viewCode ? "View Code" : "Hide Code"}</button
302
- >
303
  <button
304
  onclick={() => {
305
  viewCode = false;
@@ -417,7 +417,7 @@
417
  <div class="absolute bottom-6 left-4 flex items-center gap-2 max-md:hidden">
418
  <a
419
  target="_blank"
420
- href="https://huggingface.co/docs/api-inference/tasks/chat-completion"
421
  class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
422
  >
423
  <div class="text-xs">
 
1
  <script lang="ts">
2
+ import { type ConversationMessage, type ModelWithTokenizer, type Project } from "$lib/types.js";
3
 
4
  import { handleNonStreamingResponse, handleStreamingResponse, isSystemPromptSupported } from "./utils.js";
5
 
 
8
  import { session } from "$lib/state/session.svelte.js";
9
  import { token } from "$lib/state/token.svelte.js";
10
  import { isMac } from "$lib/utils/platform.js";
 
11
  import typia from "typia";
12
  import IconExternal from "~icons/carbon/arrow-up-right";
13
  import IconCode from "~icons/carbon/code";
 
20
  import { showShareModal } from "../share-modal.svelte";
21
  import Toaster from "../toaster.svelte";
22
  import { addToast } from "../toaster.svelte.js";
23
+ import Tooltip from "../tooltip.svelte";
24
  import PlaygroundConversationHeader from "./conversation-header.svelte";
25
  import PlaygroundConversation from "./conversation.svelte";
26
  import GenerationConfig from "./generation-config.svelte";
 
69
  if (!conversation) return;
70
 
71
  const startTime = performance.now();
 
72
 
73
  if (conversation.streaming) {
74
  let addedMessage = false;
75
  let streamingMessage = $state({ role: "assistant", content: "" });
76
 
77
  await handleStreamingResponse(
 
78
  conversation,
79
  content => {
80
  if (!streamingMessage) return;
 
90
  abortManager.createController()
91
  );
92
  } else {
93
+ const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(conversation);
 
 
 
94
  conversation.messages = [...conversation.messages, newMessage];
95
  const c = generationStats[conversationIdx];
96
  if (c) c.generatedTokensCount += newTokensCount;
 
281
  {!viewSettings ? "Settings" : "Hide Settings"}
282
  </button>
283
  {/if}
284
+ <Tooltip>
285
+ {#snippet trigger(tooltip)}
286
+ <button type="button" onclick={reset} class="btn size-[39px]" {...tooltip.trigger}>
287
+ <IconDelete />
288
+ </button>
289
+ {/snippet}
290
+ Clear conversation
291
+ </Tooltip>
292
  </div>
293
  <div class="flex flex-1 shrink-0 items-center justify-center gap-x-8 text-center text-sm text-gray-500">
294
  {#each generationStats as { latency, generatedTokensCount }}
 
298
  <div class="flex flex-1 justify-end gap-x-2">
299
  <button type="button" onclick={() => (viewCode = !viewCode)} class="btn">
300
  <IconCode />
301
+ {!viewCode ? "View Code" : "Hide Code"}
302
+ </button>
303
  <button
304
  onclick={() => {
305
  viewCode = false;
 
417
  <div class="absolute bottom-6 left-4 flex items-center gap-2 max-md:hidden">
418
  <a
419
  target="_blank"
420
+ href="https://huggingface.co/docs/inference-providers/tasks/chat-completion"
421
  class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
422
  >
423
  <div class="text-xs">
src/lib/components/inference-playground/provider-select.svelte CHANGED
@@ -1,7 +1,7 @@
1
  <script lang="ts">
2
  import { run } from "svelte/legacy";
3
 
4
- import type { Conversation } from "$lib/types.js";
5
 
6
  import { randomPick } from "$lib/utils/array.js";
7
  import { cn } from "$lib/utils/cn.js";
@@ -10,7 +10,7 @@
10
  import IconProvider from "../icon-provider.svelte";
11
 
12
  interface Props {
13
- conversation: Conversation;
14
  class?: string | undefined;
15
  }
16
 
 
1
  <script lang="ts">
2
  import { run } from "svelte/legacy";
3
 
4
+ import type { ConversationWithHFModel } from "$lib/types.js";
5
 
6
  import { randomPick } from "$lib/utils/array.js";
7
  import { cn } from "$lib/utils/cn.js";
 
10
  import IconProvider from "../icon-provider.svelte";
11
 
12
  interface Props {
13
+ conversation: ConversationWithHFModel;
14
  class?: string | undefined;
15
  }
16
 
src/lib/components/inference-playground/utils.ts CHANGED
@@ -1,8 +1,16 @@
1
- import type { Conversation, ConversationMessage, ModelWithTokenizer } from "$lib/types.js";
 
 
 
 
 
 
2
  import type { ChatCompletionInputMessage, InferenceSnippet } from "@huggingface/tasks";
3
  import { type ChatCompletionOutputMessage } from "@huggingface/tasks";
4
-
5
  import { HfInference, snippets, type InferenceProvider } from "@huggingface/inference";
 
 
6
  type ChatCompletionInputMessageChunk =
7
  NonNullable<ChatCompletionInputMessage["content"]> extends string | (infer U)[] ? U : never;
8
 
@@ -25,27 +33,107 @@ function parseMessage(message: ConversationMessage): ChatCompletionInputMessage
25
  };
26
  }
27
 
28
- export async function handleStreamingResponse(
29
- hf: HfInference,
30
- conversation: Conversation,
31
- onChunk: (content: string) => void,
32
- abortController: AbortController
33
- ): Promise<void> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  const { model, systemMessage } = conversation;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const messages = [
36
  ...(isSystemPromptSupported(model) && systemMessage.content?.length ? [systemMessage] : []),
37
  ...conversation.messages,
38
  ];
39
- let out = "";
40
- for await (const chunk of hf.chatCompletionStream(
41
- {
 
 
42
  model: model.id,
43
  messages: messages.map(parseMessage),
44
  provider: conversation.provider,
45
  ...conversation.config,
46
  },
47
- { signal: abortController.signal }
48
- )) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  if (chunk.choices && chunk.choices.length > 0 && chunk.choices[0]?.delta?.content) {
50
  out += chunk.choices[0].delta.content;
51
  onChunk(out);
@@ -54,22 +142,30 @@ export async function handleStreamingResponse(
54
  }
55
 
56
  export async function handleNonStreamingResponse(
57
- hf: HfInference,
58
  conversation: Conversation
59
  ): Promise<{ message: ChatCompletionOutputMessage; completion_tokens: number }> {
60
- const { model, systemMessage } = conversation;
61
- const messages = [
62
- ...(isSystemPromptSupported(model) && systemMessage.content?.length ? [systemMessage] : []),
63
- ...conversation.messages,
64
- ];
 
 
65
 
66
- const response = await hf.chatCompletion({
67
- model: model.id,
68
- messages: messages.map(parseMessage),
69
- provider: conversation.provider,
70
- ...conversation.config,
71
- });
 
 
 
 
 
72
 
 
 
73
  if (response.choices && response.choices.length > 0) {
74
  const { message } = response.choices[0]!;
75
  const { completion_tokens } = response.usage;
@@ -78,8 +174,10 @@ export async function handleNonStreamingResponse(
78
  throw new Error("No response from the model");
79
  }
80
 
81
- export function isSystemPromptSupported(model: ModelWithTokenizer) {
82
- return model?.tokenizerConfig?.chat_template?.includes("system");
 
 
83
  }
84
 
85
  export const defaultSystemMessage: { [key: string]: string } = {
@@ -159,6 +257,11 @@ export function getInferenceSnippet(
159
  accessToken: string,
160
  opts?: Record<string, unknown>
161
  ): GetInferenceSnippetReturn {
 
 
 
 
 
162
  const providerId = model.inferenceProviderMapping.find(p => p.provider === provider)?.providerId;
163
  const snippetsByClient = GET_SNIPPET_FN[language](
164
  { ...model, inference: "" },
@@ -178,5 +281,6 @@ export function hasInferenceSnippet(
178
  provider: InferenceProvider,
179
  language: InferenceSnippetLanguage
180
  ): boolean {
 
181
  return getInferenceSnippet(model, provider, language, "").length > 0;
182
  }
 
1
+ import {
2
+ isCustomModel,
3
+ type Conversation,
4
+ type ConversationMessage,
5
+ type CustomModel,
6
+ type ModelWithTokenizer,
7
+ } from "$lib/types.js";
8
  import type { ChatCompletionInputMessage, InferenceSnippet } from "@huggingface/tasks";
9
  import { type ChatCompletionOutputMessage } from "@huggingface/tasks";
10
+ import { token } from "$lib/state/token.svelte";
11
  import { HfInference, snippets, type InferenceProvider } from "@huggingface/inference";
12
+ import OpenAI from "openai";
13
+
14
  type ChatCompletionInputMessageChunk =
15
  NonNullable<ChatCompletionInputMessage["content"]> extends string | (infer U)[] ? U : never;
16
 
 
33
  };
34
  }
35
 
36
+ type HFCompletionMetadata = {
37
+ type: "huggingface";
38
+ client: HfInference;
39
+ args: Parameters<HfInference["chatCompletion"]>[0];
40
+ };
41
+
42
+ type OpenAICompletionMetadata = {
43
+ type: "openai";
44
+ client: OpenAI;
45
+ args: OpenAI.ChatCompletionCreateParams;
46
+ };
47
+
48
+ type CompletionMetadata = HFCompletionMetadata | OpenAICompletionMetadata;
49
+
50
+ function parseOpenAIMessages(
51
+ messages: ConversationMessage[],
52
+ systemMessage?: ConversationMessage
53
+ ): OpenAI.ChatCompletionMessageParam[] {
54
+ const parsedMessages: OpenAI.ChatCompletionMessageParam[] = [];
55
+
56
+ if (systemMessage?.content) {
57
+ parsedMessages.push({
58
+ role: "system",
59
+ content: systemMessage.content,
60
+ });
61
+ }
62
+
63
+ return [
64
+ ...parsedMessages,
65
+ ...messages.map(msg => ({
66
+ role: msg.role === "assistant" ? ("assistant" as const) : ("user" as const),
67
+ content: msg.content || "",
68
+ })),
69
+ ];
70
+ }
71
+
72
+ function getCompletionMetadata(conversation: Conversation): CompletionMetadata {
73
  const { model, systemMessage } = conversation;
74
+
75
+ // Handle OpenAI-compatible models
76
+ if (isCustomModel(model)) {
77
+ const openai = new OpenAI({
78
+ apiKey: model.accessToken,
79
+ baseURL: model.endpointUrl,
80
+ dangerouslyAllowBrowser: true,
81
+ });
82
+
83
+ return {
84
+ type: "openai",
85
+ client: openai,
86
+ args: {
87
+ messages: parseOpenAIMessages(conversation.messages, systemMessage),
88
+ model: model.id,
89
+ },
90
+ };
91
+ }
92
+
93
+ // Handle HuggingFace models
94
  const messages = [
95
  ...(isSystemPromptSupported(model) && systemMessage.content?.length ? [systemMessage] : []),
96
  ...conversation.messages,
97
  ];
98
+
99
+ return {
100
+ type: "huggingface",
101
+ client: new HfInference(token.value),
102
+ args: {
103
  model: model.id,
104
  messages: messages.map(parseMessage),
105
  provider: conversation.provider,
106
  ...conversation.config,
107
  },
108
+ };
109
+ }
110
+
111
+ export async function handleStreamingResponse(
112
+ conversation: Conversation,
113
+ onChunk: (content: string) => void,
114
+ abortController: AbortController
115
+ ): Promise<void> {
116
+ const metadata = getCompletionMetadata(conversation);
117
+
118
+ if (metadata.type === "openai") {
119
+ const stream = await metadata.client.chat.completions.create({
120
+ ...metadata.args,
121
+ stream: true,
122
+ } as OpenAI.ChatCompletionCreateParamsStreaming);
123
+
124
+ let out = "";
125
+ for await (const chunk of stream) {
126
+ if (chunk.choices[0]?.delta?.content) {
127
+ out += chunk.choices[0].delta.content;
128
+ onChunk(out);
129
+ }
130
+ }
131
+ return;
132
+ }
133
+
134
+ // HuggingFace streaming
135
+ let out = "";
136
+ for await (const chunk of metadata.client.chatCompletionStream(metadata.args, { signal: abortController.signal })) {
137
  if (chunk.choices && chunk.choices.length > 0 && chunk.choices[0]?.delta?.content) {
138
  out += chunk.choices[0].delta.content;
139
  onChunk(out);
 
142
  }
143
 
144
  export async function handleNonStreamingResponse(
 
145
  conversation: Conversation
146
  ): Promise<{ message: ChatCompletionOutputMessage; completion_tokens: number }> {
147
+ const metadata = getCompletionMetadata(conversation);
148
+
149
+ if (metadata.type === "openai") {
150
+ const response = await metadata.client.chat.completions.create({
151
+ ...metadata.args,
152
+ stream: false,
153
+ } as OpenAI.ChatCompletionCreateParamsNonStreaming);
154
 
155
+ if (response.choices && response.choices.length > 0 && response.choices[0]?.message) {
156
+ return {
157
+ message: {
158
+ role: "assistant",
159
+ content: response.choices[0].message.content || "",
160
+ },
161
+ completion_tokens: response.usage?.completion_tokens || 0,
162
+ };
163
+ }
164
+ throw new Error("No response from the model");
165
+ }
166
 
167
+ // HuggingFace non-streaming
168
+ const response = await metadata.client.chatCompletion(metadata.args);
169
  if (response.choices && response.choices.length > 0) {
170
  const { message } = response.choices[0]!;
171
  const { completion_tokens } = response.usage;
 
174
  throw new Error("No response from the model");
175
  }
176
 
177
+ export function isSystemPromptSupported(model: ModelWithTokenizer | CustomModel) {
178
+ if (isCustomModel(model)) return true; // OpenAI-compatible models support system messages
179
+ if ("tokenizerConfig" in model) return model?.tokenizerConfig?.chat_template?.includes("system");
180
+ return false;
181
  }
182
 
183
  export const defaultSystemMessage: { [key: string]: string } = {
 
257
  accessToken: string,
258
  opts?: Record<string, unknown>
259
  ): GetInferenceSnippetReturn {
260
+ // If it's a custom model, we don't generate inference snippets
261
+ if (isCustomModel(model)) {
262
+ return [];
263
+ }
264
+
265
  const providerId = model.inferenceProviderMapping.find(p => p.provider === provider)?.providerId;
266
  const snippetsByClient = GET_SNIPPET_FN[language](
267
  { ...model, inference: "" },
 
281
  provider: InferenceProvider,
282
  language: InferenceSnippetLanguage
283
  ): boolean {
284
+ if (isCustomModel(model)) return false;
285
  return getInferenceSnippet(model, provider, language, "").length > 0;
286
  }
src/lib/components/local-toasts.svelte CHANGED
@@ -6,13 +6,14 @@
6
 
7
  interface Props {
8
  children: Snippet<[{ addToast: typeof toaster.addToast; trigger: typeof trigger }]>;
 
9
  closeDelay?: number;
10
  }
11
- const { children, closeDelay = 2000 }: Props = $props();
12
 
13
  const id = $props.id();
14
 
15
- const trigger = {
16
  id,
17
  } as const;
18
 
@@ -21,11 +22,13 @@
21
  variant: "info" | "danger";
22
  };
23
 
24
- const toaster = new Toaster<ToastData>({
25
  hover: null,
26
  closeDelay: () => closeDelay,
27
  });
28
 
 
 
29
  function float(node: HTMLElement) {
30
  const triggerEl = document.getElementById(trigger.id);
31
  if (!triggerEl) return;
@@ -55,16 +58,20 @@
55
 
56
  {@render children({ trigger, addToast: toaster.addToast })}
57
 
58
- {#each toaster.toasts as toast (toast.id)}
59
  <div
60
  data-local-toast
61
  data-variant={toast.data.variant}
62
- class="rounded-full px-2 py-1 text-xs {classMap[toast.data.variant]}"
63
  in:fly={{ y: 10 }}
64
  out:fly={{ y: -4 }}
65
  use:float
66
  >
67
- {toast.data.content}
 
 
 
 
68
  </div>
69
  {/each}
70
 
 
6
 
7
  interface Props {
8
  children: Snippet<[{ addToast: typeof toaster.addToast; trigger: typeof trigger }]>;
9
+ toast?: Snippet<[{ toast: (typeof toaster.toasts)[0]; float: typeof float }]>;
10
  closeDelay?: number;
11
  }
12
+ const { children, closeDelay = 2000, toast: toastSnippet }: Props = $props();
13
 
14
  const id = $props.id();
15
 
16
+ export const trigger = {
17
  id,
18
  } as const;
19
 
 
22
  variant: "info" | "danger";
23
  };
24
 
25
+ export const toaster = new Toaster<ToastData>({
26
  hover: null,
27
  closeDelay: () => closeDelay,
28
  });
29
 
30
+ export const addToast = toaster.addToast;
31
+
32
  function float(node: HTMLElement) {
33
  const triggerEl = document.getElementById(trigger.id);
34
  if (!triggerEl) return;
 
58
 
59
  {@render children({ trigger, addToast: toaster.addToast })}
60
 
61
+ {#each toaster.toasts.slice(toaster.toasts.length - 1) as toast (toast.id)}
62
  <div
63
  data-local-toast
64
  data-variant={toast.data.variant}
65
+ class={[!toastSnippet && `${classMap[toast.data.variant]} rounded-full px-2 py-1 text-xs`]}
66
  in:fly={{ y: 10 }}
67
  out:fly={{ y: -4 }}
68
  use:float
69
  >
70
+ {#if toastSnippet}
71
+ {@render toastSnippet({ toast, float })}
72
+ {:else}
73
+ {toast.data.content}
74
+ {/if}
75
  </div>
76
  {/each}
77
 
src/lib/state/models.svelte.ts CHANGED
@@ -1,10 +1,78 @@
1
  import { page } from "$app/state";
2
- import type { ModelWithTokenizer } from "$lib/types.js";
 
 
 
 
 
 
 
3
 
4
  class Models {
5
- all = $derived(page.data.models as ModelWithTokenizer[]);
6
- trending = $derived(this.all.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
7
- nonTrending = $derived(this.all.filter(m => !this.trending.includes(m)));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  }
9
 
10
  export const models = new Models();
 
1
  import { page } from "$app/state";
2
+ import { createInit } from "$lib/spells/create-init.svelte";
3
+ import type { CustomModel, ModelWithTokenizer } from "$lib/types.js";
4
+ import { safeParse } from "$lib/utils/json.js";
5
+ import typia from "typia";
6
+ import { session } from "./session.svelte";
7
+ import { randomPick } from "$lib/utils/array.js";
8
+
9
+ const LOCAL_STORAGE_KEY = "hf_inference_playground_custom_models";
10
 
11
  class Models {
12
+ remote = $derived(page.data.models as ModelWithTokenizer[]);
13
+ trending = $derived(this.remote.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
14
+ nonTrending = $derived(this.remote.filter(m => !this.trending.includes(m)));
15
+
16
+ #custom = $state<CustomModel[]>([]);
17
+ #initCustom = createInit(() => {
18
+ const savedData = localStorage.getItem(LOCAL_STORAGE_KEY);
19
+ if (!savedData) return;
20
+ const parsed = safeParse(savedData);
21
+ const res = typia.validate<CustomModel[]>(parsed);
22
+ if (res.success) {
23
+ this.#custom = parsed;
24
+ } else {
25
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify([]));
26
+ }
27
+ });
28
+
29
+ all = $derived([...this.remote, ...this.custom]);
30
+
31
+ constructor() {
32
+ $effect.root(() => {
33
+ $effect(() => {
34
+ if (!this.#initCustom.called) return;
35
+ const v = $state.snapshot(this.#custom);
36
+ try {
37
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(v));
38
+ } catch (e) {
39
+ console.error("Failed to save session to localStorage:", e);
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ get custom() {
46
+ this.#initCustom.fn();
47
+ return this.#custom;
48
+ }
49
+
50
+ // set local(v: CustomModel[]) {
51
+ // this.local = v;
52
+ // }
53
+
54
+ addCustom(model: CustomModel) {
55
+ if (this.#custom.find(m => m.id === model.id)) return null;
56
+ this.#custom = [...this.#custom, model];
57
+ return model;
58
+ }
59
+
60
+ upsertCustom(model: CustomModel) {
61
+ const index = this.#custom.findIndex(m => m._id === model._id);
62
+ if (index === -1) {
63
+ this.addCustom(model);
64
+ } else {
65
+ this.#custom[index] = model;
66
+ }
67
+ }
68
+
69
+ removeCustom(uuid: CustomModel["_id"]) {
70
+ this.#custom = this.#custom.filter(m => m._id !== uuid);
71
+ session.project.conversations.forEach((c, i) => {
72
+ if (c.model._id !== uuid) return;
73
+ session.project.conversations[i]!.model = randomPick(models.trending)!;
74
+ });
75
+ }
76
  }
77
 
78
  export const models = new Models();
src/lib/state/session.svelte.ts CHANGED
@@ -21,7 +21,7 @@ const systemMessage: ConversationMessage = {
21
  content: "",
22
  };
23
 
24
- const emptyModel: ModelWithTokenizer = {
25
  _id: "",
26
  inferenceProviderMapping: [],
27
  pipeline_tag: PipelineTag.TextGeneration,
@@ -37,7 +37,7 @@ const emptyModel: ModelWithTokenizer = {
37
  };
38
 
39
  function getDefaults() {
40
- const defaultModel = models.trending[0] ?? models.all[0] ?? emptyModel;
41
 
42
  const defaultConversation: Conversation = {
43
  model: defaultModel,
@@ -90,7 +90,7 @@ class SessionState {
90
  const searchParams = new URLSearchParams(window.location.search);
91
  const searchProviders = searchParams.getAll("provider");
92
  const searchModelIds = searchParams.getAll("modelId");
93
- const modelsFromSearch = searchModelIds.map(id => models.all.find(model => model.id === id)).filter(Boolean);
94
  if (modelsFromSearch.length > 0) savedSession.activeProjectId = "default";
95
 
96
  let min = Math.min(dp.conversations.length, modelsFromSearch.length, searchProviders.length);
 
21
  content: "",
22
  };
23
 
24
+ export const emptyModel: ModelWithTokenizer = {
25
  _id: "",
26
  inferenceProviderMapping: [],
27
  pipeline_tag: PipelineTag.TextGeneration,
 
37
  };
38
 
39
  function getDefaults() {
40
+ const defaultModel = models.trending[0] ?? models.remote[0] ?? emptyModel;
41
 
42
  const defaultConversation: Conversation = {
43
  model: defaultModel,
 
90
  const searchParams = new URLSearchParams(window.location.search);
91
  const searchProviders = searchParams.getAll("provider");
92
  const searchModelIds = searchParams.getAll("modelId");
93
+ const modelsFromSearch = searchModelIds.map(id => models.remote.find(model => model.id === id)).filter(Boolean);
94
  if (modelsFromSearch.length > 0) savedSession.activeProjectId = "default";
95
 
96
  let min = Math.min(dp.conversations.length, modelsFromSearch.length, searchProviders.length);
src/lib/types.ts CHANGED
@@ -1,5 +1,6 @@
1
  import type { GenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
2
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
 
3
 
4
  export type ConversationMessage = Pick<ChatCompletionInputMessage, "name" | "role" | "tool_calls"> & {
5
  content?: string;
@@ -7,7 +8,7 @@ export type ConversationMessage = Pick<ChatCompletionInputMessage, "name" | "rol
7
  };
8
 
9
  export type Conversation = {
10
- model: ModelWithTokenizer;
11
  config: GenerationConfig;
12
  messages: ConversationMessage[];
13
  systemMessage: ConversationMessage;
@@ -15,6 +16,19 @@ export type Conversation = {
15
  provider?: string;
16
  };
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export type Project = {
19
  conversations: [Conversation] | [Conversation, Conversation];
20
  id: string;
@@ -51,6 +65,14 @@ export type Model = {
51
  library_name?: LibraryName;
52
  };
53
 
 
 
 
 
 
 
 
 
54
  export type Config = {
55
  architectures: string[];
56
  model_type: string;
 
1
  import type { GenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
2
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
3
+ import typia from "typia";
4
 
5
  export type ConversationMessage = Pick<ChatCompletionInputMessage, "name" | "role" | "tool_calls"> & {
6
  content?: string;
 
8
  };
9
 
10
  export type Conversation = {
11
+ model: ModelWithTokenizer | CustomModel;
12
  config: GenerationConfig;
13
  messages: ConversationMessage[];
14
  systemMessage: ConversationMessage;
 
16
  provider?: string;
17
  };
18
 
19
+ export type ConversationWithCustomModel = Conversation & {
20
+ model: CustomModel;
21
+ };
22
+
23
+ export type ConversationWithHFModel = Conversation & {
24
+ model: ModelWithTokenizer;
25
+ };
26
+
27
+ export const isConversationWithHFModel = typia.createIs<ConversationWithHFModel>();
28
+ export const isConversationWithCustomModel = typia.createIs<ConversationWithCustomModel>();
29
+
30
+ export const isCustomModel = typia.createIs<CustomModel>();
31
+
32
  export type Project = {
33
  conversations: [Conversation] | [Conversation, Conversation];
34
  id: string;
 
65
  library_name?: LibraryName;
66
  };
67
 
68
+ export type CustomModel = {
69
+ id: string;
70
+ /** UUID */
71
+ _id: string;
72
+ endpointUrl: string;
73
+ accessToken?: string;
74
+ };
75
+
76
  export type Config = {
77
  architectures: string[];
78
  model_type: string;
src/lib/utils/sleep.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export async function sleep(ms: number) {
2
+ return new Promise(resolve => setTimeout(resolve, ms));
3
+ }
src/routes/+layout.svelte CHANGED
@@ -1,4 +1,5 @@
1
  <script lang="ts">
 
2
  import DebugMenu from "$lib/components/debug-menu.svelte";
3
  import Prompts from "$lib/components/prompts.svelte";
4
  import QuotaModal from "$lib/components/quota-modal.svelte";
@@ -18,3 +19,4 @@
18
  <Prompts />
19
  <QuotaModal />
20
  <ShareModal />
 
 
1
  <script lang="ts">
2
+ import CustomModelConfig from "$lib/components/inference-playground/custom-model-config.svelte";
3
  import DebugMenu from "$lib/components/debug-menu.svelte";
4
  import Prompts from "$lib/components/prompts.svelte";
5
  import QuotaModal from "$lib/components/quota-modal.svelte";
 
19
  <Prompts />
20
  <QuotaModal />
21
  <ShareModal />
22
+ <CustomModelConfig />