mishig HF Staff commited on
Commit
6426ece
·
1 Parent(s): 50bebf4

First build

Browse files
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Dockerfile
2
+ .vscode/
3
+ .idea
4
+ .gitignore
5
+ LICENSE
6
+ README.md
7
+ node_modules/
8
+ .svelte-kit/
.github/workflows/deploy-release.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hf Hub
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ # to run this workflow manually from the Actions tab
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ sync-to-hub:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ with:
17
+ fetch-depth: 0
18
+ lfs: true
19
+ - name: Push to hub
20
+ env:
21
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
22
+ run: git push https://mishig:$HF_TOKEN@huggingface.co/spaces/huggingfacejs/chat-template-playground main -f
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # For more info: https://huggingface.co/docs/hub/spaces-sdks-docker
3
+
4
+ # ---- Base image ----
5
+ FROM node:20 as base
6
+ WORKDIR /app
7
+
8
+ # ---- Dependencies (production only) ----
9
+ FROM base as deps
10
+ COPY --link --chown=1000 package-lock.json package.json ./
11
+ RUN --mount=type=cache,target=/app/.npm \
12
+ npm set cache /app/.npm && \
13
+ npm ci --omit=dev
14
+
15
+ # ---- Builder (all dependencies) ----
16
+ FROM deps as builder
17
+ RUN --mount=type=cache,target=/app/.npm \
18
+ npm set cache /app/.npm && \
19
+ npm ci
20
+ COPY --link --chown=1000 . .
21
+ RUN npm run build
22
+
23
+ # ---- Runner ----
24
+ FROM node:20-slim as runner
25
+ RUN npm install -g pm2
26
+ WORKDIR /app
27
+ COPY --from=deps /app/node_modules /app/node_modules
28
+ COPY --link --chown=1000 package.json /app/package.json
29
+ COPY --from=builder /app/build /app/build
30
+
31
+ # Expose the port your app runs on
32
+ EXPOSE 3000
33
+
34
+ CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
README.md CHANGED
@@ -1,38 +1,71 @@
1
- # sv
 
 
 
 
 
 
 
 
 
2
 
3
- Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
4
 
5
- ## Creating a project
6
 
7
- If you're seeing this, you've probably already done this step. Congrats!
8
 
9
- ```bash
10
- # create a new project in the current directory
11
- npx sv create
12
 
13
- # create a new project in my-app
14
- npx sv create my-app
 
 
 
 
15
  ```
16
 
17
- ## Developing
18
 
19
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20
 
21
  ```bash
22
  npm run dev
23
-
24
  # or start the server and open the app in a new browser tab
25
- npm run dev -- --open
26
  ```
27
 
28
- ## Building
29
 
30
- To create a production version of your app:
31
 
32
  ```bash
33
  npm run build
34
  ```
35
 
36
- You can preview the production build with `npm run preview`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
 
1
+ ---
2
+ title: Chat Template Playground
3
+ emoji: 💻
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 3000
8
+ pinned: false
9
+ license: apache-2.0
10
+ ---
11
 
12
+ # Chat Template Playground
13
 
14
+ A modern SvelteKit playground for experimenting with chat UIs, templates, and interactive components.
15
 
16
+ ## 📦 Installing Dependencies
17
 
18
+ Install the project dependencies before starting development:
 
 
19
 
20
+ ```bash
21
+ npm install
22
+ # or
23
+ yarn install
24
+ # or
25
+ pnpm install
26
  ```
27
 
28
+ ## 💻 Developing Locally
29
 
30
+ Start a development server with hot reload:
31
 
32
  ```bash
33
  npm run dev
 
34
  # or start the server and open the app in a new browser tab
35
+ yarn dev -- --open
36
  ```
37
 
38
+ ## 🏗️ Building for Production
39
 
40
+ To create a production build:
41
 
42
  ```bash
43
  npm run build
44
  ```
45
 
46
+ Preview the production build:
47
+
48
+ ```bash
49
+ npm run preview
50
+ ```
51
+
52
+ > **Note:** To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
53
+
54
+ ## 🧰 Tech Stack
55
+
56
+ - [SvelteKit](https://kit.svelte.dev/)
57
+ - [Vite](https://vitejs.dev/)
58
+ - [sv](https://github.com/sveltejs/cli)
59
+ - [Tailwind CSS](https://tailwindcss.com/) (if enabled)
60
+
61
+ ## 🤝 Contributing
62
+
63
+ Contributions, issues, and feature requests are welcome! Feel free to open an issue or submit a pull request.
64
+
65
+ ## 📄 License
66
+
67
+ This project is licensed under the MIT License.
68
+
69
+ ## 📬 Contact
70
 
71
+ For questions or feedback, please contact the project maintainer.
package-lock.json CHANGED
@@ -7,13 +7,31 @@
7
  "": {
8
  "name": "chat-template-playground",
9
  "version": "0.0.1",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  "devDependencies": {
11
  "@eslint/compat": "^1.2.5",
12
  "@eslint/js": "^9.18.0",
13
  "@sveltejs/adapter-auto": "^6.0.0",
 
14
  "@sveltejs/kit": "^2.16.0",
15
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
16
  "@tailwindcss/vite": "^4.0.0",
 
17
  "eslint": "^9.18.0",
18
  "eslint-config-prettier": "^10.0.1",
19
  "eslint-plugin-svelte": "^3.0.0",
@@ -43,6 +61,255 @@
43
  "node": ">=6.0.0"
44
  }
45
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  "node_modules/@esbuild/aix-ppc64": {
47
  "version": "0.25.4",
48
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
@@ -637,6 +904,15 @@
637
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
638
  }
639
  },
 
 
 
 
 
 
 
 
 
640
  "node_modules/@humanfs/core": {
641
  "version": "0.19.1",
642
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -756,6 +1032,58 @@
756
  "@jridgewell/sourcemap-codec": "^1.4.14"
757
  }
758
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  "node_modules/@modelcontextprotocol/sdk": {
760
  "version": "1.11.0",
761
  "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz",
@@ -823,6 +1151,112 @@
823
  "dev": true,
824
  "license": "MIT"
825
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  "node_modules/@rollup/rollup-android-arm-eabi": {
827
  "version": "4.40.2",
828
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@@ -1126,6 +1560,22 @@
1126
  "@sveltejs/kit": "^2.0.0"
1127
  }
1128
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1129
  "node_modules/@sveltejs/kit": {
1130
  "version": "2.20.8",
1131
  "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.8.tgz",
@@ -1487,6 +1937,20 @@
1487
  "dev": true,
1488
  "license": "MIT"
1489
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1490
  "node_modules/@typescript-eslint/eslint-plugin": {
1491
  "version": "8.32.0",
1492
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz",
@@ -1956,6 +2420,13 @@
1956
  "dev": true,
1957
  "license": "MIT"
1958
  },
 
 
 
 
 
 
 
1959
  "node_modules/concat-map": {
1960
  "version": "0.0.1",
1961
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2020,6 +2491,12 @@
2020
  "node": ">= 0.10"
2021
  }
2022
  },
 
 
 
 
 
 
2023
  "node_modules/cross-spawn": {
2024
  "version": "7.0.6",
2025
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2460,6 +2937,13 @@
2460
  "node": ">=4.0"
2461
  }
2462
  },
 
 
 
 
 
 
 
2463
  "node_modules/esutils": {
2464
  "version": "2.0.3",
2465
  "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -2998,6 +3482,22 @@
2998
  "node": ">= 0.10"
2999
  }
3000
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3001
  "node_modules/is-extglob": {
3002
  "version": "2.1.1",
3003
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3021,6 +3521,13 @@
3021
  "node": ">=0.10.0"
3022
  }
3023
  },
 
 
 
 
 
 
 
3024
  "node_modules/is-number": {
3025
  "version": "7.0.0",
3026
  "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3099,6 +3606,18 @@
3099
  "dev": true,
3100
  "license": "MIT"
3101
  },
 
 
 
 
 
 
 
 
 
 
 
 
3102
  "node_modules/keyv": {
3103
  "version": "4.5.4",
3104
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3486,6 +4005,19 @@
3486
  "node": ">=8.6"
3487
  }
3488
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3489
  "node_modules/mime-db": {
3490
  "version": "1.54.0",
3491
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -3724,6 +4256,13 @@
3724
  "node": ">=8"
3725
  }
3726
  },
 
 
 
 
 
 
 
3727
  "node_modules/path-to-regexp": {
3728
  "version": "8.2.0",
3729
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
@@ -3742,13 +4281,13 @@
3742
  "license": "ISC"
3743
  },
3744
  "node_modules/picomatch": {
3745
- "version": "2.3.1",
3746
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
3747
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
3748
  "dev": true,
3749
  "license": "MIT",
3750
  "engines": {
3751
- "node": ">=8.6"
3752
  },
3753
  "funding": {
3754
  "url": "https://github.com/sponsors/jonschlinkert"
@@ -4118,6 +4657,27 @@
4118
  "url": "https://paulmillr.com/funding/"
4119
  }
4120
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4121
  "node_modules/resolve-from": {
4122
  "version": "4.0.0",
4123
  "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4474,6 +5034,12 @@
4474
  "url": "https://github.com/sponsors/sindresorhus"
4475
  }
4476
  },
 
 
 
 
 
 
4477
  "node_modules/supports-color": {
4478
  "version": "7.2.0",
4479
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4487,6 +5053,19 @@
4487
  "node": ">=8"
4488
  }
4489
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
4490
  "node_modules/svelte": {
4491
  "version": "5.28.2",
4492
  "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz",
@@ -4600,19 +5179,6 @@
4600
  "url": "https://github.com/sponsors/SuperchupuDev"
4601
  }
4602
  },
4603
- "node_modules/tinyglobby/node_modules/picomatch": {
4604
- "version": "4.0.2",
4605
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
4606
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4607
- "dev": true,
4608
- "license": "MIT",
4609
- "engines": {
4610
- "node": ">=12"
4611
- },
4612
- "funding": {
4613
- "url": "https://github.com/sponsors/jonschlinkert"
4614
- }
4615
- },
4616
  "node_modules/to-regex-range": {
4617
  "version": "5.0.1",
4618
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4836,19 +5402,6 @@
4836
  }
4837
  }
4838
  },
4839
- "node_modules/vite/node_modules/picomatch": {
4840
- "version": "4.0.2",
4841
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
4842
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4843
- "dev": true,
4844
- "license": "MIT",
4845
- "engines": {
4846
- "node": ">=12"
4847
- },
4848
- "funding": {
4849
- "url": "https://github.com/sponsors/jonschlinkert"
4850
- }
4851
- },
4852
  "node_modules/vitefu": {
4853
  "version": "1.0.6",
4854
  "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
@@ -4868,6 +5421,12 @@
4868
  }
4869
  }
4870
  },
 
 
 
 
 
 
4871
  "node_modules/which": {
4872
  "version": "2.0.2",
4873
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
7
  "": {
8
  "name": "chat-template-playground",
9
  "version": "0.0.1",
10
+ "dependencies": {
11
+ "@codemirror/autocomplete": "^6.16.0",
12
+ "@codemirror/basic-setup": "^0.20.0",
13
+ "@codemirror/commands": "^6.2.1",
14
+ "@codemirror/lang-javascript": "^6.2.3",
15
+ "@codemirror/lang-json": "^6.0.1",
16
+ "@codemirror/language": "^6.10.1",
17
+ "@codemirror/legacy-modes": "^6.3.0",
18
+ "@codemirror/lint": "^6.8.5",
19
+ "@codemirror/search": "^6.5.11",
20
+ "@codemirror/state": "^6.4.1",
21
+ "@codemirror/theme-one-dark": "^6.1.2",
22
+ "@codemirror/view": "^6.26.3",
23
+ "@huggingface/jinja": "^0.5.0",
24
+ "json5": "^2.2.3"
25
+ },
26
  "devDependencies": {
27
  "@eslint/compat": "^1.2.5",
28
  "@eslint/js": "^9.18.0",
29
  "@sveltejs/adapter-auto": "^6.0.0",
30
+ "@sveltejs/adapter-node": "^5.2.12",
31
  "@sveltejs/kit": "^2.16.0",
32
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
33
  "@tailwindcss/vite": "^4.0.0",
34
+ "@types/json5": "^0.0.30",
35
  "eslint": "^9.18.0",
36
  "eslint-config-prettier": "^10.0.1",
37
  "eslint-plugin-svelte": "^3.0.0",
 
61
  "node": ">=6.0.0"
62
  }
63
  },
64
+ "node_modules/@codemirror/autocomplete": {
65
+ "version": "6.16.0",
66
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz",
67
+ "integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==",
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "@codemirror/language": "^6.0.0",
71
+ "@codemirror/state": "^6.0.0",
72
+ "@codemirror/view": "^6.17.0",
73
+ "@lezer/common": "^1.0.0"
74
+ },
75
+ "peerDependencies": {
76
+ "@codemirror/language": "^6.0.0",
77
+ "@codemirror/state": "^6.0.0",
78
+ "@codemirror/view": "^6.0.0",
79
+ "@lezer/common": "^1.0.0"
80
+ }
81
+ },
82
+ "node_modules/@codemirror/basic-setup": {
83
+ "version": "0.20.0",
84
+ "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz",
85
+ "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==",
86
+ "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'",
87
+ "license": "MIT",
88
+ "dependencies": {
89
+ "@codemirror/autocomplete": "^0.20.0",
90
+ "@codemirror/commands": "^0.20.0",
91
+ "@codemirror/language": "^0.20.0",
92
+ "@codemirror/lint": "^0.20.0",
93
+ "@codemirror/search": "^0.20.0",
94
+ "@codemirror/state": "^0.20.0",
95
+ "@codemirror/view": "^0.20.0"
96
+ }
97
+ },
98
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": {
99
+ "version": "0.20.3",
100
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz",
101
+ "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==",
102
+ "license": "MIT",
103
+ "dependencies": {
104
+ "@codemirror/language": "^0.20.0",
105
+ "@codemirror/state": "^0.20.0",
106
+ "@codemirror/view": "^0.20.0",
107
+ "@lezer/common": "^0.16.0"
108
+ }
109
+ },
110
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": {
111
+ "version": "0.20.0",
112
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz",
113
+ "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==",
114
+ "license": "MIT",
115
+ "dependencies": {
116
+ "@codemirror/language": "^0.20.0",
117
+ "@codemirror/state": "^0.20.0",
118
+ "@codemirror/view": "^0.20.0",
119
+ "@lezer/common": "^0.16.0"
120
+ }
121
+ },
122
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": {
123
+ "version": "0.20.2",
124
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz",
125
+ "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==",
126
+ "license": "MIT",
127
+ "dependencies": {
128
+ "@codemirror/state": "^0.20.0",
129
+ "@codemirror/view": "^0.20.0",
130
+ "@lezer/common": "^0.16.0",
131
+ "@lezer/highlight": "^0.16.0",
132
+ "@lezer/lr": "^0.16.0",
133
+ "style-mod": "^4.0.0"
134
+ }
135
+ },
136
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/lint": {
137
+ "version": "0.20.3",
138
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz",
139
+ "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==",
140
+ "license": "MIT",
141
+ "dependencies": {
142
+ "@codemirror/state": "^0.20.0",
143
+ "@codemirror/view": "^0.20.2",
144
+ "crelt": "^1.0.5"
145
+ }
146
+ },
147
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": {
148
+ "version": "0.20.1",
149
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz",
150
+ "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==",
151
+ "license": "MIT",
152
+ "dependencies": {
153
+ "@codemirror/state": "^0.20.0",
154
+ "@codemirror/view": "^0.20.0",
155
+ "crelt": "^1.0.5"
156
+ }
157
+ },
158
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": {
159
+ "version": "0.20.1",
160
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz",
161
+ "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==",
162
+ "license": "MIT"
163
+ },
164
+ "node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": {
165
+ "version": "0.20.7",
166
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz",
167
+ "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==",
168
+ "license": "MIT",
169
+ "dependencies": {
170
+ "@codemirror/state": "^0.20.0",
171
+ "style-mod": "^4.0.0",
172
+ "w3c-keyname": "^2.2.4"
173
+ }
174
+ },
175
+ "node_modules/@codemirror/basic-setup/node_modules/@lezer/common": {
176
+ "version": "0.16.1",
177
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
178
+ "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==",
179
+ "license": "MIT"
180
+ },
181
+ "node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": {
182
+ "version": "0.16.0",
183
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz",
184
+ "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==",
185
+ "license": "MIT",
186
+ "dependencies": {
187
+ "@lezer/common": "^0.16.0"
188
+ }
189
+ },
190
+ "node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": {
191
+ "version": "0.16.3",
192
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz",
193
+ "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==",
194
+ "license": "MIT",
195
+ "dependencies": {
196
+ "@lezer/common": "^0.16.0"
197
+ }
198
+ },
199
+ "node_modules/@codemirror/commands": {
200
+ "version": "6.2.1",
201
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.1.tgz",
202
+ "integrity": "sha512-FFiNKGuHA5O8uC6IJE5apI5rT9gyjlw4whqy4vlcX0wE/myxL6P1s0upwDhY4HtMWLOwzwsp0ap3bjdQhvfDOA==",
203
+ "license": "MIT",
204
+ "dependencies": {
205
+ "@codemirror/language": "^6.0.0",
206
+ "@codemirror/state": "^6.2.0",
207
+ "@codemirror/view": "^6.0.0",
208
+ "@lezer/common": "^1.0.0"
209
+ }
210
+ },
211
+ "node_modules/@codemirror/lang-javascript": {
212
+ "version": "6.2.3",
213
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
214
+ "integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
215
+ "license": "MIT",
216
+ "dependencies": {
217
+ "@codemirror/autocomplete": "^6.0.0",
218
+ "@codemirror/language": "^6.6.0",
219
+ "@codemirror/lint": "^6.0.0",
220
+ "@codemirror/state": "^6.0.0",
221
+ "@codemirror/view": "^6.17.0",
222
+ "@lezer/common": "^1.0.0",
223
+ "@lezer/javascript": "^1.0.0"
224
+ }
225
+ },
226
+ "node_modules/@codemirror/lang-json": {
227
+ "version": "6.0.1",
228
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
229
+ "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
230
+ "license": "MIT",
231
+ "dependencies": {
232
+ "@codemirror/language": "^6.0.0",
233
+ "@lezer/json": "^1.0.0"
234
+ }
235
+ },
236
+ "node_modules/@codemirror/language": {
237
+ "version": "6.10.1",
238
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
239
+ "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
240
+ "license": "MIT",
241
+ "dependencies": {
242
+ "@codemirror/state": "^6.0.0",
243
+ "@codemirror/view": "^6.23.0",
244
+ "@lezer/common": "^1.1.0",
245
+ "@lezer/highlight": "^1.0.0",
246
+ "@lezer/lr": "^1.0.0",
247
+ "style-mod": "^4.0.0"
248
+ }
249
+ },
250
+ "node_modules/@codemirror/legacy-modes": {
251
+ "version": "6.3.0",
252
+ "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.0.tgz",
253
+ "integrity": "sha512-54ncmguzXZQ3u+8gx4/mpBRbkn6TLzfIrCGvFNLgB3giFYY3E2UETzv1RCFP0hM2L2bQBKBoI92i4ahrDAiE6g==",
254
+ "license": "MIT",
255
+ "dependencies": {
256
+ "@codemirror/language": "^6.0.0"
257
+ }
258
+ },
259
+ "node_modules/@codemirror/lint": {
260
+ "version": "6.8.5",
261
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
262
+ "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
263
+ "license": "MIT",
264
+ "dependencies": {
265
+ "@codemirror/state": "^6.0.0",
266
+ "@codemirror/view": "^6.35.0",
267
+ "crelt": "^1.0.5"
268
+ }
269
+ },
270
+ "node_modules/@codemirror/search": {
271
+ "version": "6.5.11",
272
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
273
+ "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
274
+ "license": "MIT",
275
+ "dependencies": {
276
+ "@codemirror/state": "^6.0.0",
277
+ "@codemirror/view": "^6.0.0",
278
+ "crelt": "^1.0.5"
279
+ }
280
+ },
281
+ "node_modules/@codemirror/state": {
282
+ "version": "6.5.2",
283
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
284
+ "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
285
+ "license": "MIT",
286
+ "dependencies": {
287
+ "@marijn/find-cluster-break": "^1.0.0"
288
+ }
289
+ },
290
+ "node_modules/@codemirror/theme-one-dark": {
291
+ "version": "6.1.2",
292
+ "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
293
+ "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
294
+ "license": "MIT",
295
+ "dependencies": {
296
+ "@codemirror/language": "^6.0.0",
297
+ "@codemirror/state": "^6.0.0",
298
+ "@codemirror/view": "^6.0.0",
299
+ "@lezer/highlight": "^1.0.0"
300
+ }
301
+ },
302
+ "node_modules/@codemirror/view": {
303
+ "version": "6.36.7",
304
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.7.tgz",
305
+ "integrity": "sha512-kCWGW/chWGPgZqfZ36Um9Iz0X2IVpmCjg1P/qY6B6a2ecXtWRRAigmpJ6YgUQ5lTWXMyyVdfmpzhLZmsZQMbtg==",
306
+ "license": "MIT",
307
+ "dependencies": {
308
+ "@codemirror/state": "^6.5.0",
309
+ "style-mod": "^4.1.0",
310
+ "w3c-keyname": "^2.2.4"
311
+ }
312
+ },
313
  "node_modules/@esbuild/aix-ppc64": {
314
  "version": "0.25.4",
315
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
 
904
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
905
  }
906
  },
907
+ "node_modules/@huggingface/jinja": {
908
+ "version": "0.5.0",
909
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.0.tgz",
910
+ "integrity": "sha512-Ptc03/jGRiYRoi0bUYKZ14MkDslsBRT24oxmsvUlfYrvQMldrxCevhPnT+hfX8awKTT8/f/0ZBBWldoeAcMHdQ==",
911
+ "license": "MIT",
912
+ "engines": {
913
+ "node": ">=18"
914
+ }
915
+ },
916
  "node_modules/@humanfs/core": {
917
  "version": "0.19.1",
918
  "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
 
1032
  "@jridgewell/sourcemap-codec": "^1.4.14"
1033
  }
1034
  },
1035
+ "node_modules/@lezer/common": {
1036
+ "version": "1.2.3",
1037
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
1038
+ "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
1039
+ "license": "MIT"
1040
+ },
1041
+ "node_modules/@lezer/highlight": {
1042
+ "version": "1.2.1",
1043
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
1044
+ "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
1045
+ "license": "MIT",
1046
+ "dependencies": {
1047
+ "@lezer/common": "^1.0.0"
1048
+ }
1049
+ },
1050
+ "node_modules/@lezer/javascript": {
1051
+ "version": "1.5.1",
1052
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
1053
+ "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
1054
+ "license": "MIT",
1055
+ "dependencies": {
1056
+ "@lezer/common": "^1.2.0",
1057
+ "@lezer/highlight": "^1.1.3",
1058
+ "@lezer/lr": "^1.3.0"
1059
+ }
1060
+ },
1061
+ "node_modules/@lezer/json": {
1062
+ "version": "1.0.3",
1063
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
1064
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
1065
+ "license": "MIT",
1066
+ "dependencies": {
1067
+ "@lezer/common": "^1.2.0",
1068
+ "@lezer/highlight": "^1.0.0",
1069
+ "@lezer/lr": "^1.0.0"
1070
+ }
1071
+ },
1072
+ "node_modules/@lezer/lr": {
1073
+ "version": "1.4.2",
1074
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
1075
+ "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
1076
+ "license": "MIT",
1077
+ "dependencies": {
1078
+ "@lezer/common": "^1.0.0"
1079
+ }
1080
+ },
1081
+ "node_modules/@marijn/find-cluster-break": {
1082
+ "version": "1.0.2",
1083
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
1084
+ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
1085
+ "license": "MIT"
1086
+ },
1087
  "node_modules/@modelcontextprotocol/sdk": {
1088
  "version": "1.11.0",
1089
  "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz",
 
1151
  "dev": true,
1152
  "license": "MIT"
1153
  },
1154
+ "node_modules/@rollup/plugin-commonjs": {
1155
+ "version": "28.0.3",
1156
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
1157
+ "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==",
1158
+ "dev": true,
1159
+ "license": "MIT",
1160
+ "dependencies": {
1161
+ "@rollup/pluginutils": "^5.0.1",
1162
+ "commondir": "^1.0.1",
1163
+ "estree-walker": "^2.0.2",
1164
+ "fdir": "^6.2.0",
1165
+ "is-reference": "1.2.1",
1166
+ "magic-string": "^0.30.3",
1167
+ "picomatch": "^4.0.2"
1168
+ },
1169
+ "engines": {
1170
+ "node": ">=16.0.0 || 14 >= 14.17"
1171
+ },
1172
+ "peerDependencies": {
1173
+ "rollup": "^2.68.0||^3.0.0||^4.0.0"
1174
+ },
1175
+ "peerDependenciesMeta": {
1176
+ "rollup": {
1177
+ "optional": true
1178
+ }
1179
+ }
1180
+ },
1181
+ "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
1182
+ "version": "1.2.1",
1183
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
1184
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
1185
+ "dev": true,
1186
+ "license": "MIT",
1187
+ "dependencies": {
1188
+ "@types/estree": "*"
1189
+ }
1190
+ },
1191
+ "node_modules/@rollup/plugin-json": {
1192
+ "version": "6.1.0",
1193
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
1194
+ "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
1195
+ "dev": true,
1196
+ "license": "MIT",
1197
+ "dependencies": {
1198
+ "@rollup/pluginutils": "^5.1.0"
1199
+ },
1200
+ "engines": {
1201
+ "node": ">=14.0.0"
1202
+ },
1203
+ "peerDependencies": {
1204
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
1205
+ },
1206
+ "peerDependenciesMeta": {
1207
+ "rollup": {
1208
+ "optional": true
1209
+ }
1210
+ }
1211
+ },
1212
+ "node_modules/@rollup/plugin-node-resolve": {
1213
+ "version": "16.0.1",
1214
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
1215
+ "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
1216
+ "dev": true,
1217
+ "license": "MIT",
1218
+ "dependencies": {
1219
+ "@rollup/pluginutils": "^5.0.1",
1220
+ "@types/resolve": "1.20.2",
1221
+ "deepmerge": "^4.2.2",
1222
+ "is-module": "^1.0.0",
1223
+ "resolve": "^1.22.1"
1224
+ },
1225
+ "engines": {
1226
+ "node": ">=14.0.0"
1227
+ },
1228
+ "peerDependencies": {
1229
+ "rollup": "^2.78.0||^3.0.0||^4.0.0"
1230
+ },
1231
+ "peerDependenciesMeta": {
1232
+ "rollup": {
1233
+ "optional": true
1234
+ }
1235
+ }
1236
+ },
1237
+ "node_modules/@rollup/pluginutils": {
1238
+ "version": "5.1.4",
1239
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
1240
+ "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
1241
+ "dev": true,
1242
+ "license": "MIT",
1243
+ "dependencies": {
1244
+ "@types/estree": "^1.0.0",
1245
+ "estree-walker": "^2.0.2",
1246
+ "picomatch": "^4.0.2"
1247
+ },
1248
+ "engines": {
1249
+ "node": ">=14.0.0"
1250
+ },
1251
+ "peerDependencies": {
1252
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
1253
+ },
1254
+ "peerDependenciesMeta": {
1255
+ "rollup": {
1256
+ "optional": true
1257
+ }
1258
+ }
1259
+ },
1260
  "node_modules/@rollup/rollup-android-arm-eabi": {
1261
  "version": "4.40.2",
1262
  "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
 
1560
  "@sveltejs/kit": "^2.0.0"
1561
  }
1562
  },
1563
+ "node_modules/@sveltejs/adapter-node": {
1564
+ "version": "5.2.12",
1565
+ "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
1566
+ "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
1567
+ "dev": true,
1568
+ "license": "MIT",
1569
+ "dependencies": {
1570
+ "@rollup/plugin-commonjs": "^28.0.1",
1571
+ "@rollup/plugin-json": "^6.1.0",
1572
+ "@rollup/plugin-node-resolve": "^16.0.0",
1573
+ "rollup": "^4.9.5"
1574
+ },
1575
+ "peerDependencies": {
1576
+ "@sveltejs/kit": "^2.4.0"
1577
+ }
1578
+ },
1579
  "node_modules/@sveltejs/kit": {
1580
  "version": "2.20.8",
1581
  "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.8.tgz",
 
1937
  "dev": true,
1938
  "license": "MIT"
1939
  },
1940
+ "node_modules/@types/json5": {
1941
+ "version": "0.0.30",
1942
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz",
1943
+ "integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==",
1944
+ "dev": true,
1945
+ "license": "MIT"
1946
+ },
1947
+ "node_modules/@types/resolve": {
1948
+ "version": "1.20.2",
1949
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
1950
+ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
1951
+ "dev": true,
1952
+ "license": "MIT"
1953
+ },
1954
  "node_modules/@typescript-eslint/eslint-plugin": {
1955
  "version": "8.32.0",
1956
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz",
 
2420
  "dev": true,
2421
  "license": "MIT"
2422
  },
2423
+ "node_modules/commondir": {
2424
+ "version": "1.0.1",
2425
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
2426
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
2427
+ "dev": true,
2428
+ "license": "MIT"
2429
+ },
2430
  "node_modules/concat-map": {
2431
  "version": "0.0.1",
2432
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 
2491
  "node": ">= 0.10"
2492
  }
2493
  },
2494
+ "node_modules/crelt": {
2495
+ "version": "1.0.6",
2496
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
2497
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
2498
+ "license": "MIT"
2499
+ },
2500
  "node_modules/cross-spawn": {
2501
  "version": "7.0.6",
2502
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
2937
  "node": ">=4.0"
2938
  }
2939
  },
2940
+ "node_modules/estree-walker": {
2941
+ "version": "2.0.2",
2942
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
2943
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
2944
+ "dev": true,
2945
+ "license": "MIT"
2946
+ },
2947
  "node_modules/esutils": {
2948
  "version": "2.0.3",
2949
  "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
 
3482
  "node": ">= 0.10"
3483
  }
3484
  },
3485
+ "node_modules/is-core-module": {
3486
+ "version": "2.16.1",
3487
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
3488
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
3489
+ "dev": true,
3490
+ "license": "MIT",
3491
+ "dependencies": {
3492
+ "hasown": "^2.0.2"
3493
+ },
3494
+ "engines": {
3495
+ "node": ">= 0.4"
3496
+ },
3497
+ "funding": {
3498
+ "url": "https://github.com/sponsors/ljharb"
3499
+ }
3500
+ },
3501
  "node_modules/is-extglob": {
3502
  "version": "2.1.1",
3503
  "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 
3521
  "node": ">=0.10.0"
3522
  }
3523
  },
3524
+ "node_modules/is-module": {
3525
+ "version": "1.0.0",
3526
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
3527
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
3528
+ "dev": true,
3529
+ "license": "MIT"
3530
+ },
3531
  "node_modules/is-number": {
3532
  "version": "7.0.0",
3533
  "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 
3606
  "dev": true,
3607
  "license": "MIT"
3608
  },
3609
+ "node_modules/json5": {
3610
+ "version": "2.2.3",
3611
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
3612
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
3613
+ "license": "MIT",
3614
+ "bin": {
3615
+ "json5": "lib/cli.js"
3616
+ },
3617
+ "engines": {
3618
+ "node": ">=6"
3619
+ }
3620
+ },
3621
  "node_modules/keyv": {
3622
  "version": "4.5.4",
3623
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 
4005
  "node": ">=8.6"
4006
  }
4007
  },
4008
+ "node_modules/micromatch/node_modules/picomatch": {
4009
+ "version": "2.3.1",
4010
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
4011
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
4012
+ "dev": true,
4013
+ "license": "MIT",
4014
+ "engines": {
4015
+ "node": ">=8.6"
4016
+ },
4017
+ "funding": {
4018
+ "url": "https://github.com/sponsors/jonschlinkert"
4019
+ }
4020
+ },
4021
  "node_modules/mime-db": {
4022
  "version": "1.54.0",
4023
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
 
4256
  "node": ">=8"
4257
  }
4258
  },
4259
+ "node_modules/path-parse": {
4260
+ "version": "1.0.7",
4261
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
4262
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
4263
+ "dev": true,
4264
+ "license": "MIT"
4265
+ },
4266
  "node_modules/path-to-regexp": {
4267
  "version": "8.2.0",
4268
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
 
4281
  "license": "ISC"
4282
  },
4283
  "node_modules/picomatch": {
4284
+ "version": "4.0.2",
4285
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
4286
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4287
  "dev": true,
4288
  "license": "MIT",
4289
  "engines": {
4290
+ "node": ">=12"
4291
  },
4292
  "funding": {
4293
  "url": "https://github.com/sponsors/jonschlinkert"
 
4657
  "url": "https://paulmillr.com/funding/"
4658
  }
4659
  },
4660
+ "node_modules/resolve": {
4661
+ "version": "1.22.10",
4662
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
4663
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
4664
+ "dev": true,
4665
+ "license": "MIT",
4666
+ "dependencies": {
4667
+ "is-core-module": "^2.16.0",
4668
+ "path-parse": "^1.0.7",
4669
+ "supports-preserve-symlinks-flag": "^1.0.0"
4670
+ },
4671
+ "bin": {
4672
+ "resolve": "bin/resolve"
4673
+ },
4674
+ "engines": {
4675
+ "node": ">= 0.4"
4676
+ },
4677
+ "funding": {
4678
+ "url": "https://github.com/sponsors/ljharb"
4679
+ }
4680
+ },
4681
  "node_modules/resolve-from": {
4682
  "version": "4.0.0",
4683
  "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
 
5034
  "url": "https://github.com/sponsors/sindresorhus"
5035
  }
5036
  },
5037
+ "node_modules/style-mod": {
5038
+ "version": "4.1.2",
5039
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
5040
+ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
5041
+ "license": "MIT"
5042
+ },
5043
  "node_modules/supports-color": {
5044
  "version": "7.2.0",
5045
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 
5053
  "node": ">=8"
5054
  }
5055
  },
5056
+ "node_modules/supports-preserve-symlinks-flag": {
5057
+ "version": "1.0.0",
5058
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
5059
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
5060
+ "dev": true,
5061
+ "license": "MIT",
5062
+ "engines": {
5063
+ "node": ">= 0.4"
5064
+ },
5065
+ "funding": {
5066
+ "url": "https://github.com/sponsors/ljharb"
5067
+ }
5068
+ },
5069
  "node_modules/svelte": {
5070
  "version": "5.28.2",
5071
  "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz",
 
5179
  "url": "https://github.com/sponsors/SuperchupuDev"
5180
  }
5181
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5182
  "node_modules/to-regex-range": {
5183
  "version": "5.0.1",
5184
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 
5402
  }
5403
  }
5404
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5405
  "node_modules/vitefu": {
5406
  "version": "1.0.6",
5407
  "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
 
5421
  }
5422
  }
5423
  },
5424
+ "node_modules/w3c-keyname": {
5425
+ "version": "2.2.8",
5426
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
5427
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
5428
+ "license": "MIT"
5429
+ },
5430
  "node_modules/which": {
5431
  "version": "2.0.2",
5432
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
package.json CHANGED
@@ -17,9 +17,11 @@
17
  "@eslint/compat": "^1.2.5",
18
  "@eslint/js": "^9.18.0",
19
  "@sveltejs/adapter-auto": "^6.0.0",
 
20
  "@sveltejs/kit": "^2.16.0",
21
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
22
  "@tailwindcss/vite": "^4.0.0",
 
23
  "eslint": "^9.18.0",
24
  "eslint-config-prettier": "^10.0.1",
25
  "eslint-plugin-svelte": "^3.0.0",
@@ -33,5 +35,21 @@
33
  "typescript": "^5.0.0",
34
  "typescript-eslint": "^8.20.0",
35
  "vite": "^6.2.6"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
  }
 
17
  "@eslint/compat": "^1.2.5",
18
  "@eslint/js": "^9.18.0",
19
  "@sveltejs/adapter-auto": "^6.0.0",
20
+ "@sveltejs/adapter-node": "^5.2.12",
21
  "@sveltejs/kit": "^2.16.0",
22
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
23
  "@tailwindcss/vite": "^4.0.0",
24
+ "@types/json5": "^0.0.30",
25
  "eslint": "^9.18.0",
26
  "eslint-config-prettier": "^10.0.1",
27
  "eslint-plugin-svelte": "^3.0.0",
 
35
  "typescript": "^5.0.0",
36
  "typescript-eslint": "^8.20.0",
37
  "vite": "^6.2.6"
38
+ },
39
+ "dependencies": {
40
+ "@codemirror/autocomplete": "^6.16.0",
41
+ "@codemirror/basic-setup": "^0.20.0",
42
+ "@codemirror/commands": "^6.2.1",
43
+ "@codemirror/lang-javascript": "^6.2.3",
44
+ "@codemirror/lang-json": "^6.0.1",
45
+ "@codemirror/language": "^6.10.1",
46
+ "@codemirror/legacy-modes": "^6.3.0",
47
+ "@codemirror/lint": "^6.8.5",
48
+ "@codemirror/search": "^6.5.11",
49
+ "@codemirror/state": "^6.4.1",
50
+ "@codemirror/theme-one-dark": "^6.1.2",
51
+ "@codemirror/view": "^6.26.3",
52
+ "@huggingface/jinja": "^0.5.0",
53
+ "json5": "^2.2.3"
54
  }
55
  }
src/app.css CHANGED
@@ -1 +1,132 @@
1
  @import 'tailwindcss';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @import 'tailwindcss';
2
+
3
+ @custom-variant dark (&:where(.dark, .dark *));
4
+
5
+ .codemirror-wrapper {
6
+ @apply overflow-auto text-sm;
7
+ }
8
+ .codemirror-wrapper .cm-editor {
9
+ @apply bg-transparent;
10
+ }
11
+ .codemirror-wrapper .cm-content {
12
+ @apply min-h-[12rem] font-mono;
13
+ }
14
+ .codemirror-wrapper .cm-gutter {
15
+ @apply min-h-[12rem];
16
+ }
17
+ .codemirror-wrapper .cm-gutters {
18
+ @apply border-gray-200 bg-gray-50 text-gray-400 dark:border-gray-900 dark:bg-gray-900;
19
+ }
20
+ .codemirror-wrapper .cm-line {
21
+ @apply pl-2 selection:bg-blue-200! dark:bg-gray-900 dark:selection:bg-gray-700!;
22
+ }
23
+ .codemirror-wrapper .cm-activeLine {
24
+ @apply bg-blue-50 dark:bg-gray-900;
25
+ }
26
+ .codemirror-wrapper .cm-activeLineGutter {
27
+ @apply bg-blue-100 dark:bg-gray-900;
28
+ }
29
+ .codemirror-wrapper .cm-scroller,
30
+ .codemirror-wrapper .cm-editor.cm-focused {
31
+ @apply outline-none;
32
+ }
33
+
34
+ /* Hide codemirror default search component since we have CodeMirrorSearch.svelte */
35
+ .codemirror-wrapper .cm-search {
36
+ @apply hidden;
37
+ }
38
+
39
+ @utility btn-base {
40
+ @apply inline-flex cursor-pointer items-center justify-center rounded-lg border px-3 py-1 whitespace-nowrap select-none focus:ring-3 focus:outline-hidden;
41
+ }
42
+
43
+ @utility btn {
44
+ @apply btn-base;
45
+ @apply border-gray-200 bg-linear-to-b from-white to-gray-100 text-gray-800 hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700 dark:hover:to-gray-950;
46
+ &:disabled {
47
+ @apply cursor-not-allowed opacity-50;
48
+ }
49
+ &.btn-lg {
50
+ @apply px-4 py-1.5 font-normal;
51
+ }
52
+ }
53
+
54
+ @utility btn-widget {
55
+ @apply btn-base;
56
+ @apply h-8 bg-linear-to-b from-gray-50 to-gray-200 hover:from-gray-100 hover:to-gray-200 dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:hover:from-gray-700 dark:hover:to-gray-950;
57
+ &:disabled {
58
+ @apply cursor-not-allowed opacity-50;
59
+ }
60
+ }
61
+
62
+ @utility btn-warning {
63
+ @apply btn-base;
64
+ @apply border-orange-200 bg-linear-to-b from-white to-orange-100 text-orange-700 hover:shadow-inner dark:border-orange-800 dark:from-orange-800 dark:to-orange-900 dark:text-orange-200 dark:hover:from-orange-700 dark:hover:to-orange-900;
65
+ }
66
+
67
+ @utility btn-green {
68
+ @apply btn-base;
69
+ @apply border-green-500 bg-green-500 text-white hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700 dark:hover:to-gray-950;
70
+ }
71
+
72
+ @utility btn-pink {
73
+ @apply btn-base;
74
+ @apply border-red-400 bg-red-400 text-white hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700 dark:hover:to-gray-950;
75
+ }
76
+
77
+ @utility btn-sky {
78
+ @apply btn-base;
79
+ @apply border-sky-600 bg-sky-600 text-white hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700 dark:hover:to-gray-950;
80
+ }
81
+
82
+ @utility btn-sky-ligher {
83
+ @apply btn-base;
84
+ @apply border-sky-500 bg-sky-500 text-white hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700 dark:hover:to-gray-950;
85
+ }
86
+
87
+ @utility btn-fuchsia {
88
+ @apply btn-base;
89
+ @apply border-fuchsia-600 bg-fuchsia-600 text-white hover:shadow-inner dark:border-gray-800 dark:from-gray-800 dark:to-gray-950 dark:text-gray-200 dark:hover:from-gray-700;
90
+ }
91
+
92
+ @utility btn-disabled {
93
+ @apply cursor-not-allowed opacity-50;
94
+ }
95
+
96
+ @utility btn-widget-disabled {
97
+ @apply cursor-not-allowed opacity-50;
98
+ }
99
+
100
+ @utility btn-lg {
101
+ &.btn {
102
+ @apply px-4 py-1.5 font-normal;
103
+ }
104
+ }
105
+
106
+ @utility btn-green-lg {
107
+ @apply px-10 py-2;
108
+ }
109
+
110
+ @utility btn-fuchsia-lg {
111
+ @apply px-10 py-2;
112
+ }
113
+
114
+ @utility tooltip-mask {
115
+ @apply pointer-events-none absolute overflow-visible bg-transparent;
116
+ }
117
+ @utility tooltip {
118
+ @apply pointer-events-auto absolute z-50 w-max max-w-44 transform rounded-lg border-black bg-black p-2 text-xs leading-tight font-normal break-words text-white shadow transition-opacity dark:bg-gray-800;
119
+ }
120
+ @utility tooltip-arrow {
121
+ @apply absolute z-0 size-2 rotate-45 transform bg-black dark:bg-gray-800;
122
+ }
123
+
124
+ .alert {
125
+ @apply rounded-md border border-blue-100 bg-blue-50 px-3 py-2 text-blue-900 dark:border-blue-800/30 dark:bg-blue-800/30 dark:text-blue-200;
126
+ }
127
+ .alert a {
128
+ @apply underline;
129
+ }
130
+ .alert-error {
131
+ @apply border-red-100 bg-red-50 text-red-900 dark:border-red-800/30 dark:bg-red-800/30 dark:text-red-200;
132
+ }
src/app.html CHANGED
@@ -6,7 +6,47 @@
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  %sveltekit.head%
8
  </head>
 
9
  <body data-sveltekit-preload-data="hover">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  <div style="display: contents">%sveltekit.body%</div>
11
  </body>
12
  </html>
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  %sveltekit.head%
8
  </head>
9
+
10
  <body data-sveltekit-preload-data="hover">
11
+ <style>
12
+ body {
13
+ background: white;
14
+ }
15
+ body.dark {
16
+ background: #101828;
17
+ }
18
+ </style>
19
+ <script>
20
+ (function () {
21
+ const urlParams = new URLSearchParams(window.location.search);
22
+ const theme = urlParams.get('_theme');
23
+
24
+ let systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
25
+
26
+ function updateTheme() {
27
+ if (theme === 'dark') {
28
+ document.body.classList.add('dark');
29
+ } else if (theme === 'light') {
30
+ document.body.classList.remove('dark');
31
+ } else if (theme === 'system' || theme === null || theme === undefined) {
32
+ if (systemPrefersDark) {
33
+ document.body.classList.add('dark');
34
+ } else {
35
+ document.body.classList.remove('dark');
36
+ }
37
+ }
38
+ }
39
+
40
+ // Initial theme update
41
+ updateTheme();
42
+
43
+ // Listen for system preference changes
44
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
45
+ systemPrefersDark = event.matches;
46
+ updateTheme();
47
+ });
48
+ })();
49
+ </script>
50
  <div style="display: contents">%sveltekit.body%</div>
51
  </body>
52
  </html>
src/lib/ChatTemplateViewer/ChatTemplateViewer.svelte ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { StreamLanguage } from '@codemirror/language';
3
+ import CodeMirror from '$lib/CodeMirror/CodeMirror.svelte';
4
+ import { jinja2 } from '@codemirror/legacy-modes/mode/jinja2';
5
+ import { EditorView, lineNumbers } from '@codemirror/view';
6
+ import CopyButton from '$lib/CopyButton/CopyButton.svelte';
7
+ import IconCodeGeneration from '$lib/Icons/IconCodeGeneration.svelte';
8
+ import { tooltip } from '$lib/utils/tooltip';
9
+ import type { FormattedChatTemplate } from './types';
10
+ import LineWrapButton from '$lib/LineWrapButton/LineWrapButton.svelte';
11
+ import { createEventDispatcher } from 'svelte';
12
+ import IconRestart from '$lib/Icons/IconRestart.svelte';
13
+
14
+ export let modelId: string;
15
+ export let formattedTemplates: FormattedChatTemplate[] = [];
16
+ export let selectedTemplate: FormattedChatTemplate | undefined = undefined;
17
+ export let showFormattedTemplate = true;
18
+
19
+ let wrapLines = true;
20
+
21
+ const dispatch = createEventDispatcher<{ modelIdChange: string; templateChange: string }>();
22
+
23
+ async function handleUpdateEditor(e: CustomEvent<string>) {
24
+ const currentCode = e.detail;
25
+ if (selectedTemplate) {
26
+ showFormattedTemplate
27
+ ? (selectedTemplate.formattedTemplate = currentCode)
28
+ : (selectedTemplate.template = currentCode);
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <div class="h-full overflow-scroll bg-white dark:bg-gray-900">
34
+ <div class="sticky top-0 z-10 bg-white dark:bg-gray-900">
35
+ <div
36
+ class="text-semibold flex items-center gap-x-2 border-b border-gray-500 bg-linear-to-r from-green-200 to-white px-3 py-1.5 text-lg dark:from-green-700 dark:to-green-900 dark:text-gray-200"
37
+ >
38
+ Chat template{formattedTemplates.length > 1 ? 's' : ''} for
39
+ <a class="font-mono underline" href="https://huggingface.co/{modelId}" target="_blank"
40
+ >{modelId}</a
41
+ >
42
+ <button
43
+ class="btn ml-auto text-sm"
44
+ on:click={() => {
45
+ const newModelId = prompt('Enter model ID (ex: deepseek-ai/DeepSeek-R1)')?.trim();
46
+ if (newModelId) {
47
+ dispatch('modelIdChange', newModelId);
48
+ }
49
+ }}>change model</button
50
+ >
51
+ </div>
52
+ <div class="flex items-center border-b px-3 py-2">
53
+ {#if formattedTemplates.length > 1}
54
+ <div class="my-1.5 flex flex-wrap items-center gap-x-1 gap-y-0.5">
55
+ {#each formattedTemplates as template (template.name)}
56
+ <button
57
+ class="text-md flex items-center rounded-lg border px-1.5 py-1 leading-none select-none
58
+ {selectedTemplate?.name === template.name
59
+ ? 'border-gray-800 bg-black text-white dark:bg-gray-700'
60
+ : 'cursor-pointer text-gray-500 opacity-90 hover:text-gray-700 hover:shadow-xs dark:hover:text-gray-200'}"
61
+ type="button"
62
+ on:click={() => {
63
+ selectedTemplate = template;
64
+ dispatch('templateChange', template.name);
65
+ }}
66
+ >
67
+ {template.name}
68
+ </button>
69
+ {/each}
70
+ </div>
71
+ {/if}
72
+ <div class="ml-auto flex items-center gap-x-2">
73
+ <!-- reset button -->
74
+ {#if showFormattedTemplate ? selectedTemplate?.formattedTemplate !== selectedTemplate?.formattedTemplateUnedited : selectedTemplate?.template !== selectedTemplate?.templateUnedited}
75
+ <button
76
+ class="relative inline-flex h-6! cursor-pointer items-center justify-center rounded-md border border-gray-500 bg-white p-0! px-1.5! text-sm shadow-xs focus:outline-hidden dark:bg-gray-900 dark:text-white [&_svg]:translate-x-px! [&_svg]:translate-y-px! [&_svg]:text-base!"
77
+ type="button"
78
+ on:click={() => {
79
+ if (selectedTemplate) {
80
+ showFormattedTemplate
81
+ ? (selectedTemplate.formattedTemplate =
82
+ selectedTemplate.formattedTemplateUnedited)
83
+ : (selectedTemplate.template = selectedTemplate.templateUnedited);
84
+ }
85
+ }}
86
+ use:tooltip={'Reset template to original'}
87
+ ><IconRestart classNames="dark:text-gray-200!" />
88
+ <span class="ml-1 text-sm select-none dark:text-gray-200!"> Reset </span>
89
+ </button>
90
+ {/if}
91
+
92
+ <CopyButton
93
+ label="Copy"
94
+ value={showFormattedTemplate
95
+ ? (selectedTemplate?.formattedTemplate ?? '')
96
+ : (selectedTemplate?.template ?? '')}
97
+ style="button-clear"
98
+ classNames="h-6! [&_svg]:text-[0.7rem]! px-1.5! text-black! dark:text-gray-200!"
99
+ />
100
+
101
+ <!-- format button -->
102
+ <button
103
+ class="relative inline-flex h-6! cursor-pointer items-center justify-center rounded-md border border-gray-500 bg-white p-0! px-1.5! text-sm shadow-xs focus:outline-hidden dark:bg-gray-900 dark:text-white [&_svg]:translate-x-px! [&_svg]:translate-y-px! [&_svg]:text-base!"
104
+ type="button"
105
+ on:click={() => {
106
+ showFormattedTemplate = !showFormattedTemplate;
107
+ }}
108
+ use:tooltip={'Format with @huggingface/jinja'}
109
+ ><IconCodeGeneration classNames={showFormattedTemplate ? 'opacity-100' : 'opacity-40'} />
110
+ <span
111
+ class="ml-1 text-sm select-none {showFormattedTemplate ? 'opacity-100' : 'opacity-40'}"
112
+ >
113
+ Formatted
114
+ </span>
115
+ </button>
116
+
117
+ <LineWrapButton
118
+ style="button-clear"
119
+ bind:wrapLines
120
+ classNames="[&_svg]:text-xs! size-6! p-0!"
121
+ />
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <CodeMirror
127
+ value={showFormattedTemplate
128
+ ? (selectedTemplate?.formattedTemplate ?? '')
129
+ : (selectedTemplate?.template ?? '')}
130
+ on:change={handleUpdateEditor}
131
+ extensions={[
132
+ lineNumbers(),
133
+ StreamLanguage.define(jinja2),
134
+ ...[wrapLines ? [EditorView.lineWrapping] : []]
135
+ ]}
136
+ />
137
+ </div>
src/lib/ChatTemplateViewer/types.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export interface FormattedChatTemplate {
2
+ name: string;
3
+ template: string;
4
+ formattedTemplate: string;
5
+ templateUnedited: string;
6
+ formattedTemplateUnedited: string;
7
+ }
8
+
9
+ export type ChatTemplate = string | Omit<FormattedChatTemplate, 'formattedTemplate'>[];
src/lib/CodeMirror/CodeMirror.svelte ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- original from: https://github.com/touchifyapp/svelte-codemirror-editor/blob/main/src/lib/CodeMirror.svelte -->
2
+ <script lang="ts">
3
+ import type { ViewUpdate } from '@codemirror/view';
4
+
5
+ import { createEventDispatcher, onMount } from 'svelte';
6
+ import { EditorView, keymap, placeholder as placeholderExt } from '@codemirror/view';
7
+ import { StateEffect, EditorState, type Extension } from '@codemirror/state';
8
+ import { indentWithTab } from '@codemirror/commands';
9
+ import { oneDark } from '@codemirror/theme-one-dark';
10
+
11
+ import IconSpin from '../Icons/IconSpin.svelte';
12
+
13
+ import { basicSetup } from './basicSetup';
14
+ import CodeMirrorSearch from '$lib/CodeMirrorSearch/CodeMirrorSearch.svelte';
15
+
16
+ export let classNames = '';
17
+ export let loaderClassNames = '';
18
+ export let value = '';
19
+ export let fontSize: string | undefined = undefined;
20
+
21
+ export let basic = true;
22
+ export let extensions: Extension[] = [];
23
+
24
+ export let useTab = true;
25
+
26
+ export let editable = true;
27
+ export let readonly = false;
28
+ export let placeholder: string | HTMLElement | null | undefined = undefined;
29
+ export let focusOnMount = false;
30
+ export let view: EditorView | undefined = undefined;
31
+
32
+ const isBrowser = typeof window !== 'undefined';
33
+ const dispatch = createEventDispatcher<{ change: string }>();
34
+ let element: HTMLDivElement;
35
+ let isSearchOpen = false;
36
+
37
+ $: reconfigure(), extensions;
38
+ $: setDoc(value);
39
+
40
+ function setDoc(newDoc: string) {
41
+ if (view && newDoc !== view.state.doc.toString()) {
42
+ view.dispatch({
43
+ changes: {
44
+ from: 0,
45
+ to: view.state.doc.length,
46
+ insert: newDoc
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ function createEditorView(): EditorView {
53
+ return new EditorView({
54
+ parent: element,
55
+ state: createEditorState(value)
56
+ });
57
+ }
58
+
59
+ function handleChange(vu: ViewUpdate): void {
60
+ if (vu.docChanged) {
61
+ const doc = vu.state.doc;
62
+ const text = doc.toString();
63
+ dispatch('change', text);
64
+ }
65
+ }
66
+
67
+ function getExtensions() {
68
+ const stateExtensions = [
69
+ ...getBaseExtensions(basic, useTab, placeholder, editable, readonly),
70
+ ...getTheme(),
71
+ ...extensions
72
+ ];
73
+ return stateExtensions;
74
+ }
75
+
76
+ function createEditorState(value: string | null | undefined): EditorState {
77
+ return EditorState.create({
78
+ doc: value ?? undefined,
79
+ extensions: getExtensions()
80
+ });
81
+ }
82
+
83
+ function getBaseExtensions(
84
+ basic: boolean,
85
+ useTab: boolean,
86
+ placeholder: string | HTMLElement | null | undefined,
87
+ editable: boolean,
88
+ readonly: boolean
89
+ ): Extension[] {
90
+ const extensions: Extension[] = [
91
+ EditorView.editable.of(editable),
92
+ EditorState.readOnly.of(readonly)
93
+ ];
94
+
95
+ if (basic) {
96
+ extensions.push(basicSetup);
97
+ }
98
+ if (useTab) {
99
+ extensions.push(keymap.of([indentWithTab]));
100
+ }
101
+ if (placeholder) {
102
+ extensions.push(placeholderExt(placeholder));
103
+ }
104
+ if (fontSize) {
105
+ extensions.push(
106
+ EditorView.theme({
107
+ '&': {
108
+ fontSize: fontSize
109
+ }
110
+ })
111
+ );
112
+ }
113
+
114
+ extensions.push(EditorView.updateListener.of(handleChange));
115
+ return extensions;
116
+ }
117
+
118
+ function getTheme(): Extension[] {
119
+ const extensions: Extension[] = [];
120
+ const isDarkMode = document.querySelector('body')?.classList.contains('dark') ?? false;
121
+ if (isDarkMode) {
122
+ extensions.push(oneDark);
123
+ }
124
+ return extensions;
125
+ }
126
+
127
+ function reconfigure(): void {
128
+ view?.dispatch({
129
+ effects: StateEffect.reconfigure.of(getExtensions())
130
+ });
131
+ }
132
+
133
+ function onKeyDown(e: KeyboardEvent) {
134
+ const { ctrlKey, metaKey, key } = e;
135
+ const isOpenShortcut = key === 'f3' || ((metaKey || ctrlKey) && key === 'f');
136
+ if (isOpenShortcut) {
137
+ isSearchOpen = true;
138
+ e.preventDefault();
139
+ }
140
+ }
141
+
142
+ onMount(() => {
143
+ view = createEditorView();
144
+ if (view && focusOnMount) {
145
+ const tr = view.state.update({
146
+ selection: { anchor: view.state.doc.length }
147
+ });
148
+ view.dispatch(tr);
149
+ view.focus();
150
+ }
151
+ return () => view?.destroy();
152
+ });
153
+ </script>
154
+
155
+ {#if isBrowser}
156
+ <div class="relative">
157
+ <div class="codemirror-wrapper {classNames}" bind:this={element} on:keydown={onKeyDown} />
158
+ {#if isSearchOpen && view}
159
+ <CodeMirrorSearch {view} on:close={() => (isSearchOpen = false)} />
160
+ {/if}
161
+ </div>
162
+ {:else}
163
+ <div class="flex h-64 items-center justify-center {loaderClassNames}">
164
+ <IconSpin classNames="animate-spin text-xs" />
165
+ </div>
166
+ {/if}
src/lib/CodeMirror/basicSetup.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// Mostly similar to https://codemirror.net/docs/ref/#codemirror.basicSetup
2
+ /// src: https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
3
+ import type { Extension } from '@codemirror/state';
4
+ import {
5
+ highlightActiveLineGutter,
6
+ highlightSpecialChars,
7
+ drawSelection,
8
+ dropCursor,
9
+ rectangularSelection,
10
+ crosshairCursor,
11
+ highlightActiveLine,
12
+ keymap
13
+ } from '@codemirror/view';
14
+ export { EditorView } from '@codemirror/view';
15
+ import { EditorState } from '@codemirror/state';
16
+ import {
17
+ indentOnInput,
18
+ syntaxHighlighting,
19
+ defaultHighlightStyle,
20
+ bracketMatching,
21
+ foldKeymap
22
+ } from '@codemirror/language';
23
+ import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
24
+ import {
25
+ closeBrackets,
26
+ autocompletion,
27
+ closeBracketsKeymap,
28
+ completionKeymap
29
+ } from '@codemirror/autocomplete';
30
+ import { indentationMarkers } from './indentationMarkers';
31
+
32
+ export const basicSetup: Extension = /*@__PURE__*/ (() => [
33
+ highlightActiveLineGutter(),
34
+ highlightSpecialChars(),
35
+ history(),
36
+ drawSelection(),
37
+ dropCursor(),
38
+ EditorState.allowMultipleSelections.of(true),
39
+ indentOnInput(),
40
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
41
+ bracketMatching(),
42
+ closeBrackets(),
43
+ autocompletion(),
44
+ rectangularSelection(),
45
+ crosshairCursor(),
46
+ highlightActiveLine(),
47
+ keymap.of([
48
+ ...closeBracketsKeymap,
49
+ ...defaultKeymap,
50
+ ...historyKeymap,
51
+ ...foldKeymap,
52
+ ...completionKeymap
53
+ ]),
54
+ indentationMarkers({
55
+ highlightActiveBlock: false
56
+ })
57
+ ])();
src/lib/CodeMirror/indentationMarkers.ts ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // original: https://github.com/replit/codemirror-indentation-markers/tree/f2f8ccdc58fcc51dbe6a82784f2f83fa6ac4648c/src/
2
+ // combined all 3 files (index.ts, map.ts, utils.ts) from original source into this one file
3
+ import { getIndentUnit } from '@codemirror/language';
4
+ import type { EditorState, Extension, Line } from '@codemirror/state';
5
+ import { combineConfig, Facet, RangeSetBuilder } from '@codemirror/state';
6
+ import type { DecorationSet, ViewUpdate, PluginValue } from '@codemirror/view';
7
+ import { Decoration, ViewPlugin, EditorView } from '@codemirror/view';
8
+
9
+ /**
10
+ * Gets the visible lines in the editor. Lines will not be repeated.
11
+ *
12
+ * @param view - The editor view to get the visible lines from.
13
+ * @param state - The editor state. Defaults to the view's current one.
14
+ */
15
+ export function getVisibleLines(view: EditorView, state = view.state): Set<Line> {
16
+ const lines = new Set<Line>();
17
+
18
+ for (const { from, to } of view.visibleRanges) {
19
+ let pos = from;
20
+
21
+ while (pos <= to) {
22
+ const line = state.doc.lineAt(pos);
23
+
24
+ if (!lines.has(line)) {
25
+ lines.add(line);
26
+ }
27
+
28
+ pos = line.to + 1;
29
+ }
30
+ }
31
+
32
+ return lines;
33
+ }
34
+
35
+ /**
36
+ * Gets the line at the position of the primary cursor.
37
+ *
38
+ * @param state - The editor state from which to extract the line.
39
+ */
40
+ export function getCurrentLine(state: EditorState): Line {
41
+ const currentPos = state.selection.main.head;
42
+ return state.doc.lineAt(currentPos);
43
+ }
44
+
45
+ /**
46
+ * Returns the number of columns that a string is indented, controlling for
47
+ * tabs. This is useful for determining the indentation level of a line.
48
+ *
49
+ * Note that this only returns the number of _visible_ columns, not the number
50
+ * of whitespace characters at the start of the string.
51
+ *
52
+ * @param str - The string to check.
53
+ * @param tabSize - The size of a tab character. Usually 2 or 4.
54
+ */
55
+ export function numColumns(str: string, tabSize: number): number {
56
+ // as far as I can tell, this is pretty much the fastest way to do this,
57
+ // at least involving iteration. `str.length - str.trimStart().length` is
58
+ // much faster, but it has some edge cases that are hard to deal with.
59
+
60
+ let col = 0;
61
+
62
+ // eslint-disable-next-line no-restricted-syntax, @typescript-eslint/prefer-for-of
63
+ loop: for (let i = 0; i < str.length; i++) {
64
+ switch (str[i]) {
65
+ case ' ': {
66
+ col += 1;
67
+ continue loop;
68
+ }
69
+
70
+ case '\t': {
71
+ // if the current column is a multiple of the tab size, we can just
72
+ // add the tab size to the column. otherwise, we need to add the
73
+ // difference between the tab size and the current column.
74
+ col += tabSize - (col % tabSize);
75
+ continue loop;
76
+ }
77
+
78
+ case '\r': {
79
+ continue loop;
80
+ }
81
+
82
+ default: {
83
+ break loop;
84
+ }
85
+ }
86
+ }
87
+
88
+ return col;
89
+ }
90
+
91
+ export interface IndentEntry {
92
+ line: Line;
93
+ col: number;
94
+ level: number;
95
+ empty: boolean;
96
+ active?: number;
97
+ }
98
+
99
+ /**
100
+ * Indentation map for a set of lines.
101
+ *
102
+ * This map will contain the indentation for lines that are not a part of the given set,
103
+ * but this is because calculating the indentation for those lines was necessary to
104
+ * calculate the indentation for the lines provided to the constructor.
105
+ *
106
+ * @see {@link IndentEntry}
107
+ */
108
+ export class IndentationMap {
109
+ /** The {@link EditorState} indentation is derived from. */
110
+ private state: EditorState;
111
+
112
+ /** The set of lines that are used as an entrypoint. */
113
+ private lines: Set<Line>;
114
+
115
+ /** The internal mapping of line numbers to {@link IndentEntry} objects. */
116
+ private map: Map<number, IndentEntry>;
117
+
118
+ /** The width of the editor's indent unit. */
119
+ private unitWidth: number;
120
+
121
+ /**
122
+ * @param lines - The set of lines to get the indentation map for.
123
+ * @param state - The {@link EditorState} to derive the indentation map from.
124
+ * @param unitWidth - The width of the editor's indent unit.
125
+ */
126
+ constructor(lines: Set<Line>, state: EditorState, unitWidth: number) {
127
+ this.lines = lines;
128
+ this.state = state;
129
+ this.map = new Map();
130
+ this.unitWidth = unitWidth;
131
+
132
+ for (const line of this.lines) {
133
+ this.add(line);
134
+ }
135
+
136
+ if (this.state.facet(indentationMarkerConfig).highlightActiveBlock) {
137
+ this.findAndSetActiveLines();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Checks if the indentation map has an entry for the given line.
143
+ *
144
+ * @param line - The {@link Line} or line number to check for.
145
+ */
146
+ has(line: Line | number): boolean {
147
+ return this.map.has(typeof line === 'number' ? line : line.number);
148
+ }
149
+
150
+ /**
151
+ * Returns the {@link IndentEntry} for the given line.
152
+ *
153
+ * Note that this function will throw an error if the line does not exist in the map.
154
+ *
155
+ * @param line - The {@link Line} or line number to get the entry for.
156
+ */
157
+ get(line: Line | number): IndentEntry {
158
+ const entry = this.map.get(typeof line === 'number' ? line : line.number);
159
+
160
+ if (!entry) {
161
+ throw new Error('Line not found in indentation map');
162
+ }
163
+
164
+ return entry;
165
+ }
166
+
167
+ /**
168
+ * Sets the {@link IndentEntry} for the given line.
169
+ *
170
+ * @param line - The {@link Line} to set the entry for.
171
+ * @param col - The visual beginning whitespace width of the line.
172
+ * @param level - The indentation level of the line.
173
+ */
174
+ private set(line: Line, col: number, level: number) {
175
+ const empty = !line.text.trim().length;
176
+ const entry: IndentEntry = { line, col, level, empty };
177
+ this.map.set(entry.line.number, entry);
178
+
179
+ return entry;
180
+ }
181
+
182
+ /**
183
+ * Adds a line to the indentation map.
184
+ *
185
+ * @param line - The {@link Line} to add to the map.
186
+ */
187
+ private add(line: Line) {
188
+ if (this.has(line)) {
189
+ return this.get(line);
190
+ }
191
+
192
+ // empty lines continue their indentation from surrounding lines
193
+ if (!line.length || !line.text.trim().length) {
194
+ // the very first line, if empty, is just ignored and set as a 0 indent level
195
+ if (line.number === 1) {
196
+ return this.set(line, 0, 0);
197
+ }
198
+
199
+ // if we're at the end, we'll just use the previous line's indentation
200
+ if (line.number === this.state.doc.lines) {
201
+ const prev = this.closestNonEmpty(line, -1);
202
+
203
+ return this.set(line, 0, prev.level);
204
+ }
205
+
206
+ const prev = this.closestNonEmpty(line, -1);
207
+ const next = this.closestNonEmpty(line, 1);
208
+
209
+ // if the next line ends the block, we'll use the previous line's indentation
210
+ if (prev.level >= next.level) {
211
+ return this.set(line, 0, prev.level);
212
+ }
213
+
214
+ // having an indent marker that starts from an empty line looks weird
215
+ if (prev.empty && prev.level === 0 && next.level !== 0) {
216
+ return this.set(line, 0, 0);
217
+ }
218
+
219
+ // if the next indentation level is greater than the previous,
220
+ // we'll only increment up to the next indentation level. this prevents
221
+ // a weirdly "backwards propagating" indentation.
222
+ if (next.level > prev.level) {
223
+ return this.set(line, 0, prev.level + 1);
224
+ }
225
+
226
+ // else, we default to the next line's indentation
227
+ return this.set(line, 0, next.level);
228
+ }
229
+
230
+ const col = numColumns(line.text, this.state.tabSize);
231
+ const level = Math.floor(col / this.unitWidth);
232
+
233
+ return this.set(line, col, level);
234
+ }
235
+
236
+ /**
237
+ * Finds the closest non-empty line, starting from the given line.
238
+ *
239
+ * @param from - The {@link Line} to start from.
240
+ * @param dir - The direction to search in. Either `1` or `-1`.
241
+ */
242
+ private closestNonEmpty(from: Line, dir: -1 | 1) {
243
+ let lineNo = from.number + dir;
244
+
245
+ while (dir === -1 ? lineNo >= 1 : lineNo <= this.state.doc.lines) {
246
+ if (this.has(lineNo)) {
247
+ const entry = this.get(lineNo);
248
+ if (!entry.empty) {
249
+ return entry;
250
+ }
251
+ }
252
+
253
+ // we can check if the line is empty, if it's not, we can
254
+ // just create a new entry for it and return it.
255
+ // this prevents us from hitting the beginning/end of the document unnecessarily.
256
+
257
+ const line = this.state.doc.line(lineNo);
258
+
259
+ if (line.text.trim().length) {
260
+ const col = numColumns(line.text, this.state.tabSize);
261
+ const level = Math.floor(col / this.unitWidth);
262
+
263
+ return this.set(line, col, level);
264
+ }
265
+
266
+ lineNo += dir;
267
+ }
268
+
269
+ // if we're here, we didn't find anything.
270
+ // that means we're at the beginning/end of the document,
271
+ // and the first/last line is empty.
272
+
273
+ const line = this.state.doc.line(dir === -1 ? 1 : this.state.doc.lines);
274
+
275
+ return this.set(line, 0, 0);
276
+ }
277
+
278
+ /**
279
+ * Finds the state's active block (via the current selection) and sets all
280
+ * the active indent level for the lines in the block.
281
+ */
282
+ private findAndSetActiveLines() {
283
+ const currentLine = getCurrentLine(this.state);
284
+
285
+ if (!this.has(currentLine)) {
286
+ return;
287
+ }
288
+
289
+ let current = this.get(currentLine);
290
+
291
+ // check if the current line is starting a new block, if yes, we want to
292
+ // start from inside the block.
293
+ if (this.has(current.line.number + 1)) {
294
+ const next = this.get(current.line.number + 1);
295
+ if (next.level > current.level) {
296
+ current = next;
297
+ }
298
+ }
299
+
300
+ // same, but if the current line is ending a block
301
+ if (this.has(current.line.number - 1)) {
302
+ const prev = this.get(current.line.number - 1);
303
+ if (prev.level > current.level) {
304
+ current = prev;
305
+ }
306
+ }
307
+
308
+ if (current.level === 0) {
309
+ return;
310
+ }
311
+
312
+ current.active = current.level;
313
+
314
+ let start: number;
315
+ let end: number;
316
+
317
+ // iterate to the start of the block
318
+ for (start = current.line.number; start > 1; start--) {
319
+ if (!this.has(start - 1)) {
320
+ continue;
321
+ }
322
+
323
+ const prev = this.get(start - 1);
324
+
325
+ if (prev.level < current.level) {
326
+ break;
327
+ }
328
+
329
+ prev.active = current.level;
330
+ }
331
+
332
+ // iterate to the end of the block
333
+ for (end = current.line.number; end < this.state.doc.lines; end++) {
334
+ if (!this.has(end + 1)) {
335
+ continue;
336
+ }
337
+
338
+ const next = this.get(end + 1);
339
+
340
+ if (next.level < current.level) {
341
+ break;
342
+ }
343
+
344
+ next.active = current.level;
345
+ }
346
+ }
347
+ }
348
+
349
+ // CSS classes:
350
+ // - .cm-indent-markers
351
+
352
+ // CSS variables:
353
+ // - --indent-marker-bg-part
354
+ // - --indent-marker-active-bg-part
355
+
356
+ /** Color of inactive indent markers. Based on RUI's var(--background-higher) */
357
+ const MARKER_COLOR_LIGHT = '#F0F1F2';
358
+ const MARKER_COLOR_DARK = '#2B3245';
359
+
360
+ /** Color of active indent markers. Based on RUI's var(--background-highest) */
361
+ const MARKER_COLOR_ACTIVE_LIGHT = '#E4E5E6';
362
+ const MARKER_COLOR_ACTIVE_DARK = '#3C445C';
363
+
364
+ /** Thickness of indent markers. Probably should be integer pixel values. */
365
+ const MARKER_THICKNESS = '1px';
366
+
367
+ const indentTheme = EditorView.baseTheme({
368
+ '&light': {
369
+ '--indent-marker-bg-color': MARKER_COLOR_LIGHT,
370
+ '--indent-marker-active-bg-color': MARKER_COLOR_ACTIVE_LIGHT
371
+ },
372
+
373
+ '&dark': {
374
+ '--indent-marker-bg-color': MARKER_COLOR_DARK,
375
+ '--indent-marker-active-bg-color': MARKER_COLOR_ACTIVE_DARK
376
+ },
377
+
378
+ '.cm-line': {
379
+ position: 'relative'
380
+ },
381
+
382
+ // this pseudo-element is used to draw the indent markers,
383
+ // while still allowing the line to have its own background.
384
+ '.cm-indent-markers::before': {
385
+ content: '""',
386
+ position: 'absolute',
387
+ top: 0,
388
+ left: '2px',
389
+ right: 0,
390
+ bottom: 0,
391
+ background: 'var(--indent-markers)',
392
+ pointerEvents: 'none'
393
+ // zIndex: '-1',
394
+ }
395
+ });
396
+
397
+ function createGradient(
398
+ markerCssProperty: string,
399
+ indentWidth: number,
400
+ startOffset: number,
401
+ columns: number
402
+ ) {
403
+ const gradient = `repeating-linear-gradient(to right, var(${markerCssProperty}) 0 ${MARKER_THICKNESS}, transparent ${MARKER_THICKNESS} ${indentWidth}ch)`;
404
+ // Subtract one pixel from the background width to get rid of artifacts of pixel rounding
405
+ return `${gradient} ${startOffset * indentWidth}.5ch/calc(${indentWidth * columns}ch - 1px) no-repeat`;
406
+ }
407
+
408
+ function makeBackgroundCSS(
409
+ entry: IndentEntry,
410
+ indentWidth: number,
411
+ hideFirstIndent: boolean
412
+ ): string {
413
+ const { level, active } = entry;
414
+ if (hideFirstIndent && level === 0) {
415
+ return '';
416
+ }
417
+ const startAt = hideFirstIndent ? 1 : 0;
418
+ const backgrounds: string[] = [];
419
+
420
+ if (active !== undefined) {
421
+ const markersBeforeActive = active - startAt - 1;
422
+ if (markersBeforeActive > 0) {
423
+ backgrounds.push(
424
+ createGradient('--indent-marker-bg-color', indentWidth, startAt, markersBeforeActive)
425
+ );
426
+ }
427
+ backgrounds.push(createGradient('--indent-marker-active-bg-color', indentWidth, active - 1, 1));
428
+ if (active !== level) {
429
+ backgrounds.push(
430
+ createGradient('--indent-marker-bg-color', indentWidth, active, level - active)
431
+ );
432
+ }
433
+ } else {
434
+ backgrounds.push(
435
+ createGradient('--indent-marker-bg-color', indentWidth, startAt, level - startAt)
436
+ );
437
+ }
438
+
439
+ return backgrounds.join(',');
440
+ }
441
+
442
+ interface IndentationMarkerConfiguration {
443
+ /**
444
+ * Determines whether active block marker is styled differently.
445
+ */
446
+ highlightActiveBlock?: boolean;
447
+
448
+ /**
449
+ * Determines whether markers in the first column are omitted.
450
+ */
451
+ hideFirstIndent?: boolean;
452
+ }
453
+
454
+ export const indentationMarkerConfig = Facet.define<
455
+ IndentationMarkerConfiguration,
456
+ Required<IndentationMarkerConfiguration>
457
+ >({
458
+ combine(configs) {
459
+ return combineConfig(configs, {
460
+ highlightActiveBlock: true,
461
+ hideFirstIndent: false
462
+ });
463
+ }
464
+ });
465
+
466
+ class IndentMarkersClass implements PluginValue {
467
+ view: EditorView;
468
+ decorations!: DecorationSet;
469
+
470
+ private unitWidth: number;
471
+ private currentLineNumber: number;
472
+
473
+ constructor(view: EditorView) {
474
+ this.view = view;
475
+ this.unitWidth = getIndentUnit(view.state);
476
+ this.currentLineNumber = getCurrentLine(view.state).number;
477
+ this.generate(view.state);
478
+ }
479
+
480
+ update(update: ViewUpdate) {
481
+ const unitWidth = getIndentUnit(update.state);
482
+ const unitWidthChanged = unitWidth !== this.unitWidth;
483
+ if (unitWidthChanged) {
484
+ this.unitWidth = unitWidth;
485
+ }
486
+ const lineNumber = getCurrentLine(update.state).number;
487
+ const lineNumberChanged = lineNumber !== this.currentLineNumber;
488
+ this.currentLineNumber = lineNumber;
489
+ const activeBlockUpdateRequired =
490
+ update.state.facet(indentationMarkerConfig).highlightActiveBlock && lineNumberChanged;
491
+ if (
492
+ update.docChanged ||
493
+ update.viewportChanged ||
494
+ unitWidthChanged ||
495
+ activeBlockUpdateRequired
496
+ ) {
497
+ this.generate(update.state);
498
+ }
499
+ }
500
+
501
+ private generate(state: EditorState) {
502
+ const builder = new RangeSetBuilder<Decoration>();
503
+
504
+ const lines = getVisibleLines(this.view, state);
505
+ const map = new IndentationMap(lines, state, this.unitWidth);
506
+ const { hideFirstIndent } = state.facet(indentationMarkerConfig);
507
+
508
+ for (const line of lines) {
509
+ const entry = map.get(line.number);
510
+
511
+ if (!entry?.level) {
512
+ continue;
513
+ }
514
+
515
+ const backgrounds = makeBackgroundCSS(entry, this.unitWidth, hideFirstIndent);
516
+
517
+ builder.add(
518
+ line.from,
519
+ line.from,
520
+ Decoration.line({
521
+ class: 'cm-indent-markers',
522
+ attributes: {
523
+ style: `--indent-markers: ${backgrounds}`
524
+ }
525
+ })
526
+ );
527
+ }
528
+
529
+ this.decorations = builder.finish();
530
+ }
531
+ }
532
+
533
+ export function indentationMarkers(config: IndentationMarkerConfiguration = {}): Extension {
534
+ return [
535
+ indentationMarkerConfig.of(config),
536
+ indentTheme,
537
+ ViewPlugin.fromClass(IndentMarkersClass, {
538
+ decorations: (v) => v.decorations
539
+ })
540
+ ];
541
+ }
src/lib/CodeMirrorSearch/CodeMirrorSearch.svelte ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- references are:
2
+ https://github.com/codemirror/search
3
+ https://codemirror.net/docs/ref/#search
4
+ -->
5
+ <script lang="ts">
6
+ import type { EditorView } from '@codemirror/view';
7
+
8
+ import { onMount, createEventDispatcher } from 'svelte';
9
+ import {
10
+ SearchQuery,
11
+ findPrevious,
12
+ findNext,
13
+ setSearchQuery,
14
+ replaceNext,
15
+ replaceAll
16
+ } from '@codemirror/search';
17
+
18
+ import IconCaretV2 from '../Icons/IconCaretV2.svelte';
19
+ import IconArrowLeft from '../Icons/IconArrowLeft.svelte';
20
+ import IconCross from '../Icons/IconCross.svelte';
21
+ import IconReplace from '../Icons/IconReplace.svelte';
22
+ import IconReplaceAll from '../Icons/IconReplaceAll.svelte';
23
+
24
+ export let view: EditorView;
25
+
26
+ let el: HTMLDivElement;
27
+ let searchTxtEl: HTMLInputElement;
28
+ let searchTxt = '';
29
+ let replaceTxt = '';
30
+ let isCaseSensitive = false;
31
+ let isRegexp = false;
32
+ let isWholeWord = false;
33
+ let isReplacePanelOpen = false;
34
+
35
+ const dispatch = createEventDispatcher<{ close: void }>();
36
+
37
+ $: query = new SearchQuery({
38
+ search: searchTxt,
39
+ caseSensitive: isCaseSensitive,
40
+ wholeWord: isWholeWord,
41
+ regexp: isRegexp,
42
+ replace: replaceTxt
43
+ });
44
+
45
+ $: query, search();
46
+
47
+ // positionSearchPanel is no longer needed since we use CSS for top-right positioning.
48
+
49
+ function destroyDefaultPanel() {
50
+ // hack to prevent default CodeMirror search box appearing
51
+ // when you use {findNext, etc...} from @codemirror/search, it tries to automatically create default CodeMirror search box
52
+ const el = document.querySelector('.codemirror-wrapper .cm-search');
53
+ el?.parentElement?.removeChild(el);
54
+ }
55
+
56
+ function getSelectedText(editorView: EditorView) {
57
+ const state = editorView.state;
58
+ const selection = state.selection;
59
+ const selectedText = selection.ranges
60
+ .map((range) => state.doc.sliceString(range.from, range.to))
61
+ .join('\n');
62
+ return selectedText;
63
+ }
64
+
65
+ function search() {
66
+ destroyDefaultPanel();
67
+ view?.dispatch({ effects: setSearchQuery.of(query) });
68
+ if (searchTxt && view) {
69
+ findPrevious(view);
70
+ findNext(view);
71
+ } else {
72
+ reset();
73
+ }
74
+ }
75
+
76
+ function reset() {
77
+ // send and empty query
78
+ view?.dispatch({
79
+ effects: setSearchQuery.of(
80
+ new SearchQuery({
81
+ search: ''
82
+ })
83
+ )
84
+ });
85
+ }
86
+
87
+ function onKeyDownWindow(e: KeyboardEvent) {
88
+ const { ctrlKey, metaKey, key, shiftKey } = e;
89
+ const cmdKey = metaKey || ctrlKey;
90
+ const isOpenShortcut = key === 'f3' || (cmdKey && key === 'f');
91
+ const isNextOrPrevShortcut = cmdKey && key === 'g';
92
+ const isCloseShortcut = key === 'Escape' || key === 'Esc';
93
+ if (isOpenShortcut) {
94
+ e.preventDefault();
95
+ searchTxt = getSelectedText(view);
96
+ searchTxtEl.focus();
97
+ } else if (isNextOrPrevShortcut) {
98
+ e.preventDefault();
99
+ shiftKey ? findPrevious(view) : findNext(view);
100
+ } else if (isCloseShortcut) {
101
+ dispatch('close');
102
+ }
103
+ }
104
+
105
+ function onKeyDownEl(e: KeyboardEvent) {
106
+ const { key } = e;
107
+ const isNextShortcut = key === 'Enter';
108
+ if (isNextShortcut) {
109
+ e.preventDefault();
110
+ findNext(view);
111
+ }
112
+ }
113
+
114
+ onMount(() => {
115
+ searchTxt = getSelectedText(view);
116
+ searchTxtEl.focus();
117
+
118
+ // Switch from absolute to fixed and preserve position
119
+ if (el) {
120
+ const rect = el.getBoundingClientRect();
121
+ el.style.position = 'fixed';
122
+ el.style.top = rect.top + 'px';
123
+ el.style.left = rect.left + 'px';
124
+ el.style.right = 'auto'; // Remove right if present
125
+ el.classList.remove('absolute');
126
+ el.classList.add('fixed');
127
+ }
128
+
129
+ return reset;
130
+ });
131
+ </script>
132
+
133
+ <svelte:window on:keydown={onKeyDownWindow} />
134
+
135
+ <div
136
+ bind:this={el}
137
+ class="absolute top-0 right-0 z-20 rounded-sm border border-gray-500 bg-white dark:bg-gray-900 dark:text-white"
138
+ on:keydown={onKeyDownEl}
139
+ >
140
+ <div class="flex border-b border-gray-500">
141
+ <button
142
+ type="button"
143
+ class="border-r border-gray-500 px-0.5"
144
+ on:click={() => (isReplacePanelOpen = !isReplacePanelOpen)}
145
+ >
146
+ <IconCaretV2 classNames="h-full {isReplacePanelOpen ? '' : '-rotate-90'}" />
147
+ </button>
148
+ <div class="my-1">
149
+ <div class="flex items-center">
150
+ <div class="flex w-[250px] items-center bg-gray-100 dark:bg-gray-800">
151
+ <input
152
+ type="text"
153
+ class="w-full border-0 bg-transparent py-1 pr-[4.3rem] pl-2 text-sm"
154
+ bind:value={searchTxt}
155
+ bind:this={searchTxtEl}
156
+ />
157
+ <!-- buttons: case sensetive, full word, regex -->
158
+ <div
159
+ class="-ml-[4.3rem] flex w-16 items-center justify-between gap-0.5 font-mono select-none"
160
+ >
161
+ <button
162
+ type="button"
163
+ title="Match Case"
164
+ class="rounded-sm px-0.5 text-sm {isCaseSensitive ? 'bg-black text-white' : ''}"
165
+ on:click={() => (isCaseSensitive = !isCaseSensitive)}
166
+ >
167
+ Aa
168
+ </button>
169
+ <button
170
+ type="button"
171
+ title="Match Whole Word"
172
+ class="rounded-sm px-0.5 text-sm underline {isWholeWord ? 'bg-black text-white' : ''}"
173
+ on:click={() => (isWholeWord = !isWholeWord)}
174
+ >
175
+ ab
176
+ </button>
177
+ <button
178
+ type="button"
179
+ title="Use Regular Expression"
180
+ class="rounded-sm px-0.5 text-sm {isRegexp ? 'bg-black text-white' : ''}"
181
+ on:click={() => (isRegexp = !isRegexp)}
182
+ >
183
+ re
184
+ </button>
185
+ </div>
186
+ </div>
187
+ <!-- buttons: find next, find previous, close search panel -->
188
+ <div class="mx-2 flex items-center gap-0.5 select-none">
189
+ <button type="button" title="Next Match" on:click={() => findNext(view)}>
190
+ <IconArrowLeft classNames="-rotate-90" />
191
+ </button>
192
+ <button type="button" title="Previous Match" on:click={() => findPrevious(view)}>
193
+ <IconArrowLeft classNames="rotate-90" />
194
+ </button>
195
+ <button type="button" title="Close" on:click={() => dispatch('close')}>
196
+ <IconCross />
197
+ </button>
198
+ </div>
199
+ </div>
200
+ {#if isReplacePanelOpen}
201
+ <div class="mt-1 flex items-center">
202
+ <input
203
+ type="text"
204
+ bind:value={replaceTxt}
205
+ class="w-[250px] border-0 bg-gray-100 py-1 pl-2 text-sm dark:bg-gray-800"
206
+ />
207
+ <div class="ml-1 flex items-center gap-0.5 select-none">
208
+ <button type="button" title="Replace" on:click={() => replaceNext(view)}>
209
+ <IconReplace classNames="text-base" />
210
+ </button>
211
+ <button type="button" title="Replace All" on:click={() => replaceAll(view)}>
212
+ <IconReplaceAll classNames="text-base" />
213
+ </button>
214
+ </div>
215
+ </div>
216
+ {/if}
217
+ </div>
218
+ </div>
219
+ </div>
src/lib/CopyButton/CopyButton.svelte ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { SvelteComponent } from 'svelte';
3
+
4
+ import { onDestroy } from 'svelte';
5
+
6
+ import IconCopy from '../Icons/IconCopy.svelte';
7
+ import { tooltip } from '../utils/tooltip';
8
+
9
+ export const hydrate = true;
10
+
11
+ export let classNames = '';
12
+ export let label = '';
13
+ export let noIcon = false;
14
+ export let icon: typeof SvelteComponent | undefined = undefined;
15
+ export let style: 'blank' | 'button' | 'button-clear' | 'text' = 'text';
16
+ export let title = '';
17
+ export let value: string;
18
+ export let successType: 'tooltip' | 'text' = 'tooltip';
19
+ export let successText: string = 'Copied';
20
+
21
+ let isSuccess = false;
22
+ let timeout: any;
23
+
24
+ onDestroy(() => {
25
+ if (timeout) {
26
+ clearTimeout(timeout);
27
+ }
28
+ });
29
+
30
+ function handleClick() {
31
+ copyToClipboard(value);
32
+ isSuccess = true;
33
+ if (timeout) {
34
+ clearTimeout(timeout);
35
+ }
36
+ timeout = setTimeout(() => {
37
+ isSuccess = false;
38
+ }, 1000);
39
+ }
40
+
41
+ function copyToClipboard(value: string): void {
42
+ const textArea = document.createElement('textarea');
43
+ document.body.appendChild(textArea);
44
+ textArea.value = value;
45
+ textArea.select();
46
+ document.execCommand('copy');
47
+ document.body.removeChild(textArea);
48
+ }
49
+ </script>
50
+
51
+ {#key isSuccess}
52
+ <button
53
+ class="border-gray-500 {classNames}
54
+ {style !== 'blank' ? 'inline-flex cursor-pointer items-center text-sm focus:outline-hidden' : ''}
55
+ {['button', 'button-clear'].includes(style) ? 'bg-white dark:bg-gray-900' : ''}
56
+ {style === 'text' ? 'mx-0.5' : ''}
57
+ {style === 'button' ? 'btn' : ''}
58
+ {style === 'button-clear' ? 'rounded-md border p-1 shadow-xs' : ''}
59
+ {!isSuccess && ['button-clear', 'text'].includes(style) ? 'text-gray-600' : ''}
60
+ {isSuccess && style !== 'blank' ? 'text-green-500' : ''}
61
+ "
62
+ on:click|preventDefault|stopPropagation={handleClick}
63
+ title={title || label || 'Copy to clipboard'}
64
+ type="button"
65
+ use:tooltip={{
66
+ content: successText,
67
+ disabled: successType !== 'tooltip' || !isSuccess,
68
+ showOn: 'always',
69
+ opts: { placement: 'bottom' }
70
+ }}
71
+ >
72
+ {#if !noIcon}
73
+ <svelte:component this={icon ?? IconCopy} />
74
+ {/if}
75
+ {#if label}
76
+ {#if isSuccess && successType === 'text'}
77
+ <span class="ml-1.5">{successText}</span>
78
+ {:else}
79
+ <span class="ml-1.5 {style === 'text' ? 'underline' : ''}">
80
+ {label}
81
+ </span>
82
+ {/if}
83
+ {/if}
84
+ </button>
85
+ {/key}
src/lib/Icons/IconArrowLeft.svelte ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path d="M14 26l1.41-1.41L7.83 17H28v-2H7.83l7.58-7.59L14 6L4 16l10 10z" fill="currentColor" />
18
+ </svg>
src/lib/Icons/IconCaretV2.svelte ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 12 7"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path d="M1 1L6 6L11 1" stroke="currentColor" />
14
+ </svg>
src/lib/Icons/IconCodeGeneration.svelte ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ fill-rule="evenodd"
19
+ clip-rule="evenodd"
20
+ d="M24.4 16.83c-.4-1.74-2.9-1.73-3.28.02a5.58 5.58 0 0 1-4.43 4.4c-1.82.31-1.82 2.92 0 3.24a5.55 5.55 0 0 1 4.43 4.4c.36 1.68 2.9 1.7 3.27.01a5.6 5.6 0 0 1 4.46-4.4c1.75-.31 1.75-2.95 0-3.26a5.68 5.68 0 0 1-4.46-4.4Z"
21
+ fill="currentColor"
22
+ fill-opacity=".5"
23
+ /><path
24
+ fill-rule="evenodd"
25
+ clip-rule="evenodd"
26
+ d="M7.06 7.94a1 1 0 0 1 1.42 0l3.44 3.44a1 1 0 0 1 0 1.42l-3.44 3.44a1 1 0 0 1-1.42-1.42L9.8 12.1 7.06 9.36a1 1 0 0 1 0-1.42Zm5.73 7.59a1 1 0 0 1 1-1h4.3a1 1 0 1 1 0 2h-4.3a1 1 0 0 1-1-1Z"
27
+ fill="currentColor"
28
+ fill-opacity=".8"
29
+ /><path
30
+ d="M5.83 3.84h14.2a2 2 0 0 1 2 2v7.73c.66-.1 1.36-.02 2 .23V5.84a4 4 0 0 0-4-4H5.82a4 4 0 0 0-4 4v13.32a4 4 0 0 0 4 4h7.35c-.06-.69.07-1.39.39-2H5.83a2 2 0 0 1-2-2V5.84c0-1.1.9-2 2-2Z"
31
+ fill="currentColor"
32
+ fill-opacity=".8"
33
+ />
34
+ </svg>
src/lib/Icons/IconCopy.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ fill="currentColor"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"
19
+ transform="translate(0)"
20
+ />
21
+ <path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)" /><rect
22
+ fill="none"
23
+ width="32"
24
+ height="32"
25
+ />
26
+ </svg>
src/lib/Icons/IconCross.svelte ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ focusable="false"
11
+ role="img"
12
+ width="1.1em"
13
+ height="1.1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M24 9.4L22.6 8L16 14.6L9.4 8L8 9.4l6.6 6.6L8 22.6L9.4 24l6.6-6.6l6.6 6.6l1.4-1.4l-6.6-6.6L24 9.4z"
19
+ fill="currentColor"
20
+ />
21
+ </svg>
src/lib/Icons/IconLineWrap.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 12 11"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M0.75 1.25H11.25M0.75 5H9C9.75 5 11.25 5.375 11.25 6.875C11.25 8.375 9.99975 8.75 9.375 8.75H6M6 8.75L7.5 7.25M6 8.75L7.5 10.25M0.75 8.75H3.75"
15
+ stroke="currentColor"
16
+ stroke-width="1.125"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
src/lib/Icons/IconReplace.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ viewBox="0 0 16 16"
8
+ height="1em"
9
+ width="1em"
10
+ preserveAspectRatio="xMidYMid meet"
11
+ ><path
12
+ fill="currentColor"
13
+ fill-rule="evenodd"
14
+ d="m3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7l-1.012 1.007l-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467c.393 0 .706-.168.94-.503c.236-.335.353-.78.353-1.333c0-.511-.1-.913-.301-1.207c-.201-.295-.488-.442-.86-.442c-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23a.49.49 0 0 1 .436.233c.104.154.155.368.155.643c0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27a.495.495 0 0 1-.411-.211a.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231c-.563 0-1.02-.178-1.369-.533c-.349-.355-.523-.813-.523-1.374c0-.648.186-1.158.56-1.53c.374-.376.875-.563 1.5-.563c.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298c-.19.196-.283.468-.283.816c0 .338.09.603.272.797c.182.191.431.287.749.287c.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z"
15
+ clip-rule="evenodd"
16
+ /></svg
17
+ >
src/lib/Icons/IconReplaceAll.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ viewBox="0 0 16 16"
8
+ height="1em"
9
+ width="1em"
10
+ preserveAspectRatio="xMidYMid meet"
11
+ ><path
12
+ fill="currentColor"
13
+ fill-rule="evenodd"
14
+ d="M11.6 2.677c.147-.31.356-.465.626-.465c.248 0 .44.118.573.353c.134.236.201.557.201.966c0 .443-.078.798-.235 1.067c-.156.268-.365.402-.627.402c-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169c.136 0 .24-.072.314-.216c.075-.145.113-.35.113-.615c0-.22-.035-.39-.104-.514c-.067-.124-.164-.187-.29-.187c-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662l1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5l.949-.944l.656.656l-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304c.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961c0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436a.368.368 0 0 1-.313.17a.276.276 0 0 1-.22-.095a.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63c0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47c-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167c.165-.049.315-.073.45-.073c.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388c0 .1.027.183.08.248a.276.276 0 0 0 .22.095a.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207a.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598c0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134c-.417 0-.751.14-1.001.422c-.249.28-.373.662-.373 1.148c0 .42.116.764.349 1.03c.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z"
15
+ clip-rule="evenodd"
16
+ /></svg
17
+ >
src/lib/Icons/IconRestart.svelte ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ role="img"
11
+ width="1em"
12
+ height="1em"
13
+ preserveAspectRatio="xMidYMid meet"
14
+ viewBox="0 0 12 12"
15
+ >
16
+ <path
17
+ d="M9.21866 6.82043C9.21866 7.4695 9.02989 8.10399 8.67622 8.64367C8.32255 9.18334 7.81986 9.60397 7.23173 9.85236C6.6436 10.1007 5.99643 10.1657 5.37207 10.0391C4.74771 9.91248 4.1742 9.59993 3.72406 9.14097C3.27393 8.68201 2.96738 8.09726 2.84319 7.46067C2.71899 6.82407 2.78273 6.16423 3.02635 5.56457C3.26996 4.96491 3.6825 4.45237 4.21181 4.09177C4.74112 3.73117 5.36341 3.5387 6 3.5387H7.98978L6.83621 4.7152L7.28746 5.17957L9.21866 3.21053L7.28746 1.24149L6.83621 1.70552L7.99074 2.88235H6C5.23609 2.88235 4.48934 3.11332 3.85417 3.54604C3.219 3.97876 2.72395 4.5938 2.43162 5.31339C2.13928 6.03298 2.06279 6.8248 2.21182 7.58871C2.36086 8.35263 2.72871 9.05433 3.26888 9.60508C3.80904 10.1558 4.49726 10.5309 5.24649 10.6828C5.99572 10.8348 6.77231 10.7568 7.47807 10.4587C8.18383 10.1607 8.78706 9.65593 9.21146 9.00831C9.63587 8.3607 9.86239 7.59931 9.86239 6.82043H9.21866Z"
18
+ fill="currentColor"
19
+ stroke="currentColor"
20
+ stroke-width="0.5"
21
+ />
22
+ </svg>
src/lib/Icons/IconSpin.svelte ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ fill="none"
11
+ focusable="false"
12
+ role="img"
13
+ width="1em"
14
+ height="1em"
15
+ preserveAspectRatio="xMidYMid meet"
16
+ viewBox="0 0 12 12"
17
+ >
18
+ <path
19
+ class="opacity-75"
20
+ fill-rule="evenodd"
21
+ clip-rule="evenodd"
22
+ d="M6 0C2.6862 0 0 2.6862 0 6H1.8C1.8 4.88609 2.2425 3.8178 3.03015 3.03015C3.8178 2.2425 4.88609 1.8 6 1.8V0ZM12 6C12 9.3138 9.3138 12 6 12V10.2C7.11391 10.2 8.1822 9.7575 8.96985 8.96985C9.7575 8.1822 10.2 7.11391 10.2 6H12Z"
23
+ fill="currentColor"
24
+ />
25
+ <path
26
+ class="opacity-25"
27
+ fill-rule="evenodd"
28
+ clip-rule="evenodd"
29
+ d="M3.03015 8.96985C3.8178 9.7575 4.88609 10.2 6 10.2V12C2.6862 12 0 9.3138 0 6H1.8C1.8 7.11391 2.2425 8.1822 3.03015 8.96985ZM7.60727 2.11971C7.0977 1.90864 6.55155 1.8 6 1.8V0C9.3138 0 12 2.6862 12 6H10.2C10.2 5.44845 10.0914 4.9023 9.88029 4.39273C9.66922 3.88316 9.35985 3.42016 8.96985 3.03015C8.57984 2.64015 8.11684 2.33078 7.60727 2.11971Z"
30
+ fill="currentColor"
31
+ />
32
+ </svg>
src/lib/JsonEditor/JsonEditor.svelte ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CodeMirror from '$lib/CodeMirror/CodeMirror.svelte';
3
+ import { EditorView, lineNumbers } from '@codemirror/view';
4
+ import CopyButton from '$lib/CopyButton/CopyButton.svelte';
5
+ import { javascript } from '@codemirror/lang-javascript';
6
+ import { linter, lintGutter, type Diagnostic } from '@codemirror/lint';
7
+ import LineWrapButton from '$lib/LineWrapButton/LineWrapButton.svelte';
8
+ import JSON5 from 'json5';
9
+ import type { FormattedChatTemplate } from '../ChatTemplateViewer/types';
10
+ import { getExampleHelloWorld } from '$lib/example-inputs/helloWorld';
11
+ import { onMount } from 'svelte';
12
+ import { getExampleToolUsage } from '$lib/example-inputs/toolUsage';
13
+ import { foldGutter } from '@codemirror/language';
14
+ import { getExampleReasoning } from '$lib/example-inputs/reasoning';
15
+ import { tooltip } from '$lib/utils/tooltip';
16
+ import IconRestart from '$lib/Icons/IconRestart.svelte';
17
+ import { createEventDispatcher } from 'svelte';
18
+ import { page } from '$app/stores';
19
+ import { transformInput } from '$lib/utils/transformInput';
20
+
21
+ export let content: Record<string, unknown> = {};
22
+ export let error = '';
23
+ export let selectedTemplate: FormattedChatTemplate | undefined = undefined;
24
+ export let selectedExampleInputId = '';
25
+
26
+ const dispatch = createEventDispatcher<{ exampleChange: void }>();
27
+
28
+ let value = JSON5.stringify(content, null, 2);
29
+ let wrapLines = true;
30
+ let exampleInputs: { id: string; label: string; content: Record<string, unknown> }[] = [];
31
+ let exampleValue = '';
32
+
33
+ async function handleUpdateEditor(e: CustomEvent<string>) {
34
+ const currentCode = e.detail;
35
+ try {
36
+ content = JSON5.parse(currentCode);
37
+ value = currentCode;
38
+ error = '';
39
+ } catch (e) {
40
+ console.error(e);
41
+ error = 'Error in input JSON';
42
+ }
43
+ }
44
+
45
+ function jsonLinter() {
46
+ return (view: EditorView): Diagnostic[] => {
47
+ const diagnostics: Diagnostic[] = [];
48
+ const text = view.state.doc.toString();
49
+
50
+ try {
51
+ // Attempt to parse the JSON5
52
+ JSON5.parse(text);
53
+ } catch (e) {
54
+ let pos = 0;
55
+ let errorMessage = '';
56
+
57
+ if (e && typeof e === 'object') {
58
+ errorMessage = e.message || '';
59
+ // Prefer JSON5 error properties if available
60
+ const line = e.lineNumber;
61
+ const column = e.columnNumber;
62
+ if (typeof line === 'number' && typeof column === 'number') {
63
+ // Convert line/column to character offset
64
+ let runningPos = 0;
65
+ let currentLine = 1;
66
+ for (let i = 0; i < text.length; i++) {
67
+ if (currentLine === line && runningPos === column - 1) {
68
+ pos = i;
69
+ break;
70
+ }
71
+ if (text[i] === '\n') {
72
+ currentLine++;
73
+ runningPos = 0;
74
+ } else {
75
+ runningPos++;
76
+ }
77
+ }
78
+ } else {
79
+ // Fallback: try to extract from message
80
+ const match = /at position (\d+)/.exec(errorMessage);
81
+ if (match) {
82
+ pos = parseInt(match[1], 10);
83
+ } else {
84
+ const lineMatch = /line (\d+) column (\d+)/.exec(errorMessage);
85
+ if (lineMatch) {
86
+ const l = parseInt(lineMatch[1], 10);
87
+ const c = parseInt(lineMatch[2], 10);
88
+ let runningPos = 0;
89
+ let currentLine = 1;
90
+ for (let i = 0; i < text.length; i++) {
91
+ if (currentLine === l && runningPos === c - 1) {
92
+ pos = i;
93
+ break;
94
+ }
95
+ if (text[i] === '\n') {
96
+ currentLine++;
97
+ runningPos = 0;
98
+ } else {
99
+ runningPos++;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ diagnostics.push({
107
+ from: Math.max(0, pos - 1),
108
+ to: Math.min(text.length, pos + 1),
109
+ severity: 'error',
110
+ message: `JSON Error: ${errorMessage}`
111
+ });
112
+ }
113
+
114
+ return diagnostics;
115
+ };
116
+ }
117
+
118
+ function handleExampleInputChange(e: Event) {
119
+ const target = e.target as HTMLSelectElement;
120
+ const selectedId = target.value;
121
+ const selectedExampleInput = exampleInputs.find(
122
+ (exampleInput) => exampleInput.id === selectedId
123
+ );
124
+ if (selectedExampleInput) {
125
+ selectedExampleInputId = selectedId;
126
+ content = selectedExampleInput.content;
127
+ value = JSON5.stringify(content, null, 2);
128
+ exampleValue = value;
129
+ dispatch('exampleChange');
130
+ }
131
+ }
132
+
133
+ onMount(() => {
134
+ if (selectedTemplate) {
135
+ const exampleHelloWorld = getExampleHelloWorld(selectedTemplate.template);
136
+ if (exampleHelloWorld) {
137
+ exampleInputs = [
138
+ ...exampleInputs,
139
+ {
140
+ id: 'hello-world',
141
+ label: 'hello world example',
142
+ content: transformInput(exampleHelloWorld, selectedTemplate.template)
143
+ }
144
+ ];
145
+ }
146
+
147
+ const exampleReasoning = getExampleReasoning(selectedTemplate.template);
148
+ if (exampleReasoning) {
149
+ exampleInputs = [
150
+ ...exampleInputs,
151
+ {
152
+ id: 'reasoning',
153
+ label: 'reasoning example',
154
+ content: transformInput(exampleReasoning, selectedTemplate.template)
155
+ }
156
+ ];
157
+ }
158
+
159
+ const exampleToolUsage = getExampleToolUsage(selectedTemplate.template);
160
+ if (exampleToolUsage) {
161
+ exampleInputs = [
162
+ ...exampleInputs,
163
+ {
164
+ id: 'tool-usage',
165
+ label: 'tool usage example',
166
+ content: transformInput(exampleToolUsage, selectedTemplate.template)
167
+ }
168
+ ];
169
+ }
170
+
171
+ const exampleFromQuery = $page.url.searchParams.get('example');
172
+ if (exampleFromQuery) {
173
+ const exampleInput = exampleInputs.find(
174
+ (exampleInput) => exampleInput.id === exampleFromQuery
175
+ );
176
+ if (exampleInput) {
177
+ content = exampleInput.content;
178
+ value = JSON5.stringify(content, null, 2);
179
+ exampleValue = value;
180
+ selectedExampleInputId = exampleInput.id;
181
+ return;
182
+ }
183
+ }
184
+
185
+ content = exampleInputs.at(-1)?.content ?? {};
186
+ selectedExampleInputId = exampleInputs.at(-1)?.id ?? '';
187
+ value = JSON5.stringify(content, null, 2);
188
+ exampleValue = value;
189
+ }
190
+ });
191
+ </script>
192
+
193
+ <div class="h-full overflow-scroll bg-white dark:bg-gray-900">
194
+ <div class="sticky top-0 z-10 bg-white dark:bg-gray-900">
195
+ <div
196
+ class="text-semibold flex items-center gap-x-2 border-b border-gray-500 bg-linear-to-r from-orange-200 to-white px-3 py-1.5 text-lg dark:from-orange-700 dark:to-orange-900 dark:text-gray-200"
197
+ >
198
+ JSON Input
199
+
200
+ {#if exampleInputs.length > 1}
201
+ <select
202
+ class="ml-auto rounded border px-1 py-0.5 text-sm"
203
+ on:change={handleExampleInputChange}
204
+ >
205
+ {#each exampleInputs as exampleInput}
206
+ <option value={exampleInput.id} selected={exampleInput.id === selectedExampleInputId}
207
+ >{exampleInput.label}</option
208
+ >
209
+ {/each}
210
+ </select>
211
+ {/if}
212
+ </div>
213
+ <div class="flex items-center border-b px-3 py-2">
214
+ <div class="ml-auto flex items-center gap-x-2">
215
+ <!-- reset button -->
216
+ {#if exampleValue && exampleValue !== value}
217
+ <button
218
+ class="relative inline-flex h-6! cursor-pointer items-center justify-center rounded-md border border-gray-500 bg-white p-0! px-1.5! text-sm shadow-xs focus:outline-hidden dark:bg-gray-900 dark:text-white [&_svg]:translate-x-px! [&_svg]:translate-y-px! [&_svg]:text-base!"
219
+ type="button"
220
+ on:click={() => {
221
+ value = exampleValue;
222
+ }}
223
+ use:tooltip={'Reset example to original'}
224
+ ><IconRestart classNames="dark:text-gray-200!" />
225
+ <span class="ml-1 text-sm select-none dark:text-gray-200!"> Reset </span>
226
+ </button>
227
+ {/if}
228
+
229
+ <CopyButton
230
+ label="Copy"
231
+ {value}
232
+ style="button-clear"
233
+ classNames="h-6! [&_svg]:text-[0.7rem]! px-1.5! text-black! dark:text-gray-200!"
234
+ />
235
+
236
+ <LineWrapButton
237
+ style="button-clear"
238
+ bind:wrapLines
239
+ classNames="[&_svg]:text-xs! size-6! p-0!"
240
+ />
241
+ </div>
242
+ </div>
243
+ </div>
244
+ <CodeMirror
245
+ {value}
246
+ on:change={handleUpdateEditor}
247
+ extensions={[
248
+ lineNumbers(),
249
+ javascript({ jsx: false, typescript: false }),
250
+ linter(jsonLinter()),
251
+ lintGutter(),
252
+ foldGutter(),
253
+ ...[wrapLines ? [EditorView.lineWrapping] : []]
254
+ ]}
255
+ />
256
+ </div>
src/lib/LineWrapButton/LineWrapButton.svelte ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { tooltip } from '$lib/utils/tooltip';
3
+ import IconLineWrap from '../Icons/IconLineWrap.svelte';
4
+
5
+ export const hydrate = true;
6
+
7
+ export let classNames = '';
8
+ export let wrapLines = false;
9
+ export let style: 'blank' | 'button' | 'button-clear' | 'text' = 'text';
10
+
11
+ function toggleWrapLines() {
12
+ wrapLines = !wrapLines;
13
+ }
14
+
15
+ function onKeyDown(e: KeyboardEvent) {
16
+ const { key, code, altKey } = e;
17
+ // support QWERTY & AZERTY (key "Â" for mac os AZERTY)
18
+ if (((key === 'z' || code === 'KeyZ') && altKey) || key === 'Â') {
19
+ e.preventDefault();
20
+ toggleWrapLines();
21
+ }
22
+ }
23
+ </script>
24
+
25
+ <svelte:window on:keydown={onKeyDown} />
26
+
27
+ {#key wrapLines}
28
+ <button
29
+ class="border-gray-500 {classNames}
30
+ {style !== 'blank'
31
+ ? 'inline-flex cursor-pointer items-center justify-center text-sm focus:outline-hidden'
32
+ : ''}
33
+ {['button', 'button-clear'].includes(style) ? 'bg-white dark:bg-gray-900 dark:text-white' : ''}
34
+ {style === 'text' ? 'mx-0.5' : ''}
35
+ {style === 'button' ? 'btn' : ''}
36
+ {style === 'button-clear' ? 'rounded-md border p-1 shadow-xs' : ''}
37
+ "
38
+ type="button"
39
+ on:click={toggleWrapLines}
40
+ use:tooltip={wrapLines ? 'Unwrap lines' : 'Wrap lines'}
41
+ ><IconLineWrap classNames={wrapLines ? 'opacity-100' : 'opacity-40'} /></button
42
+ >
43
+ {/key}
src/lib/OutputViewer/OutputViewer.svelte ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CodeMirror from '$lib/CodeMirror/CodeMirror.svelte';
3
+ import CopyButton from '$lib/CopyButton/CopyButton.svelte';
4
+ import LineWrapButton from '$lib/LineWrapButton/LineWrapButton.svelte';
5
+ import { EditorView } from '@codemirror/view';
6
+
7
+ export let content = '';
8
+ export let error;
9
+
10
+ let wrapLines = true;
11
+ </script>
12
+
13
+ <div class="h-full overflow-scroll bg-white dark:bg-gray-900">
14
+ <div class="sticky top-0 z-10 bg-white dark:bg-gray-900">
15
+ <div
16
+ class="text-semibold flex items-center gap-x-2 border-b border-gray-500 bg-linear-to-r from-purple-200 to-white px-3 py-1.5 text-lg dark:from-purple-700 dark:to-purple-900 dark:text-gray-200"
17
+ >
18
+ Rendered Output
19
+ </div>
20
+ {#if error}
21
+ <div class="alert alert-error">
22
+ {error}
23
+ </div>
24
+ {:else}
25
+ <div class="flex items-center border-b px-3 py-2">
26
+ <div class="ml-auto flex items-center gap-x-2">
27
+ <CopyButton
28
+ label="Copy"
29
+ value={content}
30
+ style="button-clear"
31
+ classNames="h-6! [&_svg]:text-[0.7rem]! px-1.5! text-black! dark:text-gray-200!"
32
+ />
33
+
34
+ <LineWrapButton
35
+ style="button-clear"
36
+ bind:wrapLines
37
+ classNames="[&_svg]:text-xs! size-6! p-0!"
38
+ />
39
+ </div>
40
+ </div>
41
+ {/if}
42
+ </div>
43
+ {#if !error}
44
+ <CodeMirror
45
+ value={content}
46
+ readonly
47
+ extensions={[wrapLines ? [EditorView.lineWrapping] : []]}
48
+ />
49
+ {/if}
50
+ </div>
src/lib/example-inputs/helloWorld.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Template } from '@huggingface/jinja';
2
+ import { transformInput } from '$lib/utils/transformInput';
3
+
4
+ const variations = {
5
+ variation1_with_system_prompt: {
6
+ description: 'Variation with system prompt',
7
+ example: {
8
+ messages: [
9
+ {
10
+ role: 'system',
11
+ content: 'You are a helpful assistant.'
12
+ },
13
+ {
14
+ role: 'user',
15
+ content: 'Hello, how are you?'
16
+ },
17
+ {
18
+ role: 'assistant',
19
+ content: "I'm doing great. How can I help you today?"
20
+ },
21
+ {
22
+ role: 'user',
23
+ content: 'Can you tell me a joke?'
24
+ }
25
+ ],
26
+ add_generation_prompt: true
27
+ }
28
+ },
29
+ variation2_without_system_prompt: {
30
+ description: 'Variation without system prompt',
31
+ example: {
32
+ messages: [
33
+ {
34
+ role: 'user',
35
+ content: 'Hello, how are you?'
36
+ },
37
+ {
38
+ role: 'assistant',
39
+ content: "I'm doing great. How can I help you today?"
40
+ },
41
+ {
42
+ role: 'user',
43
+ content: 'Can you tell me a joke?'
44
+ }
45
+ ],
46
+ add_generation_prompt: true
47
+ }
48
+ }
49
+ };
50
+
51
+ export function getExampleHelloWorld(templateStr: string): Record<string, unknown> | undefined {
52
+ const template = new Template(templateStr);
53
+ const variationSystemPrompt = variations.variation1_with_system_prompt.example;
54
+ const variationSystemPromptRendered = template.render(
55
+ transformInput(variationSystemPrompt, templateStr)
56
+ );
57
+ if (variationSystemPromptRendered.includes('You are a helpful assistant.')) {
58
+ return variations.variation1_with_system_prompt.example;
59
+ }
60
+ return variations.variation2_without_system_prompt.example;
61
+ }
src/lib/example-inputs/reasoning.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { transformInput } from '$lib/utils/transformInput';
2
+ import { Template } from '@huggingface/jinja';
3
+
4
+ const variations = {
5
+ variation1_with_system_prompt: {
6
+ description: 'Variation with system prompt',
7
+ example: {
8
+ messages: [
9
+ {
10
+ role: 'system',
11
+ content: 'You are a helpful assistant.'
12
+ },
13
+ {
14
+ role: 'user',
15
+ content: 'What is the capital of France?'
16
+ },
17
+ {
18
+ role: 'assistant',
19
+ content:
20
+ '<think>The user is asking for the capital of France. This is a factual question. I know this information.</think>The capital of France is Paris.'
21
+ },
22
+ {
23
+ role: 'user',
24
+ content: 'What about Chile?'
25
+ }
26
+ ],
27
+ add_generation_prompt: true
28
+ }
29
+ },
30
+ variation2_without_system_prompt: {
31
+ description: 'Variation without system prompt',
32
+ example: {
33
+ messages: [
34
+ {
35
+ role: 'user',
36
+ content: 'What is the capital of France?'
37
+ },
38
+ {
39
+ role: 'assistant',
40
+ content:
41
+ '<think>The user is asking for the capital of France. This is a factual question. I know this information.</think>The capital of France is Paris.'
42
+ },
43
+ {
44
+ role: 'user',
45
+ content: 'What about Chile?'
46
+ }
47
+ ],
48
+ add_generation_prompt: true
49
+ }
50
+ }
51
+ };
52
+
53
+ export function getExampleReasoning(templateStr: string): Record<string, unknown> | undefined {
54
+ if (!templateStr.includes('think>')) {
55
+ return;
56
+ }
57
+ const template = new Template(templateStr);
58
+ const variationSystemPrompt = variations.variation1_with_system_prompt.example;
59
+ const variationSystemPromptRendered = template.render(
60
+ transformInput(variationSystemPrompt, templateStr)
61
+ );
62
+ if (variationSystemPromptRendered.includes('You are a helpful assistant.')) {
63
+ return variations.variation1_with_system_prompt.example;
64
+ }
65
+ return variations.variation2_without_system_prompt.example;
66
+ }
src/lib/example-inputs/toolUsage.ts ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { transformInput } from '$lib/utils/transformInput';
2
+ import { Template } from '@huggingface/jinja';
3
+
4
+ const variations = {
5
+ variation1_qwen_xml_style: {
6
+ description:
7
+ "This variation reflects how Qwen-like models might structure tool definitions in the system message using XML-like tags and how tool responses are often wrapped. The assistant's tool invocation uses a standard `tool_calls` array which the template would then format into the model's expected string.",
8
+ example: {
9
+ messages: [
10
+ {
11
+ role: 'system',
12
+ content:
13
+ 'You are a helpful assistant that can use tools to get information for the user.\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{"name": "get_weather", "description": "Get current weather information for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature to use"}}, "required": ["location"]}}\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags.'
14
+ },
15
+ {
16
+ role: 'user',
17
+ content: "What's the weather like in New York?"
18
+ },
19
+ {
20
+ role: 'assistant',
21
+ content:
22
+ "<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI'll check the current weather in New York for you.",
23
+ tool_calls: [
24
+ {
25
+ function: {
26
+ name: 'get_weather',
27
+ arguments: {
28
+ location: 'New York',
29
+ unit: 'celsius'
30
+ }
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ role: 'user',
37
+ content:
38
+ '<tool_response>\n{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}\n</tool_response>'
39
+ },
40
+ {
41
+ role: 'assistant',
42
+ content:
43
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
44
+ },
45
+ {
46
+ role: 'user',
47
+ content: 'Thanks! What about Boston?'
48
+ }
49
+ ],
50
+ tools: [
51
+ {
52
+ name: 'get_weather',
53
+ description: 'Get current weather information for a location',
54
+ parameters: {
55
+ type: 'object',
56
+ properties: {
57
+ location: {
58
+ type: 'string',
59
+ description: 'The city and state, e.g. San Francisco, CA'
60
+ },
61
+ unit: {
62
+ type: 'string',
63
+ enum: ['celsius', 'fahrenheit'],
64
+ description: 'The unit of temperature to use'
65
+ }
66
+ },
67
+ required: ['location']
68
+ }
69
+ }
70
+ ],
71
+ add_generation_prompt: true
72
+ }
73
+ },
74
+ variation3_deepseek_special_tags_style: {
75
+ description:
76
+ 'This variation reflects DeepSeek-like models using specialized tags for tool calls. The `tool_calls` array in the assistant message would contain arguments as a JSON string, which the template then formats with specific tags and markdown.',
77
+ example: {
78
+ messages: [
79
+ {
80
+ role: 'system',
81
+ content: 'You are a helpful assistant.'
82
+ },
83
+ {
84
+ role: 'user',
85
+ content: "What's the weather like in New York?"
86
+ },
87
+ {
88
+ role: 'assistant',
89
+ content:
90
+ "<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI'll check the current weather in New York for you.",
91
+ tool_calls: [
92
+ {
93
+ type: 'function',
94
+ function: {
95
+ name: 'get_weather',
96
+ arguments: '{"location": "New York", "unit": "celsius"}'
97
+ }
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ role: 'tool',
103
+ content: '{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}'
104
+ },
105
+ {
106
+ role: 'assistant',
107
+ content:
108
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
109
+ },
110
+ {
111
+ role: 'user',
112
+ content: 'Thanks! What about Boston?'
113
+ }
114
+ ],
115
+ tools: [
116
+ {
117
+ name: 'get_weather',
118
+ description: 'Get current weather information for a location',
119
+ parameters: {
120
+ type: 'object',
121
+ properties: {
122
+ location: {
123
+ type: 'string',
124
+ description: 'The city and state, e.g. San Francisco, CA'
125
+ },
126
+ unit: {
127
+ type: 'string',
128
+ enum: ['celsius', 'fahrenheit'],
129
+ description: 'The unit of temperature to use'
130
+ }
131
+ },
132
+ required: ['location']
133
+ }
134
+ }
135
+ ],
136
+ add_generation_prompt: true
137
+ }
138
+ },
139
+ variation4_mistral_tags_style: {
140
+ description:
141
+ "This variation demonstrates the Mistral-like approach using `[AVAILABLE_TOOLS]` (implicitly handled by the template from the 'tools' array), `[TOOL_CALLS]` with IDs, and `[TOOL_RESULTS]`.",
142
+ example: {
143
+ messages: [
144
+ {
145
+ role: 'system',
146
+ content: 'You are a helpful assistant that can use tools to get information for the user.'
147
+ },
148
+ {
149
+ role: 'user',
150
+ content: "What's the weather like in New York?"
151
+ },
152
+ {
153
+ role: 'assistant',
154
+ content:
155
+ "<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI'll check the current weather in New York for you.",
156
+ tool_calls: [
157
+ {
158
+ id: 'call_weather_nyc_001',
159
+ function: {
160
+ name: 'get_weather',
161
+ arguments: {
162
+ location: 'New York',
163
+ unit: 'celsius'
164
+ }
165
+ }
166
+ }
167
+ ]
168
+ },
169
+ {
170
+ role: 'tool',
171
+ tool_call_id: 'call_weather_nyc_001',
172
+ content: '{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}'
173
+ },
174
+ {
175
+ role: 'assistant',
176
+ content:
177
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
178
+ },
179
+ {
180
+ role: 'user',
181
+ content: 'Thanks! What about Boston?'
182
+ }
183
+ ],
184
+ tools: [
185
+ {
186
+ name: 'get_weather',
187
+ description: 'Get current weather information for a location',
188
+ parameters: {
189
+ type: 'object',
190
+ properties: {
191
+ location: {
192
+ type: 'string',
193
+ description: 'The city and state, e.g. San Francisco, CA'
194
+ },
195
+ unit: {
196
+ type: 'string',
197
+ enum: ['celsius', 'fahrenheit'],
198
+ description: 'The unit of temperature to use'
199
+ }
200
+ },
201
+ required: ['location']
202
+ }
203
+ }
204
+ ],
205
+ add_generation_prompt: true
206
+ }
207
+ },
208
+ variation5_generic_openai_anthropic_style: {
209
+ description:
210
+ 'This is the generic style, often compatible with OpenAI and Anthropic models, similar to your provided example. It serves as a baseline.',
211
+ example: {
212
+ messages: [
213
+ {
214
+ role: 'system',
215
+ content: 'You are a helpful assistant that can use tools to get information for the user.'
216
+ },
217
+ {
218
+ role: 'user',
219
+ content: "What's the weather like in New York?"
220
+ },
221
+ {
222
+ role: 'assistant',
223
+ content:
224
+ "<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI'll check the current weather in New York for you.",
225
+ tool_calls: [
226
+ {
227
+ function: {
228
+ name: 'get_weather',
229
+ arguments: {
230
+ location: 'New York',
231
+ unit: 'celsius'
232
+ }
233
+ }
234
+ }
235
+ ]
236
+ },
237
+ {
238
+ role: 'tool',
239
+ content: '{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}'
240
+ },
241
+ {
242
+ role: 'assistant',
243
+ content:
244
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
245
+ },
246
+ {
247
+ role: 'user',
248
+ content: 'Thanks! What about Boston?'
249
+ }
250
+ ],
251
+ tools: [
252
+ {
253
+ name: 'get_weather',
254
+ description: 'Get current weather information for a location',
255
+ parameters: {
256
+ type: 'object',
257
+ properties: {
258
+ location: {
259
+ type: 'string',
260
+ description: 'The city and state, e.g. San Francisco, CA'
261
+ },
262
+ unit: {
263
+ type: 'string',
264
+ enum: ['celsius', 'fahrenheit'],
265
+ description: 'The unit of temperature to use'
266
+ }
267
+ },
268
+ required: ['location']
269
+ }
270
+ }
271
+ ],
272
+ add_generation_prompt: true
273
+ }
274
+ },
275
+ variation6_granite_style: {
276
+ description:
277
+ "This variation reflects Granite-like models where the tool call might be embedded directly in the assistant's content string, prefixed by a special tag like `<|tool_call|>`. The `available_tools` would be passed to the template engine.",
278
+ example: {
279
+ messages: [
280
+ {
281
+ role: 'system',
282
+ content:
283
+ "You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used."
284
+ },
285
+ {
286
+ role: 'user',
287
+ content: "What's the weather like in New York?"
288
+ },
289
+ {
290
+ role: 'assistant',
291
+ content:
292
+ '<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI\'ll check the current weather in New York for you.\n<|tool_call|>[{"name": "get_weather", "arguments": {"location": "New York", "unit": "celsius"}}]'
293
+ },
294
+ {
295
+ role: 'tool',
296
+ content: '{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}'
297
+ },
298
+ {
299
+ role: 'assistant',
300
+ content:
301
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
302
+ },
303
+ {
304
+ role: 'user',
305
+ content: 'Thanks! What about Boston?'
306
+ }
307
+ ],
308
+ tools: [
309
+ {
310
+ name: 'get_weather',
311
+ description: 'Get current weather information for a location',
312
+ parameters: {
313
+ type: 'object',
314
+ properties: {
315
+ location: {
316
+ type: 'string',
317
+ description: 'The city and state, e.g. San Francisco, CA'
318
+ },
319
+ unit: {
320
+ type: 'string',
321
+ enum: ['celsius', 'fahrenheit'],
322
+ description: 'The unit of temperature to use'
323
+ }
324
+ },
325
+ required: ['location']
326
+ }
327
+ }
328
+ ],
329
+ add_generation_prompt: true
330
+ }
331
+ },
332
+ variation2_llama3_style: {
333
+ description:
334
+ "This variation shows how Llama-3.1-like models might handle tool definitions passed within the first user message. The assistant's invocation uses a standard `tool_calls` array.",
335
+ example: {
336
+ messages: [
337
+ {
338
+ role: 'system',
339
+ content:
340
+ 'Environment: ipython\nCutting Knowledge Date: December 2023\nToday Date: 2025-05-14\n\nYou are a helpful assistant.'
341
+ },
342
+ {
343
+ role: 'user',
344
+ content:
345
+ 'Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.\n\nRespond in the format {"name": function name, "parameters": dictionary of argument name and its value}.\nDo not use variables.\n\n[\n {\n "name": "get_weather",\n "description": "Get current weather information for a location",\n "parameters": {\n "type": "object",\n "properties": {\n "location": {\n "type": "string",\n "description": "The city and state, e.g. San Francisco, CA"\n },\n "unit": {\n "type": "string",\n "enum": ["celsius", "fahrenheit"],\n "description": "The unit of temperature to use"\n }\n },\n "required": ["location"]\n }\n }\n]\n\nWhat\'s the weather like in New York?'
346
+ },
347
+ {
348
+ role: 'assistant',
349
+ content:
350
+ "<think>\nThe user is asking about the weather in New York. I should use the weather tool to get this information.\n</think>\nI'll check the current weather in New York for you.",
351
+ tool_calls: [
352
+ {
353
+ function: {
354
+ name: 'get_weather',
355
+ arguments: {
356
+ location: 'New York',
357
+ unit: 'celsius'
358
+ }
359
+ }
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ role: 'tool',
365
+ content: '{"temperature": 22, "condition": "Sunny", "humidity": 45, "wind_speed": 10}'
366
+ },
367
+ {
368
+ role: 'assistant',
369
+ content:
370
+ "The weather in New York is currently sunny with a temperature of 22°C. The humidity is at 45% with a wind speed of 10 km/h. It's a great day to be outside!"
371
+ },
372
+ {
373
+ role: 'user',
374
+ content: 'Thanks! What about Boston?'
375
+ }
376
+ ],
377
+ tools: null,
378
+ add_generation_prompt: true
379
+ }
380
+ }
381
+ };
382
+
383
+ export function getExampleToolUsage(templateStr: string): Record<string, unknown> | undefined {
384
+ const template = new Template(templateStr);
385
+ for (const variation of Object.values(variations)) {
386
+ try {
387
+ const variationRendered = template.render(transformInput(variation.example, templateStr));
388
+ if (variationRendered.includes('get_weather')) {
389
+ return variation.example;
390
+ }
391
+ } catch (e) {
392
+ console.error(e);
393
+ }
394
+ }
395
+ return undefined;
396
+ }
src/lib/utils/tooltip.ts ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ActionReturn } from 'svelte/action';
2
+
3
+ /// placement of the floating element around the anchor element
4
+ export type Placement =
5
+ | 'left'
6
+ | 'right'
7
+ | 'top'
8
+ | 'bottom'
9
+ | 'auto'
10
+ | 'prefer-left'
11
+ | 'prefer-right'
12
+ | 'prefer-top'
13
+ | 'prefer-bottom';
14
+
15
+ /// placement axis
16
+ export type Axis = 'x' | 'y';
17
+
18
+ /// alignment of the floating element against the anchor element
19
+ export type Alignment = 'start' | 'center' | 'end' | 'screen' | 'auto' | 'prefer-center';
20
+
21
+ export interface AbsolutePosition {
22
+ left: string;
23
+ top: string;
24
+ right: string;
25
+ bottom: string;
26
+ }
27
+
28
+ export interface PositionOptions {
29
+ placement?: Placement; /// preferred placement of the floating element
30
+ alignment?: Alignment; /// preferred alignment of the floating element
31
+ hitZoneXMargin?: number; /// page x margin before hitting the edge of the screen
32
+ hitZoneYMargin?: number; /// page y margin before hitting the edge of the screen
33
+ shift?: boolean; /// shift the floating element to center it against the anchor element
34
+ arrowPadding?: number; /// padding for the arrow
35
+ arrowSize?: number; /// size of the arrow
36
+ minMargin?: number; /// minimum margin around the floating element in "screen" alignment
37
+ }
38
+
39
+ /**
40
+ * Compute the best placement for the floating element based on
41
+ * the anchor element position, the page size and a preferred placement.
42
+ */
43
+ export function computePlacement(
44
+ anchorBBox: DOMRect,
45
+ floatBBox: DOMRect,
46
+ preferredPlacement: Placement = 'auto',
47
+ pageWidth: number,
48
+ pageHeight: number,
49
+ opts: {
50
+ hitZoneXMargin: number;
51
+ hitZoneYMargin: number;
52
+ } = {
53
+ hitZoneXMargin: 0,
54
+ hitZoneYMargin: 0
55
+ }
56
+ ): Placement {
57
+ let computedPlacement = preferredPlacement === 'auto' ? 'bottom' : preferredPlacement;
58
+ if (pageHeight > 0 && pageWidth > 0) {
59
+ if (preferredPlacement === 'auto') {
60
+ /// check if the anchor is closer to the top or bottom of the page
61
+ computedPlacement = anchorBBox.top > pageHeight / 2 ? 'top' : 'bottom';
62
+ } else if (
63
+ preferredPlacement === 'prefer-top' ||
64
+ floatBBox.width + opts.hitZoneXMargin >= pageWidth
65
+ ) {
66
+ /// check if the toast has enough space to be placed above the anchor
67
+ computedPlacement =
68
+ anchorBBox.top > floatBBox.height + opts.hitZoneYMargin ? 'top' : 'bottom';
69
+ } else if (preferredPlacement === 'prefer-bottom') {
70
+ /// check if the toast has enough space to be placed below the anchor
71
+ computedPlacement =
72
+ anchorBBox.top + anchorBBox.height + floatBBox.height + opts.hitZoneYMargin > pageHeight
73
+ ? 'top'
74
+ : 'bottom';
75
+ } else if (preferredPlacement === 'prefer-left') {
76
+ /// check if the toast has enough space to be placed on the left of the anchor
77
+ computedPlacement =
78
+ anchorBBox.left > floatBBox.width + opts.hitZoneXMargin ? 'left' : 'right';
79
+ } else if (preferredPlacement === 'prefer-right') {
80
+ /// check if the toast has enough space to be placed on the right of the anchor
81
+ computedPlacement =
82
+ anchorBBox.left + anchorBBox.width + floatBBox.width + opts.hitZoneXMargin > pageWidth
83
+ ? 'left'
84
+ : 'right';
85
+ }
86
+ }
87
+ return computedPlacement;
88
+ }
89
+
90
+ /**
91
+ * Compute the best alignment for the floating element based on
92
+ * the anchor element position, the page size, the placement axis and a preferred alignment.
93
+ */
94
+ export function computeAlignment(
95
+ anchorBBox: DOMRect,
96
+ floatingBBox: DOMRect,
97
+ preferredAlignment: Alignment = 'auto',
98
+ axis: Axis = 'y',
99
+ pageWidth: number,
100
+ pageHeight: number,
101
+ opts: {
102
+ hitZoneXMargin: number;
103
+ hitZoneYMargin: number;
104
+ } = {
105
+ hitZoneXMargin: 0,
106
+ hitZoneYMargin: 0
107
+ }
108
+ ): Alignment {
109
+ let computedAlignment =
110
+ preferredAlignment === 'auto' ? (axis === 'y' ? 'center' : 'start') : preferredAlignment;
111
+ if (['prefer-center', 'auto'].includes(preferredAlignment) && pageWidth > 0) {
112
+ if (floatingBBox.width + opts.hitZoneXMargin * 2 >= pageWidth) {
113
+ /// on mobile, large popovers should be centered and screen wide
114
+ computedAlignment = 'screen';
115
+ } else if (
116
+ axis === 'y' &&
117
+ anchorBBox.left + floatingBBox.width > pageWidth - opts.hitZoneXMargin &&
118
+ anchorBBox.left - floatingBBox.width - opts.hitZoneXMargin < 0
119
+ ) {
120
+ /// if the floating element is too wide we center it
121
+ computedAlignment = 'center';
122
+ } else if (
123
+ axis === 'y' &&
124
+ anchorBBox.left + floatingBBox.width > pageWidth - opts.hitZoneXMargin
125
+ ) {
126
+ /// align at the end of the anchor when the right edge of the page is too close
127
+ computedAlignment = 'end';
128
+ } else if (axis === 'y' && anchorBBox.left - floatingBBox.width - opts.hitZoneXMargin < 0) {
129
+ /// align at the start of the anchor when the left edge of the page is too close
130
+ computedAlignment = 'start';
131
+ } else if (
132
+ axis === 'x' &&
133
+ anchorBBox.top + floatingBBox.height > pageHeight - opts.hitZoneYMargin
134
+ ) {
135
+ /// when placed horizontally, align at the end of the anchor when the top edge of the page is too close
136
+ computedAlignment = 'end';
137
+ } else {
138
+ /// start by default except if prefer-center is set
139
+ computedAlignment = preferredAlignment === 'prefer-center' ? 'center' : 'start';
140
+ }
141
+ }
142
+ return computedAlignment;
143
+ }
144
+
145
+ /**
146
+ * Compute the position of the floating element
147
+ */
148
+ export function computePosition(
149
+ anchorBBox: DOMRect,
150
+ floatBBox: DOMRect,
151
+ {
152
+ placement = 'auto',
153
+ alignment = 'auto',
154
+ hitZoneXMargin = 0,
155
+ hitZoneYMargin = 0,
156
+ shift = false,
157
+ arrowPadding = 10,
158
+ arrowSize = 8,
159
+ minMargin = 20
160
+ }: PositionOptions = {}
161
+ ): { float: AbsolutePosition; arrow: AbsolutePosition } {
162
+ const axis: Axis = ['auto', 'top', 'bottom', 'prefer-top', 'prefer-bottom'].includes(placement)
163
+ ? 'y'
164
+ : 'x';
165
+
166
+ const computedAlignment = computeAlignment(
167
+ anchorBBox,
168
+ floatBBox,
169
+ alignment,
170
+ axis,
171
+ window.innerWidth,
172
+ window.innerHeight,
173
+ { hitZoneXMargin, hitZoneYMargin }
174
+ );
175
+ const computedPlacement = computePlacement(
176
+ anchorBBox,
177
+ floatBBox,
178
+ placement,
179
+ window.innerWidth,
180
+ window.innerHeight,
181
+ {
182
+ hitZoneXMargin,
183
+ hitZoneYMargin
184
+ }
185
+ );
186
+
187
+ /// position of the anchor element
188
+ const left = anchorBBox.left + window.scrollX;
189
+ const width = anchorBBox.width;
190
+ const height = anchorBBox.height;
191
+
192
+ const halfArrowSize = arrowSize / 2;
193
+
194
+ let floatingLeft: string = '';
195
+ let floatingTop: string = '';
196
+ let floatingRight: string = '';
197
+ let floatingBottom: string = '';
198
+ let arrowLeft: string = '';
199
+ let arrowTop: string = '';
200
+ let arrowRight: string = '';
201
+ let arrowBottom: string = '';
202
+
203
+ switch (computedPlacement) {
204
+ case 'top': {
205
+ floatingBottom = `calc(100% + ${arrowSize}px)`;
206
+ arrowTop = `calc(100% - ${halfArrowSize}px)`;
207
+ break;
208
+ }
209
+ case 'bottom': {
210
+ floatingTop = `calc(100% + ${arrowSize}px)`;
211
+ arrowBottom = `calc(100% - ${halfArrowSize}px)`;
212
+ break;
213
+ }
214
+ case 'left': {
215
+ console.log('left');
216
+ floatingRight = `calc(100% + ${arrowSize}px)`;
217
+ arrowRight = `-${halfArrowSize}px`;
218
+ break;
219
+ }
220
+ case 'right': {
221
+ floatingLeft = `calc(100% + ${arrowSize}px)`;
222
+ arrowLeft = `-${halfArrowSize}px`;
223
+ break;
224
+ }
225
+ default: {
226
+ break;
227
+ }
228
+ }
229
+ if (axis === 'y') {
230
+ /// shift the floating element so the arrow is exactly at the middle of the anchor
231
+ const shiftFloating = shift ? width / 2 - halfArrowSize - arrowPadding : 0;
232
+ switch (computedAlignment) {
233
+ case 'start': {
234
+ floatingLeft = `${shiftFloating}px`;
235
+ floatingRight = 'auto';
236
+ arrowLeft = `${arrowPadding}px`;
237
+ break;
238
+ }
239
+ case 'center': {
240
+ floatingLeft = `-${floatBBox.width / 2 - width / 2}px`;
241
+ floatingRight = 'auto';
242
+ arrowLeft = `calc(50% - ${halfArrowSize}px)`;
243
+ break;
244
+ }
245
+ case 'end': {
246
+ floatingLeft = 'auto';
247
+ floatingRight = `${shiftFloating}px`;
248
+ arrowRight = `${arrowPadding}px`;
249
+ break;
250
+ }
251
+ case 'screen': {
252
+ floatingLeft = `${minMargin - left}px`;
253
+ floatingRight = 'auto';
254
+ arrowLeft = `${left - minMargin + width / 2 - halfArrowSize}px`;
255
+ break;
256
+ }
257
+ default:
258
+ break;
259
+ }
260
+ } else {
261
+ /// shift the floating element so the arrow is exactly at the middle of the anchor
262
+ const popoverShift = shift ? height / 2 - halfArrowSize - arrowPadding : 0;
263
+ switch (computedAlignment) {
264
+ case 'start': {
265
+ floatingTop = `${popoverShift}px`;
266
+ arrowTop = `${floatBBox.height < arrowPadding * 2 ? floatBBox.height / 2 : arrowPadding}px`;
267
+ break;
268
+ }
269
+ case 'center': {
270
+ floatingTop = `-${floatBBox.height / 2 - height / 2}px`;
271
+ arrowTop = `calc(50% - ${halfArrowSize}px)`;
272
+ break;
273
+ }
274
+ case 'end': {
275
+ floatingBottom = `${popoverShift}px`;
276
+ arrowBottom = `${floatBBox.height < arrowPadding * 2 ? floatBBox.height / 2 : arrowPadding}px`;
277
+ break;
278
+ }
279
+ case 'screen': {
280
+ floatingLeft = `${minMargin - left}px`;
281
+ floatingRight = `auto`;
282
+ arrowLeft = `${left - minMargin + width / 2 - halfArrowSize}px`;
283
+ break;
284
+ }
285
+ default:
286
+ break;
287
+ }
288
+ }
289
+ return {
290
+ arrow: { left: arrowLeft, top: arrowTop, right: arrowRight, bottom: arrowBottom },
291
+ float: { left: floatingLeft, top: floatingTop, right: floatingRight, bottom: floatingBottom }
292
+ };
293
+ }
294
+
295
+ const defaultOptions: PositionOptions = {
296
+ placement: 'prefer-top',
297
+ alignment: 'prefer-center',
298
+ hitZoneXMargin: 20,
299
+ hitZoneYMargin: 20,
300
+ shift: true,
301
+ arrowPadding: 10,
302
+ arrowSize: 8,
303
+ minMargin: 10
304
+ };
305
+
306
+ /**
307
+ * Tooltip svelte action,
308
+ *
309
+ * use it with the tooltip text content as a string.
310
+ * ```html
311
+ * <div use:tooltip={"Tooltip content"} />
312
+ * ```
313
+ * or with the tooltip options as an object.
314
+ * ```html
315
+ * <div use:tooltip={{ content: "Tooltip content", opts: { placement: "left", alignment: "end" } }} />
316
+ * ```
317
+ */
318
+ export function tooltip(
319
+ node: HTMLElement,
320
+ parameter:
321
+ | string
322
+ | {
323
+ content: string | undefined;
324
+ opts?: PositionOptions;
325
+ showOn?: 'click' | 'hover' | 'hoverTouch' | 'always';
326
+ disabled?: boolean;
327
+ }
328
+ | undefined
329
+ ): ActionReturn {
330
+ /// if the tooltip is disabled, or without content, we do nothing
331
+ if (
332
+ parameter === undefined ||
333
+ (typeof parameter === 'string' && !parameter) ||
334
+ (typeof parameter === 'object' && (!parameter.content || parameter.disabled === true))
335
+ ) {
336
+ return {};
337
+ }
338
+
339
+ let content: string;
340
+ const opts: PositionOptions = { ...defaultOptions };
341
+ let showOn = 'hover';
342
+ if (typeof parameter === 'string') {
343
+ content = parameter;
344
+ } else {
345
+ content = parameter.content as string;
346
+ Object.assign(opts, parameter.opts);
347
+ showOn = parameter.showOn ?? 'hover';
348
+ }
349
+
350
+ /// create elements
351
+ const tooltipMask = document.createElement('div');
352
+ tooltipMask.className = 'tooltip-mask hidden';
353
+
354
+ const tooltipElt = document.createElement('div');
355
+ tooltipElt.className = 'tooltip';
356
+ tooltipElt.setAttribute('role', 'tooltip');
357
+
358
+ const arrowElt = document.createElement('div');
359
+ arrowElt.className = 'tooltip-arrow';
360
+
361
+ tooltipElt.appendChild(arrowElt);
362
+ tooltipElt.appendChild(document.createTextNode(content));
363
+ tooltipMask.appendChild(tooltipElt);
364
+ document.body.appendChild(tooltipMask);
365
+
366
+ function updateElementPosition(
367
+ element: HTMLElement,
368
+ position: AbsolutePosition | { top: string; left?: string; width?: string; height?: string }
369
+ ) {
370
+ for (const [key, value] of Object.entries(position)) {
371
+ element.style[key] = value;
372
+ }
373
+ }
374
+
375
+ function updatePositions() {
376
+ updateElementPosition(tooltipMask, {
377
+ top: `${node.getBoundingClientRect().top + window.scrollY}px`,
378
+ left: `${node.getBoundingClientRect().left + window.scrollX}px`,
379
+ width: `${node.getBoundingClientRect().width}px`,
380
+ height: `${node.getBoundingClientRect().height}px`
381
+ });
382
+
383
+ const positions = computePosition(
384
+ node.getBoundingClientRect(),
385
+ tooltipElt.getBoundingClientRect(),
386
+ opts
387
+ );
388
+
389
+ updateElementPosition(tooltipElt, positions.float);
390
+ updateElementPosition(arrowElt, positions.arrow);
391
+ }
392
+
393
+ function show() {
394
+ tooltipMask.classList.remove('hidden');
395
+ updatePositions();
396
+
397
+ // eslint-disable-next-line github/prefer-observers
398
+ document.addEventListener('scroll', updatePositions);
399
+ // eslint-disable-next-line github/prefer-observers
400
+ document.addEventListener('resize', updatePositions);
401
+ }
402
+
403
+ function hide() {
404
+ tooltipMask.classList.add('hidden');
405
+ document.removeEventListener('scroll', updatePositions);
406
+ document.removeEventListener('resize', updatePositions);
407
+ }
408
+
409
+ switch (showOn) {
410
+ case 'click': {
411
+ node.addEventListener('click', show);
412
+ break;
413
+ }
414
+ case 'hoverTouch': {
415
+ node.addEventListener('mouseenter', show);
416
+ node.addEventListener('mouseleave', hide);
417
+ node.addEventListener('touchstart', show);
418
+ node.addEventListener('touchend', hide);
419
+ break;
420
+ }
421
+ case 'hover': {
422
+ node.addEventListener('mouseenter', show);
423
+ node.addEventListener('mouseleave', hide);
424
+ break;
425
+ }
426
+ case 'always': {
427
+ show();
428
+ break;
429
+ }
430
+ }
431
+
432
+ return {
433
+ destroy() {
434
+ document.removeEventListener('scroll', updatePositions);
435
+ document.removeEventListener('resize', updatePositions);
436
+
437
+ switch (showOn) {
438
+ case 'click': {
439
+ node.removeEventListener('click', show);
440
+ break;
441
+ }
442
+ case 'hoverTouch': {
443
+ node.removeEventListener('mouseenter', show);
444
+ node.removeEventListener('mouseleave', hide);
445
+ node.removeEventListener('touchstart', show);
446
+ node.removeEventListener('touchend', hide);
447
+ break;
448
+ }
449
+ case 'hover': {
450
+ node.removeEventListener('mouseenter', show);
451
+ node.removeEventListener('mouseleave', hide);
452
+ break;
453
+ }
454
+ }
455
+ if (showOn === 'always') {
456
+ tooltipElt.style.opacity = '0';
457
+ setTimeout(() => document.body.removeChild(tooltipMask), 150);
458
+ } else {
459
+ document.body.removeChild(tooltipMask);
460
+ }
461
+ }
462
+ };
463
+ }
src/lib/utils/transformInput.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export function transformInput(input: Record<string, unknown>, templateStr: string) {
2
+ // handle cohere special case
3
+ if (templateStr.includes('safety_mode')) {
4
+ input.safety_mode = '';
5
+ }
6
+ return input;
7
+ }
src/routes/+page.svelte CHANGED
@@ -1,2 +1,289 @@
1
- <h1>Welcome to SvelteKit</h1>
2
- <p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import JsonEditor from '$lib/JsonEditor/JsonEditor.svelte';
3
+ import ChatTemplateViewer from '$lib/ChatTemplateViewer/ChatTemplateViewer.svelte';
4
+ import OutputViewer from '$lib/OutputViewer/OutputViewer.svelte';
5
+ import type { ChatTemplate, FormattedChatTemplate } from '$lib/ChatTemplateViewer/types';
6
+ import { onMount } from 'svelte';
7
+ import { Template } from '@huggingface/jinja';
8
+ import { goto } from '$app/navigation';
9
+ import { page } from '$app/stores';
10
+
11
+ let modelId = $page.url.searchParams.get('modelId') ?? 'Qwen/Qwen3-235B-A22B';
12
+ let formattedTemplates: FormattedChatTemplate[] = [];
13
+ let selectedTemplate: FormattedChatTemplate | undefined = undefined;
14
+ let showFormattedTemplate = true;
15
+ let selectedExampleInputId = '';
16
+
17
+ let leftWidth = 50; // percent
18
+ let isDraggingVertical = false;
19
+
20
+ let topHeight = 50; // percent (for right pane)
21
+ let isDraggingHorizontal = false;
22
+
23
+ let error = '';
24
+ let output = '';
25
+
26
+ let input = {
27
+ messages: [
28
+ {
29
+ role: 'user',
30
+ content: 'Hello, how are you?'
31
+ },
32
+ {
33
+ role: 'assistant',
34
+ content: "I'm doing great. How can I help you today?"
35
+ },
36
+ {
37
+ role: 'user',
38
+ content: 'Can you tell me a joke?'
39
+ }
40
+ ],
41
+ add_generation_prompt: true
42
+ };
43
+
44
+ $: {
45
+ try {
46
+ if (!input.messages) {
47
+ error = "Invalid JSON: missing 'messages' key";
48
+ }
49
+
50
+ if (selectedTemplate) {
51
+ const template = new Template(
52
+ showFormattedTemplate ? selectedTemplate.formattedTemplate : selectedTemplate.template
53
+ );
54
+ output = template.render(input);
55
+ error = '';
56
+ }
57
+ } catch (e) {
58
+ error = e instanceof Error ? e.message : 'Unknown error';
59
+ }
60
+ }
61
+
62
+ function startDragVertical(e: MouseEvent) {
63
+ isDraggingVertical = true;
64
+ document.body.style.cursor = 'col-resize';
65
+ }
66
+
67
+ function stopDragVertical() {
68
+ isDraggingVertical = false;
69
+ document.body.style.cursor = '';
70
+ }
71
+
72
+ function onDragVertical(e: MouseEvent) {
73
+ if (!isDraggingVertical) return;
74
+ const playground = document.getElementById('playground-container');
75
+ if (!playground) return;
76
+ const rect = playground.getBoundingClientRect();
77
+ const offsetX = e.clientX - rect.left;
78
+ let percent = (offsetX / rect.width) * 100;
79
+ if (percent < 10) percent = 10;
80
+ if (percent > 90) percent = 90;
81
+ leftWidth = percent;
82
+ }
83
+
84
+ function startDragHorizontal(e: MouseEvent) {
85
+ isDraggingHorizontal = true;
86
+ document.body.style.cursor = 'row-resize';
87
+ }
88
+
89
+ function stopDragHorizontal() {
90
+ isDraggingHorizontal = false;
91
+ document.body.style.cursor = '';
92
+ }
93
+
94
+ function onDragHorizontal(e: MouseEvent) {
95
+ if (!isDraggingHorizontal) return;
96
+ const rightPane = document.getElementById('right-pane');
97
+ if (!rightPane) return;
98
+ const rect = rightPane.getBoundingClientRect();
99
+ const offsetY = e.clientY - rect.top;
100
+ let percent = (offsetY / rect.height) * 100;
101
+ if (percent < 10) percent = 10;
102
+ if (percent > 90) percent = 90;
103
+ topHeight = percent;
104
+ }
105
+
106
+ async function getChatTemplate(modelId: string) {
107
+ try {
108
+ const res = await fetch('https://huggingface.co/api/models/' + modelId);
109
+
110
+ if (!res.ok) {
111
+ alert(`Failed to fetch model "${modelId}": ${res.status} ${res.statusText}`);
112
+ return;
113
+ }
114
+
115
+ const model = await res.json();
116
+
117
+ let chatTemplate: ChatTemplate | undefined = undefined;
118
+
119
+ if (model.config?.chat_template_jinja) {
120
+ // model.config.chat_template_jinja & optional model.config.additional_chat_templates
121
+ chatTemplate = model.config.chat_template_jinja;
122
+ if (model.config?.additional_chat_templates) {
123
+ chatTemplate = [
124
+ {
125
+ name: 'default',
126
+ template: model.config.chat_template_jinja
127
+ },
128
+ ...(model.config?.additional_chat_templates
129
+ ? Object.keys(model.config.additional_chat_templates).map((name) => ({
130
+ name,
131
+ template: model.config?.additional_chat_templates?.[name] ?? ''
132
+ }))
133
+ : [])
134
+ ];
135
+ }
136
+ } else if (model.config?.processor_config?.chat_template) {
137
+ // for backward compatibility VLM
138
+ chatTemplate = model.config.processor_config.chat_template;
139
+ } else if (model.config?.tokenizer_config?.chat_template) {
140
+ // for backward compatibility
141
+ chatTemplate = model.config.tokenizer_config.chat_template;
142
+ } else if (model.gguf?.chat_template) {
143
+ // for GGUF models
144
+ chatTemplate = model.gguf.chat_template;
145
+ }
146
+
147
+ const formattedTemplates: FormattedChatTemplate[] = (
148
+ typeof chatTemplate === 'string'
149
+ ? [{ name: 'default', template: chatTemplate }]
150
+ : chatTemplate
151
+ ) // Convert string template to array for unified handling
152
+ .map(({ name, template }) => ({
153
+ name,
154
+ template,
155
+ formattedTemplate: (() => {
156
+ try {
157
+ return new Template(template).format();
158
+ } catch (error) {
159
+ console.error(`Error formatting chat template ${name}:`, error);
160
+ return template; // Return the original template in case of an error
161
+ }
162
+ })()
163
+ })) // add formatted template attribute
164
+ .map(({ name, template, formattedTemplate }) => ({
165
+ name,
166
+ template,
167
+ formattedTemplate,
168
+ templateUnedited: template,
169
+ formattedTemplateUnedited: formattedTemplate
170
+ }));
171
+
172
+ let selectedTemplate =
173
+ formattedTemplates.find(({ name }) => name === $page.url.searchParams.get('template')) ??
174
+ formattedTemplates[0];
175
+
176
+ return { formattedTemplates, selectedTemplate, model };
177
+ } catch (error) {
178
+ console.error(error);
179
+ }
180
+ }
181
+
182
+ async function handleModelIdChange(newModelId: string, opts?: { replaceState?: boolean }) {
183
+ const modelTemplate = await getChatTemplate(newModelId);
184
+ if (modelTemplate) {
185
+ modelId = newModelId;
186
+ formattedTemplates = modelTemplate.formattedTemplates;
187
+ selectedTemplate = modelTemplate.selectedTemplate;
188
+ const model = modelTemplate.model;
189
+ input = {
190
+ ...input,
191
+ bos_token: model?.config?.tokenizer_config?.bos_token?.content ?? model?.gguf?.bos_token,
192
+ eos_token: model?.config?.tokenizer_config?.eos_token?.content ?? model?.gguf?.eos_token,
193
+ pad_token: model?.config?.tokenizer_config?.pad_token?.content ?? model?.gguf?.pad_token,
194
+ unk_token: model?.config?.tokenizer_config?.unk_token?.content ?? model?.gguf?.unk_token
195
+ };
196
+
197
+ if (opts?.replaceState) {
198
+ updateParams();
199
+ }
200
+ }
201
+ }
202
+
203
+ function updateParams() {
204
+ let searchParams = '?modelId=' + modelId;
205
+ if (selectedTemplate && selectedTemplate.name !== 'default') {
206
+ searchParams += '&template=' + selectedTemplate.name;
207
+ }
208
+ if (selectedExampleInputId) {
209
+ searchParams += '&example=' + selectedExampleInputId;
210
+ }
211
+
212
+ goto(searchParams, { replaceState: true });
213
+
214
+ // post message to parent
215
+ const parentOrigin = 'https://huggingface.co';
216
+ window.parent.postMessage({ queryString: searchParams }, parentOrigin);
217
+ }
218
+
219
+ onMount(async () => {
220
+ await handleModelIdChange(modelId);
221
+ });
222
+ </script>
223
+
224
+ <svelte:window
225
+ on:mousemove={onDragVertical}
226
+ on:mouseup={stopDragVertical}
227
+ on:mousemove={onDragHorizontal}
228
+ on:mouseup={stopDragHorizontal}
229
+ />
230
+
231
+ <div
232
+ id="playground-container"
233
+ class="relative flex h-screen w-full overflow-hidden border bg-white shadow select-none dark:bg-gray-950"
234
+ >
235
+ <div class="overflow-auto" style="width: {leftWidth}%">
236
+ {#if formattedTemplates.length}
237
+ <ChatTemplateViewer
238
+ {modelId}
239
+ {formattedTemplates}
240
+ bind:selectedTemplate
241
+ bind:showFormattedTemplate
242
+ on:modelIdChange={(e) => handleModelIdChange(e.detail, { replaceState: true })}
243
+ on:templateChange={(e) => updateParams()}
244
+ />
245
+ {/if}
246
+ </div>
247
+
248
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
249
+ <div
250
+ class="hidden h-full w-1 cursor-col-resize items-center justify-center bg-gray-100 select-none hover:bg-blue-200 active:bg-blue-200 sm:flex dark:bg-gray-700 dark:hover:bg-blue-900 dark:active:bg-blue-900"
251
+ style="left: calc({leftWidth}% - 4px); z-index:10;"
252
+ on:mousedown={startDragVertical}
253
+ >
254
+ <div class="h-12 w-[0.05rem] rounded-full bg-gray-400"></div>
255
+ </div>
256
+
257
+ <div
258
+ id="right-pane"
259
+ class="relative flex h-full flex-col bg-gray-100"
260
+ style="width: {100 - leftWidth}%"
261
+ >
262
+ {#key `${modelId}-${selectedTemplate?.name}`}
263
+ <div class="w-full" style="height: {topHeight}%">
264
+ <!-- Right top pane -->
265
+ <JsonEditor
266
+ bind:error
267
+ bind:content={input}
268
+ bind:selectedTemplate
269
+ bind:selectedExampleInputId
270
+ on:exampleChange={(e) => updateParams()}
271
+ />
272
+ </div>
273
+ {/key}
274
+
275
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
276
+ <div
277
+ class="hidden h-1 w-full cursor-row-resize items-center justify-center bg-gray-100 select-none hover:bg-blue-200 active:bg-blue-200 sm:flex dark:bg-gray-700 dark:hover:bg-blue-900 dark:active:bg-blue-900"
278
+ style="top: calc({topHeight}% - 4px); z-index:10;"
279
+ on:mousedown={startDragHorizontal}
280
+ >
281
+ <div class="h-[0.05rem] w-12 rounded-full bg-gray-400"></div>
282
+ </div>
283
+
284
+ <div class="w-full" style="height: {100 - topHeight}%">
285
+ <!-- Right bottom pane -->
286
+ <OutputViewer content={output} {error} />
287
+ </div>
288
+ </div>
289
+ </div>
svelte.config.js CHANGED
@@ -1,4 +1,4 @@
1
- import adapter from '@sveltejs/adapter-auto';
2
  import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
 
4
  /** @type {import('@sveltejs/kit').Config} */
 
1
+ import adapter from '@sveltejs/adapter-node';
2
  import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
 
4
  /** @type {import('@sveltejs/kit').Config} */