Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d567fa78cb | |||
| 187f1ae27a | |||
| 0ae56b1eb9 | |||
| e46cbeb2bf | |||
| a0578f0007 | |||
| d207a059a4 | |||
| 040ee1e284 | |||
| 82cad1b84e | |||
| 55c3678906 | |||
| 8b5a62d92a | |||
| d1e4c8ee77 | |||
| 11f8afead8 | |||
| 749819ef47 | |||
| 0ab9e74333 | |||
| b20be6dcd1 | |||
| fc24722258 | |||
| 2b087dffb1 | |||
| 746c083a87 | |||
| 8dd91e99e8 | |||
| 136dcdc25f | |||
| 767b8015fa | |||
| f0144a2361 |
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "zinc"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry",
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
Generated
+421
-120
@@ -8,10 +8,10 @@
|
|||||||
"name": "ui-svelte",
|
"name": "ui-svelte",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "^8.21.3",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"lucide-svelte": "^0.563.0",
|
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
@@ -23,14 +23,22 @@
|
|||||||
"unist-util-visit": "^5.1.0"
|
"unist-util-visit": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@internationalized/date": "^3.12.2",
|
||||||
|
"@lucide/svelte": "^1.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
|
"bits-ui": "^2.18.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"paneforge": "^1.0.2",
|
||||||
"svelte": "^5.46.4",
|
"svelte": "^5.46.4",
|
||||||
"svelte-check": "^4.1.4",
|
"svelte-check": "^4.1.4",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.1.8",
|
"tailwindcss": "^4.1.8",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-compression2": "^2.5.1",
|
"vite-plugin-compression2": "^2.5.1",
|
||||||
@@ -38,21 +46,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
"@emnapi/wasi-threads": "1.2.2",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -61,9 +69,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -71,6 +79,44 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@internationalized/date": {
|
||||||
|
"version": "3.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz",
|
||||||
|
"integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -127,15 +173,25 @@
|
|||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@lucide/svelte": {
|
||||||
|
"version": "1.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.21.0.tgz",
|
||||||
|
"integrity": "sha512-MEv//A7Jv3kHukZowv/DWp1MAtUzJKYwtJsmnQ7X98lCgtac3z3NbaToDl3Q6jO3gS9sougFpcD+t+YuxOkRMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
||||||
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
|
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -147,9 +203,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.124.0",
|
"version": "0.137.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
|
||||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
"integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -157,9 +213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
|
||||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -174,9 +230,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
|
||||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -191,9 +247,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
|
||||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -208,9 +264,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
|
||||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -225,9 +281,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
|
||||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -242,13 +298,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
|
||||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -259,13 +318,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
|
||||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -276,13 +338,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
|
||||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -293,13 +358,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
|
||||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -310,13 +378,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
|
||||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -327,13 +398,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
|
||||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -344,9 +418,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
|
||||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -361,9 +435,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
|
||||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -371,18 +445,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.11.1",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.11.1",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
"@napi-rs/wasm-runtime": "^1.1.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
|
||||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -397,9 +471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
|
||||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -414,9 +488,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -480,6 +554,16 @@
|
|||||||
"vite": "^8.0.0-beta.7 || ^8.0.0"
|
"vite": "^8.0.0-beta.7 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||||
|
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
||||||
@@ -752,6 +836,19 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/svelte": {
|
"node_modules/@tsconfig/svelte": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz",
|
||||||
@@ -760,9 +857,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1053,6 +1150,31 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bits-ui": {
|
||||||
|
"version": "2.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz",
|
||||||
|
"integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.1",
|
||||||
|
"@floating-ui/dom": "^1.7.1",
|
||||||
|
"esm-env": "^1.1.2",
|
||||||
|
"runed": "^0.35.1",
|
||||||
|
"svelte-toolbelt": "^0.10.6",
|
||||||
|
"tabbable": "^6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/huntabyte"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@internationalized/date": "^3.8.1",
|
||||||
|
"svelte": "^5.33.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ccount": {
|
"node_modules/ccount": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||||
@@ -1555,6 +1677,13 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inline-style-parser": {
|
||||||
|
"version": "0.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||||
|
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-plain-obj": {
|
"node_modules/is-plain-obj": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||||
@@ -1881,13 +2010,14 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-svelte": {
|
"node_modules/lz-string": {
|
||||||
"version": "0.563.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.563.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
"integrity": "sha512-pjZKw7TpQcamfQrx7YdbOHgmrcNeKiGGMD0tKZQaVktwSsbqw28CsKc2Q97ttwjytiCWkJyOa8ij2Q+Og0nPfQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"license": "ISC",
|
"dev": true,
|
||||||
"peerDependencies": {
|
"license": "MIT",
|
||||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
@@ -2738,9 +2868,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2767,6 +2897,74 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/paneforge": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"runed": "^0.23.4",
|
||||||
|
"svelte-toolbelt": "^0.9.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.29.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/paneforge/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/paneforge/node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.9.3.tgz",
|
||||||
|
"integrity": "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.29.0",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/paneforge/node_modules/svelte-toolbelt/node_modules/runed": {
|
||||||
|
"version": "0.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.29.2.tgz",
|
||||||
|
"integrity": "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
@@ -2807,9 +3005,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2827,7 +3025,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -2985,14 +3183,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
|
||||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.124.0",
|
"@oxc-project/types": "=0.137.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3001,21 +3199,46 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-android-arm64": "1.1.3",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-arm64": "1.1.3",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-x64": "1.1.3",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
"@rolldown/binding-freebsd-x64": "1.1.3",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-musl": "1.1.3",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-gnu": "1.1.3",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-musl": "1.1.3",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-openharmony-arm64": "1.1.3",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
"@rolldown/binding-wasm32-wasi": "1.1.3",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
"@rolldown/binding-win32-x64-msvc": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/runed": {
|
||||||
|
"version": "0.35.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
|
||||||
|
"integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"esm-env": "^1.0.0",
|
||||||
|
"lz-string": "^1.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@sveltejs/kit": "^2.21.0",
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@sveltejs/kit": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
@@ -3086,6 +3309,16 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-to-object": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inline-style-parser": "0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.55.7",
|
"version": "5.55.7",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz",
|
||||||
@@ -3150,6 +3383,65 @@
|
|||||||
"url": "https://github.com/sponsors/ItalyPaleAle"
|
"url": "https://github.com/sponsors/ItalyPaleAle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.10.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||||
|
"integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.35.1",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tailwind-variants": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.x",
|
||||||
|
"pnpm": ">=7.x"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwind-merge": ">=3.0.0",
|
||||||
|
"tailwindcss": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"tailwind-merge": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
||||||
@@ -3196,14 +3488,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -3247,8 +3539,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"optional": true
|
},
|
||||||
|
"node_modules/tw-animate-css": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Wombosvideo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
@@ -3429,17 +3730,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.8",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
|
||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.15",
|
||||||
"rolldown": "1.0.0-rc.15",
|
"rolldown": "~1.1.2",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -3455,7 +3756,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"@vitejs/devtools": "^0.1.0",
|
"@vitejs/devtools": "^0.3.0",
|
||||||
"esbuild": "^0.27.0 || ^0.28.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
+10
-2
@@ -12,30 +12,38 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@internationalized/date": "^3.12.2",
|
||||||
|
"@lucide/svelte": "^1.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
|
"bits-ui": "^2.18.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"paneforge": "^1.0.2",
|
||||||
"svelte": "^5.46.4",
|
"svelte": "^5.46.4",
|
||||||
"svelte-check": "^4.1.4",
|
"svelte-check": "^4.1.4",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.1.8",
|
"tailwindcss": "^4.1.8",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-compression2": "^2.5.1",
|
"vite-plugin-compression2": "^2.5.1",
|
||||||
"vitest": "^4.1.0"
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "^8.21.3",
|
||||||
|
"chart.js": "4.5.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"lucide-svelte": "^0.563.0",
|
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
"chart.js": "4.5.1",
|
|
||||||
"svelte-spa-router": "^4.0.1",
|
"svelte-spa-router": "^4.0.1",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.1.0"
|
"unist-util-visit": "^5.1.0"
|
||||||
|
|||||||
+56
-10
@@ -1,33 +1,68 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Router from "svelte-spa-router";
|
import Router from "svelte-spa-router";
|
||||||
import Header from "./components/Header.svelte";
|
import AppSidebar from "./components/AppSidebar.svelte";
|
||||||
import LogViewer from "./routes/LogViewer.svelte";
|
import LogViewer from "./routes/LogViewer.svelte";
|
||||||
import Models from "./routes/Models.svelte";
|
import ModelDetail from "./routes/ModelDetail.svelte";
|
||||||
|
import ModelsDash from "./routes/ModelsDash.svelte";
|
||||||
import Activity from "./routes/Activity.svelte";
|
import Activity from "./routes/Activity.svelte";
|
||||||
import Performance from "./routes/Performance.svelte";
|
import Performance from "./routes/Performance.svelte";
|
||||||
import Playground from "./routes/Playground.svelte";
|
import Playground from "./routes/Playground.svelte";
|
||||||
import PlaygroundStub from "./routes/PlaygroundStub.svelte";
|
import PlaygroundStub from "./routes/PlaygroundStub.svelte";
|
||||||
|
import Settings from "./routes/Settings.svelte";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
import { enableAPIEvents, checkPerformanceEnabled } from "./stores/api";
|
import { enableAPIEvents, checkPerformanceEnabled } from "./stores/api";
|
||||||
import { initScreenWidth, initSystemThemeListener, isDarkMode, appTitle, connectionState } from "./stores/theme";
|
import { initScreenWidth, initSystemThemeListener, isDarkMode, appTitle, connectionState } from "./stores/theme";
|
||||||
import { currentRoute } from "./stores/route";
|
import { currentRoute } from "./stores/route";
|
||||||
|
import { selectedPlaygroundTab, playgroundTabs } from "./stores/playground";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
"/": PlaygroundStub,
|
"/": PlaygroundStub,
|
||||||
"/models": Models,
|
"/models": ModelsDash,
|
||||||
|
"/models/:id": ModelDetail,
|
||||||
"/logs": LogViewer,
|
"/logs": LogViewer,
|
||||||
"/activity": Activity,
|
"/activity": Activity,
|
||||||
|
"/settings": Settings,
|
||||||
"/performance": Performance,
|
"/performance": Performance,
|
||||||
"*": PlaygroundStub,
|
"*": PlaygroundStub,
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleRouteLoaded(event: { detail: { route: string | RegExp } }) {
|
const routeTitles: Record<string, string> = {
|
||||||
|
"/": "Playground",
|
||||||
|
"/models": "Models",
|
||||||
|
"/activity": "Activity",
|
||||||
|
"/logs": "Logs",
|
||||||
|
"/settings": "Settings",
|
||||||
|
"/performance": "Performance",
|
||||||
|
};
|
||||||
|
|
||||||
|
let sectionTitle = $derived.by(() => {
|
||||||
|
if ($currentRoute === "/") {
|
||||||
|
const tab = playgroundTabs.find((t) => t.id === $selectedPlaygroundTab);
|
||||||
|
return `Playground / ${tab?.label ?? ""}`;
|
||||||
|
}
|
||||||
|
if ($currentRoute.startsWith("/models/")) {
|
||||||
|
const id = $currentRoute.slice("/models/".length);
|
||||||
|
return id ? `Models / ${decodeURIComponent(id)}` : "Models";
|
||||||
|
}
|
||||||
|
if ($currentRoute === "/models") {
|
||||||
|
return "Models";
|
||||||
|
}
|
||||||
|
return routeTitles[$currentRoute] ?? "Playground";
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRouteLoaded(event: { detail: { route: string | RegExp; location?: string } }) {
|
||||||
const route = event.detail.route;
|
const route = event.detail.route;
|
||||||
currentRoute.set(typeof route === "string" ? route : "/");
|
// Prefer the actual URL path so parameterised routes (e.g. /models/:id)
|
||||||
|
// are reflected accurately in currentRoute for sidebar highlighting.
|
||||||
|
const loc = event.detail.location;
|
||||||
|
currentRoute.set(loc ?? (typeof route === "string" ? route : "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
document.documentElement.setAttribute("data-theme", $isDarkMode ? "dark" : "light");
|
document.documentElement.classList.toggle("dark", $isDarkMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -49,10 +84,19 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen">
|
<Tooltip.Provider>
|
||||||
<Header />
|
<Sidebar.Provider>
|
||||||
|
<AppSidebar />
|
||||||
|
<Sidebar.Inset class="h-screen min-w-0 overflow-hidden">
|
||||||
|
<header
|
||||||
|
class="bg-background sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b px-4"
|
||||||
|
>
|
||||||
|
<Sidebar.Trigger class="-ml-1" />
|
||||||
|
<Separator orientation="vertical" class="mr-2 !h-4" />
|
||||||
|
<h2 class="truncate pb-0 text-sm font-semibold">{sectionTitle}</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-auto p-4">
|
<main class="min-h-0 flex-1 overflow-auto p-4">
|
||||||
<div class="h-full" class:hidden={$currentRoute !== "/"}>
|
<div class="h-full" class:hidden={$currentRoute !== "/"}>
|
||||||
<Playground />
|
<Playground />
|
||||||
</div>
|
</div>
|
||||||
@@ -60,4 +104,6 @@
|
|||||||
<Router {routes} on:routeLoaded={handleRouteLoaded} />
|
<Router {routes} on:routeLoaded={handleRouteLoaded} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</Sidebar.Inset>
|
||||||
|
</Sidebar.Provider>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { persistentStore } from "../stores/persistent";
|
import { persistentStore } from "../stores/persistent";
|
||||||
import { calculateHistogramData } from "../lib/histogram";
|
import { calculateHistogramData } from "../lib/histogram";
|
||||||
import TokenHistogram from "./TokenHistogram.svelte";
|
import TokenHistogram from "./TokenHistogram.svelte";
|
||||||
|
import { ChevronDown, X } from "@lucide/svelte";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
const nf = new Intl.NumberFormat();
|
const nf = new Intl.NumberFormat();
|
||||||
const histogramCollapsed = persistentStore<boolean>("activity-histogram-collapsed", false);
|
const histogramCollapsed = persistentStore<boolean>("activity-histogram-collapsed", false);
|
||||||
@@ -35,26 +38,24 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card relative p-3">
|
<Card.Root class="relative p-3">
|
||||||
<button
|
<Button
|
||||||
class="absolute top-2 right-2 w-6 h-6 flex items-center justify-center rounded-full border border-gray-300 dark:border-gray-600 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:border-gray-400 dark:hover:border-gray-400 transition-colors"
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
class="text-muted-foreground absolute right-2 top-2 rounded-full"
|
||||||
onclick={() => ($histogramCollapsed = !$histogramCollapsed)}
|
onclick={() => ($histogramCollapsed = !$histogramCollapsed)}
|
||||||
title={$histogramCollapsed ? "Show histograms" : "Hide histograms"}
|
title={$histogramCollapsed ? "Show histograms" : "Hide histograms"}
|
||||||
>
|
>
|
||||||
{#if $histogramCollapsed}
|
{#if $histogramCollapsed}
|
||||||
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
<ChevronDown />
|
||||||
<path d="M4.5 6l3.5 4 3.5-4H4.5z" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
{:else}
|
||||||
<svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor">
|
<X />
|
||||||
<path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</Button>
|
||||||
{#if !$histogramCollapsed}
|
{#if !$histogramCollapsed}
|
||||||
<div class="flex flex-col sm:flex-row gap-6 mb-3">
|
<div class="mb-3 flex flex-col gap-6 sm:flex-row">
|
||||||
<div class="w-full sm:w-1/2 min-w-0">
|
<div class="w-full min-w-0 sm:w-1/2">
|
||||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Prompt Processing</div>
|
<div class="text-muted-foreground mb-1 text-sm font-medium">Prompt Processing</div>
|
||||||
{#if stats.promptHistogramData}
|
{#if stats.promptHistogramData}
|
||||||
<TokenHistogram
|
<TokenHistogram
|
||||||
data={stats.promptHistogramData}
|
data={stats.promptHistogramData}
|
||||||
@@ -62,36 +63,36 @@
|
|||||||
colorClass="text-amber-500 dark:text-amber-400"
|
colorClass="text-amber-500 dark:text-amber-400"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">No prompt speed data yet</div>
|
<div class="text-muted-foreground py-6 text-center text-sm">No prompt speed data yet</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full sm:w-1/2 min-w-0">
|
<div class="w-full min-w-0 sm:w-1/2">
|
||||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Token Generation</div>
|
<div class="text-muted-foreground mb-1 text-sm font-medium">Token Generation</div>
|
||||||
{#if stats.genHistogramData}
|
{#if stats.genHistogramData}
|
||||||
<TokenHistogram data={stats.genHistogramData} unit="tokens/sec" />
|
<TokenHistogram data={stats.genHistogramData} unit="tokens/sec" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">No generation speed data yet</div>
|
<div class="text-muted-foreground py-6 text-center text-sm">No generation speed data yet</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-4 gap-x-6 gap-y-1 text-sm">
|
<div class="grid grid-cols-4 gap-x-6 gap-y-1 text-sm">
|
||||||
<div class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400">Requests</div>
|
<div class="text-muted-foreground text-xs uppercase tracking-wider">Requests</div>
|
||||||
<div class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400">Cached</div>
|
<div class="text-muted-foreground text-xs uppercase tracking-wider">Cached</div>
|
||||||
<div class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400">Processed</div>
|
<div class="text-muted-foreground text-xs uppercase tracking-wider">Processed</div>
|
||||||
<div class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400">Generated</div>
|
<div class="text-muted-foreground text-xs uppercase tracking-wider">Generated</div>
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
<div class="text-sm">
|
||||||
<span class="font-semibold">{nf.format(stats.totalRequests)}</span> completed,
|
<span class="font-semibold">{nf.format(stats.totalRequests)}</span> completed,
|
||||||
<span class="font-semibold">{nf.format(stats.inFlightRequests)}</span> waiting
|
<span class="font-semibold">{nf.format(stats.inFlightRequests)}</span> waiting
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
<div class="text-sm">
|
||||||
<span class="font-semibold">{nf.format(stats.totalCacheTokens)}</span> tokens
|
<span class="font-semibold">{nf.format(stats.totalCacheTokens)}</span> tokens
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
<div class="text-sm">
|
||||||
<span class="font-semibold">{nf.format(stats.totalInputTokens)}</span> tokens
|
<span class="font-semibold">{nf.format(stats.totalInputTokens)}</span> tokens
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
<div class="text-sm">
|
||||||
<span class="font-semibold">{nf.format(stats.totalOutputTokens)}</span> tokens
|
<span class="font-semibold">{nf.format(stats.totalOutputTokens)}</span> tokens
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card.Root>
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
import type { ActivityLogEntry, ReqRespCapture } from "../lib/types";
|
||||||
|
import { getCapture } from "../stores/api";
|
||||||
|
import { persistentStore } from "../stores/persistent";
|
||||||
|
import CaptureDialog from "./CaptureDialog.svelte";
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type PaginationState,
|
||||||
|
type VisibilityState,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
|
import {
|
||||||
|
FlexRender,
|
||||||
|
createSvelteTable,
|
||||||
|
renderComponent,
|
||||||
|
} from "$lib/components/ui/data-table/index.js";
|
||||||
|
import * as Table from "$lib/components/ui/table/index.js";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import {
|
||||||
|
Columns3,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
} from "@lucide/svelte";
|
||||||
|
import HeaderLabel from "./activity-table/HeaderLabel.svelte";
|
||||||
|
import ViewCaptureButton from "./activity-table/ViewCaptureButton.svelte";
|
||||||
|
import MetaCell from "./activity-table/MetaCell.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
metrics: ActivityLogEntry[];
|
||||||
|
storagePrefix: string;
|
||||||
|
showModelColumn?: boolean;
|
||||||
|
showPagination?: boolean;
|
||||||
|
title?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
emptyMessage?: string;
|
||||||
|
cardClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
metrics,
|
||||||
|
storagePrefix,
|
||||||
|
showModelColumn = true,
|
||||||
|
showPagination = false,
|
||||||
|
title,
|
||||||
|
compact = false,
|
||||||
|
emptyMessage = "No activity recorded",
|
||||||
|
cardClass = "",
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function formatSpeed(speed: number): string {
|
||||||
|
return speed < 0 ? "unknown" : speed.toFixed(2) + " t/s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
return (ms / 1000).toFixed(2) + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(timestamp: string): string {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
|
if (diffInSeconds < 5) return "now";
|
||||||
|
if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
|
||||||
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
||||||
|
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
|
||||||
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||||
|
if (diffInHours < 24) return `${diffInHours}h ago`;
|
||||||
|
return "a while ago";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDrafted(drafted: number, accepted: number): string {
|
||||||
|
return drafted > 0
|
||||||
|
? ((accepted * 100) / drafted).toFixed(1) + "% (" + accepted + "/" + drafted + ")"
|
||||||
|
: "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColMeta {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
defaultVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildColumnMeta(withModel: boolean): ColMeta[] {
|
||||||
|
const cols: ColMeta[] = [
|
||||||
|
{ id: "id", label: "ID", defaultVisible: true },
|
||||||
|
{ id: "time", label: "Time", defaultVisible: true },
|
||||||
|
];
|
||||||
|
if (withModel) cols.push({ id: "model", label: "Model", defaultVisible: true });
|
||||||
|
cols.push(
|
||||||
|
{ id: "req_path", label: "Path", defaultVisible: false },
|
||||||
|
{ id: "resp_status_code", label: "Status", defaultVisible: true },
|
||||||
|
{ id: "resp_content_type", label: "Content-Type", defaultVisible: false },
|
||||||
|
{ id: "cached", label: "Cached", defaultVisible: true },
|
||||||
|
{ id: "prompt", label: "Prompt", defaultVisible: true },
|
||||||
|
{ id: "generated", label: "Generated", defaultVisible: true },
|
||||||
|
{ id: "drafted", label: "Drafted", defaultVisible: false },
|
||||||
|
{ id: "prompt_speed", label: "Prompt Speed", defaultVisible: true },
|
||||||
|
{ id: "gen_speed", label: "Gen Speed", defaultVisible: true },
|
||||||
|
{ id: "duration", label: "Duration", defaultVisible: true },
|
||||||
|
{ id: "capture", label: "Capture", defaultVisible: true },
|
||||||
|
{ id: "meta", label: "Meta", defaultVisible: false }
|
||||||
|
);
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
let columnMeta = $derived(buildColumnMeta(showModelColumn));
|
||||||
|
|
||||||
|
let columnLabelMap = $derived(
|
||||||
|
Object.fromEntries(columnMeta.map((c) => [c.id, c.label])) as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
let defaultVisibility = $derived.by(() => {
|
||||||
|
const v: VisibilityState = {};
|
||||||
|
for (const c of columnMeta) v[c.id] = c.defaultVisible;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const storedVisibility = persistentStore<VisibilityState>(
|
||||||
|
`${storagePrefix}-columns`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let columnVisibility = $state<VisibilityState>(
|
||||||
|
Object.keys($storedVisibility).length > 0 ? $storedVisibility : defaultVisibility
|
||||||
|
);
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const storedPageSize = persistentStore<number>(`${storagePrefix}-page-size`, 10);
|
||||||
|
|
||||||
|
// When not paginating, use a large page size so all rows render in one page.
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let pagination = $state<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: showPagination ? $storedPageSize : Number.MAX_SAFE_INTEGER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset to the first page when the data source changes. We deliberately do
|
||||||
|
// NOT track pagination here — page-size changes reset pageIndex inside
|
||||||
|
// onPaginationChange instead, to avoid clobbering page navigation.
|
||||||
|
$effect(() => {
|
||||||
|
metrics;
|
||||||
|
untrack(() => {
|
||||||
|
pagination = { ...pagination, pageIndex: 0 };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedCapture = $state<ReqRespCapture | null>(null);
|
||||||
|
let dialogOpen = $state(false);
|
||||||
|
let loadingCaptureId = $state<number | null>(null);
|
||||||
|
|
||||||
|
async function viewCapture(id: number) {
|
||||||
|
loadingCaptureId = id;
|
||||||
|
const capture = await getCapture(id);
|
||||||
|
loadingCaptureId = null;
|
||||||
|
selectedCapture = capture;
|
||||||
|
dialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialogOpen = false;
|
||||||
|
selectedCapture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildColumns(withModel: boolean): ColumnDef<ActivityLogEntry>[] {
|
||||||
|
const cols: ColumnDef<ActivityLogEntry>[] = [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: ({ row }) => String(row.original.id + 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "time",
|
||||||
|
header: "Time",
|
||||||
|
cell: ({ row }) => formatRelativeTime(row.original.timestamp),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (withModel) {
|
||||||
|
cols.push({
|
||||||
|
id: "model",
|
||||||
|
header: "Model",
|
||||||
|
cell: ({ row }) => row.original.model ?? "-",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
id: "req_path",
|
||||||
|
header: "Path",
|
||||||
|
cell: ({ row }) => row.original.req_path || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "resp_status_code",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => String(row.original.resp_status_code || "-"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "resp_content_type",
|
||||||
|
header: "Content-Type",
|
||||||
|
cell: ({ row }) => row.original.resp_content_type || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cached",
|
||||||
|
header: () => renderComponent(HeaderLabel, { label: "Cached", tooltip: "prompt tokens from cache" }),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.tokens.cache_tokens > 0
|
||||||
|
? row.original.tokens.cache_tokens.toLocaleString()
|
||||||
|
: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prompt",
|
||||||
|
header: () => renderComponent(HeaderLabel, { label: "Prompt", tooltip: "new prompt tokens processed" }),
|
||||||
|
cell: ({ row }) => row.original.tokens.input_tokens.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "generated",
|
||||||
|
header: "Generated",
|
||||||
|
cell: ({ row }) => row.original.tokens.output_tokens.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "drafted",
|
||||||
|
header: () => renderComponent(HeaderLabel, { label: "Drafted", tooltip: "acceptance rate (accepted/drafted)" }),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
formatDrafted(row.original.tokens.draft_tokens, row.original.tokens.draft_acc_tokens),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prompt_speed",
|
||||||
|
header: "Prompt Speed",
|
||||||
|
cell: ({ row }) => formatSpeed(row.original.tokens.prompt_per_second),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gen_speed",
|
||||||
|
header: "Gen Speed",
|
||||||
|
cell: ({ row }) => formatSpeed(row.original.tokens.tokens_per_second),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "duration",
|
||||||
|
header: "Duration",
|
||||||
|
cell: ({ row }) => formatDuration(row.original.duration_ms),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "capture",
|
||||||
|
header: "Capture",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
renderComponent(ViewCaptureButton, {
|
||||||
|
hasCapture: row.original.has_capture,
|
||||||
|
loading: loadingCaptureId === row.original.id,
|
||||||
|
onclick: () => viewCapture(row.original.id),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "meta",
|
||||||
|
header: "Meta",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
renderComponent(MetaCell, { metadata: row.original.metadata }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
let columns = $derived(buildColumns(showModelColumn));
|
||||||
|
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return metrics;
|
||||||
|
},
|
||||||
|
get columns() {
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get pagination() {
|
||||||
|
return pagination;
|
||||||
|
},
|
||||||
|
get columnVisibility() {
|
||||||
|
return columnVisibility;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
const prev = pagination;
|
||||||
|
const next =
|
||||||
|
typeof updater === "function" ? updater(prev) : updater;
|
||||||
|
// Reassign so the table's $effect.pre (which reads state.pagination)
|
||||||
|
// picks up the new value. Reset to first page when the page size
|
||||||
|
// changes so we don't land on an empty page.
|
||||||
|
pagination =
|
||||||
|
next.pageSize !== prev.pageSize
|
||||||
|
? { pageIndex: 0, pageSize: next.pageSize }
|
||||||
|
: next;
|
||||||
|
if (showPagination) storedPageSize.set(pagination.pageSize);
|
||||||
|
},
|
||||||
|
onColumnVisibilityChange: (updater) => {
|
||||||
|
columnVisibility =
|
||||||
|
typeof updater === "function" ? updater(columnVisibility) : updater;
|
||||||
|
storedVisibility.set(columnVisibility);
|
||||||
|
},
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let thClass = $derived(compact ? "px-4 py-2 h-9" : "px-6 py-3 h-12");
|
||||||
|
let tdClass = $derived(compact ? "px-4 py-2" : "px-6 py-4");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root class="shrink-0 gap-0 overflow-hidden py-0 {cardClass}">
|
||||||
|
<Card.Header class="flex items-center justify-between border-b px-4 py-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if title}
|
||||||
|
<Card.Title class="text-sm font-semibold">
|
||||||
|
{title}
|
||||||
|
<span class="text-muted-foreground text-xs font-normal">({metrics.length})</span>
|
||||||
|
</Card.Title>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if showPagination}
|
||||||
|
<span class="text-muted-foreground text-xs">Rows</span>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
value={String(pagination.pageSize)}
|
||||||
|
onValueChange={(v) => table.setPageSize(Number(v))}
|
||||||
|
>
|
||||||
|
<Select.Trigger size="sm" class="h-7 w-[4.5rem] text-xs">
|
||||||
|
{pagination.pageSize}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each [5, 10, 25, 50] as size (size)}
|
||||||
|
<Select.Item value={String(size)}>{size}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
{/if}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
class="hover:bg-muted inline-flex size-7 items-center justify-center rounded-[min(var(--radius-md),12px)]"
|
||||||
|
title="Select columns"
|
||||||
|
>
|
||||||
|
<Columns3 class="size-4" />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end" class="min-w-[16rem] p-0">
|
||||||
|
<DropdownMenu.Label class="text-muted-foreground border-b px-3 py-2 text-xs font-medium uppercase tracking-wider">
|
||||||
|
Columns
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
{#each table.getAllColumns() as column (column.id)}
|
||||||
|
{#if column.getCanHide()}
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(v) => column.toggleVisibility(!!v)}
|
||||||
|
closeOnSelect={false}
|
||||||
|
>
|
||||||
|
{columnLabelMap[column.id] ?? column.id}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="overflow-x-auto p-0">
|
||||||
|
<Table.Root class="min-w-full">
|
||||||
|
<Table.Header>
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<Table.Row>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<Table.Head class={thClass} colspan={header.colSpan}>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||||
|
{/if}
|
||||||
|
</Table.Head>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<Table.Row>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<Table.Cell class={tdClass}>
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</Table.Cell>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class="text-muted-foreground py-6 text-center text-sm">
|
||||||
|
{emptyMessage}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
|
||||||
|
{#if showPagination && metrics.length > 0}
|
||||||
|
<div class="flex items-center justify-between gap-2 border-t px-4 py-2 text-sm">
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
Page {pagination.pageIndex + 1} of {table.getPageCount()} · {metrics.length} total
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onclick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
title="First page"
|
||||||
|
>
|
||||||
|
<ChevronsLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onclick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
title="Previous page"
|
||||||
|
>
|
||||||
|
<ChevronLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onclick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
title="Next page"
|
||||||
|
>
|
||||||
|
<ChevronRight />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onclick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
title="Last page"
|
||||||
|
>
|
||||||
|
<ChevronsRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
<CaptureDialog capture={selectedCapture} open={dialogOpen} onclose={closeDialog} />
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { link } from "svelte-spa-router";
|
||||||
|
import { House, Boxes, Activity, ScrollText, Gauge, Sun, Moon, Monitor, ChevronRight, Settings } from "@lucide/svelte";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { toggleTheme, themeMode, appTitle } from "../stores/theme";
|
||||||
|
import { currentRoute } from "../stores/route";
|
||||||
|
import { playgroundActivity } from "../stores/playgroundActivity";
|
||||||
|
import { performanceEnabled, models } from "../stores/api";
|
||||||
|
import { selectedPlaygroundTab, playgroundTabs, playgroundMenuOpen } from "../stores/playground";
|
||||||
|
import { modelsMenuOpen } from "../stores/sidebar";
|
||||||
|
import type { Model } from "../lib/types";
|
||||||
|
import ConnectionStatus from "./ConnectionStatus.svelte";
|
||||||
|
|
||||||
|
function handleTitleChange(newTitle: string): void {
|
||||||
|
const sanitized = newTitle.replace(/\n/g, "").trim().substring(0, 64) || "llama-swap";
|
||||||
|
appTitle.set(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent): void {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
handleTitleChange(target.textContent || "(set title)");
|
||||||
|
target.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBlur(e: FocusEvent): void {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
handleTitleChange(target.textContent || "(set title)");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(path: string, current: string): boolean {
|
||||||
|
return path === "/" ? current === "/" : current.startsWith(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DotColor = "grey" | "yellow" | "green";
|
||||||
|
function statusDotColor(model: Model): DotColor {
|
||||||
|
if (model.state === "ready") return "green";
|
||||||
|
if (model.state === "starting" || model.state === "stopping") return "yellow";
|
||||||
|
return "grey";
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotClass: Record<DotColor, string> = {
|
||||||
|
grey: "bg-muted-foreground/40",
|
||||||
|
yellow: "bg-warning",
|
||||||
|
green: "bg-success",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root collapsible="icon">
|
||||||
|
<Sidebar.Header>
|
||||||
|
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||||
|
<div class="flex shrink-0 items-center justify-center">
|
||||||
|
<ConnectionStatus />
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
contenteditable="true"
|
||||||
|
class="truncate pb-0 text-base font-semibold outline-none rounded-md px-1 hover:bg-sidebar-accent group-data-[collapsible=icon]:hidden"
|
||||||
|
onblur={handleBlur}
|
||||||
|
onkeydown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{$appTitle}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Header>
|
||||||
|
|
||||||
|
<Sidebar.Content>
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu class="gap-1">
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton isActive={isActive("/activity", $currentRoute)} tooltipContent="Activity">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/activity" use:link {...props}>
|
||||||
|
<Activity />
|
||||||
|
<span>Activity</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={$playgroundMenuOpen}
|
||||||
|
onOpenChange={(v) => playgroundMenuOpen.set(v)}
|
||||||
|
class="gap-0"
|
||||||
|
>
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
isActive={isActive("/", $currentRoute)}
|
||||||
|
tooltipContent="Playground"
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/" use:link {...props}>
|
||||||
|
<House />
|
||||||
|
<span class={$playgroundActivity ? "activity-link" : ""}>Playground</span>
|
||||||
|
<span
|
||||||
|
class="ml-auto transition-transform duration-200 {$playgroundMenuOpen ? 'rotate-90' : ''}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Toggle playground section"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
playgroundMenuOpen.update((v) => !v);
|
||||||
|
}}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
playgroundMenuOpen.update((v) => !v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRight />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<Sidebar.MenuSub>
|
||||||
|
{#each playgroundTabs as tab (tab.id)}
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton
|
||||||
|
isActive={isActive("/", $currentRoute) && $selectedPlaygroundTab === tab.id}
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
use:link
|
||||||
|
{...props}
|
||||||
|
onclick={() => selectedPlaygroundTab.set(tab.id)}
|
||||||
|
>
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.MenuSub>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Collapsible.Root
|
||||||
|
open={$modelsMenuOpen}
|
||||||
|
onOpenChange={(v) => modelsMenuOpen.set(v)}
|
||||||
|
class="gap-0"
|
||||||
|
>
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
isActive={$currentRoute.startsWith("/models")}
|
||||||
|
tooltipContent="Models"
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/models" use:link {...props}>
|
||||||
|
<Boxes />
|
||||||
|
<span>Models</span>
|
||||||
|
<span
|
||||||
|
class="ml-auto transition-transform duration-200 {$modelsMenuOpen ? 'rotate-90' : ''}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Toggle models section"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
modelsMenuOpen.update((v) => !v);
|
||||||
|
}}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
modelsMenuOpen.update((v) => !v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRight />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<Sidebar.MenuSub>
|
||||||
|
{#each $models as model (model.id)}
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton
|
||||||
|
isActive={$currentRoute === `/models/${encodeURIComponent(model.id)}`}
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/models/{encodeURIComponent(model.id)}" use:link {...props}>
|
||||||
|
<span class={`size-2 shrink-0 rounded-full ${dotClass[statusDotColor(model)]}`}></span>
|
||||||
|
<span class="flex-1 truncate">{model.id}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.MenuSub>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton isActive={isActive("/logs", $currentRoute)} tooltipContent="Logs">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/logs" use:link {...props}>
|
||||||
|
<ScrollText />
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
|
||||||
|
{#if $performanceEnabled}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton isActive={isActive("/performance", $currentRoute)} tooltipContent="Performance">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/performance" use:link {...props}>
|
||||||
|
<Gauge />
|
||||||
|
<span>Performance</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/if}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
</Sidebar.Content>
|
||||||
|
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-2 px-1 group-data-[collapsible=icon]:flex-col-reverse"
|
||||||
|
>
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
isActive={isActive("/settings", $currentRoute)}
|
||||||
|
tooltipContent="Settings"
|
||||||
|
>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="/settings" use:link {...props}>
|
||||||
|
<Settings />
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onclick={toggleTheme}
|
||||||
|
title="Toggle theme (current: {$themeMode})"
|
||||||
|
>
|
||||||
|
{#if $themeMode === "system"}
|
||||||
|
<Monitor />
|
||||||
|
{:else if $themeMode === "light"}
|
||||||
|
<Sun />
|
||||||
|
{:else}
|
||||||
|
<Moon />
|
||||||
|
{/if}
|
||||||
|
<span class="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Footer>
|
||||||
|
<Sidebar.Rail />
|
||||||
|
</Sidebar.Root>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ReqRespCapture } from "../lib/types";
|
import type { ReqRespCapture } from "../lib/types";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
capture: ReqRespCapture | null;
|
capture: ReqRespCapture | null;
|
||||||
@@ -9,22 +11,12 @@
|
|||||||
|
|
||||||
let { capture, open, onclose }: Props = $props();
|
let { capture, open, onclose }: Props = $props();
|
||||||
|
|
||||||
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
||||||
|
|
||||||
type BodyTab = "raw" | "pretty" | "chat";
|
type BodyTab = "raw" | "pretty" | "chat";
|
||||||
let reqBodyTab: BodyTab = $state("pretty");
|
let reqBodyTab: BodyTab = $state("pretty");
|
||||||
let respBodyTab: BodyTab = $state("pretty");
|
let respBodyTab: BodyTab = $state("pretty");
|
||||||
let copiedReq = $state(false);
|
let copiedReq = $state(false);
|
||||||
let copiedResp = $state(false);
|
let copiedResp = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (open && dialogEl) {
|
|
||||||
dialogEl.showModal();
|
|
||||||
} else if (!open && dialogEl) {
|
|
||||||
dialogEl.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset tabs when capture changes
|
// Reset tabs when capture changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (capture) {
|
if (capture) {
|
||||||
@@ -39,10 +31,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDialogClose() {
|
|
||||||
onclose();
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeBody(body: string | null | undefined): string {
|
function decodeBody(body: string | null | undefined): string {
|
||||||
if (!body) return "";
|
if (!body) return "";
|
||||||
try {
|
try {
|
||||||
@@ -190,40 +178,36 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog
|
<Dialog.Root
|
||||||
bind:this={dialogEl}
|
{open}
|
||||||
onclose={handleDialogClose}
|
onOpenChange={(v) => {
|
||||||
class="bg-surface text-txtmain rounded-lg shadow-xl max-w-[80%] w-full max-h-[90vh] p-0 backdrop:bg-black/50 m-auto"
|
if (!v) onclose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Dialog.Content class="flex max-h-[90vh] w-[90%] sm:max-w-[90%] flex-col gap-0 p-0">
|
||||||
{#if capture}
|
{#if capture}
|
||||||
<div class="flex flex-col max-h-[90vh]">
|
<Dialog.Header class="border-b border-border px-4 py-3">
|
||||||
<div
|
<Dialog.Title class="text-lg font-bold">
|
||||||
class="flex justify-between items-center p-4 border-b border-card-border"
|
Capture #{capture.id + 1}{#if capture.req_path}
|
||||||
>
|
<span class="font-mono text-base font-normal text-muted-foreground">{capture.req_path}</span>{/if}
|
||||||
<h2 class="text-xl font-bold pb-0">Capture #{capture.id + 1}{#if capture.req_path} <span class="text-base font-mono font-normal text-txtsecondary">{capture.req_path}</span>{/if}</h2>
|
</Dialog.Title>
|
||||||
<button
|
</Dialog.Header>
|
||||||
onclick={() => dialogEl?.close()}
|
|
||||||
class="text-txtsecondary hover:text-txtmain text-2xl leading-none"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto flex-1 p-4 space-y-4">
|
<div class="min-h-0 flex-1 overflow-y-auto space-y-4 p-4">
|
||||||
<!-- Request Headers -->
|
<!-- Request Headers -->
|
||||||
<details class="group" open>
|
<details class="group" open>
|
||||||
<summary
|
<summary
|
||||||
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-txtsecondary hover:text-txtmain"
|
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Request Headers
|
Request Headers
|
||||||
</summary>
|
</summary>
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-48"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-48"
|
||||||
>
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(capture.req_headers || {}) as [key, value]}
|
{#each Object.entries(capture.req_headers || {}) as [key, value]}
|
||||||
<tr class="border-b border-card-border-inner last:border-0">
|
<tr class="border-b border-border last:border-0">
|
||||||
<td class="px-3 py-1 font-mono text-primary whitespace-nowrap"
|
<td class="px-3 py-1 font-mono text-primary whitespace-nowrap"
|
||||||
>{key}</td
|
>{key}</td
|
||||||
>
|
>
|
||||||
@@ -238,7 +222,7 @@
|
|||||||
<!-- Request Body -->
|
<!-- Request Body -->
|
||||||
<details class="group" open>
|
<details class="group" open>
|
||||||
<summary
|
<summary
|
||||||
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-txtsecondary hover:text-txtmain"
|
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Request Body
|
Request Body
|
||||||
</summary>
|
</summary>
|
||||||
@@ -271,14 +255,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-1 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-1 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre
|
<pre
|
||||||
class="p-3 text-sm font-mono whitespace-pre-wrap break-all">{displayedRequestBody}</pre>
|
class="p-3 text-sm font-mono whitespace-pre-wrap break-all">{displayedRequestBody}</pre>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre class="p-3 text-sm font-mono whitespace-pre-wrap break-all"
|
<pre class="p-3 text-sm font-mono whitespace-pre-wrap break-all"
|
||||||
>(empty)</pre
|
>(empty)</pre
|
||||||
@@ -290,17 +274,17 @@
|
|||||||
<!-- Response Headers -->
|
<!-- Response Headers -->
|
||||||
<details class="group" open>
|
<details class="group" open>
|
||||||
<summary
|
<summary
|
||||||
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-txtsecondary hover:text-txtmain"
|
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Response Headers
|
Response Headers
|
||||||
</summary>
|
</summary>
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-48"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-48"
|
||||||
>
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(capture.resp_headers || {}) as [key, value]}
|
{#each Object.entries(capture.resp_headers || {}) as [key, value]}
|
||||||
<tr class="border-b border-card-border-inner last:border-0">
|
<tr class="border-b border-border last:border-0">
|
||||||
<td class="px-3 py-1 font-mono text-primary whitespace-nowrap"
|
<td class="px-3 py-1 font-mono text-primary whitespace-nowrap"
|
||||||
>{key}</td
|
>{key}</td
|
||||||
>
|
>
|
||||||
@@ -315,13 +299,13 @@
|
|||||||
<!-- Response Body -->
|
<!-- Response Body -->
|
||||||
<details class="group" open>
|
<details class="group" open>
|
||||||
<summary
|
<summary
|
||||||
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-txtsecondary hover:text-txtmain"
|
class="cursor-pointer font-semibold text-sm uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Response Body
|
Response Body
|
||||||
</summary>
|
</summary>
|
||||||
{#if isResponseImage && capture.resp_body}
|
{#if isResponseImage && capture.resp_body}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<div class="p-3 flex justify-center">
|
<div class="p-3 flex justify-center">
|
||||||
<img
|
<img
|
||||||
@@ -368,26 +352,26 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-1 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-1 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
{#if respBodyTab === "chat"}
|
{#if respBodyTab === "chat"}
|
||||||
<div class="p-3 text-sm space-y-3">
|
<div class="p-3 text-sm space-y-3">
|
||||||
{#if sseChat.reasoning}
|
{#if sseChat.reasoning}
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="text-xs font-semibold uppercase tracking-wider text-txtsecondary mb-1"
|
class="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1"
|
||||||
>
|
>
|
||||||
Reasoning
|
Reasoning
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
class="font-mono whitespace-pre-wrap break-all text-txtsecondary">{sseChat.reasoning}</pre>
|
class="font-mono whitespace-pre-wrap break-all text-muted-foreground">{sseChat.reasoning}</pre>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if sseChat.content}
|
{#if sseChat.content}
|
||||||
<div>
|
<div>
|
||||||
{#if sseChat.reasoning}
|
{#if sseChat.reasoning}
|
||||||
<div
|
<div
|
||||||
class="text-xs font-semibold uppercase tracking-wider text-txtsecondary mb-1"
|
class="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1"
|
||||||
>
|
>
|
||||||
Response
|
Response
|
||||||
</div>
|
</div>
|
||||||
@@ -407,15 +391,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if responseBodyRaw}
|
{:else if responseBodyRaw}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<div class="p-3 text-sm text-txtsecondary italic">
|
<div class="p-3 text-sm text-muted-foreground italic">
|
||||||
(binary data - {responseContentType || "unknown content type"})
|
(binary data - {responseContentType || "unknown content type"})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="mt-2 bg-background rounded border border-card-border overflow-auto max-h-96"
|
class="mt-2 bg-background rounded-md border border-border overflow-auto max-h-96"
|
||||||
>
|
>
|
||||||
<pre class="p-3 text-sm font-mono">(empty)</pre>
|
<pre class="p-3 text-sm font-mono">(empty)</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,39 +407,39 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t border-card-border flex justify-end">
|
<Dialog.Footer class="border-t border-border px-4 py-3 sm:justify-end">
|
||||||
<button onclick={() => dialogEl?.close()} class="btn"> Close </button>
|
<Button variant="outline" onclick={onclose}>Close</Button>
|
||||||
</div>
|
</Dialog.Footer>
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center p-12">
|
<div class="flex flex-col items-center justify-center p-12">
|
||||||
<p class="text-lg text-txtsecondary">Capture not found</p>
|
<p class="text-lg text-muted-foreground">Capture not found</p>
|
||||||
<p class="text-sm text-txtsecondary mt-1">The capture may have expired or was never recorded.</p>
|
<p class="text-sm text-muted-foreground mt-1">The capture may have expired or was never recorded.</p>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button onclick={() => dialogEl?.close()} class="btn">Close</button>
|
<Button variant="outline" onclick={onclose}>Close</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</dialog>
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
color: var(--color-txtsecondary);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
.tab-btn:hover {
|
.tab-btn:hover {
|
||||||
color: var(--color-txtmain);
|
color: var(--foreground);
|
||||||
background: var(--color-secondary);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
.tab-btn-active {
|
.tab-btn-active {
|
||||||
color: var(--color-primary);
|
color: var(--primary);
|
||||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--color-primary) 25%, transparent);
|
border-color: color-mix(in srgb, var(--primary) 25%, transparent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { connectionState } from "../stores/theme";
|
import { connectionState } from "../stores/theme";
|
||||||
import { versionInfo } from "../stores/api";
|
|
||||||
|
|
||||||
let eventStatusColor = $derived.by(() => {
|
let eventStatusColor = $derived.by(() => {
|
||||||
switch ($connectionState) {
|
switch ($connectionState) {
|
||||||
@@ -14,9 +13,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let tooltipText = $derived(
|
let tooltipText = $derived(`Event Stream: ${$connectionState ?? "unknown"}`);
|
||||||
`Event Stream: ${$connectionState ?? "unknown"}\nAPI Version: ${$versionInfo?.version ?? "unknown"}\nCommit Hash: ${$versionInfo?.commit?.substring(0, 7) ?? "unknown"}\nBuild Date: ${$versionInfo?.build_date ?? "unknown"}`
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center" title={tooltipText}>
|
<div class="flex items-center" title={tooltipText}>
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { link } from "svelte-spa-router";
|
|
||||||
import { screenWidth, toggleTheme, themeMode, appTitle, isNarrow } from "../stores/theme";
|
|
||||||
import { currentRoute } from "../stores/route";
|
|
||||||
import { playgroundActivity } from "../stores/playgroundActivity";
|
|
||||||
import { performanceEnabled } from "../stores/api";
|
|
||||||
import ConnectionStatus from "./ConnectionStatus.svelte";
|
|
||||||
|
|
||||||
function handleTitleChange(newTitle: string): void {
|
|
||||||
const sanitized = newTitle.replace(/\n/g, "").trim().substring(0, 64) || "llama-swap";
|
|
||||||
appTitle.set(sanitized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent): void {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = e.currentTarget as HTMLElement;
|
|
||||||
handleTitleChange(target.textContent || "(set title)");
|
|
||||||
target.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBlur(e: FocusEvent): void {
|
|
||||||
const target = e.currentTarget as HTMLElement;
|
|
||||||
handleTitleChange(target.textContent || "(set title)");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActive(path: string, current: string): boolean {
|
|
||||||
return path === "/" ? current === "/" : current.startsWith(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header
|
|
||||||
class="flex items-center justify-between bg-surface border-b border-border px-4 {$isNarrow
|
|
||||||
? 'py-1 h-[60px]'
|
|
||||||
: 'p-2 h-[75px]'}"
|
|
||||||
>
|
|
||||||
{#if $screenWidth !== "xs" && $screenWidth !== "sm"}
|
|
||||||
<h1
|
|
||||||
contenteditable="true"
|
|
||||||
class="p-0 outline-none hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
|
||||||
onblur={handleBlur}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
>
|
|
||||||
{$appTitle}
|
|
||||||
</h1>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<menu class="flex items-center gap-4 overflow-x-auto">
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
use:link
|
|
||||||
class="p-1 whitespace-nowrap {isActive('/', $currentRoute) ? 'font-semibold underline underline-offset-4' : ''} {$playgroundActivity ? 'activity-link' : 'text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100'}"
|
|
||||||
>
|
|
||||||
Playground
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/models"
|
|
||||||
use:link
|
|
||||||
class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap"
|
|
||||||
class:font-semibold={isActive("/models", $currentRoute)}
|
|
||||||
class:underline={isActive("/models", $currentRoute)}
|
|
||||||
class:underline-offset-4={isActive("/models", $currentRoute)}
|
|
||||||
>
|
|
||||||
Models
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/activity"
|
|
||||||
use:link
|
|
||||||
class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap"
|
|
||||||
class:font-semibold={isActive("/activity", $currentRoute)}
|
|
||||||
class:underline={isActive("/activity", $currentRoute)}
|
|
||||||
class:underline-offset-4={isActive("/activity", $currentRoute)}
|
|
||||||
>
|
|
||||||
Activity
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/logs"
|
|
||||||
use:link
|
|
||||||
class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap"
|
|
||||||
class:font-semibold={isActive("/logs", $currentRoute)}
|
|
||||||
class:underline={isActive("/logs", $currentRoute)}
|
|
||||||
class:underline-offset-4={isActive("/logs", $currentRoute)}
|
|
||||||
>
|
|
||||||
Logs
|
|
||||||
</a>
|
|
||||||
{#if $performanceEnabled}
|
|
||||||
<a
|
|
||||||
href="/performance"
|
|
||||||
use:link
|
|
||||||
class="text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 whitespace-nowrap"
|
|
||||||
class:font-semibold={isActive("/performance", $currentRoute)}
|
|
||||||
class:underline={isActive("/performance", $currentRoute)}
|
|
||||||
class:underline-offset-4={isActive("/performance", $currentRoute)}
|
|
||||||
>
|
|
||||||
Performance
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<button onclick={toggleTheme} title="Toggle theme (current: {$themeMode})">
|
|
||||||
{#if $themeMode === "system"}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M0,9c0-.552,.448-1,1-1H3.108c.147-.874,.472-1.721,1.006-2.471l-1.478-1.478c-.391-.391-.391-1.023,0-1.414s1.023-.391,1.414,0l1.478,1.478c.751-.534,1.598-.859,2.471-1.006V1c0-.552,.448-1,1-1s1,.448,1,1V3.108c.874,.147,1.725,.466,2.477,1.001l1.473-1.473c.391-.391,1.023-.391,1.414,0s.391,1.023,0,1.414L3.963,15.45c-.195,.195-.451,.293-.707,.293s-.512-.098-.707-.293c-.391-.391-.391-1.023,0-1.414l1.56-1.56c-.535-.751-.854-1.602-1.001-2.477H1c-.552,0-1-.448-1-1ZM23.707,.293c-.391-.391-1.023-.391-1.414,0L.293,22.293c-.391,.391-.391,1.023,0,1.414,.195,.195,.451,.293,.707,.293s.512-.098,.707-.293L23.707,1.707c.391-.391,.391-1.023,0-1.414Zm-.283,10.954c.32-.15,.538-.458,.572-.81,.034-.353-.121-.696-.407-.904-.858-.625-1.833-1.066-2.897-1.315-.335-.078-.69,.022-.934,.267l-8.392,8.391c-.244,.244-.345,.597-.267,.933,.843,3.646,4.047,6.191,7.792,6.191,1.695,0,3.32-.53,4.697-1.533,.286-.208,.441-.553,.407-.904-.034-.353-.251-.66-.572-.811-1.842-.861-3.033-2.727-3.033-4.752s1.19-3.891,3.033-4.753Z"/>
|
|
||||||
</svg>
|
|
||||||
{:else if $themeMode === "light"}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.591 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6 12a.75.75 0 0 1-.75.75H3a.75.75 0 0 1 0-1.5h2.25A.75.75 0 0 1 6 12ZM6.697 7.757a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 0 0-1.061 1.06l1.59 1.591Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.98 10.503 10.503 0 0 1-9.694 6.46c-5.799 0-10.5-4.7-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 0 1 .818.162Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<ConnectionStatus />
|
|
||||||
</menu>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.activity-link {
|
|
||||||
background: linear-gradient(90deg, #6366f1, #8b5cf6, #a855f7, #8b5cf6, #6366f1);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
animation: gradient-shift 2s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient-shift {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { persistentStore } from "../stores/persistent";
|
import { persistentStore } from "../stores/persistent";
|
||||||
|
import { Type, WrapText, Search, SearchX, CircleX } from "@lucide/svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -81,59 +85,32 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-lg overflow-hidden flex flex-col bg-gray-950/5 dark:bg-white/10 h-full w-full p-1">
|
<Card.Root class="bg-muted/30 h-full w-full gap-0 overflow-hidden py-0">
|
||||||
<div class="p-4">
|
<Card.Header class="border-b px-4 py-2">
|
||||||
<div class="flex items-center justify-between">
|
<Card.Title class="text-sm font-semibold">{title}</Card.Title>
|
||||||
<h3 class="m-0 text-lg p-0">{title}</h3>
|
<Card.Action>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex gap-2 items-center">
|
<Button variant="ghost" size="icon-sm" onclick={toggleFontSize} title="Change font size">
|
||||||
<button class="btn border-0" onclick={toggleFontSize} title="Change font size">
|
<Type />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
</Button>
|
||||||
<path d="M2 4v3h5v12h3V7h5V4H2zm19 5h-9v3h3v7h3v-7h3V9z"/>
|
<Button variant="ghost" size="icon-sm" onclick={toggleWrapText} title="Toggle text wrap">
|
||||||
</svg>
|
<WrapText class={$wrapTextStore ? "text-primary" : ""} />
|
||||||
</button>
|
</Button>
|
||||||
<button class="btn border-0" onclick={toggleWrapText} title="Toggle text wrap">
|
<Button variant="ghost" size="icon-sm" onclick={toggleFilter} title="Toggle filter">
|
||||||
{#if $wrapTextStore}
|
{#if $showFilterStore}<SearchX />{:else}<Search />{/if}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
</Button>
|
||||||
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
</div>
|
||||||
</svg>
|
</Card.Action>
|
||||||
{:else}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
|
||||||
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<button class="btn border-0" onclick={toggleFilter} title="Toggle filter">
|
|
||||||
{#if $showFilterStore}
|
{#if $showFilterStore}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">
|
<div class="flex w-full items-center gap-2 pt-2">
|
||||||
<path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
|
<Input type="text" class="h-8" placeholder="Filter logs (regex)..." bind:value={filterRegex} />
|
||||||
</svg>
|
<Button variant="ghost" size="icon-sm" onclick={() => (filterRegex = "")} aria-label="Clear filter">
|
||||||
{:else}
|
<CircleX />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
</Button>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $showFilterStore}
|
|
||||||
<div class="mt-2 flex gap-2 items-center w-full">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full text-sm border border-gray-950/10 dark:border-white/5 p-2 rounded outline-none"
|
|
||||||
placeholder="Filter logs (regex)..."
|
|
||||||
bind:value={filterRegex}
|
|
||||||
/>
|
|
||||||
<button class="pl-2" onclick={() => (filterRegex = "")} aria-label="Clear filter">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Card.Header>
|
||||||
<div class="rounded-lg bg-background font-mono text-sm flex-1 overflow-hidden">
|
<Card.Content class="bg-background min-h-0 flex-1 p-0 font-mono text-sm">
|
||||||
<pre bind:this={preElement} onscroll={handleScroll} class="{textWrapClass} {fontSizeClass} h-full overflow-auto p-4">{filteredLogs}</pre>
|
<pre bind:this={preElement} onscroll={handleScroll} class="{textWrapClass} {fontSizeClass} h-full overflow-auto p-4">{filteredLogs}</pre>
|
||||||
</div>
|
</Card.Content>
|
||||||
</div>
|
</Card.Root>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
metadata: Record<string, string> | undefined;
|
metadata: Record<string, string> | undefined;
|
||||||
@@ -9,68 +10,15 @@
|
|||||||
let { metadata, children }: Props = $props();
|
let { metadata, children }: Props = $props();
|
||||||
|
|
||||||
let entries = $derived(Object.entries(metadata || {}));
|
let entries = $derived(Object.entries(metadata || {}));
|
||||||
let triggerEl: HTMLElement | undefined = $state();
|
|
||||||
let tooltipEl: HTMLDivElement | undefined = $state();
|
|
||||||
let show = $state(false);
|
|
||||||
let tooltipStyle = $state("");
|
|
||||||
|
|
||||||
function positionTooltip() {
|
|
||||||
if (!triggerEl || !tooltipEl) return;
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect();
|
|
||||||
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
||||||
const margin = 8;
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
|
|
||||||
let left = triggerRect.left;
|
|
||||||
let top = triggerRect.bottom + margin;
|
|
||||||
|
|
||||||
// Keep tooltip within horizontal viewport bounds
|
|
||||||
if (left + tooltipRect.width > viewportWidth - margin) {
|
|
||||||
left = triggerRect.right - tooltipRect.width;
|
|
||||||
}
|
|
||||||
if (left < margin) {
|
|
||||||
left = margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip above trigger if it would overflow the bottom
|
|
||||||
if (top + tooltipRect.height > viewportHeight - margin) {
|
|
||||||
top = triggerRect.top - tooltipRect.height - margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipStyle = `left: ${left}px; top: ${top}px; max-width: calc(100vw - ${margin * 2}px);`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEnter() {
|
|
||||||
show = true;
|
|
||||||
requestAnimationFrame(positionTooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLeave() {
|
|
||||||
show = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
{#if entries.length > 0}
|
||||||
bind:this={triggerEl}
|
<Tooltip.Provider>
|
||||||
onmouseenter={onEnter}
|
<Tooltip.Root>
|
||||||
onmouseleave={onLeave}
|
<Tooltip.Trigger>
|
||||||
onfocus={onEnter}
|
|
||||||
onblur={onLeave}
|
|
||||||
class="inline-flex"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Show metadata"
|
|
||||||
>
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</span>
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="min-w-[12rem] max-w-[24rem] normal-case">
|
||||||
{#if show && entries.length > 0}
|
|
||||||
<div
|
|
||||||
bind:this={tooltipEl}
|
|
||||||
style={tooltipStyle}
|
|
||||||
class="fixed px-3 py-2 bg-gray-900 text-white text-sm rounded-md z-50 normal-case min-w-[12rem] max-w-[24rem] shadow-lg whitespace-normal"
|
|
||||||
>
|
|
||||||
<table class="w-full text-left">
|
<table class="w-full text-left">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each entries as [key, value]}
|
{#each entries as [key, value]}
|
||||||
@@ -81,5 +29,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
{:else}
|
||||||
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { models, loadModel, unloadAllModels, unloadSingleModel } from "../stores/api";
|
|
||||||
import { isNarrow } from "../stores/theme";
|
|
||||||
import { persistentStore } from "../stores/persistent";
|
|
||||||
import type { Model } from "../lib/types";
|
|
||||||
|
|
||||||
let isUnloading = $state(false);
|
|
||||||
let menuOpen = $state(false);
|
|
||||||
let pendingLoads = $state<Record<string, boolean>>({});
|
|
||||||
const loadControllers = new Map<string, AbortController>();
|
|
||||||
|
|
||||||
const showUnlistedStore = persistentStore<boolean>("showUnlisted", true);
|
|
||||||
const showIdorNameStore = persistentStore<"id" | "name">("showIdorName", "id");
|
|
||||||
|
|
||||||
let filteredModels = $derived.by(() => {
|
|
||||||
const filtered = $models.filter((model) => $showUnlistedStore || !model.unlisted);
|
|
||||||
const peerModels = filtered.filter((m) => m.peerID);
|
|
||||||
|
|
||||||
// Group peer models by peerID
|
|
||||||
const grouped = peerModels.reduce(
|
|
||||||
(acc, model) => {
|
|
||||||
const peerId = model.peerID || "unknown";
|
|
||||||
if (!acc[peerId]) acc[peerId] = [];
|
|
||||||
acc[peerId].push(model);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, Model[]>
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
regularModels: filtered.filter((m) => !m.peerID),
|
|
||||||
peerModelsByPeerId: grouped,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleUnloadAllModels(): Promise<void> {
|
|
||||||
isUnloading = true;
|
|
||||||
try {
|
|
||||||
await unloadAllModels();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => (isUnloading = false), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLoadModel(modelId: string): Promise<void> {
|
|
||||||
if (pendingLoads[modelId]) return;
|
|
||||||
const controller = new AbortController();
|
|
||||||
loadControllers.set(modelId, controller);
|
|
||||||
pendingLoads[modelId] = true;
|
|
||||||
try {
|
|
||||||
await loadModel(modelId, controller.signal);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
loadControllers.delete(modelId);
|
|
||||||
delete pendingLoads[modelId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelLoad(modelId: string): void {
|
|
||||||
loadControllers.get(modelId)?.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleIdorName(): void {
|
|
||||||
showIdorNameStore.update((prev) => (prev === "name" ? "id" : "name"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleShowUnlisted(): void {
|
|
||||||
showUnlistedStore.update((prev) => !prev);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelDisplay(model: Model): string {
|
|
||||||
return $showIdorNameStore === "id" ? model.id : (model.name || model.id);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card h-full flex flex-col">
|
|
||||||
<div class="shrink-0">
|
|
||||||
<div class="flex justify-between items-baseline">
|
|
||||||
<h2 class={$isNarrow ? "text-xl" : ""}>Models</h2>
|
|
||||||
{#if $isNarrow}
|
|
||||||
<div class="relative">
|
|
||||||
<button class="btn text-base flex items-center gap-2 py-1" onclick={() => (menuOpen = !menuOpen)} aria-label="Toggle menu">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{#if menuOpen}
|
|
||||||
<div class="absolute right-0 mt-2 w-48 bg-surface border border-gray-200 dark:border-white/10 rounded shadow-lg z-20">
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-4 py-2 hover:bg-secondary-hover flex items-center gap-2"
|
|
||||||
onclick={() => { toggleIdorName(); menuOpen = false; }}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M15.97 2.47a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l3.22-3.22H7.5a.75.75 0 0 1 0-1.5h11.69l-3.22-3.22a.75.75 0 0 1 0-1.06Zm-7.94 9a.75.75 0 0 1 0 1.06l-3.22 3.22H16.5a.75.75 0 0 1 0 1.5H4.81l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{$showIdorNameStore === "id" ? "Show Name" : "Show ID"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-4 py-2 hover:bg-secondary-hover flex items-center gap-2"
|
|
||||||
onclick={() => { toggleShowUnlisted(); menuOpen = false; }}
|
|
||||||
>
|
|
||||||
{#if $showUnlistedStore}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M3.53 2.47a.75.75 0 0 0-1.06 1.06l18 18a.75.75 0 1 0 1.06-1.06l-18-18ZM22.676 12.553a11.249 11.249 0 0 1-2.631 4.31l-3.099-3.099a5.25 5.25 0 0 0-6.71-6.71L7.759 4.577a11.217 11.217 0 0 1 4.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113Z" />
|
|
||||||
<path d="M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0 1 15.75 12ZM12.53 15.713l-4.243-4.244a3.75 3.75 0 0 0 4.244 4.243Z" />
|
|
||||||
<path d="M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 0 0-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 0 1 6.75 12Z" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
|
|
||||||
<path fill-rule="evenodd" d="M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 0 1 0-1.113ZM17.25 12a5.25 5.25 0 1 1-10.5 0 5.25 5.25 0 0 1 10.5 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
{$showUnlistedStore ? "Hide Unlisted" : "Show Unlisted"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-4 py-2 hover:bg-secondary-hover flex items-center gap-2"
|
|
||||||
onclick={() => { handleUnloadAllModels(); menuOpen = false; }}
|
|
||||||
disabled={isUnloading}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm.53 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v5.69a.75.75 0 0 0 1.5 0v-5.69l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{isUnloading ? "Unloading..." : "Unload All"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if !$isNarrow}
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button class="btn text-base flex items-center gap-2" onclick={toggleIdorName} style="line-height: 1.2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M15.97 2.47a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l3.22-3.22H7.5a.75.75 0 0 1 0-1.5h11.69l-3.22-3.22a.75.75 0 0 1 0-1.06Zm-7.94 9a.75.75 0 0 1 0 1.06l-3.22 3.22H16.5a.75.75 0 0 1 0 1.5H4.81l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{$showIdorNameStore === "id" ? "ID" : "Name"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn text-base flex items-center gap-2" onclick={toggleShowUnlisted} style="line-height: 1.2">
|
|
||||||
{#if $showUnlistedStore}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
|
|
||||||
<path fill-rule="evenodd" d="M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 0 1 0-1.113ZM17.25 12a5.25 5.25 0 1 1-10.5 0 5.25 5.25 0 0 1 10.5 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M3.53 2.47a.75.75 0 0 0-1.06 1.06l18 18a.75.75 0 1 0 1.06-1.06l-18-18ZM22.676 12.553a11.249 11.249 0 0 1-2.631 4.31l-3.099-3.099a5.25 5.25 0 0 0-6.71-6.71L7.759 4.577a11.217 11.217 0 0 1 4.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113Z" />
|
|
||||||
<path d="M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0 1 15.75 12ZM12.53 15.713l-4.243-4.244a3.75 3.75 0 0 0 4.244 4.243Z" />
|
|
||||||
<path d="M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 0 0-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 0 1 6.75 12Z" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
unlisted
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button class="btn text-base flex items-center gap-2" onclick={handleUnloadAllModels} disabled={isUnloading}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
||||||
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm.53 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v5.69a.75.75 0 0 0 1.5 0v-5.69l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{isUnloading ? "Unloading..." : "Unload All"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="sticky top-0 bg-card z-10">
|
|
||||||
<tr class="text-left border-b border-gray-200 dark:border-white/10 bg-surface">
|
|
||||||
<th>{$showIdorNameStore === "id" ? "Model ID" : "Name"}</th>
|
|
||||||
<th></th>
|
|
||||||
<th>State</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each filteredModels.regularModels as model (model.id)}
|
|
||||||
<tr class="border-b hover:bg-secondary-hover border-gray-200">
|
|
||||||
<td class={model.unlisted ? "text-txtsecondary" : ""}>
|
|
||||||
<a href="/upstream/{model.id}/" class="font-semibold" target="_blank">
|
|
||||||
{getModelDisplay(model)}
|
|
||||||
</a>
|
|
||||||
{#if model.description}
|
|
||||||
<p class={model.unlisted ? "text-opacity-70" : ""}><em>{model.description}</em></p>
|
|
||||||
{/if}
|
|
||||||
{#if model.aliases && model.aliases.length > 0}
|
|
||||||
<p class="text-xs text-txtsecondary">Aliases: {model.aliases.join(", ")}</p>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
<td class="w-12">
|
|
||||||
{#if model.state === "stopped" && pendingLoads[model.id]}
|
|
||||||
<button class="btn btn--sm" onclick={() => cancelLoad(model.id)}>Cancel</button>
|
|
||||||
{:else if model.state === "stopped"}
|
|
||||||
<button class="btn btn--sm" onclick={() => handleLoadModel(model.id)}>Load</button>
|
|
||||||
{:else}
|
|
||||||
<button class="btn btn--sm" onclick={() => unloadSingleModel(model.id)} disabled={model.state !== "ready"}>Unload</button>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
<td class="w-20">
|
|
||||||
{#if model.state === "stopped" && pendingLoads[model.id]}
|
|
||||||
<span class="w-16 text-center status status--queued">queued</span>
|
|
||||||
{:else}
|
|
||||||
<span class="w-16 text-center status status--{model.state}">{model.state}</span>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{#if Object.keys(filteredModels.peerModelsByPeerId).length > 0}
|
|
||||||
<h3 class="mt-8 mb-2">Peer Models</h3>
|
|
||||||
{#each Object.entries(filteredModels.peerModelsByPeerId).sort(([a], [b]) => a.localeCompare(b)) as [peerId, peerModels] (peerId)}
|
|
||||||
<div class="mb-4">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="sticky top-0 bg-card z-10">
|
|
||||||
<tr class="text-left border-b border-gray-200 dark:border-white/10 bg-surface">
|
|
||||||
<th class="font-semibold">{peerId}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each peerModels as model (model.id)}
|
|
||||||
<tr class="border-b hover:bg-secondary-hover border-gray-200">
|
|
||||||
<td class="pl-8 {model.unlisted ? 'text-txtsecondary' : ''}">
|
|
||||||
<span>{model.id}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import { isDarkMode } from "../stores/theme";
|
import { isDarkMode } from "../stores/theme";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
@@ -143,6 +144,8 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card p-4 h-[300px]">
|
<Card.Root class="h-[300px] py-0">
|
||||||
|
<Card.Content class="h-full p-4">
|
||||||
<canvas bind:this={canvas}></canvas>
|
<canvas bind:this={canvas}></canvas>
|
||||||
</div>
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
<div
|
<div
|
||||||
role="separator"
|
role="separator"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="{handleClass} bg-primary hover:bg-success transition-colors rounded flex-shrink-0"
|
class="{handleClass} bg-primary hover:bg-success transition-colors rounded-md flex-shrink-0"
|
||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
ontouchstart={handleTouchStart}
|
ontouchstart={handleTouchStart}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
@@ -6,15 +8,7 @@
|
|||||||
let { content }: Props = $props();
|
let { content }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative group inline-block">
|
<Tooltip.Root>
|
||||||
<span class="cursor-help">ⓘ</span>
|
<Tooltip.Trigger class="cursor-help align-middle normal-case">ⓘ</Tooltip.Trigger>
|
||||||
<div
|
<Tooltip.Content>{content}</Tooltip.Content>
|
||||||
class="absolute top-full left-1/2 transform -translate-x-1/2 mt-2
|
</Tooltip.Root>
|
||||||
px-3 py-2 bg-gray-900 text-white text-sm rounded-md
|
|
||||||
opacity-0 group-hover:opacity-100 transition-opacity
|
|
||||||
duration-200 pointer-events-none whitespace-nowrap z-50 normal-case"
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-900"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { label, tooltip }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if tooltip}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger class="cursor-help border-b border-dotted border-current align-middle">
|
||||||
|
{label}
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>{tooltip}</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else}
|
||||||
|
{label}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
metadata: Record<string, string> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { metadata }: Props = $props();
|
||||||
|
|
||||||
|
let entries = $derived(Object.entries(metadata || {}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if entries.length > 0}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<span class="text-muted-foreground hover:text-foreground cursor-help">...</span>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content class="min-w-[12rem] max-w-[24rem] normal-case">
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<tbody>
|
||||||
|
{#each entries as [key, value]}
|
||||||
|
<tr class="border-b border-white/10 last:border-0">
|
||||||
|
<td class="py-1 pr-3 font-medium whitespace-nowrap text-primary">{key}</td>
|
||||||
|
<td class="py-1 break-all">{value}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
{:else}
|
||||||
|
<span class="text-muted-foreground">-</span>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hasCapture: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
onclick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { hasCapture, loading, onclick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if hasCapture}
|
||||||
|
<Button variant="outline" size="xs" {onclick} disabled={loading}>
|
||||||
|
{loading ? "..." : "View"}
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<span class="text-muted-foreground">-</span>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { metrics } from "../../stores/api";
|
||||||
|
import ActivityTable from "../ActivityTable.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { modelId }: Props = $props();
|
||||||
|
|
||||||
|
let modelMetrics = $derived(
|
||||||
|
[...$metrics].filter((m) => m.model === modelId).sort((a, b) => b.id - a.id)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActivityTable
|
||||||
|
metrics={modelMetrics}
|
||||||
|
storagePrefix="model-detail"
|
||||||
|
showModelColumn={false}
|
||||||
|
showPagination={true}
|
||||||
|
compact={true}
|
||||||
|
title="Recent Activity"
|
||||||
|
emptyMessage="No activity recorded for this model"
|
||||||
|
/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Model } from "../../lib/types";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
model: Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { model }: Props = $props();
|
||||||
|
|
||||||
|
const capabilityLabels: Record<string, string> = {
|
||||||
|
vision: "Vision",
|
||||||
|
audio_transcriptions: "Transcription",
|
||||||
|
audio_speech: "Speech",
|
||||||
|
image_generation: "Image Gen",
|
||||||
|
image_to_image: "Img→Img",
|
||||||
|
function_calling: "Function Calling",
|
||||||
|
reranker: "Reranker",
|
||||||
|
};
|
||||||
|
|
||||||
|
let capabilities = $derived.by(() => {
|
||||||
|
const caps = model?.capabilities ?? {};
|
||||||
|
return Object.entries(caps).filter(([, v]) => v);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root class="shrink-0 gap-0 overflow-hidden py-0">
|
||||||
|
<Card.Header class="border-b px-4 py-2">
|
||||||
|
<Card.Title class="text-sm font-semibold">Capabilities</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="p-3">
|
||||||
|
{#if capabilities.length === 0}
|
||||||
|
<span class="text-muted-foreground text-sm">No capabilities reported.</span>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
{#each capabilities as [key] (key)}
|
||||||
|
<span class="bg-muted text-muted-foreground rounded-md px-2 py-0.5 text-xs font-medium">
|
||||||
|
{capabilityLabels[key] ?? key}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { streamModelLog } from "../../stores/modelLogs";
|
||||||
|
import LogPanel from "../LogPanel.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { modelId }: Props = $props();
|
||||||
|
|
||||||
|
let logData = $state("");
|
||||||
|
$effect(() => {
|
||||||
|
const id = modelId;
|
||||||
|
if (!id) {
|
||||||
|
logData = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const store = streamModelLog(id);
|
||||||
|
const unsub = store.subscribe((v) => (logData = v));
|
||||||
|
return () => unsub();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-full">
|
||||||
|
<LogPanel id={`model-${modelId}`} title="Model Logs" {logData} />
|
||||||
|
</div>
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
import { transcribeAudio } from "../../lib/audioApi";
|
import { transcribeAudio } from "../../lib/audioApi";
|
||||||
import { playgroundStores } from "../../stores/playgroundActivity";
|
import { playgroundStores } from "../../stores/playgroundActivity";
|
||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Copy, Check } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-audio-model", "");
|
const selectedModelStore = persistentStore<string>("playground-audio-model", "");
|
||||||
|
|
||||||
@@ -150,14 +152,14 @@
|
|||||||
|
|
||||||
<!-- Empty state for no models configured -->
|
<!-- Empty state for no models configured -->
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="flex-1 flex items-center justify-center text-txtsecondary">
|
<div class="flex-1 flex items-center justify-center text-muted-foreground">
|
||||||
<p>No models configured. Add models to your configuration to transcribe audio.</p>
|
<p>No models configured. Add models to your configuration to transcribe audio.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- File upload / Result display area -->
|
<!-- File upload / Result display area -->
|
||||||
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-surface border border-gray-200 dark:border-white/10 rounded">
|
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded-md">
|
||||||
{#if isTranscribing}
|
{#if isTranscribing}
|
||||||
<div class="text-center text-txtsecondary">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
||||||
<p>Transcribing audio...</p>
|
<p>Transcribing audio...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,29 +171,26 @@
|
|||||||
{:else if transcriptionResult}
|
{:else if transcriptionResult}
|
||||||
<div class="w-full h-full flex flex-col p-4">
|
<div class="w-full h-full flex flex-col p-4">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<h3 class="font-medium">Transcription Result</h3>
|
<h3 class="pb-0 font-medium">Transcription Result</h3>
|
||||||
<button
|
<Button
|
||||||
class="btn btn-sm"
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
onclick={copyToClipboard}
|
onclick={copyToClipboard}
|
||||||
title={copied ? 'Copied!' : 'Copy to clipboard'}
|
title={copied ? 'Copied!' : 'Copy to clipboard'}
|
||||||
>
|
>
|
||||||
{#if copied}
|
{#if copied}
|
||||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Check class="text-success" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
{:else}
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Copy />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto p-3 rounded border border-gray-200 dark:border-white/10 bg-background whitespace-pre-wrap">
|
<div class="flex-1 overflow-auto p-3 rounded-md border border-border bg-background whitespace-pre-wrap">
|
||||||
{transcriptionResult}
|
{transcriptionResult}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedFile}
|
{:else if selectedFile}
|
||||||
<div class="text-center text-txtsecondary p-4">
|
<div class="text-center text-muted-foreground p-4">
|
||||||
<p class="font-medium mb-2">File Selected</p>
|
<p class="font-medium mb-2">File Selected</p>
|
||||||
<p class="text-sm">{selectedFile.name}</p>
|
<p class="text-sm">{selectedFile.name}</p>
|
||||||
<p class="text-xs mt-1">{formatFileSize(selectedFile.size)}</p>
|
<p class="text-xs mt-1">{formatFileSize(selectedFile.size)}</p>
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
<div
|
<div
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Audio file drop zone"
|
aria-label="Audio file drop zone"
|
||||||
class="w-full h-full flex items-center justify-center text-center text-txtsecondary p-8 {isDragging ? 'bg-primary/10' : ''}"
|
class="w-full h-full flex items-center justify-center text-center text-muted-foreground p-8 {isDragging ? 'bg-primary/10' : ''}"
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
ondragleave={handleDragLeave}
|
ondragleave={handleDragLeave}
|
||||||
ondrop={handleDrop}
|
ondrop={handleDrop}
|
||||||
@@ -223,33 +222,21 @@
|
|||||||
onchange={handleFileSelect}
|
onchange={handleFileSelect}
|
||||||
bind:this={fileInput}
|
bind:this={fileInput}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button variant="outline" onclick={() => fileInput?.click()} disabled={isTranscribing}>
|
||||||
class="btn"
|
|
||||||
onclick={() => fileInput?.click()}
|
|
||||||
disabled={isTranscribing}
|
|
||||||
>
|
|
||||||
Browse Files
|
Browse Files
|
||||||
</button>
|
</Button>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
{#if isTranscribing}
|
{#if isTranscribing}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white" onclick={cancelTranscription}>
|
<Button variant="destructive" onclick={cancelTranscription}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button onclick={transcribe} disabled={!canTranscribe}>Transcribe</Button>
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90"
|
<Button
|
||||||
onclick={transcribe}
|
variant="outline"
|
||||||
disabled={!canTranscribe}
|
|
||||||
>
|
|
||||||
Transcribe
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
onclick={clearAll}
|
onclick={clearAll}
|
||||||
disabled={!selectedFile && !transcriptionResult && !error}
|
disabled={!selectedFile && !transcriptionResult && !error}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
import ChatMessageComponent from "./ChatMessage.svelte";
|
import ChatMessageComponent from "./ChatMessage.svelte";
|
||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
||||||
|
import { Settings, Paperclip } from "@lucide/svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||||
|
import { X } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-selected-model", "");
|
const selectedModelStore = persistentStore<string>("playground-selected-model", "");
|
||||||
const systemPromptStore = persistentStore<string>("playground-system-prompt", "");
|
const systemPromptStore = persistentStore<string>("playground-system-prompt", "");
|
||||||
@@ -30,6 +38,7 @@
|
|||||||
let reasoningStartTime = $state<number>(0);
|
let reasoningStartTime = $state<number>(0);
|
||||||
let abortController = $state<AbortController | null>(null);
|
let abortController = $state<AbortController | null>(null);
|
||||||
let messagesContainer: HTMLDivElement | undefined = $state();
|
let messagesContainer: HTMLDivElement | undefined = $state();
|
||||||
|
let inputRef: HTMLTextAreaElement | null = $state(null);
|
||||||
let showSettings = $state(false);
|
let showSettings = $state(false);
|
||||||
let attachedImages = $state<string[]>([]);
|
let attachedImages = $state<string[]>([]);
|
||||||
let fileInput = $state<HTMLInputElement | null>(null);
|
let fileInput = $state<HTMLInputElement | null>(null);
|
||||||
@@ -42,6 +51,14 @@
|
|||||||
playgroundStores.chatStreaming.set(isStreaming);
|
playgroundStores.chatStreaming.set(isStreaming);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let wasStreaming = $state(false);
|
||||||
|
$effect(() => {
|
||||||
|
if (wasStreaming && !isStreaming) {
|
||||||
|
inputRef?.focus();
|
||||||
|
}
|
||||||
|
wasStreaming = isStreaming;
|
||||||
|
});
|
||||||
|
|
||||||
function handleMessagesScroll() {
|
function handleMessagesScroll() {
|
||||||
if (!messagesContainer) return;
|
if (!messagesContainer) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
||||||
@@ -303,96 +320,95 @@
|
|||||||
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a model..." disabled={isStreaming} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a model..." disabled={isStreaming} />
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<Button variant="outline" size="icon" onclick={() => (showSettings = true)} title="Settings">
|
||||||
class="btn"
|
<Settings />
|
||||||
onclick={() => (showSettings = !showSettings)}
|
</Button>
|
||||||
title="Settings"
|
<Button variant="outline" onclick={newChat} disabled={messages.length === 0 && !isStreaming}>
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M8.34 1.804A1 1 0 0 1 9.32 1h1.36a1 1 0 0 1 .98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 0 1 1.262.125l.962.962a1 1 0 0 1 .125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.295a1 1 0 0 1 .804.98v1.36a1 1 0 0 1-.804.98l-1.473.295a6.95 6.95 0 0 1-.587 1.416l.834 1.25a1 1 0 0 1-.125 1.262l-.962.962a1 1 0 0 1-1.262.125l-1.25-.834a6.953 6.953 0 0 1-1.416.587l-.295 1.473a1 1 0 0 1-.98.804H9.32a1 1 0 0 1-.98-.804l-.295-1.473a6.957 6.957 0 0 1-1.416-.587l-1.25.834a1 1 0 0 1-1.262-.125l-.962-.962a1 1 0 0 1-.125-1.262l.834-1.25a6.957 6.957 0 0 1-.587-1.416l-1.473-.295A1 1 0 0 1 1 10.68V9.32a1 1 0 0 1 .804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 0 1 .125-1.262l.962-.962A1 1 0 0 1 5.38 3.03l1.25.834a6.957 6.957 0 0 1 1.416-.587l.294-1.473ZM13 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn" onclick={newChat} disabled={messages.length === 0 && !isStreaming}>
|
|
||||||
New Chat
|
New Chat
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings panel -->
|
<!-- Settings dialog -->
|
||||||
{#if showSettings}
|
<Dialog.Root bind:open={showSettings}>
|
||||||
<div class="shrink-0 mb-4 p-4 bg-surface border border-gray-200 dark:border-white/10 rounded">
|
<Dialog.Content class="max-w-xl">
|
||||||
<div class="mb-4">
|
<Dialog.Header>
|
||||||
<label class="block text-sm font-medium mb-1" for="endpoint">Endpoint</label>
|
<Dialog.Title>Chat Settings</Dialog.Title>
|
||||||
<select
|
</Dialog.Header>
|
||||||
id="endpoint"
|
|
||||||
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary"
|
<div class="space-y-4">
|
||||||
bind:value={$endpointStore}
|
<div>
|
||||||
disabled={isStreaming}
|
<Label class="mb-1" for="endpoint">Endpoint</Label>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
value={$endpointStore}
|
||||||
|
onValueChange={(v) => v && endpointStore.set(v as Endpoint)}
|
||||||
>
|
>
|
||||||
<option value="v1/chat/completions">/v1/chat/completions</option>
|
<Select.Trigger class="w-full">/{$endpointStore}</Select.Trigger>
|
||||||
<option value="v1/messages">/v1/messages</option>
|
<Select.Content>
|
||||||
<option value="v1/responses">/v1/responses</option>
|
<Select.Item value="v1/chat/completions">/v1/chat/completions</Select.Item>
|
||||||
</select>
|
<Select.Item value="v1/messages">/v1/messages</Select.Item>
|
||||||
|
<Select.Item value="v1/responses">/v1/responses</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1" for="system-prompt">System Prompt</label>
|
<Label class="mb-1" for="system-prompt">System Prompt</Label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="system-prompt"
|
id="system-prompt"
|
||||||
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
class="resize-none"
|
||||||
placeholder="You are a helpful assistant..."
|
placeholder="You are a helpful assistant..."
|
||||||
rows="3"
|
rows={3}
|
||||||
bind:value={$systemPromptStore}
|
bind:value={$systemPromptStore}
|
||||||
disabled={isStreaming}
|
disabled={isStreaming}
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1" for="temperature">
|
<Label class="mb-1" for="temperature">
|
||||||
Temperature: {$temperatureStore.toFixed(2)}
|
Temperature: {$temperatureStore.toFixed(2)}
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<input
|
||||||
id="temperature"
|
id="temperature"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="2"
|
max="2"
|
||||||
step="0.05"
|
step="0.05"
|
||||||
class="w-full"
|
class="accent-primary w-full"
|
||||||
bind:value={$temperatureStore}
|
bind:value={$temperatureStore}
|
||||||
disabled={isStreaming}
|
disabled={isStreaming}
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-between text-xs text-txtsecondary mt-1">
|
<div class="text-muted-foreground mt-1 flex justify-between text-xs">
|
||||||
<span>Precise (0)</span>
|
<span>Precise (0)</span>
|
||||||
<span>Creative (2)</span>
|
<span>Creative (2)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1" for="max-tokens">Max Tokens</label>
|
<Label class="mb-1" for="max-tokens">Max Tokens</Label>
|
||||||
<input
|
<Input id="max-tokens" type="number" min="1" bind:value={$maxTokensStore} disabled={isStreaming} />
|
||||||
id="max-tokens"
|
<p class="text-muted-foreground mt-1 text-xs">Required for /v1/messages.</p>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary"
|
|
||||||
bind:value={$maxTokensStore}
|
|
||||||
disabled={isStreaming}
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-txtsecondary mt-1">Required for /v1/messages.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={() => (showSettings = false)}>Done</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
<!-- Empty state for no models configured -->
|
<!-- Empty state for no models configured -->
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="flex-1 flex items-center justify-center text-txtsecondary">
|
<div class="text-muted-foreground flex flex-1 items-center justify-center">
|
||||||
<p>No models configured. Add models to your configuration to start chatting.</p>
|
<p>No models configured. Add models to your configuration to start chatting.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Messages area -->
|
<!-- Messages area -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 overflow-y-auto mb-4 px-2"
|
class="mb-4 flex-1 overflow-y-auto px-2"
|
||||||
bind:this={messagesContainer}
|
bind:this={messagesContainer}
|
||||||
onscroll={handleMessagesScroll}
|
onscroll={handleMessagesScroll}
|
||||||
>
|
>
|
||||||
{#if messages.length === 0}
|
{#if messages.length === 0}
|
||||||
<div class="h-full flex items-center justify-center text-txtsecondary">
|
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||||
<p>Start a conversation by typing a message below.</p>
|
<p>Start a conversation by typing a message below.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -419,19 +435,21 @@
|
|||||||
{#if attachedImages.length > 0}
|
{#if attachedImages.length > 0}
|
||||||
<div class="mb-2 flex flex-wrap gap-2">
|
<div class="mb-2 flex flex-wrap gap-2">
|
||||||
{#each attachedImages as imageUrl, idx (idx)}
|
{#each attachedImages as imageUrl, idx (idx)}
|
||||||
<div class="relative group">
|
<div class="group relative">
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Attached image {idx + 1}"
|
alt="Attached image {idx + 1}"
|
||||||
class="w-20 h-20 object-cover rounded border border-gray-200 dark:border-white/10"
|
class="h-20 w-20 rounded-md border object-cover"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
variant="destructive"
|
||||||
|
size="icon-sm"
|
||||||
|
class="absolute -right-2 -top-2 h-6 w-6 rounded-full opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onclick={() => removeImage(idx)}
|
onclick={() => removeImage(idx)}
|
||||||
title="Remove image"
|
title="Remove image"
|
||||||
>
|
>
|
||||||
×
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -439,7 +457,7 @@
|
|||||||
|
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
{#if imageError}
|
{#if imageError}
|
||||||
<div class="mb-2 p-2 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded text-sm">
|
<div class="bg-destructive/10 text-destructive mb-2 rounded-md p-2 text-sm">
|
||||||
{imageError}
|
{imageError}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -456,6 +474,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ExpandableTextarea
|
<ExpandableTextarea
|
||||||
|
bind:ref={inputRef}
|
||||||
bind:value={userInput}
|
bind:value={userInput}
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -464,27 +483,23 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#if isStreaming}
|
{#if isStreaming}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white" onclick={cancelStreaming}>
|
<Button variant="destructive" onclick={cancelStreaming}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button
|
||||||
class="btn"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
onclick={() => fileInput?.click()}
|
onclick={() => fileInput?.click()}
|
||||||
disabled={isStreaming || !$selectedModelStore}
|
disabled={isStreaming || !$selectedModelStore}
|
||||||
title="Attach image"
|
title="Attach image"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
<Paperclip />
|
||||||
<path fill-rule="evenodd" d="M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.219a.75.75 0 0 0-1.06 0l-1.91 1.909.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.091a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
|
</Button>
|
||||||
</svg>
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90"
|
|
||||||
onclick={sendMessage}
|
onclick={sendMessage}
|
||||||
disabled={(!userInput.trim() && attachedImages.length === 0) || !$selectedModelStore}
|
disabled={(!userInput.trim() && attachedImages.length === 0) || !$selectedModelStore}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { renderMarkdown, escapeHtml, renderStreamingMarkdown, createStreamingCache } from "../../lib/markdown";
|
import { renderMarkdown, escapeHtml, renderStreamingMarkdown, createStreamingCache } from "../../lib/markdown";
|
||||||
import type { RenderedBlock } from "../../lib/markdown";
|
import type { RenderedBlock } from "../../lib/markdown";
|
||||||
import { Copy, Check, Pencil, X, Save, RefreshCw, ChevronDown, ChevronRight, Brain, Code } from "lucide-svelte";
|
import { Copy, Check, Pencil, X, Save, RefreshCw, ChevronDown, ChevronRight, Brain, Code } from "@lucide/svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
import { getTextContent, getImageUrls } from "../../lib/types";
|
import { getTextContent, getImageUrls } from "../../lib/types";
|
||||||
import type { ContentPart } from "../../lib/types";
|
import type { ContentPart } from "../../lib/types";
|
||||||
|
|
||||||
@@ -161,37 +163,37 @@
|
|||||||
|
|
||||||
<div class="flex {role === 'user' ? 'justify-end' : 'justify-start'} mb-4">
|
<div class="flex {role === 'user' ? 'justify-end' : 'justify-start'} mb-4">
|
||||||
<div
|
<div
|
||||||
class="relative group rounded-lg px-4 py-2 {role === 'user'
|
class="group relative rounded-lg px-4 py-2 {role === 'user'
|
||||||
? 'max-w-[85%] bg-primary text-btn-primary-text'
|
? 'bg-primary text-primary-foreground max-w-[85%]'
|
||||||
: 'w-full sm:w-4/5 bg-surface border border-gray-200 dark:border-white/10'}"
|
: 'bg-card w-full border sm:w-4/5'}"
|
||||||
>
|
>
|
||||||
{#if role === "assistant"}
|
{#if role === "assistant"}
|
||||||
{#if reasoning_content || isReasoning}
|
{#if reasoning_content || isReasoning}
|
||||||
<div class="mb-3 border border-gray-200 dark:border-white/10 rounded overflow-hidden">
|
<div class="mb-3 overflow-hidden rounded-md border">
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors text-sm"
|
class="bg-muted/50 hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors"
|
||||||
onclick={() => showReasoning = !showReasoning}
|
onclick={() => showReasoning = !showReasoning}
|
||||||
>
|
>
|
||||||
{#if showReasoning}
|
{#if showReasoning}
|
||||||
<ChevronDown class="w-4 h-4" />
|
<ChevronDown class="size-4" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight class="w-4 h-4" />
|
<ChevronRight class="size-4" />
|
||||||
{/if}
|
{/if}
|
||||||
<Brain class="w-4 h-4" />
|
<Brain class="size-4" />
|
||||||
<span class="font-medium">Reasoning</span>
|
<span class="font-medium">Reasoning</span>
|
||||||
<span class="text-txtsecondary ml-2">
|
<span class="text-muted-foreground ml-2">
|
||||||
({reasoning_content.length} chars{#if !isReasoning && reasoningTimeMs > 0}, {formatDuration(reasoningTimeMs)}{/if})
|
({reasoning_content.length} chars{#if !isReasoning && reasoningTimeMs > 0}, {formatDuration(reasoningTimeMs)}{/if})
|
||||||
</span>
|
</span>
|
||||||
{#if isReasoning}
|
{#if isReasoning}
|
||||||
<span class="ml-auto flex items-center gap-1 text-txtsecondary">
|
<span class="text-muted-foreground ml-auto flex items-center gap-1">
|
||||||
<span class="w-1.5 h-1.5 bg-primary rounded-full animate-pulse"></span>
|
<span class="bg-primary h-1.5 w-1.5 animate-pulse rounded-full"></span>
|
||||||
reasoning...
|
reasoning...
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if showReasoning}
|
{#if showReasoning}
|
||||||
<div class="px-3 py-2 bg-gray-50/50 dark:bg-white/[0.02] text-sm text-txtsecondary whitespace-pre-wrap font-mono">
|
<div class="bg-muted/30 text-muted-foreground whitespace-pre-wrap px-3 py-2 font-mono text-sm">
|
||||||
{reasoning_content}{#if isReasoning}<span class="inline-block w-1.5 h-4 bg-current animate-pulse ml-0.5"></span>{/if}
|
{reasoning_content}{#if isReasoning}<span class="ml-0.5 inline-block h-4 w-1.5 animate-pulse bg-current"></span>{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -201,12 +203,12 @@
|
|||||||
{#each imageUrls as imageUrl, idx (idx)}
|
{#each imageUrls as imageUrl, idx (idx)}
|
||||||
<button
|
<button
|
||||||
onclick={() => openModal(imageUrl)}
|
onclick={() => openModal(imageUrl)}
|
||||||
class="cursor-pointer rounded border border-gray-200 dark:border-white/10 hover:opacity-80 transition-opacity"
|
class="cursor-pointer rounded-md border transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Image {idx + 1}"
|
alt="Image {idx + 1}"
|
||||||
class="max-h-64 rounded"
|
class="max-h-64 rounded-md"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -226,60 +228,47 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !isStreaming}
|
{#if !isStreaming}
|
||||||
<div class="flex gap-1 mt-2 pt-1 border-t border-gray-200 dark:border-white/10">
|
<div class="mt-2 flex gap-1 border-t pt-1">
|
||||||
{#if onRegenerate}
|
{#if onRegenerate}
|
||||||
<button
|
<Button variant="ghost" size="icon-xs" class="text-muted-foreground" onclick={onRegenerate} title="Regenerate response">
|
||||||
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 text-txtsecondary"
|
<RefreshCw />
|
||||||
onclick={onRegenerate}
|
</Button>
|
||||||
title="Regenerate response"
|
|
||||||
>
|
|
||||||
<RefreshCw class="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<Button
|
||||||
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 text-txtsecondary"
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
class="text-muted-foreground"
|
||||||
onclick={copyToClipboard}
|
onclick={copyToClipboard}
|
||||||
title={copied ? "Copied!" : "Copy to clipboard"}
|
title={copied ? "Copied!" : "Copy to clipboard"}
|
||||||
>
|
>
|
||||||
{#if copied}
|
{#if copied}
|
||||||
<Check class="w-4 h-4 text-green-500" />
|
<Check class="text-success" />
|
||||||
{:else}
|
{:else}
|
||||||
<Copy class="w-4 h-4" />
|
<Copy />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 {showRaw ? 'text-primary' : 'text-txtsecondary'}"
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
class={showRaw ? "text-primary" : "text-muted-foreground"}
|
||||||
onclick={() => showRaw = !showRaw}
|
onclick={() => showRaw = !showRaw}
|
||||||
title={showRaw ? "Show rendered" : "Show raw"}
|
title={showRaw ? "Show rendered" : "Show raw"}
|
||||||
>
|
>
|
||||||
<Code class="w-4 h-4" />
|
<Code />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<div class="flex flex-col gap-2 min-w-[300px]">
|
<div class="flex min-w-[300px] flex-col gap-2">
|
||||||
<textarea
|
<Textarea class="resize-none" rows={3} bind:value={editContent} onkeydown={handleKeyDown} />
|
||||||
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface text-txtmain focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
|
||||||
rows="3"
|
|
||||||
bind:value={editContent}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
></textarea>
|
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button
|
<Button variant="ghost" size="icon-sm" onclick={cancelEdit} title="Cancel">
|
||||||
class="p-1.5 rounded hover:bg-white/20"
|
<X />
|
||||||
onclick={cancelEdit}
|
</Button>
|
||||||
title="Cancel"
|
<Button variant="ghost" size="icon-sm" onclick={saveEdit} title="Save">
|
||||||
>
|
<Save />
|
||||||
<X class="w-4 h-4" />
|
</Button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1.5 rounded hover:bg-white/20"
|
|
||||||
onclick={saveEdit}
|
|
||||||
title="Save"
|
|
||||||
>
|
|
||||||
<Save class="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -288,12 +277,12 @@
|
|||||||
{#each imageUrls as imageUrl, idx (idx)}
|
{#each imageUrls as imageUrl, idx (idx)}
|
||||||
<button
|
<button
|
||||||
onclick={() => openModal(imageUrl)}
|
onclick={() => openModal(imageUrl)}
|
||||||
class="cursor-pointer rounded border border-white/20 hover:opacity-80 transition-opacity"
|
class="cursor-pointer rounded-md border border-white/20 transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Image {idx + 1}"
|
alt="Image {idx + 1}"
|
||||||
class="max-w-[200px] rounded"
|
class="max-w-[200px] rounded-md"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -302,11 +291,11 @@
|
|||||||
<div class="whitespace-pre-wrap pr-8">{textContent}</div>
|
<div class="whitespace-pre-wrap pr-8">{textContent}</div>
|
||||||
{#if canEdit}
|
{#if canEdit}
|
||||||
<button
|
<button
|
||||||
class="absolute top-2 right-2 p-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity bg-white/20 hover:bg-white/30 shadow-sm"
|
class="absolute right-2 top-2 rounded-lg bg-white/20 p-1.5 opacity-0 shadow-sm transition-opacity hover:bg-white/30 group-hover:opacity-100"
|
||||||
onclick={startEdit}
|
onclick={startEdit}
|
||||||
title="Edit message"
|
title="Edit message"
|
||||||
>
|
>
|
||||||
<Pencil class="w-4 h-4" />
|
<Pencil class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -324,16 +313,16 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="absolute top-4 right-4 p-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
class="absolute right-4 top-4 rounded-lg bg-white/10 p-2 text-white transition-colors hover:bg-white/20"
|
||||||
onclick={() => closeModal()}
|
onclick={() => closeModal()}
|
||||||
title="Close"
|
title="Close"
|
||||||
>
|
>
|
||||||
<X class="w-6 h-6" />
|
<X class="size-6" />
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
src={modalImageUrl}
|
src={modalImageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
class="max-w-full max-h-full rounded pointer-events-none"
|
class="max-w-full max-h-full rounded-md pointer-events-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -341,8 +330,8 @@
|
|||||||
<style>
|
<style>
|
||||||
.prose :global(pre) {
|
.prose :global(pre) {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--color-surface);
|
background-color: var(--muted);
|
||||||
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2));
|
border: 1px solid var(--border);
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
padding-right: 2.5rem;
|
padding-right: 2.5rem;
|
||||||
@@ -359,20 +348,20 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border);
|
||||||
background: var(--color-surface);
|
background: var(--muted);
|
||||||
color: var(--color-txtsecondary);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(.code-copy-btn:hover) {
|
.prose :global(.code-copy-btn:hover) {
|
||||||
background: var(--color-secondary);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(.code-copy-btn.copied) {
|
.prose :global(.code-copy-btn.copied) {
|
||||||
color: var(--color-success);
|
color: var(--success);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,10 +376,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(code:not(pre code)) {
|
.prose :global(code:not(pre code)) {
|
||||||
background-color: var(--color-surface);
|
background-color: var(--muted);
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.125rem 0.25rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2));
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(p) {
|
.prose :global(p) {
|
||||||
@@ -431,14 +420,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(blockquote) {
|
.prose :global(blockquote) {
|
||||||
border-left: 3px solid var(--color-primary);
|
border-left: 3px solid var(--primary);
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(a) {
|
.prose :global(a) {
|
||||||
color: var(--color-primary);
|
color: var(--primary);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,13 +439,13 @@
|
|||||||
|
|
||||||
.prose :global(th),
|
.prose :global(th),
|
||||||
.prose :global(td) {
|
.prose :global(td) {
|
||||||
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2));
|
border: 1px solid var(--border);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose :global(th) {
|
.prose :global(th) {
|
||||||
background-color: var(--color-surface);
|
background-color: var(--muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
import { models } from "../../stores/api";
|
import { models } from "../../stores/api";
|
||||||
import { persistentStore } from "../../stores/persistent";
|
import { persistentStore } from "../../stores/persistent";
|
||||||
import { streamChatCompletion } from "../../lib/chatApi";
|
import { streamChatCompletion } from "../../lib/chatApi";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import { X } from "@lucide/svelte";
|
||||||
|
|
||||||
type Status = "waiting" | "streaming" | "done" | "error";
|
type Status = "waiting" | "streaming" | "done" | "error";
|
||||||
type Phase = "waiting" | "loading" | "reasoning" | "content";
|
type Phase = "waiting" | "loading" | "reasoning" | "content";
|
||||||
@@ -366,38 +370,37 @@
|
|||||||
<!-- Run controls -->
|
<!-- Run controls -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if isRunning}
|
{#if isRunning}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white border-red-500" onclick={stop}>
|
<Button variant="destructive" onclick={stop}>
|
||||||
<span class="inline-block w-3 h-3 bg-white align-middle mr-2"></span>Stop
|
<span class="mr-1 inline-block h-3 w-3 bg-current align-middle"></span>Stop
|
||||||
</button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90"
|
|
||||||
onclick={run}
|
onclick={run}
|
||||||
disabled={!canRun}
|
disabled={!canRun}
|
||||||
title={$testListStore.length === 0 ? "Add models from the list below" : "Run concurrent requests"}
|
title={$testListStore.length === 0 ? "Add models from the list below" : "Run concurrent requests"}
|
||||||
>
|
>
|
||||||
<span class="inline-block align-middle mr-2" aria-hidden="true">▶</span>Go
|
<span class="mr-1 inline-block align-middle" aria-hidden="true">▶</span>Go
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="btn btn--sm" onclick={clearAll} disabled={isRunning || $testListStore.length === 0}>
|
<Button variant="outline" size="sm" onclick={clearAll} disabled={isRunning || $testListStore.length === 0}>
|
||||||
Clear ({$testListStore.length})
|
Clear ({$testListStore.length})
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Available models -->
|
<!-- Available models -->
|
||||||
<div class="flex flex-col min-h-0 flex-1">
|
<div class="flex flex-col min-h-0 flex-1">
|
||||||
<div class="text-xs font-medium text-txtsecondary mb-1">
|
<div class="text-xs font-medium text-muted-foreground mb-1">
|
||||||
Models <span class="text-[10px] font-normal">— click to queue (add the same model more than once to test parallel requests)</span>
|
Models <span class="text-[10px] font-normal">— click to queue (add the same model more than once to test parallel requests)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 border border-gray-200 dark:border-white/10 rounded overflow-y-auto min-h-0">
|
<div class="flex-1 border border-border rounded-md overflow-y-auto min-h-0">
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="p-3 text-sm text-txtsecondary text-center">No models configured.</div>
|
<div class="p-3 text-sm text-muted-foreground text-center">No models configured.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-white/5">
|
<div class="divide-y divide-gray-100 dark:divide-white/5">
|
||||||
{#each availableModels as m (m.id)}
|
{#each availableModels as m (m.id)}
|
||||||
<li>
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-left px-2 py-1.5 text-sm hover:bg-secondary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
type="button"
|
||||||
|
class="hover:bg-accent hover:text-foreground flex w-full items-center gap-1.5 px-2 py-1.5 text-left text-sm font-normal transition-colors disabled:pointer-events-none disabled:opacity-50"
|
||||||
onclick={() => addModel(m.id)}
|
onclick={() => addModel(m.id)}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
title="Add {m.id}"
|
title="Add {m.id}"
|
||||||
@@ -405,38 +408,39 @@
|
|||||||
<span class="text-primary" aria-hidden="true">+</span>
|
<span class="text-primary" aria-hidden="true">+</span>
|
||||||
<span class="truncate flex-1">{m.id}</span>
|
<span class="truncate flex-1">{m.id}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div class="flex flex-col gap-2 border-t border-gray-200 dark:border-white/10 pt-3">
|
<div class="flex flex-col gap-2 border-t border-border pt-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<label for="concurrency-prompt" class="text-xs font-medium text-txtsecondary">Prompt</label>
|
<label for="concurrency-prompt" class="text-xs font-medium text-muted-foreground">Prompt</label>
|
||||||
<button
|
<Button
|
||||||
class="text-[10px] text-txtsecondary hover:text-txtmain underline"
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
class="h-auto p-0 text-[10px]"
|
||||||
onclick={resetDefaults}
|
onclick={resetDefaults}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
>
|
>
|
||||||
reset defaults
|
reset defaults
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<Textarea
|
||||||
id="concurrency-prompt"
|
id="concurrency-prompt"
|
||||||
class="w-full px-2 py-1.5 text-sm rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
class="resize-none text-sm"
|
||||||
rows="3"
|
rows={3}
|
||||||
bind:value={$promptStore}
|
bind:value={$promptStore}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
></textarea>
|
></Textarea>
|
||||||
<label for="concurrency-max-tokens" class="text-xs font-medium text-txtsecondary">max_tokens</label>
|
<label for="concurrency-max-tokens" class="text-xs font-medium text-muted-foreground">max_tokens</label>
|
||||||
<input
|
<Input
|
||||||
id="concurrency-max-tokens"
|
id="concurrency-max-tokens"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-full px-2 py-1.5 text-sm rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8 text-sm"
|
||||||
bind:value={$maxTokensStore}
|
bind:value={$maxTokensStore}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
/>
|
/>
|
||||||
@@ -447,8 +451,8 @@
|
|||||||
<div class="flex-1 min-w-0 min-h-0 overflow-y-auto">
|
<div class="flex-1 min-w-0 min-h-0 overflow-y-auto">
|
||||||
{#if $testListStore.length === 0}
|
{#if $testListStore.length === 0}
|
||||||
<div class="h-full flex items-center justify-center px-6">
|
<div class="h-full flex items-center justify-center px-6">
|
||||||
<div class="max-w-md text-sm text-txtsecondary space-y-4">
|
<div class="max-w-md text-sm text-muted-foreground space-y-4">
|
||||||
<h4 class="text-base font-semibold text-txtmain pb-0">Load Test</h4>
|
<h4 class="text-base font-semibold text-foreground pb-0">Load Test</h4>
|
||||||
<p>
|
<p>
|
||||||
Fire several streaming chat completions at llama-swap at the same time to see how it handles parallel
|
Fire several streaming chat completions at llama-swap at the same time to see how it handles parallel
|
||||||
loading and concurrent inference. Each request streams into its own panel with a live timer and status.
|
loading and concurrent inference. Each request streams into its own panel with a live timer and status.
|
||||||
@@ -456,16 +460,16 @@
|
|||||||
<ol class="list-decimal list-inside space-y-1">
|
<ol class="list-decimal list-inside space-y-1">
|
||||||
<li>Click models on the left to queue them — repeat a model to hit it with parallel requests.</li>
|
<li>Click models on the left to queue them — repeat a model to hit it with parallel requests.</li>
|
||||||
<li>Tweak the prompt and <code>max_tokens</code> if you want.</li>
|
<li>Tweak the prompt and <code>max_tokens</code> if you want.</li>
|
||||||
<li>Press <span class="font-semibold text-txtmain">Go</span> to launch them concurrently.</li>
|
<li>Press <span class="font-semibold text-foreground">Go</span> to launch them concurrently.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p class="text-xs">Tip: drag a result card's header to reorder, or hit × to drop it.</p>
|
<p class="text-xs">Tip: drag a result card's header to reorder, or hit × to drop it.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Gantt-style timeline -->
|
<!-- Gantt-style timeline -->
|
||||||
<div class="mb-3 border border-gray-200 dark:border-white/10 rounded">
|
<div class="mb-3 border border-border rounded-md">
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-txtsecondary hover:bg-secondary-hover transition-colors {$timelineCollapsedStore ? 'rounded' : 'rounded-t border-b border-gray-200 dark:border-white/10'}"
|
class="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent transition-colors {$timelineCollapsedStore ? 'rounded-md' : 'rounded-t border-b border-border'}"
|
||||||
onclick={() => timelineCollapsedStore.update((v) => !v)}
|
onclick={() => timelineCollapsedStore.update((v) => !v)}
|
||||||
aria-expanded={!$timelineCollapsedStore}
|
aria-expanded={!$timelineCollapsedStore}
|
||||||
>
|
>
|
||||||
@@ -480,7 +484,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Timeline</span>
|
<span>Timeline</span>
|
||||||
{#if !$timelineCollapsedStore}
|
{#if !$timelineCollapsedStore}
|
||||||
<span class="flex items-center gap-3 text-[10px] text-txtsecondary font-normal ml-3" aria-hidden="true">
|
<span class="flex items-center gap-3 text-[10px] text-muted-foreground font-normal ml-3" aria-hidden="true">
|
||||||
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-slate-200 dark:bg-white/10 border border-gray-300 dark:border-white/10"></span>waiting</span>
|
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-slate-200 dark:bg-white/10 border border-gray-300 dark:border-white/10"></span>waiting</span>
|
||||||
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-slate-400 dark:bg-slate-500"></span>loading</span>
|
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-slate-400 dark:bg-slate-500"></span>loading</span>
|
||||||
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-purple-500"></span>reasoning</span>
|
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-purple-500"></span>reasoning</span>
|
||||||
@@ -489,7 +493,7 @@
|
|||||||
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-red-500"></span>error</span>
|
<span class="flex items-center gap-1"><span class="inline-block w-2.5 h-2.5 rounded-sm bg-red-500"></span>error</span>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="ml-auto tabular-nums text-txtsecondary">
|
<span class="ml-auto tabular-nums text-muted-foreground">
|
||||||
max {formatElapsed(timelineMaxMs)} · {$testListStore.length} request{$testListStore.length === 1 ? "" : "s"}
|
max {formatElapsed(timelineMaxMs)} · {$testListStore.length} request{$testListStore.length === 1 ? "" : "s"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -498,13 +502,13 @@
|
|||||||
<!-- X axis ticks -->
|
<!-- X axis ticks -->
|
||||||
<div class="flex" aria-hidden="true">
|
<div class="flex" aria-hidden="true">
|
||||||
<div class="w-40 shrink-0"></div>
|
<div class="w-40 shrink-0"></div>
|
||||||
<div class="relative flex-1 h-4 border-b border-gray-200 dark:border-white/10">
|
<div class="relative flex-1 h-4 border-b border-border">
|
||||||
{#each timelineTicks as t (t)}
|
{#each timelineTicks as t (t)}
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 border-l border-gray-200 dark:border-white/10"
|
class="absolute top-0 bottom-0 border-l border-border"
|
||||||
style="left: {(t / timelineMaxMs) * 100}%;"
|
style="left: {(t / timelineMaxMs) * 100}%;"
|
||||||
>
|
>
|
||||||
<span class="absolute -top-0.5 left-1 text-[10px] text-txtsecondary tabular-nums">{formatTickMs(t)}</span>
|
<span class="absolute -top-0.5 left-1 text-[10px] text-muted-foreground tabular-nums">{formatTickMs(t)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -519,14 +523,14 @@
|
|||||||
{@const reasoningPct = run ? (run.reasoningMs / timelineMaxMs) * 100 : 0}
|
{@const reasoningPct = run ? (run.reasoningMs / timelineMaxMs) * 100 : 0}
|
||||||
{@const contentPct = run ? (run.contentMs / timelineMaxMs) * 100 : 0}
|
{@const contentPct = run ? (run.contentMs / timelineMaxMs) * 100 : 0}
|
||||||
<div class="flex items-center text-xs">
|
<div class="flex items-center text-xs">
|
||||||
<div class="w-40 shrink-0 flex items-center gap-1 pr-2 text-txtsecondary">
|
<div class="w-40 shrink-0 flex items-center gap-1 pr-2 text-muted-foreground">
|
||||||
<span class="tabular-nums w-5 text-right">{i + 1}.</span>
|
<span class="tabular-nums w-5 text-right">{i + 1}.</span>
|
||||||
<span class="truncate" title={entry.model}>{entry.model}</span>
|
<span class="truncate" title={entry.model}>{entry.model}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex-1 h-4">
|
<div class="relative flex-1 h-4">
|
||||||
{#each timelineTicks as t (t)}
|
{#each timelineTicks as t (t)}
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 border-l border-gray-100 dark:border-white/5"
|
class="absolute top-0 bottom-0 border-l border-border"
|
||||||
style="left: {(t / timelineMaxMs) * 100}%;"
|
style="left: {(t / timelineMaxMs) * 100}%;"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
></div>
|
||||||
@@ -560,7 +564,7 @@
|
|||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-16 shrink-0 pl-2 tabular-nums text-txtsecondary text-right">
|
<div class="w-16 shrink-0 pl-2 tabular-nums text-muted-foreground text-right">
|
||||||
{run ? formatElapsed(run.elapsedMs) : "—"}
|
{run ? formatElapsed(run.elapsedMs) : "—"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -574,16 +578,16 @@
|
|||||||
{@const run = runs[entry.id]}
|
{@const run = runs[entry.id]}
|
||||||
{@const status = run?.status ?? "waiting"}
|
{@const status = run?.status ?? "waiting"}
|
||||||
<div
|
<div
|
||||||
class="border rounded flex flex-col min-h-0 transition-colors {dragOverIndex === i && dragIndex !== i
|
class="border rounded-md flex flex-col min-h-0 transition-colors {dragOverIndex === i && dragIndex !== i
|
||||||
? 'border-primary ring-2 ring-primary/40'
|
? 'border-primary ring-2 ring-primary/40'
|
||||||
: 'border-gray-200 dark:border-white/10'} {dragIndex === i ? 'opacity-40' : ''}"
|
: 'border-border'} {dragIndex === i ? 'opacity-40' : ''}"
|
||||||
style="height: 280px;"
|
style="height: 280px;"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
ondragover={(e) => onDragOver(i, e)}
|
ondragover={(e) => onDragOver(i, e)}
|
||||||
ondrop={(e) => onDrop(i, e)}
|
ondrop={(e) => onDrop(i, e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="shrink-0 flex items-center gap-2 px-2 py-1.5 border-b border-gray-200 dark:border-white/10 bg-secondary/40 rounded-t"
|
class="shrink-0 flex items-center gap-2 px-2 py-1.5 border-b border-border bg-secondary/40 rounded-t"
|
||||||
draggable={!isRunning}
|
draggable={!isRunning}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -593,26 +597,28 @@
|
|||||||
class:cursor-grab={!isRunning}
|
class:cursor-grab={!isRunning}
|
||||||
title={isRunning ? "" : "Drag to reorder"}
|
title={isRunning ? "" : "Drag to reorder"}
|
||||||
>
|
>
|
||||||
<span class="text-txtsecondary select-none" aria-hidden="true">⋮⋮</span>
|
<span class="text-muted-foreground select-none" aria-hidden="true">⋮⋮</span>
|
||||||
<span class="text-txtsecondary tabular-nums text-xs w-5 text-right">{i + 1}.</span>
|
<span class="text-muted-foreground tabular-nums text-xs w-5 text-right">{i + 1}.</span>
|
||||||
<span class="flex-1 truncate text-sm font-medium" title={entry.model}>{entry.model}</span>
|
<span class="flex-1 truncate text-sm font-medium" title={entry.model}>{entry.model}</span>
|
||||||
<span class="text-xs tabular-nums text-txtsecondary">
|
<span class="text-xs tabular-nums text-muted-foreground">
|
||||||
{run ? formatElapsed(run.elapsedMs) : "—"}
|
{run ? formatElapsed(run.elapsedMs) : "—"}
|
||||||
</span>
|
</span>
|
||||||
<span class="status text-[10px] {statusBadgeClass(status)}">{status}</span>
|
<span class="status text-[10px] {statusBadgeClass(status)}">{status}</span>
|
||||||
<button
|
<Button
|
||||||
class="w-5 h-5 flex items-center justify-center text-txtsecondary hover:text-red-500 transition-colors rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="h-5 w-5 text-muted-foreground hover:text-red-500"
|
||||||
onclick={() => removeEntry(entry.id)}
|
onclick={() => removeEntry(entry.id)}
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
aria-label="Remove"
|
aria-label="Remove"
|
||||||
tabindex="-1"
|
tabindex={-1}
|
||||||
>
|
>
|
||||||
×
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto font-mono text-xs px-2 py-1.5">
|
<div class="flex-1 min-h-0 overflow-y-auto font-mono text-xs px-2 py-1.5">
|
||||||
{#if run?.loadingText}
|
{#if run?.loadingText}
|
||||||
<div class="bg-secondary/40 dark:bg-white/5 text-txtsecondary rounded px-2 py-1 mb-2 whitespace-pre-wrap">{run.loadingText.trim()}</div>
|
<div class="bg-secondary/40 dark:bg-white/5 text-muted-foreground rounded-md px-2 py-1 mb-2 whitespace-pre-wrap">{run.loadingText.trim()}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if run?.reasoningContent}
|
{#if run?.reasoningContent}
|
||||||
<div class="text-purple-700 dark:text-purple-300 whitespace-pre-wrap">{run.reasoningContent}</div>
|
<div class="text-purple-700 dark:text-purple-300 whitespace-pre-wrap">{run.reasoningContent}</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import { Maximize2, X } from "lucide-svelte";
|
import { Maximize2, X } from "@lucide/svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
|
ref?: HTMLTextAreaElement | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -12,6 +15,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
|
ref = $bindable(null),
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
rows = 3,
|
rows = 3,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -52,69 +56,55 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-1 relative group flex items-stretch min-h-0">
|
<div class="group relative flex min-h-0 flex-1 items-stretch">
|
||||||
<textarea
|
<Textarea
|
||||||
class="w-full px-3 py-2 pr-10 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary resize-none"
|
class="resize-none pr-10"
|
||||||
|
bind:ref
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{rows}
|
{rows}
|
||||||
bind:value
|
bind:value
|
||||||
{onkeydown}
|
{onkeydown}
|
||||||
{disabled}
|
{disabled}
|
||||||
></textarea>
|
/>
|
||||||
<button
|
<Button
|
||||||
class="absolute top-2 right-2 p-1.5 rounded-lg opacity-60 md:opacity-0 group-hover:opacity-100 transition-opacity bg-surface/90 hover:bg-surface border border-gray-200 dark:border-white/10 shadow-sm"
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
class="absolute right-2 top-2 opacity-60 transition-opacity group-hover:opacity-100 md:opacity-0"
|
||||||
onclick={openExpanded}
|
onclick={openExpanded}
|
||||||
title="Expand to edit"
|
title="Expand to edit"
|
||||||
type="button"
|
type="button"
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
<Maximize2 class="w-4 h-4" />
|
<Maximize2 />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||||
<div class="w-full max-w-4xl h-[80vh] flex flex-col bg-surface rounded-lg shadow-xl border border-gray-200 dark:border-white/10">
|
<div class="bg-card flex h-[80vh] w-full max-w-4xl flex-col rounded-lg border shadow-xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-white/10">
|
<div class="flex items-center justify-between border-b p-4">
|
||||||
<h3 class="font-medium">Edit Text</h3>
|
<h3 class="pb-0 font-medium">Edit Text</h3>
|
||||||
<button
|
<Button variant="ghost" size="icon-sm" onclick={closeExpanded} title="Close" type="button">
|
||||||
class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10"
|
<X />
|
||||||
onclick={closeExpanded}
|
</Button>
|
||||||
title="Close"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X class="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Textarea -->
|
<!-- Textarea -->
|
||||||
<div class="flex-1 p-4">
|
<div class="flex-1 p-4">
|
||||||
<textarea
|
<Textarea
|
||||||
bind:this={expandedTextarea}
|
bind:ref={expandedTextarea}
|
||||||
class="w-full h-full px-4 py-3 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
class="h-full resize-none"
|
||||||
placeholder={placeholder}
|
{placeholder}
|
||||||
bind:value={expandedValue}
|
bind:value={expandedValue}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="flex justify-end gap-2 p-4 border-t border-gray-200 dark:border-white/10">
|
<div class="flex justify-end gap-2 border-t p-4">
|
||||||
<button
|
<Button variant="outline" onclick={closeExpanded} type="button">Cancel</Button>
|
||||||
class="btn"
|
<Button onclick={saveExpanded} type="button">Done</Button>
|
||||||
onclick={closeExpanded}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90"
|
|
||||||
onclick={saveExpanded}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
||||||
import type { ImageApiMode, SdApiLora, SdApiLoraRef } from "../../lib/types";
|
import type { ImageApiMode, SdApiLora, SdApiLoraRef } from "../../lib/types";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import { Download, X } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-image-model", "");
|
const selectedModelStore = persistentStore<string>("playground-image-model", "");
|
||||||
const selectedSizeStore = persistentStore<string>("playground-image-size", "1024x1024");
|
const selectedSizeStore = persistentStore<string>("playground-image-size", "1024x1024");
|
||||||
@@ -61,18 +66,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLora(event: Event) {
|
|
||||||
const select = event.target as HTMLSelectElement;
|
|
||||||
const path = select.value;
|
|
||||||
if (!path) return;
|
|
||||||
|
|
||||||
const lora = availableLoras.find((l) => l.path === path);
|
|
||||||
if (lora && !selectedLoras.some((l) => l.path === path)) {
|
|
||||||
selectedLoras = [...selectedLoras, { path: lora.path, multiplier: 1.0 }];
|
|
||||||
}
|
|
||||||
select.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeLora(path: string) {
|
function removeLora(path: string) {
|
||||||
selectedLoras = selectedLoras.filter((l) => l.path !== path);
|
selectedLoras = selectedLoras.filter((l) => l.path !== path);
|
||||||
}
|
}
|
||||||
@@ -195,65 +188,73 @@
|
|||||||
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select an image model..." disabled={isGenerating} capabilities={["image_generation", "image_to_image"]} matchAny={true} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select an image model..." disabled={isGenerating} capabilities={["image_generation", "image_to_image"]} matchAny={true} />
|
||||||
|
|
||||||
<select
|
<Select.Root
|
||||||
class="px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$apiModeStore}
|
value={$apiModeStore}
|
||||||
disabled={isGenerating}
|
onValueChange={(v) => v && apiModeStore.set(v as ImageApiMode)}
|
||||||
>
|
>
|
||||||
<option value="openai">OpenAI</option>
|
<Select.Trigger class="h-9 w-32">{$apiModeStore}</Select.Trigger>
|
||||||
<option value="sdapi">SDAPI</option>
|
<Select.Content>
|
||||||
</select>
|
<Select.Item value="openai">OpenAI</Select.Item>
|
||||||
|
<Select.Item value="sdapi">SDAPI</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
|
||||||
<select
|
<Select.Root
|
||||||
class="px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$selectedSizeStore}
|
value={$selectedSizeStore}
|
||||||
disabled={isGenerating}
|
onValueChange={(v) => v && selectedSizeStore.set(v)}
|
||||||
>
|
>
|
||||||
<optgroup label="Square">
|
<Select.Trigger class="h-9 w-40">{$selectedSizeStore}</Select.Trigger>
|
||||||
<option value="512x512">512x512</option>
|
<Select.Content>
|
||||||
<option value="1024x1024">1024x1024</option>
|
<Select.Group>
|
||||||
</optgroup>
|
<Select.Label>Square</Select.Label>
|
||||||
<optgroup label="Landscape">
|
<Select.Item value="512x512">512x512</Select.Item>
|
||||||
<option value="1024x768">1024x768 (4:3)</option>
|
<Select.Item value="1024x1024">1024x1024</Select.Item>
|
||||||
<option value="1280x720">1280x720 (16:9)</option>
|
</Select.Group>
|
||||||
<option value="1792x1024">1792x1024 (SDXL)</option>
|
<Select.Separator />
|
||||||
</optgroup>
|
<Select.Group>
|
||||||
<optgroup label="Portrait">
|
<Select.Label>Landscape</Select.Label>
|
||||||
<option value="768x1024">768x1024 (3:4)</option>
|
<Select.Item value="1024x768">1024x768 (4:3)</Select.Item>
|
||||||
<option value="720x1280">720x1280 (9:16)</option>
|
<Select.Item value="1280x720">1280x720 (16:9)</Select.Item>
|
||||||
<option value="1024x1792">1024x1792 (SDXL)</option>
|
<Select.Item value="1792x1024">1792x1024 (SDXL)</Select.Item>
|
||||||
</optgroup>
|
</Select.Group>
|
||||||
</select>
|
<Select.Separator />
|
||||||
|
<Select.Group>
|
||||||
|
<Select.Label>Portrait</Select.Label>
|
||||||
|
<Select.Item value="768x1024">768x1024 (3:4)</Select.Item>
|
||||||
|
<Select.Item value="720x1280">720x1280 (9:16)</Select.Item>
|
||||||
|
<Select.Item value="1024x1792">1024x1792 (SDXL)</Select.Item>
|
||||||
|
</Select.Group>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
|
||||||
{#if isSdapi}
|
{#if isSdapi}
|
||||||
<button
|
<Button variant="outline" onclick={() => showSettings = !showSettings}>
|
||||||
class="px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface hover:bg-secondary-hover transition-colors"
|
|
||||||
onclick={() => showSettings = !showSettings}
|
|
||||||
>
|
|
||||||
{showSettings ? "Hide Settings" : "Settings"}
|
{showSettings ? "Hide Settings" : "Settings"}
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SDAPI Settings Panel -->
|
<!-- SDAPI Settings Panel -->
|
||||||
{#if isSdapi && showSettings}
|
{#if isSdapi && showSettings}
|
||||||
<div class="shrink-0 mb-4 p-4 rounded border border-gray-200 dark:border-white/10 bg-surface">
|
<div class="shrink-0 mb-4 p-4 rounded-md border border-border bg-background">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">Steps</span>
|
<span class="text-xs text-muted-foreground">Steps</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdStepsStore}
|
bind:value={$sdStepsStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="150"
|
max="150"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">CFG Scale</span>
|
<span class="text-xs text-muted-foreground">CFG Scale</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdCfgScaleStore}
|
bind:value={$sdCfgScaleStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="30"
|
max="30"
|
||||||
@@ -261,121 +262,141 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">Seed (-1 = random)</span>
|
<span class="text-xs text-muted-foreground">Seed (-1 = random)</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdSeedStore}
|
bind:value={$sdSeedStore}
|
||||||
min="-1"
|
min="-1"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">Batch Size</span>
|
<span class="text-xs text-muted-foreground">Batch Size</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="h-8"
|
||||||
bind:value={$sdBatchSizeStore}
|
bind:value={$sdBatchSizeStore}
|
||||||
min="1"
|
min="1"
|
||||||
max="8"
|
max="8"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">Sampler</span>
|
<span class="text-xs text-muted-foreground">Sampler</span>
|
||||||
<select
|
<Select.Root
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$sdSamplerStore}
|
value={$sdSamplerStore}
|
||||||
|
onValueChange={(v) => sdSamplerStore.set(v ?? "")}
|
||||||
>
|
>
|
||||||
<option value="">Default</option>
|
<Select.Trigger class="h-8">{$sdSamplerStore || "Default"}</Select.Trigger>
|
||||||
<option value="euler_a">euler_a</option>
|
<Select.Content>
|
||||||
<option value="euler">euler</option>
|
<Select.Item value="">Default</Select.Item>
|
||||||
<option value="heun">heun</option>
|
<Select.Item value="euler_a">euler_a</Select.Item>
|
||||||
<option value="dpm2">dpm2</option>
|
<Select.Item value="euler">euler</Select.Item>
|
||||||
<option value="dpmpp2s_a">dpmpp2s_a</option>
|
<Select.Item value="heun">heun</Select.Item>
|
||||||
<option value="dpmpp2m">dpmpp2m</option>
|
<Select.Item value="dpm2">dpm2</Select.Item>
|
||||||
<option value="dpmpp2mv2">dpmpp2mv2</option>
|
<Select.Item value="dpmpp2s_a">dpmpp2s_a</Select.Item>
|
||||||
<option value="ipndm">ipndm</option>
|
<Select.Item value="dpmpp2m">dpmpp2m</Select.Item>
|
||||||
<option value="ipndm_v">ipndm_v</option>
|
<Select.Item value="dpmpp2mv2">dpmpp2mv2</Select.Item>
|
||||||
<option value="lcm">lcm</option>
|
<Select.Item value="ipndm">ipndm</Select.Item>
|
||||||
<option value="ddim_trailing">ddim_trailing</option>
|
<Select.Item value="ipndm_v">ipndm_v</Select.Item>
|
||||||
<option value="tcd">tcd</option>
|
<Select.Item value="lcm">lcm</Select.Item>
|
||||||
</select>
|
<Select.Item value="ddim_trailing">ddim_trailing</Select.Item>
|
||||||
|
<Select.Item value="tcd">tcd</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="text-xs text-txtsecondary">Scheduler</span>
|
<span class="text-xs text-muted-foreground">Scheduler</span>
|
||||||
<select
|
<Select.Root
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value={$sdSchedulerStore}
|
value={$sdSchedulerStore}
|
||||||
|
onValueChange={(v) => sdSchedulerStore.set(v ?? "")}
|
||||||
>
|
>
|
||||||
<option value="">Auto for model</option>
|
<Select.Trigger class="h-8">{$sdSchedulerStore || "Auto for model"}</Select.Trigger>
|
||||||
<option value="discrete">discrete</option>
|
<Select.Content>
|
||||||
<option value="karras">karras</option>
|
<Select.Item value="">Auto for model</Select.Item>
|
||||||
<option value="exponential">exponential</option>
|
<Select.Item value="discrete">discrete</Select.Item>
|
||||||
<option value="ays">ays</option>
|
<Select.Item value="karras">karras</Select.Item>
|
||||||
<option value="gits">gits</option>
|
<Select.Item value="exponential">exponential</Select.Item>
|
||||||
</select>
|
<Select.Item value="ays">ays</Select.Item>
|
||||||
|
<Select.Item value="gits">gits</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="flex flex-col gap-1 mb-3">
|
<label class="flex flex-col gap-1 mb-3">
|
||||||
<span class="text-xs text-txtsecondary">Negative Prompt</span>
|
<span class="text-xs text-muted-foreground">Negative Prompt</span>
|
||||||
<textarea
|
<Textarea
|
||||||
class="px-2 py-1 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary resize-y text-sm"
|
|
||||||
bind:value={$sdNegativePromptStore}
|
bind:value={$sdNegativePromptStore}
|
||||||
rows="2"
|
rows={2}
|
||||||
placeholder="Elements to avoid..."
|
placeholder="Elements to avoid..."
|
||||||
></textarea>
|
></Textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- LoRA Selection -->
|
<!-- LoRA Selection -->
|
||||||
<div>
|
<div>
|
||||||
<span class="text-xs text-txtsecondary block mb-1">LoRAs</span>
|
<span class="text-xs text-muted-foreground block mb-1">LoRAs</span>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<button
|
<Button
|
||||||
class="px-3 py-1.5 text-sm rounded border border-gray-200 dark:border-white/10 bg-surface hover:bg-secondary-hover transition-colors disabled:opacity-50"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onclick={loadLoras}
|
onclick={loadLoras}
|
||||||
disabled={!$selectedModelStore || isLoadingLoras}
|
disabled={!$selectedModelStore || isLoadingLoras}
|
||||||
>
|
>
|
||||||
{isLoadingLoras ? "Loading..." : lorasLoaded ? "Reload LoRAs" : "Load LoRAs"}
|
{isLoadingLoras ? "Loading..." : lorasLoaded ? "Reload LoRAs" : "Load LoRAs"}
|
||||||
</button>
|
</Button>
|
||||||
{#if lorasLoaded && availableLoras.length > 0}
|
{#if lorasLoaded && availableLoras.length > 0}
|
||||||
<select
|
<Select.Root
|
||||||
class="flex-1 px-2 py-1.5 text-sm rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
onchange={addLora}
|
value=""
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) {
|
||||||
|
const lora = availableLoras.find((l) => l.path === v);
|
||||||
|
if (lora && !selectedLoras.some((s) => s.path === v)) {
|
||||||
|
selectedLoras = [...selectedLoras, { path: lora.path, multiplier: 1.0 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Add a LoRA...</option>
|
<Select.Trigger class="h-8 flex-1">Add a LoRA...</Select.Trigger>
|
||||||
{#each availableLoras.filter((l) => !selectedLoras.some((s) => s.path === l.path)) as lora}
|
<Select.Content>
|
||||||
<option value={lora.path}>{lora.name}</option>
|
{#each availableLoras.filter((l) => !selectedLoras.some((s) => s.path === l.path)) as lora (lora.path)}
|
||||||
|
<Select.Item value={lora.path}>{lora.name}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if loraError}
|
{#if loraError}
|
||||||
<p class="text-xs text-red-500 mb-1">{loraError}</p>
|
<p class="text-xs text-red-500 mb-1">{loraError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if lorasLoaded && availableLoras.length === 0}
|
{#if lorasLoaded && availableLoras.length === 0}
|
||||||
<p class="text-xs text-txtsecondary">No LoRAs available</p>
|
<p class="text-xs text-muted-foreground">No LoRAs available</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if selectedLoras.length > 0}
|
{#if selectedLoras.length > 0}
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
{#each selectedLoras as lora}
|
{#each selectedLoras as lora}
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<span class="flex-1 truncate">{getLoraName(lora.path)}</span>
|
<span class="flex-1 truncate">{getLoraName(lora.path)}</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
class="w-20 px-1.5 py-1 text-xs rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-1 focus:ring-primary"
|
class="h-7 w-20 text-xs"
|
||||||
value={lora.multiplier}
|
value={lora.multiplier}
|
||||||
oninput={(e) => updateLoraMultiplier(lora.path, parseFloat((e.target as HTMLInputElement).value) || 1)}
|
oninput={(e) => updateLoraMultiplier(lora.path, parseFloat((e.target as HTMLInputElement).value) || 1)}
|
||||||
min="0"
|
min="0"
|
||||||
max="2"
|
max="2"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
class="px-1.5 py-0.5 text-xs rounded border border-gray-200 dark:border-white/10 hover:bg-red-500 hover:text-white hover:border-red-500 transition-colors"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 px-1.5 text-xs hover:bg-destructive hover:text-destructive-foreground"
|
||||||
onclick={() => removeLora(lora.path)}
|
onclick={() => removeLora(lora.path)}
|
||||||
aria-label="Remove LoRA"
|
aria-label="Remove LoRA"
|
||||||
>
|
>
|
||||||
x
|
<X class="size-3" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -386,14 +407,14 @@
|
|||||||
|
|
||||||
<!-- Empty state for no models configured -->
|
<!-- Empty state for no models configured -->
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="flex-1 flex items-center justify-center text-txtsecondary">
|
<div class="flex-1 flex items-center justify-center text-muted-foreground">
|
||||||
<p>No models configured. Add models to your configuration to generate images.</p>
|
<p>No models configured. Add models to your configuration to generate images.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Image display area -->
|
<!-- Image display area -->
|
||||||
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-surface border border-gray-200 dark:border-white/10 rounded">
|
<div class="flex-1 overflow-auto mb-4 flex items-center justify-center bg-background border border-border rounded-md">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<div class="text-center text-txtsecondary">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
||||||
<p>Generating image...</p>
|
<p>Generating image...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,7 +424,6 @@
|
|||||||
<p class="text-sm mt-1">{error}</p>
|
<p class="text-sm mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if generatedImages.length > 1}
|
{:else if generatedImages.length > 1}
|
||||||
<!-- Grid for multiple images (batch) -->
|
|
||||||
<div class="grid grid-cols-2 gap-2 p-2 w-full h-full overflow-auto">
|
<div class="grid grid-cols-2 gap-2 p-2 w-full h-full overflow-auto">
|
||||||
{#each generatedImages as img, i}
|
{#each generatedImages as img, i}
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
@@ -418,15 +438,15 @@
|
|||||||
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Button
|
||||||
class="absolute bottom-2 right-2 p-1.5 bg-black/60 hover:bg-black/80 text-white rounded-full transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute bottom-2 right-2 h-8 w-8 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={(e) => { e.stopPropagation(); downloadImage(i); }}
|
onclick={(e) => { e.stopPropagation(); downloadImage(i); }}
|
||||||
aria-label="Download image"
|
aria-label="Download image"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Download class="size-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
</Button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -443,18 +463,18 @@
|
|||||||
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
class="max-w-full max-h-full object-contain hover:opacity-90 transition-opacity"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Button
|
||||||
class="absolute bottom-2 right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-full transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute bottom-2 right-2 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={(e) => { e.stopPropagation(); downloadImage(0); }}
|
onclick={(e) => { e.stopPropagation(); downloadImage(0); }}
|
||||||
aria-label="Download image"
|
aria-label="Download image"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Download class="size-5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
</Button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center text-txtsecondary">
|
<div class="text-center text-muted-foreground">
|
||||||
<p>Enter a prompt below to generate an image</p>
|
<p>Enter a prompt below to generate an image</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -471,24 +491,25 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-row md:flex-col gap-2">
|
<div class="flex flex-row md:flex-col gap-2">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white flex-1 md:flex-none" onclick={cancelGeneration}>
|
<Button variant="destructive" class="flex-1 md:flex-none" onclick={cancelGeneration}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90 flex-1 md:flex-none"
|
class="flex-1 md:flex-none"
|
||||||
onclick={generate}
|
onclick={generate}
|
||||||
disabled={!prompt.trim() || !$selectedModelStore}
|
disabled={!prompt.trim() || !$selectedModelStore}
|
||||||
>
|
>
|
||||||
Generate
|
Generate
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
class="btn flex-1 md:flex-none"
|
variant="outline"
|
||||||
|
class="flex-1 md:flex-none"
|
||||||
onclick={clearImage}
|
onclick={clearImage}
|
||||||
disabled={generatedImages.length === 0 && !error && !prompt.trim()}
|
disabled={generatedImages.length === 0 && !error && !prompt.trim()}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,13 +526,15 @@
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
class="absolute top-4 right-4 text-white hover:text-gray-300 text-2xl w-10 h-10 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors"
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
class="absolute top-4 right-4 bg-black/60 hover:bg-black/80 text-white"
|
||||||
onclick={() => closeFullscreen()}
|
onclick={() => closeFullscreen()}
|
||||||
aria-label="Close fullscreen"
|
aria-label="Close fullscreen"
|
||||||
>
|
>
|
||||||
×
|
<X class="size-6" />
|
||||||
</button>
|
</Button>
|
||||||
<img
|
<img
|
||||||
src={generatedImages[fullscreenIndex]}
|
src={generatedImages[fullscreenIndex]}
|
||||||
alt="AI generated content"
|
alt="AI generated content"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { models } from "../../stores/api";
|
import { models } from "../../stores/api";
|
||||||
import { groupModels } from "../../lib/modelUtils";
|
import { groupModels } from "../../lib/modelUtils";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -18,42 +19,51 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasModels}
|
{#if hasModels}
|
||||||
<select
|
<Select.Root
|
||||||
class="min-w-0 flex-1 basis-48 px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
bind:value
|
{value}
|
||||||
|
onValueChange={(v) => v !== undefined && (value = v)}
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
<option value="">{placeholder}</option>
|
<Select.Trigger class="min-w-0 flex-1 basis-48">{value || placeholder}</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="">{placeholder}</Select.Item>
|
||||||
{#if hasMatching}
|
{#if hasMatching}
|
||||||
<optgroup label="Matching Capabilities">
|
<Select.Group>
|
||||||
|
<Select.Label>Matching Capabilities</Select.Label>
|
||||||
{#each grouped.localMatching as model (model.id)}
|
{#each grouped.localMatching as model (model.id)}
|
||||||
<option value={model.id}>{model.id}</option>
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
{#if model.aliases}
|
{#if model.aliases}
|
||||||
{#each model.aliases as alias (alias)}
|
{#each model.aliases as alias (alias)}
|
||||||
<option value={alias}> ↳ {alias}</option>
|
<Select.Item value={alias}>↳ {alias}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</optgroup>
|
</Select.Group>
|
||||||
|
<Select.Separator />
|
||||||
{/if}
|
{/if}
|
||||||
{#if grouped.local.length > 0}
|
{#if grouped.local.length > 0}
|
||||||
<optgroup label="Local">
|
<Select.Group>
|
||||||
|
<Select.Label>Local</Select.Label>
|
||||||
{#each grouped.local as model (model.id)}
|
{#each grouped.local as model (model.id)}
|
||||||
<option value={model.id}>{model.id}</option>
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
{#if model.aliases}
|
{#if model.aliases}
|
||||||
{#each model.aliases as alias (alias)}
|
{#each model.aliases as alias (alias)}
|
||||||
<option value={alias}> ↳ {alias}</option>
|
<Select.Item value={alias}>↳ {alias}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</optgroup>
|
</Select.Group>
|
||||||
|
<Select.Separator />
|
||||||
{/if}
|
{/if}
|
||||||
{#each Object.entries(grouped.peersByProvider).sort(([a], [b]) => a.localeCompare(b)) as [peerId, peerModels] (peerId)}
|
{#each Object.entries(grouped.peersByProvider).sort(([a], [b]) => a.localeCompare(b)) as [peerId, peerModels] (peerId)}
|
||||||
<optgroup label="Peer: {peerId}">
|
<Select.Group>
|
||||||
|
<Select.Label>Peer: {peerId}</Select.Label>
|
||||||
{#each peerModels as model (model.id)}
|
{#each peerModels as model (model.id)}
|
||||||
<option value={model.id}>{model.id}</option>
|
<Select.Item value={model.id}>{model.id}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</optgroup>
|
</Select.Group>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex items-center justify-center h-full">
|
||||||
<div class="text-center text-txtsecondary">
|
<div class="text-muted-foreground text-center">
|
||||||
<p class="text-lg">{featureName}</p>
|
<p class="text-lg">{featureName}</p>
|
||||||
<p class="text-sm mt-2">To be implemented</p>
|
<p class="text-sm mt-2">To be implemented</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
import { rerank } from "../../lib/rerankApi";
|
import { rerank } from "../../lib/rerankApi";
|
||||||
import { playgroundStores } from "../../stores/playgroundActivity";
|
import { playgroundStores } from "../../stores/playgroundActivity";
|
||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||||
|
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
|
||||||
|
|
||||||
type RerankRow = { doc: string; score: number | null };
|
type RerankRow = { doc: string; score: number | null };
|
||||||
type SortOrder = "none" | "asc" | "desc";
|
type SortOrder = "none" | "asc" | "desc";
|
||||||
@@ -234,9 +238,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scoreColor(score: number | null): string {
|
function scoreColor(score: number | null): string {
|
||||||
if (score === null) return "text-txtsecondary";
|
if (score === null) return "text-muted-foreground";
|
||||||
if (score > 0) return "text-green-600 dark:text-green-400";
|
if (score > 0) return "text-green-600 dark:text-green-400";
|
||||||
return "text-red-500 dark:text-red-400";
|
return "text-destructive";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatScore(score: number | null): string {
|
function formatScore(score: number | null): string {
|
||||||
@@ -266,9 +270,9 @@
|
|||||||
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a rerank model..." disabled={isLoading} capabilities={["reranker"]} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a rerank model..." disabled={isLoading} capabilities={["reranker"]} />
|
||||||
{#if editorMode === "table"}
|
{#if editorMode === "table"}
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
class="min-w-0 flex-1 basis-48 px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
class="min-w-0 flex-1 basis-48"
|
||||||
placeholder="Query..."
|
placeholder="Query..."
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -276,60 +280,50 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Table / JSON toggle -->
|
<!-- Table / JSON toggle -->
|
||||||
<div class="flex rounded border border-gray-200 dark:border-white/10 overflow-hidden shrink-0">
|
<ToggleGroup.Root
|
||||||
<button
|
type="single"
|
||||||
class="px-3 py-1.5 text-sm transition-colors {editorMode === 'table'
|
variant="outline"
|
||||||
? 'bg-primary text-btn-primary-text'
|
value={editorMode}
|
||||||
: 'bg-surface hover:bg-secondary-hover'}"
|
onValueChange={(v) => v && (v === "table" ? switchToTable() : switchToJson())}
|
||||||
onclick={switchToTable}
|
class="shrink-0"
|
||||||
disabled={isLoading}
|
|
||||||
>
|
>
|
||||||
Table
|
<ToggleGroup.Item value="table" disabled={isLoading}>Table</ToggleGroup.Item>
|
||||||
</button>
|
<ToggleGroup.Item value="json" disabled={isLoading}>JSON</ToggleGroup.Item>
|
||||||
<button
|
</ToggleGroup.Root>
|
||||||
class="px-3 py-1.5 text-sm border-l border-gray-200 dark:border-white/10 transition-colors {editorMode === 'json'
|
|
||||||
? 'bg-primary text-btn-primary-text'
|
|
||||||
: 'bg-surface hover:bg-secondary-hover'}"
|
|
||||||
onclick={switchToJson}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
JSON
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="flex-1 flex items-center justify-center text-txtsecondary">
|
<div class="text-muted-foreground flex flex-1 items-center justify-center">
|
||||||
<p>No models configured. Add models to your configuration to use reranking.</p>
|
<p>No models configured. Add models to your configuration to use reranking.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if editorMode === "json"}
|
{:else if editorMode === "json"}
|
||||||
<!-- JSON editor -->
|
<!-- JSON editor -->
|
||||||
<div class="flex-1 flex flex-col min-h-0 mb-4">
|
<div class="mb-4 flex min-h-0 flex-1 flex-col">
|
||||||
<textarea
|
<Textarea
|
||||||
class="flex-1 w-full font-mono text-sm px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
class="w-full flex-1 resize-none font-mono text-sm"
|
||||||
bind:value={jsonText}
|
bind:value={jsonText}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
placeholder={'{\n "query": "your search query",\n "documents": [\n "document one",\n "document two"\n ]\n}'}
|
placeholder={'{\n "query": "your search query",\n "documents": [\n "document one",\n "document two"\n ]\n}'}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
></textarea>
|
/>
|
||||||
{#if jsonError}
|
{#if jsonError}
|
||||||
<p class="mt-1 text-sm text-red-500">{jsonError}</p>
|
<p class="text-destructive mt-1 text-sm">{jsonError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Document table -->
|
<!-- Document table -->
|
||||||
<div class="flex-1 overflow-y-auto mb-4 border border-gray-200 dark:border-white/10 rounded">
|
<div class="mb-4 flex-1 overflow-y-auto rounded-lg border">
|
||||||
<table class="w-full border-collapse table-fixed">
|
<table class="w-full table-fixed border-collapse">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="w-auto" />
|
<col class="w-auto" />
|
||||||
<col style="width: 120px" />
|
<col style="width: 120px" />
|
||||||
<col style="width: 40px" />
|
<col style="width: 40px" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead class="sticky top-0 bg-surface border-b border-gray-200 dark:border-white/10">
|
<thead class="bg-card sticky top-0 border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2 text-left text-sm font-medium text-txtsecondary">Document</th>
|
<th class="text-muted-foreground px-3 py-2 text-left text-sm font-medium">Document</th>
|
||||||
<th
|
<th
|
||||||
class="px-3 py-2 text-right text-sm font-medium text-txtsecondary cursor-pointer select-none hover:text-txtprimary transition-colors"
|
class="text-muted-foreground hover:text-foreground cursor-pointer select-none px-3 py-2 text-right text-sm font-medium transition-colors"
|
||||||
onclick={cycleSortOrder}
|
onclick={cycleSortOrder}
|
||||||
>
|
>
|
||||||
Score{sortIndicator()}
|
Score{sortIndicator()}
|
||||||
@@ -339,11 +333,11 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each displayRows as { row, i } (i)}
|
{#each displayRows as { row, i } (i)}
|
||||||
<tr class="border-b border-gray-100 dark:border-white/5 last:border-0">
|
<tr class="border-b last:border-0">
|
||||||
<td class="px-3 py-1.5">
|
<td class="px-3 py-1.5">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full bg-transparent focus:outline-none focus:ring-1 focus:ring-primary rounded px-1 py-0.5"
|
class="border-0 focus-visible:ring-1 h-7 px-1 py-0.5 bg-transparent"
|
||||||
placeholder={i === rows.length - 1 ? "Add document..." : "Document text..."}
|
placeholder={i === rows.length - 1 ? "Add document..." : "Document text..."}
|
||||||
value={row.doc}
|
value={row.doc}
|
||||||
oninput={(e) => updateDoc(i, (e.target as HTMLInputElement).value)}
|
oninput={(e) => updateDoc(i, (e.target as HTMLInputElement).value)}
|
||||||
@@ -353,21 +347,23 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right font-mono text-sm {scoreColor(row.score)}">
|
<td class="px-3 py-1.5 text-right font-mono text-sm {scoreColor(row.score)}">
|
||||||
{#if isLoading && row.score === null && row.doc.trim() !== ""}
|
{#if isLoading && row.score === null && row.doc.trim() !== ""}
|
||||||
<span class="inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin align-middle"></span>
|
<span class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent align-middle"></span>
|
||||||
{:else}
|
{:else}
|
||||||
{formatScore(row.score)}
|
{formatScore(row.score)}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-1.5 text-center">
|
<td class="px-2 py-1.5 text-center">
|
||||||
<button
|
<Button
|
||||||
class="w-7 h-7 flex items-center justify-center text-txtsecondary hover:text-red-500 transition-colors rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
onclick={() => deleteRow(i)}
|
onclick={() => deleteRow(i)}
|
||||||
disabled={rows.length <= 1}
|
disabled={rows.length <= 1}
|
||||||
tabindex="-1"
|
tabindex={-1}
|
||||||
aria-label="Remove row"
|
aria-label="Remove row"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -378,28 +374,18 @@
|
|||||||
|
|
||||||
<!-- Bottom toolbar -->
|
<!-- Bottom toolbar -->
|
||||||
{#if hasModels}
|
{#if hasModels}
|
||||||
<div class="shrink-0 flex flex-wrap items-center gap-2">
|
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white" onclick={cancel}>
|
<Button variant="destructive" onclick={cancel}>Cancel</Button>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button onclick={submit} disabled={!canSubmit}>Rerank</Button>
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90"
|
<Button variant="outline" onclick={clear} disabled={isCleared}>Clear</Button>
|
||||||
onclick={submit}
|
|
||||||
disabled={!canSubmit}
|
|
||||||
>
|
|
||||||
Rerank
|
|
||||||
</button>
|
|
||||||
<button class="btn" onclick={clear} disabled={isCleared}>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<span class="text-sm text-red-500 ml-2">{error}</span>
|
<span class="text-destructive ml-2 text-sm">{error}</span>
|
||||||
{:else if usage}
|
{:else if usage}
|
||||||
<span class="text-sm text-txtsecondary ml-2">{usage.total_tokens} tokens</span>
|
<span class="text-muted-foreground ml-2 text-sm">{usage.total_tokens} tokens</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
import { playgroundStores } from "../../stores/playgroundActivity";
|
import { playgroundStores } from "../../stores/playgroundActivity";
|
||||||
import ModelSelector from "./ModelSelector.svelte";
|
import ModelSelector from "./ModelSelector.svelte";
|
||||||
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
import ExpandableTextarea from "./ExpandableTextarea.svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import { RefreshCw, Download } from "@lucide/svelte";
|
||||||
|
|
||||||
const selectedModelStore = persistentStore<string>("playground-speech-model", "");
|
const selectedModelStore = persistentStore<string>("playground-speech-model", "");
|
||||||
const selectedVoiceStore = persistentStore<string>("playground-speech-voice", "coral");
|
const selectedVoiceStore = persistentStore<string>("playground-speech-voice", "coral");
|
||||||
@@ -106,8 +109,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVoiceChange(event: Event) {
|
function handleVoiceChange(value: string) {
|
||||||
const value = (event.target as HTMLSelectElement).value;
|
|
||||||
if (value === "(refresh)") {
|
if (value === "(refresh)") {
|
||||||
refreshVoices();
|
refreshVoices();
|
||||||
} else {
|
} else {
|
||||||
@@ -208,49 +210,44 @@
|
|||||||
<div class="shrink-0 flex gap-2 mb-4">
|
<div class="shrink-0 flex gap-2 mb-4">
|
||||||
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a speech model..." disabled={isGenerating} capabilities={["audio_speech"]} />
|
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a speech model..." disabled={isGenerating} capabilities={["audio_speech"]} />
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select
|
<Select.Root
|
||||||
class="shrink-0 px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
|
type="single"
|
||||||
value={$selectedVoiceStore}
|
value={$selectedVoiceStore}
|
||||||
onchange={handleVoiceChange}
|
onValueChange={(v) => v && handleVoiceChange(v)}
|
||||||
disabled={isGenerating || isLoadingVoices || !$selectedModelStore}
|
|
||||||
>
|
>
|
||||||
|
<Select.Trigger class="h-9 w-40">{$selectedVoiceStore}</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
{#each availableVoices as voice (voice)}
|
{#each availableVoices as voice (voice)}
|
||||||
<option value={voice}>{voice}</option>
|
<Select.Item value={voice}>{voice}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
<option value="(refresh)">(refresh)</option>
|
<Select.Item value="(refresh)">(refresh)</Select.Item>
|
||||||
</select>
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{#if $selectedModelStore && !getVoicesCache()[$selectedModelStore]}
|
{#if $selectedModelStore && !getVoicesCache()[$selectedModelStore]}
|
||||||
<button
|
<Button
|
||||||
class="btn shrink-0"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="shrink-0"
|
||||||
onclick={refreshVoices}
|
onclick={refreshVoices}
|
||||||
disabled={isLoadingVoices}
|
disabled={isLoadingVoices}
|
||||||
title={isLoadingVoices ? "Loading voices..." : "Load voices for this model"}
|
title={isLoadingVoices ? "Loading voices..." : "Load voices for this model"}
|
||||||
>
|
>
|
||||||
{#if isLoadingVoices}
|
<RefreshCw class={isLoadingVoices ? "animate-spin" : ""} />
|
||||||
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
</Button>
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" 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"></path>
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state for no models configured -->
|
<!-- Empty state for no models configured -->
|
||||||
{#if !hasModels}
|
{#if !hasModels}
|
||||||
<div class="flex-1 flex items-center justify-center text-txtsecondary">
|
<div class="flex-1 flex items-center justify-center text-muted-foreground">
|
||||||
<p>No models configured. Add models to your configuration to generate speech.</p>
|
<p>No models configured. Add models to your configuration to generate speech.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Audio display area -->
|
<!-- Audio display area -->
|
||||||
<div class="shrink-0 mb-4 bg-surface border border-gray-200 dark:border-white/10 rounded p-4 md:p-6">
|
<div class="shrink-0 mb-4 bg-background border border-border rounded-md p-4 md:p-6">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<div class="flex items-center justify-center text-txtsecondary py-8">
|
<div class="flex items-center justify-center text-muted-foreground py-8">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
<div class="inline-block w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-2"></div>
|
||||||
<p>Generating speech...</p>
|
<p>Generating speech...</p>
|
||||||
@@ -267,7 +264,7 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<!-- Header with metadata and download -->
|
<!-- Header with metadata and download -->
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="flex flex-wrap gap-3 text-sm text-txtsecondary">
|
<div class="flex flex-wrap gap-3 text-sm text-muted-foreground">
|
||||||
{#if generatedVoice}
|
{#if generatedVoice}
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -285,15 +282,9 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button variant="outline" size="icon" class="shrink-0" onclick={downloadAudio} title="Download audio file">
|
||||||
class="btn shrink-0"
|
<Download />
|
||||||
onclick={downloadAudio}
|
</Button>
|
||||||
title="Download audio file"
|
|
||||||
>
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audio player with larger controls -->
|
<!-- Audio player with larger controls -->
|
||||||
@@ -305,7 +296,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center text-txtsecondary py-8">
|
<div class="flex items-center justify-center text-muted-foreground py-8">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<svg class="w-12 h-12 md:w-16 md:h-16 mx-auto mb-2 opacity-40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-12 h-12 md:w-16 md:h-16 mx-auto mb-2 opacity-40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
||||||
@@ -327,24 +318,25 @@
|
|||||||
/>
|
/>
|
||||||
<div class="shrink-0 flex md:flex-col gap-2">
|
<div class="shrink-0 flex md:flex-col gap-2">
|
||||||
{#if isGenerating}
|
{#if isGenerating}
|
||||||
<button class="btn bg-red-500 hover:bg-red-600 text-white flex-1 md:flex-none" onclick={cancelGeneration}>
|
<Button variant="destructive" class="flex-1 md:flex-none" onclick={cancelGeneration}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<Button
|
||||||
class="btn bg-primary text-btn-primary-text hover:opacity-90 flex-1 md:flex-none"
|
class="flex-1 md:flex-none"
|
||||||
onclick={generate}
|
onclick={generate}
|
||||||
disabled={!inputText.trim() || !$selectedModelStore}
|
disabled={!inputText.trim() || !$selectedModelStore}
|
||||||
>
|
>
|
||||||
Generate
|
Generate
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
class="btn flex-1 md:flex-none"
|
variant="outline"
|
||||||
|
class="flex-1 md:flex-none"
|
||||||
onclick={clearInput}
|
onclick={clearInput}
|
||||||
disabled={!inputText.trim()}
|
disabled={!inputText.trim()}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</Button>
|
||||||
<label class="flex items-center justify-center gap-2 text-sm cursor-pointer">
|
<label class="flex items-center justify-center gap-2 text-sm cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
+154
-136
@@ -1,178 +1,196 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
@import "katex/dist/katex.min.css";
|
@import "katex/dist/katex.min.css";
|
||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
|
||||||
|
|
||||||
@theme {
|
@custom-variant dark (&:is(.dark *));
|
||||||
--color-background: rgba(252, 252, 249, 1);
|
|
||||||
--color-surface: rgba(255, 255, 253, 1);
|
|
||||||
|
|
||||||
/* text colors */
|
:root {
|
||||||
--color-txtmain: rgba(19, 52, 59, 1);
|
--radius: 0;
|
||||||
--color-txtsecondary: rgba(98, 108, 113, 1);
|
|
||||||
--color-navlink-active: rgba(245, 245, 245, 1);
|
|
||||||
|
|
||||||
--color-primary: rgba(50, 184, 198, 1);
|
/* shadcn base palette (zinc) */
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
|
||||||
--color-primary-hover: rgba(29, 116, 128, 1);
|
/* brand accent: llama-swap teal */
|
||||||
--color-primary-active: rgba(26, 104, 115, 1);
|
--primary: rgb(50 184 198);
|
||||||
--color-secondary: rgba(94, 82, 64, 0.12);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--color-secondary-hover: rgba(94, 82, 64, 0.2);
|
|
||||||
--color-secondary-active: rgba(94, 82, 64, 0.25);
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
--color-border: rgba(94, 82, 64, 0.3);
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--color-btn-primary-text: rgba(252, 252, 249, 1);
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
--color-card-border: rgba(94, 82, 64, 0.12);
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
--color-card-border-inner: rgba(94, 82, 64, 0.12);
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
--color-error: rgba(192, 21, 47, 1);
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--color-success: rgba(33, 128, 141, 1);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--color-warning: rgb(244, 155, 0);
|
--border: oklch(0.92 0.004 286.32);
|
||||||
--color-info: rgba(98, 108, 113, 1);
|
--input: oklch(0.92 0.004 286.32);
|
||||||
--color-focus-ring: rgba(33, 128, 141, 0.4);
|
--ring: rgb(50 184 198);
|
||||||
--color-select-caret: rgba(19, 52, 59, 0.8);
|
|
||||||
--color-btn-border: rgba(94, 82, 64, 0.7);
|
--chart-1: rgb(50 184 198);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: rgb(50 184 198);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: rgb(50 184 198);
|
||||||
|
|
||||||
|
/* semantic status colors (shared light/dark-aware below) */
|
||||||
|
--success: oklch(0.6 0.118 184.704);
|
||||||
|
--warning: oklch(0.769 0.17 70.08);
|
||||||
|
--info: oklch(0.552 0.016 285.938);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer theme {
|
.dark {
|
||||||
/* over ride theme for dark mode */
|
--background: oklch(0.141 0.005 285.823);
|
||||||
[data-theme="dark"] {
|
--foreground: oklch(0.985 0 0);
|
||||||
--color-background: rgba(31, 33, 33, 1);
|
--card: oklch(0.21 0.006 285.885);
|
||||||
--color-surface: rgba(38, 40, 40, 1);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
/* text colors */
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
--color-txtmain: rgba(245, 245, 245, 1);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--color-txtsecondary: rgba(167, 169, 169, 0.7);
|
|
||||||
|
|
||||||
--color-navlink-active: rgba(245, 245, 245, 1);
|
/* brand accent: deeper teal for dark surfaces */
|
||||||
|
--primary: rgb(45 166 178);
|
||||||
|
--primary-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
|
||||||
--color-primary: rgba(33, 128, 141, 1);
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
--color-primary-hover: rgba(45, 166, 178, 1);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--color-primary-active: rgba(41, 150, 161, 1);
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
--color-secondary: rgba(119, 124, 124, 0.15);
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
--color-secondary-hover: rgba(119, 124, 124, 0.25);
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
--color-secondary-active: rgba(119, 124, 124, 0.3);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--color-border: rgba(119, 124, 124, 0.3);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--color-error: rgba(255, 84, 89, 1);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--color-success: rgba(50, 184, 198, 1);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--color-warning: rgb(244, 155, 0);
|
--ring: rgb(50 184 198);
|
||||||
--color-info: rgba(167, 169, 169, 1);
|
|
||||||
--color-focus-ring: rgba(50, 184, 198, 0.4);
|
--chart-1: rgb(50 184 198);
|
||||||
--color-btn-primary-text: rgba(19, 52, 59, 1);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--color-card-border: rgba(119, 124, 124, 0.2);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--color-card-border-inner: rgba(119, 124, 124, 0.15);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--button-border-secondary: rgba(119, 124, 124, 0.2);
|
|
||||||
}
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: rgb(50 184 198);
|
||||||
|
--sidebar-primary-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: rgb(50 184 198);
|
||||||
|
|
||||||
|
--success: oklch(0.696 0.17 162.48);
|
||||||
|
--warning: oklch(0.769 0.17 70.08);
|
||||||
|
--info: oklch(0.705 0.015 286.067);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--color-success: var(--success);
|
||||||
|
--color-warning: var(--warning);
|
||||||
|
--color-info: var(--info);
|
||||||
|
--color-error: var(--destructive);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* example of how colors using theme colors*/
|
@apply bg-background text-foreground;
|
||||||
@apply bg-background text-txtmain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@apply text-4xl text-txtmain font-bold pb-4;
|
@apply text-3xl font-bold tracking-tight pb-4;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
@apply text-3xl text-txtmain font-bold pb-4;
|
@apply text-2xl font-bold tracking-tight pb-4;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
@apply text-2xl text-txtmain font-bold pb-4;
|
@apply text-xl font-semibold tracking-tight pb-4;
|
||||||
}
|
}
|
||||||
h4 {
|
h4 {
|
||||||
@apply text-xl text-txtmain font-bold pb-4;
|
@apply text-lg font-semibold pb-4;
|
||||||
}
|
}
|
||||||
h5 {
|
h5 {
|
||||||
@apply text-lg text-txtmain font-bold pb-4;
|
@apply text-base font-semibold pb-4;
|
||||||
}
|
}
|
||||||
h6 {
|
h6 {
|
||||||
@apply text-base text-txtmain font-bold pb-4;
|
@apply text-sm font-semibold pb-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* define CSS classes here for specific types of components */
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.container {
|
/* default padding for ad-hoc tables (header/detail views) */
|
||||||
@apply px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
table th {
|
table th {
|
||||||
@apply p-2 font-semibold;
|
@apply p-2 font-semibold;
|
||||||
}
|
}
|
||||||
table td {
|
table td {
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation Header */
|
|
||||||
|
|
||||||
.navlink {
|
|
||||||
@apply text-txtsecondary hover:bg-secondary hover:text-txtmain rounded-lg p-2;
|
|
||||||
}
|
|
||||||
.navlink.active {
|
|
||||||
@apply bg-primary text-navlink-active;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card component */
|
|
||||||
.card {
|
|
||||||
@apply bg-surface rounded-lg border border-card-border shadow-sm overflow-hidden p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
@apply shadow-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__body {
|
|
||||||
@apply p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__header,
|
|
||||||
.card__footer {
|
|
||||||
@apply p-4 border-b border-card-border-inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status Badges */
|
|
||||||
.status {
|
|
||||||
@apply inline-block px-2 py-1 text-xs font-medium rounded-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--ready {
|
|
||||||
@apply bg-success/10 text-success;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--starting,
|
|
||||||
.status--stopping,
|
|
||||||
.status--queued {
|
|
||||||
@apply bg-warning/10 text-warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--stopped {
|
|
||||||
@apply bg-error/10 text-error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
@apply bg-surface py-2 px-4 text-sm rounded-md border transition-colors duration-200 border-btn-border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn--sm {
|
|
||||||
@apply px-2 py-0.5 text-xs;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
@apply opacity-50 cursor-not-allowed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@utility activity-link {
|
||||||
.ml-2 {
|
background: linear-gradient(90deg, #6366f1, #8b5cf6, #a855f7, #8b5cf6, #6366f1);
|
||||||
margin-left: 0.5rem;
|
background-size: 200% 100%;
|
||||||
}
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradient-shift 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.my-8 {
|
@keyframes gradient-shift {
|
||||||
margin-top: 2rem;
|
0% {
|
||||||
margin-bottom: 2rem;
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
|
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||||
|
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
|
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||||
|
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn(
|
||||||
|
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-content"
|
||||||
|
class={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
size = "default",
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: CollapsiblePrimitive.ContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
class={className}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</CollapsiblePrimitive.Content>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: CollapsiblePrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
class={className}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</CollapsiblePrimitive.Trigger>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: CollapsiblePrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="collapsible"
|
||||||
|
class={className}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</CollapsiblePrimitive.Root>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Root from "./collapsible.svelte";
|
||||||
|
import Trigger from "./collapsible-trigger.svelte";
|
||||||
|
import Content from "./collapsible-content.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
//
|
||||||
|
Root as Collapsible,
|
||||||
|
Trigger as CollapsibleTrigger,
|
||||||
|
Content as CollapsibleContent,
|
||||||
|
};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
type RowData,
|
||||||
|
type TableOptions,
|
||||||
|
type TableOptionsResolved,
|
||||||
|
type TableState,
|
||||||
|
type Updater,
|
||||||
|
createTable,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive TanStack table object for Svelte.
|
||||||
|
* @param options Table options to create the table with.
|
||||||
|
* @returns A reactive table object.
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* <script>
|
||||||
|
* const table = createSvelteTable({ ... })
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* <table>
|
||||||
|
* <thead>
|
||||||
|
* {#each table.getHeaderGroups() as headerGroup}
|
||||||
|
* <tr>
|
||||||
|
* {#each headerGroup.headers as header}
|
||||||
|
* <th colspan={header.colSpan}>
|
||||||
|
* <FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||||
|
* </th>
|
||||||
|
* {/each}
|
||||||
|
* </tr>
|
||||||
|
* {/each}
|
||||||
|
* </thead>
|
||||||
|
* <!-- ... -->
|
||||||
|
* </table>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createSvelteTable<TData extends RowData>(options: TableOptions<TData>) {
|
||||||
|
const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(
|
||||||
|
{
|
||||||
|
state: {},
|
||||||
|
onStateChange() {},
|
||||||
|
renderFallbackValue: null,
|
||||||
|
mergeOptions: (
|
||||||
|
defaultOptions: TableOptions<TData>,
|
||||||
|
options: Partial<TableOptions<TData>>
|
||||||
|
) => {
|
||||||
|
return mergeObjects(defaultOptions, options);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = createTable(resolvedOptions);
|
||||||
|
let state = $state<TableState>(table.initialState);
|
||||||
|
|
||||||
|
function updateOptions() {
|
||||||
|
table.setOptions(() => {
|
||||||
|
return mergeObjects(resolvedOptions, options, {
|
||||||
|
state: mergeObjects(state, options.state || {}),
|
||||||
|
|
||||||
|
onStateChange: (updater: Updater<TableState>) => {
|
||||||
|
if (updater instanceof Function) state = updater(state);
|
||||||
|
else state = mergeObjects(state, updater);
|
||||||
|
|
||||||
|
options.onStateChange?.(updater);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions();
|
||||||
|
|
||||||
|
$effect.pre(() => {
|
||||||
|
updateOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaybeThunk<T extends object> = T | (() => T | null | undefined);
|
||||||
|
type Intersection<T extends readonly unknown[]> = (T extends [infer H, ...infer R]
|
||||||
|
? H & Intersection<R>
|
||||||
|
: unknown) & {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily merges several objects (or thunks) while preserving
|
||||||
|
* getter semantics from every source.
|
||||||
|
*
|
||||||
|
* Proxy-based to avoid known WebKit recursion issue.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function mergeObjects<Sources extends readonly MaybeThunk<any>[]>(
|
||||||
|
...sources: Sources
|
||||||
|
): Intersection<{ [K in keyof Sources]: Sources[K] }> {
|
||||||
|
const resolve = <T extends object>(src: MaybeThunk<T>): T | undefined =>
|
||||||
|
typeof src === "function" ? (src() ?? undefined) : src;
|
||||||
|
|
||||||
|
const findSourceWithKey = (key: PropertyKey) => {
|
||||||
|
for (let i = sources.length - 1; i >= 0; i--) {
|
||||||
|
const obj = resolve(sources[i]);
|
||||||
|
if (obj && key in obj) return obj;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Proxy(Object.create(null), {
|
||||||
|
get(_, key) {
|
||||||
|
const src = findSourceWithKey(key);
|
||||||
|
|
||||||
|
return src?.[key as never];
|
||||||
|
},
|
||||||
|
|
||||||
|
has(_, key) {
|
||||||
|
return !!findSourceWithKey(key);
|
||||||
|
},
|
||||||
|
|
||||||
|
ownKeys(): (string | symbol)[] {
|
||||||
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||||
|
const all = new Set<string | symbol>();
|
||||||
|
for (const s of sources) {
|
||||||
|
const obj = resolve(s);
|
||||||
|
if (obj) {
|
||||||
|
for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) {
|
||||||
|
all.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...all];
|
||||||
|
},
|
||||||
|
|
||||||
|
getOwnPropertyDescriptor(_, key) {
|
||||||
|
const src = findSourceWithKey(key);
|
||||||
|
if (!src) return undefined;
|
||||||
|
return {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
value: (src as any)[key],
|
||||||
|
writable: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}) as Intersection<{ [K in keyof Sources]: Sources[K] }>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script
|
||||||
|
lang="ts"
|
||||||
|
generics="TData, TValue, TContext extends HeaderContext<TData, TValue> | CellContext<TData, TValue>"
|
||||||
|
>
|
||||||
|
import type { CellContext, ColumnDefTemplate, HeaderContext } from "@tanstack/table-core";
|
||||||
|
import { RenderComponentConfig, RenderSnippetConfig } from "./render-helpers.js";
|
||||||
|
import type { Attachment } from "svelte/attachments";
|
||||||
|
type Props = {
|
||||||
|
/** The cell or header field of the current cell's column definition. */
|
||||||
|
content?: TContext extends HeaderContext<TData, TValue>
|
||||||
|
? ColumnDefTemplate<HeaderContext<TData, TValue>>
|
||||||
|
: TContext extends CellContext<TData, TValue>
|
||||||
|
? ColumnDefTemplate<CellContext<TData, TValue>>
|
||||||
|
: never;
|
||||||
|
/** The result of the `getContext()` function of the header or cell */
|
||||||
|
context: TContext;
|
||||||
|
|
||||||
|
/** Used to pass attachments that can't be gotten through context */
|
||||||
|
attach?: Attachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { content, context, attach }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if typeof content === "string"}
|
||||||
|
{content}
|
||||||
|
{:else if content instanceof Function}
|
||||||
|
<!-- It's unlikely that a CellContext will be passed to a Header -->
|
||||||
|
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
||||||
|
{@const result = content(context as any)}
|
||||||
|
{#if result instanceof RenderComponentConfig}
|
||||||
|
{@const { component: Component, props } = result}
|
||||||
|
<Component {...props} {attach} />
|
||||||
|
{:else if result instanceof RenderSnippetConfig}
|
||||||
|
{@const { snippet, params } = result}
|
||||||
|
{@render snippet({ ...params, attach })}
|
||||||
|
{:else}
|
||||||
|
{result}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as FlexRender } from "./flex-render.svelte";
|
||||||
|
export { renderComponent, renderSnippet } from "./render-helpers.js";
|
||||||
|
export { createSvelteTable } from "./data-table.svelte.js";
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { Component, ComponentProps, Snippet } from "svelte";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to make it easy to identify Svelte components in
|
||||||
|
* `columnDef.cell` and `columnDef.header` properties.
|
||||||
|
*
|
||||||
|
* > NOTE: This class should only be used internally by the adapter. If you're
|
||||||
|
* reading this and you don't know what this is for, you probably don't need it.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* {@const result = content(context as any)}
|
||||||
|
* {#if result instanceof RenderComponentConfig}
|
||||||
|
* {@const { component: Component, props } = result}
|
||||||
|
* <Component {...props} />
|
||||||
|
* {/if}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class RenderComponentConfig<TComponent extends Component> {
|
||||||
|
component: TComponent;
|
||||||
|
props: ComponentProps<TComponent> | Record<string, never>;
|
||||||
|
constructor(
|
||||||
|
component: TComponent,
|
||||||
|
props: ComponentProps<TComponent> | Record<string, never> = {}
|
||||||
|
) {
|
||||||
|
this.component = component;
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties.
|
||||||
|
*
|
||||||
|
* > NOTE: This class should only be used internally by the adapter. If you're
|
||||||
|
* reading this and you don't know what this is for, you probably don't need it.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* {@const result = content(context as any)}
|
||||||
|
* {#if result instanceof RenderSnippetConfig}
|
||||||
|
* {@const { snippet, params } = result}
|
||||||
|
* {@render snippet(params)}
|
||||||
|
* {/if}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class RenderSnippetConfig<TProps> {
|
||||||
|
snippet: Snippet<[TProps]>;
|
||||||
|
params: TProps;
|
||||||
|
constructor(snippet: Snippet<[TProps]>, params: TProps) {
|
||||||
|
this.snippet = snippet;
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties.
|
||||||
|
*
|
||||||
|
* This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets.
|
||||||
|
*
|
||||||
|
* @param component A Svelte component
|
||||||
|
* @param props The props to pass to `component`
|
||||||
|
* @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // +page.svelte
|
||||||
|
* const defaultColumns = [
|
||||||
|
* columnHelper.accessor('name', {
|
||||||
|
* header: header => renderComponent(SortHeader, { label: 'Name', header }),
|
||||||
|
* }),
|
||||||
|
* columnHelper.accessor('state', {
|
||||||
|
* header: header => renderComponent(SortHeader, { label: 'State', header }),
|
||||||
|
* }),
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
* @see {@link https://tanstack.com/table/latest/docs/guide/column-defs}
|
||||||
|
*/
|
||||||
|
export function renderComponent<
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
T extends Component<any>,
|
||||||
|
Props extends ComponentProps<T>,
|
||||||
|
>(component: T, props: Props = {} as Props) {
|
||||||
|
return new RenderComponentConfig(component, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties.
|
||||||
|
*
|
||||||
|
* The snippet must only take one parameter.
|
||||||
|
*
|
||||||
|
* This is only to be used with Snippets - use `renderComponent` for Svelte Components.
|
||||||
|
*
|
||||||
|
* @param snippet
|
||||||
|
* @param params
|
||||||
|
* @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // +page.svelte
|
||||||
|
* const defaultColumns = [
|
||||||
|
* columnHelper.accessor('name', {
|
||||||
|
* cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }),
|
||||||
|
* }),
|
||||||
|
* columnHelper.accessor('state', {
|
||||||
|
* cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }),
|
||||||
|
* }),
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
* @see {@link https://tanstack.com/table/latest/docs/guide/column-defs}
|
||||||
|
*/
|
||||||
|
export function renderSnippet<TProps>(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) {
|
||||||
|
return new RenderSnippetConfig(snippet, params);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
type = "button",
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.CloseProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import DialogPortal from "./dialog-portal.svelte";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import * as Dialog from "./index.js";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||||
|
children: Snippet;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPortal {...portalProps}>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
{#if showCloseButton}
|
||||||
|
<DialogPrimitive.Close data-slot="dialog-close">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="ghost" class="absolute top-2 right-2" size="icon-sm" {...props}>
|
||||||
|
<XIcon />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
{/if}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-description"
|
||||||
|
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
showCloseButton = false,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
class={cn("bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
{#if showCloseButton}
|
||||||
|
<DialogPrimitive.Close>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" {...props}>Close</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dialog-header"
|
||||||
|
class={cn("gap-2 flex flex-col", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Portal {...restProps} />
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-title"
|
||||||
|
class={cn("text-base leading-none font-medium", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
type = "button",
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Root bind:open {...restProps} />
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Root from "./dialog.svelte";
|
||||||
|
import Portal from "./dialog-portal.svelte";
|
||||||
|
import Title from "./dialog-title.svelte";
|
||||||
|
import Footer from "./dialog-footer.svelte";
|
||||||
|
import Header from "./dialog-header.svelte";
|
||||||
|
import Overlay from "./dialog-overlay.svelte";
|
||||||
|
import Content from "./dialog-content.svelte";
|
||||||
|
import Description from "./dialog-description.svelte";
|
||||||
|
import Trigger from "./dialog-trigger.svelte";
|
||||||
|
import Close from "./dialog-close.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Dialog,
|
||||||
|
Title as DialogTitle,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Footer as DialogFooter,
|
||||||
|
Header as DialogHeader,
|
||||||
|
Trigger as DialogTrigger,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Description as DialogDescription,
|
||||||
|
Close as DialogClose,
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable([]),
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxGroup
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="dropdown-menu-checkbox-group"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||||
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<span
|
||||||
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||||
|
>
|
||||||
|
{#if indeterminate}
|
||||||
|
<MinusIcon />
|
||||||
|
{:else if checked}
|
||||||
|
<CheckIcon />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
sideOffset = 4,
|
||||||
|
align = "start",
|
||||||
|
portalProps,
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ContentProps & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPortal {...portalProps}>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
{sideOffset}
|
||||||
|
{align}
|
||||||
|
class={cn(
|
||||||
|
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPortal>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-group-heading"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<span
|
||||||
|
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||||
|
data-slot="dropdown-menu-radio-item-indicator"
|
||||||
|
>
|
||||||
|
{#if checked}
|
||||||
|
<CheckIcon />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronRightIcon class="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import Root from "./dropdown-menu.svelte";
|
||||||
|
import Sub from "./dropdown-menu-sub.svelte";
|
||||||
|
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
|
||||||
|
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||||
|
import Content from "./dropdown-menu-content.svelte";
|
||||||
|
import Group from "./dropdown-menu-group.svelte";
|
||||||
|
import Item from "./dropdown-menu-item.svelte";
|
||||||
|
import Label from "./dropdown-menu-label.svelte";
|
||||||
|
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||||
|
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||||
|
import Separator from "./dropdown-menu-separator.svelte";
|
||||||
|
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||||
|
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||||
|
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||||
|
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||||
|
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||||
|
import Portal from "./dropdown-menu-portal.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
CheckboxGroup,
|
||||||
|
CheckboxItem,
|
||||||
|
Content,
|
||||||
|
Portal,
|
||||||
|
Root as DropdownMenu,
|
||||||
|
CheckboxGroup as DropdownMenuCheckboxGroup,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Portal as DropdownMenuPortal,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
GroupHeading as DropdownMenuGroupHeading,
|
||||||
|
Group,
|
||||||
|
GroupHeading,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
RadioGroup,
|
||||||
|
RadioItem,
|
||||||
|
Root,
|
||||||
|
Separator,
|
||||||
|
Shortcut,
|
||||||
|
Sub,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
Trigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, "type"> &
|
||||||
|
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
"data-slot": dataSlot = "input",
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === "file"}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label as LabelPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: LabelPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="label"
|
||||||
|
class={cn(
|
||||||
|
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Pane } from "paneforge";
|
||||||
|
import Handle from "./resizable-handle.svelte";
|
||||||
|
import PaneGroup from "./resizable-pane-group.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
PaneGroup,
|
||||||
|
Pane,
|
||||||
|
Handle,
|
||||||
|
//
|
||||||
|
PaneGroup as ResizablePaneGroup,
|
||||||
|
Pane as ResizablePane,
|
||||||
|
Handle as ResizableHandle,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as ResizablePrimitive from "paneforge";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
withHandle = false,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<ResizablePrimitive.PaneResizerProps> & {
|
||||||
|
withHandle?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ResizablePrimitive.PaneResizer
|
||||||
|
bind:ref
|
||||||
|
data-slot="resizable-handle"
|
||||||
|
class={cn(
|
||||||
|
"cn-resizable-handle bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[direction=vertical]:h-px data-[direction=vertical]:w-full data-[direction=vertical]:after:left-0 data-[direction=vertical]:after:h-1 data-[direction=vertical]:after:w-full data-[direction=vertical]:after:translate-x-0 data-[direction=vertical]:after:-translate-y-1/2 [&[data-direction=vertical]>div]:rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if withHandle}
|
||||||
|
<div class="bg-border h-6 w-1 rounded-lg z-10 flex shrink-0"></div>
|
||||||
|
{/if}
|
||||||
|
</ResizablePrimitive.PaneResizer>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as ResizablePrimitive from "paneforge";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
this: paneGroup = $bindable(),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: ResizablePrimitive.PaneGroupProps & {
|
||||||
|
this?: ResizablePrimitive.PaneGroup;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ResizablePrimitive.PaneGroup
|
||||||
|
bind:ref
|
||||||
|
bind:this={paneGroup}
|
||||||
|
data-slot="resizable-pane-group"
|
||||||
|
class={cn(
|
||||||
|
"cn-resizable-panel-group flex h-full w-full data-[direction=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Scrollbar from "./scroll-area-scrollbar.svelte";
|
||||||
|
import Root from "./scroll-area.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Scrollbar,
|
||||||
|
//,
|
||||||
|
Root as ScrollArea,
|
||||||
|
Scrollbar as ScrollAreaScrollbar,
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
bind:ref
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
data-orientation={orientation}
|
||||||
|
{orientation}
|
||||||
|
class={cn(
|
||||||
|
"data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ScrollAreaPrimitive.Thumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
class="rounded-full bg-border relative flex-1"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||||
|
import { Scrollbar } from "./index.js";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
viewportRef = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
scrollbarXClasses = "",
|
||||||
|
scrollbarYClasses = "",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||||
|
orientation?: "vertical" | "horizontal" | "both" | undefined;
|
||||||
|
scrollbarXClasses?: string | undefined;
|
||||||
|
scrollbarYClasses?: string | undefined;
|
||||||
|
viewportRef?: HTMLElement | null;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="scroll-area"
|
||||||
|
class={cn("relative", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
bind:ref={viewportRef}
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
class="cn-scroll-area-viewport focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
{#if orientation === "vertical" || orientation === "both"}
|
||||||
|
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||||
|
{/if}
|
||||||
|
{#if orientation === "horizontal" || orientation === "both"}
|
||||||
|
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||||
|
{/if}
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Root from "./select.svelte";
|
||||||
|
import Group from "./select-group.svelte";
|
||||||
|
import Label from "./select-label.svelte";
|
||||||
|
import Item from "./select-item.svelte";
|
||||||
|
import Content from "./select-content.svelte";
|
||||||
|
import Trigger from "./select-trigger.svelte";
|
||||||
|
import Separator from "./select-separator.svelte";
|
||||||
|
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import GroupHeading from "./select-group-heading.svelte";
|
||||||
|
import Portal from "./select-portal.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Label,
|
||||||
|
Item,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Separator,
|
||||||
|
ScrollDownButton,
|
||||||
|
ScrollUpButton,
|
||||||
|
GroupHeading,
|
||||||
|
Portal,
|
||||||
|
//
|
||||||
|
Root as Select,
|
||||||
|
Group as SelectGroup,
|
||||||
|
Label as SelectLabel,
|
||||||
|
Item as SelectItem,
|
||||||
|
Content as SelectContent,
|
||||||
|
Trigger as SelectTrigger,
|
||||||
|
Separator as SelectSeparator,
|
||||||
|
ScrollDownButton as SelectScrollDownButton,
|
||||||
|
ScrollUpButton as SelectScrollUpButton,
|
||||||
|
GroupHeading as SelectGroupHeading,
|
||||||
|
Portal as SelectPortal,
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import SelectPortal from "./select-portal.svelte";
|
||||||
|
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
preventScroll = true,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPortal {...portalProps}>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
{sideOffset}
|
||||||
|
{preventScroll}
|
||||||
|
data-slot="select-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
class={cn(
|
||||||
|
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPortal>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-group-heading"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.GroupHeading>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SelectPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Group
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-group"
|
||||||
|
class={cn("scroll-my-1 p-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
{value}
|
||||||
|
data-slot="select-item"
|
||||||
|
class={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 focus:bg-accent data-highlighted:bg-accent data-highlighted:text-accent-foreground focus:text-accent-foreground relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ selected, highlighted })}
|
||||||
|
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if selected}
|
||||||
|
<CheckIcon class="cn-select-item-indicator-icon" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
||||||
|
{#if childrenProp}
|
||||||
|
{@render childrenProp({ selected, highlighted })}
|
||||||
|
{:else}
|
||||||
|
{label || value}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</SelectPrimitive.Item>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="select-label"
|
||||||
|
class={cn("text-muted-foreground px-1.5 py-1 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Portal {...restProps} />
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user