diff --git a/package.json b/package.json index e5a78d2ba..96e4af9a6 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "sync": "svelte-kit sync" }, "devDependencies": { - "@inlang/paraglide-js": "^2.0.13", + "@inlang/paraglide-js": "^2.1.0", "@sveltejs/adapter-netlify": "^5.0.2", - "@sveltejs/kit": "^2.21.2", + "@sveltejs/kit": "^2.22.0", "@sveltejs/vite-plugin-svelte": "^3.1.2", "@types/escape-html": "^1.0.4", "@types/glidejs__glide": "^3.6.6", @@ -48,26 +48,26 @@ "husky": "^9.1.7", "lint-staged": "^15.5.2", "mdsvex": "^0.11.2", - "minimatch": "9", + "minimatch": "^9.0.5", "minimist": "^1.2.8", "npm-run-all2": "^6.2.6", "openai": "^4.104.0", "p-queue": "^8.1.0", - "prettier": "^3.5.3", + "prettier": "^3.6.0", "prettier-plugin-svelte": "^3.4.0", "remark-heading-id": "^1.0.1", "remove-markdown": "^0.5.5", "simple-git": "^3.28.0", "svelte": "^4.2.20", - "svelte-check": "^4.2.1", + "svelte-check": "^4.2.2", "tslib": "^2.8.1", - "tsx": "^4.19.4", + "tsx": "^4.20.3", "typescript": "^5.8.3", "vite": "^5.4.19", "vitest": "^2.1.9" }, "dependencies": { - "@anthropic-ai/sdk": "^0.39.0", + "@anthropic-ai/sdk": "^0.54.0", "@fontsource/roboto-slab": "^5.2.6", "@fontsource/saira-condensed": "^5.2.6", "@glidejs/glide": "~3.6.2", @@ -77,7 +77,7 @@ "@turf/distance": "^7.2.0", "@types/mapbox-gl": "^2.7.21", "airtable": "^0.12.2", - "axios": "^1.9.0", + "axios": "^1.10.0", "clipboard-polyfill": "^4.1.1", "easy-web-worker": "^7.0.2", "escape-html": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edb90b9a3..eff4a9827 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@anthropic-ai/sdk': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.54.0 + version: 0.54.0 '@fontsource/roboto-slab': specifier: ^5.2.6 version: 5.2.6 @@ -25,10 +25,10 @@ importers: version: 1.3.0 '@prgm/sveltekit-progress-bar': specifier: ^2.0.0 - version: 2.0.0(@sveltejs/kit@2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20) + version: 2.0.0(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20) '@sveltejs/enhanced-img': specifier: ~0.3.10 - version: 0.3.10(rollup@4.42.0)(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) + version: 0.3.10(rollup@4.44.0)(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) '@turf/distance': specifier: ^7.2.0 version: 7.2.0 @@ -39,8 +39,8 @@ importers: specifier: ^0.12.2 version: 0.12.2 axios: - specifier: ^1.9.0 - version: 1.9.0 + specifier: ^1.10.0 + version: 1.10.0 clipboard-polyfill: specifier: ^4.1.1 version: 4.1.1 @@ -103,14 +103,14 @@ importers: version: 1.0.40 devDependencies: '@inlang/paraglide-js': - specifier: ^2.0.13 - version: 2.0.13 + specifier: ^2.1.0 + version: 2.1.0 '@sveltejs/adapter-netlify': specifier: ^5.0.2 - version: 5.0.2(@sveltejs/kit@2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))) + version: 5.0.2(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))) '@sveltejs/kit': - specifier: ^2.21.2 - version: 2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) + specifier: ^2.22.0 + version: 2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.2 version: 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) @@ -146,7 +146,7 @@ importers: version: 5.62.0(eslint@8.57.1)(typescript@5.8.3) axios-retry: specifier: ^4.5.0 - version: 4.5.0(axios@1.9.0) + version: 4.5.0(axios@1.10.0) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -172,7 +172,7 @@ importers: specifier: ^0.11.2 version: 0.11.2(svelte@4.2.20) minimatch: - specifier: '9' + specifier: ^9.0.5 version: 9.0.5 minimist: specifier: ^1.2.8 @@ -187,11 +187,11 @@ importers: specifier: ^8.1.0 version: 8.1.0 prettier: - specifier: ^3.5.3 - version: 3.5.3 + specifier: ^3.6.0 + version: 3.6.0 prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.5.3)(svelte@4.2.20) + version: 3.4.0(prettier@3.6.0)(svelte@4.2.20) remark-heading-id: specifier: ^1.0.1 version: 1.0.1 @@ -205,14 +205,14 @@ importers: specifier: ^4.2.20 version: 4.2.20 svelte-check: - specifier: ^4.2.1 - version: 4.2.1(picomatch@4.0.2)(svelte@4.2.20)(typescript@5.8.3) + specifier: ^4.2.2 + version: 4.2.2(picomatch@4.0.2)(svelte@4.2.20)(typescript@5.8.3) tslib: specifier: ^2.8.1 version: 2.8.1 tsx: - specifier: ^4.19.4 - version: 4.19.4 + specifier: ^4.20.3 + version: 4.20.3 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -229,8 +229,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@anthropic-ai/sdk@0.39.0': - resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==} + '@anthropic-ai/sdk@0.54.0': + resolution: {integrity: sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw==} + hasBin: true '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} @@ -682,15 +683,15 @@ packages: cpu: [x64] os: [win32] - '@inlang/paraglide-js@2.0.13': - resolution: {integrity: sha512-8tccsLzGa9uw0rufFqbHSM6GDF8+X1BgfBOyjG7PweBF2zGhN5fMu/nVNbsZiVKpXyR7lcfMxajIBwKhZ/zGKw==} + '@inlang/paraglide-js@2.1.0': + resolution: {integrity: sha512-hpj5AglQphR91IH2qXPRp78fTkRVdhlNl09rZZBTtB18fcmJpCScDFckUEaR0UhOARySd4SiH/7/GbVenFcrbQ==} hasBin: true '@inlang/recommend-sherlock@0.2.1': resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} - '@inlang/sdk@2.4.8': - resolution: {integrity: sha512-tyXNe/5+1Vn/eDt3mVklVjZh5qxFwqdF9+hdB6wRUCexVRw6w/w854TIRFrHuaAwFq/0N/ij/yXzll9oScAB+Q==} + '@inlang/sdk@2.4.9': + resolution: {integrity: sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ==} engines: {node: '>=18.0.0'} '@jridgewell/gen-mapping@0.3.8': @@ -802,8 +803,8 @@ packages: '@sveltejs/kit': ^2.0.0 svelte: ^4.0.0 - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -811,103 +812,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.42.0': - resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==} + '@rollup/rollup-android-arm-eabi@4.44.0': + resolution: {integrity: sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.42.0': - resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==} + '@rollup/rollup-android-arm64@4.44.0': + resolution: {integrity: sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.42.0': - resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==} + '@rollup/rollup-darwin-arm64@4.44.0': + resolution: {integrity: sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.42.0': - resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==} + '@rollup/rollup-darwin-x64@4.44.0': + resolution: {integrity: sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.42.0': - resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==} + '@rollup/rollup-freebsd-arm64@4.44.0': + resolution: {integrity: sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.42.0': - resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==} + '@rollup/rollup-freebsd-x64@4.44.0': + resolution: {integrity: sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.42.0': - resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.0': + resolution: {integrity: sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.42.0': - resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==} + '@rollup/rollup-linux-arm-musleabihf@4.44.0': + resolution: {integrity: sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.42.0': - resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==} + '@rollup/rollup-linux-arm64-gnu@4.44.0': + resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.42.0': - resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==} + '@rollup/rollup-linux-arm64-musl@4.44.0': + resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.42.0': - resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.0': + resolution: {integrity: sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': - resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': + resolution: {integrity: sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.42.0': - resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==} + '@rollup/rollup-linux-riscv64-gnu@4.44.0': + resolution: {integrity: sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.42.0': - resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==} + '@rollup/rollup-linux-riscv64-musl@4.44.0': + resolution: {integrity: sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.42.0': - resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==} + '@rollup/rollup-linux-s390x-gnu@4.44.0': + resolution: {integrity: sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.42.0': - resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==} + '@rollup/rollup-linux-x64-gnu@4.44.0': + resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.42.0': - resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==} + '@rollup/rollup-linux-x64-musl@4.44.0': + resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.42.0': - resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==} + '@rollup/rollup-win32-arm64-msvc@4.44.0': + resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.42.0': - resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==} + '@rollup/rollup-win32-ia32-msvc@4.44.0': + resolution: {integrity: sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.42.0': - resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==} + '@rollup/rollup-win32-x64-msvc@4.44.0': + resolution: {integrity: sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==} cpu: [x64] os: [win32] @@ -934,14 +935,14 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: '>= 5.0.0' - '@sveltejs/kit@2.21.2': - resolution: {integrity: sha512-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA==} + '@sveltejs/kit@2.22.0': + resolution: {integrity: sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 '@sveltejs/vite-plugin-svelte-inspector@2.1.0': resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==} @@ -973,9 +974,6 @@ packages: '@types/escape-html@1.0.4': resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1027,11 +1025,8 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@18.19.111': - resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==} - - '@types/node@20.19.0': - resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} + '@types/node@18.19.112': + resolution: {integrity: sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog==} '@types/node@24.0.3': resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} @@ -1162,8 +1157,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1227,8 +1222,8 @@ packages: peerDependencies: axios: 0.x || 1.x - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1240,11 +1235,11 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1592,8 +1587,8 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.4.5: - resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1941,8 +1936,8 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -2061,6 +2056,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -2213,8 +2209,8 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss@8.5.4: - resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} potpack@2.0.0: @@ -2230,8 +2226,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.6.0: + resolution: {integrity: sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==} engines: {node: '>=14'} hasBin: true @@ -2312,8 +2308,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.42.0: - resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==} + rollup@4.44.0: + resolution: {integrity: sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2436,8 +2432,8 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - svelte-check@4.2.1: - resolution: {integrity: sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==} + svelte-check@4.2.2: + resolution: {integrity: sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -2503,8 +2499,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinypool@1.1.0: - resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} tinyqueue@3.0.0: @@ -2544,8 +2540,8 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.19.4: - resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -2573,9 +2569,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} @@ -2701,6 +2694,14 @@ packages: vite: optional: true + vitefu@1.0.7: + resolution: {integrity: sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + vitest@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2799,17 +2800,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@anthropic-ai/sdk@0.39.0': - dependencies: - '@types/node': 18.19.111 - '@types/node-fetch': 2.6.12 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + '@anthropic-ai/sdk@0.54.0': {} '@emnapi/runtime@1.4.3': dependencies: @@ -3084,10 +3075,10 @@ snapshots: '@img/sharp-win32-x64@0.34.2': optional: true - '@inlang/paraglide-js@2.0.13': + '@inlang/paraglide-js@2.1.0': dependencies: '@inlang/recommend-sherlock': 0.2.1 - '@inlang/sdk': 2.4.8 + '@inlang/sdk': 2.4.9 commander: 11.1.0 consola: 3.4.0 json5: 2.2.3 @@ -3100,7 +3091,7 @@ snapshots: dependencies: comment-json: 4.2.5 - '@inlang/sdk@2.4.8': + '@inlang/sdk@2.4.9': dependencies: '@lix-js/sdk': 0.4.7 '@sinclair/typebox': 0.31.28 @@ -3209,110 +3200,110 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@prgm/sveltekit-progress-bar@2.0.0(@sveltejs/kit@2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)': + '@prgm/sveltekit-progress-bar@2.0.0(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)': dependencies: - '@sveltejs/kit': 2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) + '@sveltejs/kit': 2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) svelte: 4.2.20 - '@rollup/pluginutils@5.1.4(rollup@4.42.0)': + '@rollup/pluginutils@5.2.0(rollup@4.44.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.42.0 + rollup: 4.44.0 - '@rollup/rollup-android-arm-eabi@4.42.0': + '@rollup/rollup-android-arm-eabi@4.44.0': optional: true - '@rollup/rollup-android-arm64@4.42.0': + '@rollup/rollup-android-arm64@4.44.0': optional: true - '@rollup/rollup-darwin-arm64@4.42.0': + '@rollup/rollup-darwin-arm64@4.44.0': optional: true - '@rollup/rollup-darwin-x64@4.42.0': + '@rollup/rollup-darwin-x64@4.44.0': optional: true - '@rollup/rollup-freebsd-arm64@4.42.0': + '@rollup/rollup-freebsd-arm64@4.44.0': optional: true - '@rollup/rollup-freebsd-x64@4.42.0': + '@rollup/rollup-freebsd-x64@4.44.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.42.0': + '@rollup/rollup-linux-arm-gnueabihf@4.44.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.42.0': + '@rollup/rollup-linux-arm-musleabihf@4.44.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.42.0': + '@rollup/rollup-linux-arm64-gnu@4.44.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.42.0': + '@rollup/rollup-linux-arm64-musl@4.44.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.42.0': + '@rollup/rollup-linux-loongarch64-gnu@4.44.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.42.0': + '@rollup/rollup-linux-riscv64-gnu@4.44.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.42.0': + '@rollup/rollup-linux-riscv64-musl@4.44.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.42.0': + '@rollup/rollup-linux-s390x-gnu@4.44.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.42.0': + '@rollup/rollup-linux-x64-gnu@4.44.0': optional: true - '@rollup/rollup-linux-x64-musl@4.42.0': + '@rollup/rollup-linux-x64-musl@4.44.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.42.0': + '@rollup/rollup-win32-arm64-msvc@4.44.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.42.0': + '@rollup/rollup-win32-ia32-msvc@4.44.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.42.0': + '@rollup/rollup-win32-x64-msvc@4.44.0': optional: true '@sinclair/typebox@0.31.28': {} '@sqlite.org/sqlite-wasm@3.48.0-build4': {} - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - '@sveltejs/adapter-netlify@5.0.2(@sveltejs/kit@2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))': + '@sveltejs/adapter-netlify@5.0.2(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))': dependencies: '@iarna/toml': 2.2.5 - '@sveltejs/kit': 2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) + '@sveltejs/kit': 2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) esbuild: 0.25.5 set-cookie-parser: 2.7.1 - '@sveltejs/enhanced-img@0.3.10(rollup@4.42.0)(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))': + '@sveltejs/enhanced-img@0.3.10(rollup@4.44.0)(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))': dependencies: magic-string: 0.30.17 svelte: 4.2.20 svelte-parse-markup: 0.1.5(svelte@4.2.20) vite: 5.4.19(@types/node@24.0.3) - vite-imagetools: 7.1.0(rollup@4.42.0) + vite-imagetools: 7.1.0(rollup@4.44.0) transitivePeerDependencies: - rollup - '@sveltejs/kit@2.21.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))': + '@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))': dependencies: - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)) '@types/cookie': 0.6.0 - acorn: 8.14.1 + acorn: 8.15.0 cookie: 0.6.0 devalue: 5.1.1 esm-env: 1.2.2 @@ -3324,6 +3315,7 @@ snapshots: sirv: 3.0.1 svelte: 4.2.20 vite: 5.4.19(@types/node@24.0.3) + vitefu: 1.0.7(vite@5.4.19(@types/node@24.0.3)) '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3)))(svelte@4.2.20)(vite@5.4.19(@types/node@24.0.3))': dependencies: @@ -3370,8 +3362,6 @@ snapshots: '@types/escape-html@1.0.4': {} - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/geojson-vt@3.2.5': @@ -3418,19 +3408,15 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 20.19.0 + '@types/node': 24.0.3 form-data: 4.0.3 '@types/node@14.18.63': {} - '@types/node@18.19.111': + '@types/node@18.19.112': dependencies: undici-types: 5.26.5 - '@types/node@20.19.0': - dependencies: - undici-types: 6.21.0 - '@types/node@24.0.3': dependencies: undici-types: 7.8.0 @@ -3578,7 +3564,7 @@ snapshots: '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 1.2.0 abort-controller@3.0.0: @@ -3587,11 +3573,11 @@ snapshots: abortcontroller-polyfill@1.7.8: {} - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - acorn@8.14.1: {} + acorn@8.15.0: {} agentkeepalive@4.6.0: dependencies: @@ -3642,12 +3628,12 @@ snapshots: asynckit@0.4.0: {} - axios-retry@4.5.0(axios@1.9.0): + axios-retry@4.5.0(axios@1.10.0): dependencies: - axios: 1.9.0 + axios: 1.10.0 is-retry-allowed: 2.2.0 - axios@1.9.0: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 form-data: 4.0.3 @@ -3661,12 +3647,12 @@ snapshots: balanced-match@1.0.2: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -3688,7 +3674,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 + loupe: 3.1.4 pathval: 2.0.0 chalk@4.1.2: @@ -3719,7 +3705,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 '@types/estree': 1.0.8 - acorn: 8.14.1 + acorn: 8.15.0 estree-walker: 3.0.3 periscopic: 3.1.0 @@ -3931,9 +3917,9 @@ snapshots: eslint-compat-utils: 0.5.1(eslint@8.57.1) esutils: 2.0.3 known-css-properties: 0.35.0 - postcss: 8.5.4 - postcss-load-config: 3.1.4(postcss@8.5.4) - postcss-safe-parser: 6.0.0(postcss@8.5.4) + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 6.0.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 semver: 7.7.2 svelte-eslint-parser: 0.43.0(svelte@4.2.20) @@ -4001,8 +3987,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -4065,7 +4051,7 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.4.5(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -4371,7 +4357,7 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 - loupe@3.1.3: {} + loupe@3.1.4: {} lower-case@2.0.2: dependencies: @@ -4469,11 +4455,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -4533,7 +4519,7 @@ snapshots: openai@4.104.0: dependencies: - '@types/node': 18.19.111 + '@types/node': 18.19.112 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 agentkeepalive: 4.6.0 @@ -4612,27 +4598,27 @@ snapshots: pidtree@0.6.0: {} - postcss-load-config@3.1.4(postcss@8.5.4): + postcss-load-config@3.1.4(postcss@8.5.6): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-safe-parser@6.0.0(postcss@8.5.4): + postcss-safe-parser@6.0.0(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.5.4): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.4: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -4642,12 +4628,12 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@4.2.20): + prettier-plugin-svelte@3.4.0(prettier@3.6.0)(svelte@4.2.20): dependencies: - prettier: 3.5.3 + prettier: 3.6.0 svelte: 4.2.20 - prettier@3.5.3: {} + prettier@3.6.0: {} prism-svelte@0.4.7: {} @@ -4722,30 +4708,30 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.42.0: + rollup@4.44.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.42.0 - '@rollup/rollup-android-arm64': 4.42.0 - '@rollup/rollup-darwin-arm64': 4.42.0 - '@rollup/rollup-darwin-x64': 4.42.0 - '@rollup/rollup-freebsd-arm64': 4.42.0 - '@rollup/rollup-freebsd-x64': 4.42.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.42.0 - '@rollup/rollup-linux-arm-musleabihf': 4.42.0 - '@rollup/rollup-linux-arm64-gnu': 4.42.0 - '@rollup/rollup-linux-arm64-musl': 4.42.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.42.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.42.0 - '@rollup/rollup-linux-riscv64-gnu': 4.42.0 - '@rollup/rollup-linux-riscv64-musl': 4.42.0 - '@rollup/rollup-linux-s390x-gnu': 4.42.0 - '@rollup/rollup-linux-x64-gnu': 4.42.0 - '@rollup/rollup-linux-x64-musl': 4.42.0 - '@rollup/rollup-win32-arm64-msvc': 4.42.0 - '@rollup/rollup-win32-ia32-msvc': 4.42.0 - '@rollup/rollup-win32-x64-msvc': 4.42.0 + '@rollup/rollup-android-arm-eabi': 4.44.0 + '@rollup/rollup-android-arm64': 4.44.0 + '@rollup/rollup-darwin-arm64': 4.44.0 + '@rollup/rollup-darwin-x64': 4.44.0 + '@rollup/rollup-freebsd-arm64': 4.44.0 + '@rollup/rollup-freebsd-x64': 4.44.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.0 + '@rollup/rollup-linux-arm-musleabihf': 4.44.0 + '@rollup/rollup-linux-arm64-gnu': 4.44.0 + '@rollup/rollup-linux-arm64-musl': 4.44.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.0 + '@rollup/rollup-linux-riscv64-gnu': 4.44.0 + '@rollup/rollup-linux-riscv64-musl': 4.44.0 + '@rollup/rollup-linux-s390x-gnu': 4.44.0 + '@rollup/rollup-linux-x64-gnu': 4.44.0 + '@rollup/rollup-linux-x64-musl': 4.44.0 + '@rollup/rollup-win32-arm64-msvc': 4.44.0 + '@rollup/rollup-win32-ia32-msvc': 4.44.0 + '@rollup/rollup-win32-x64-msvc': 4.44.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -4889,11 +4875,11 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.2.1(picomatch@4.0.2)(svelte@4.2.20)(typescript@5.8.3): + svelte-check@4.2.2(picomatch@4.0.2)(svelte@4.2.20)(typescript@5.8.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.3 - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picocolors: 1.1.1 sade: 1.8.1 svelte: 4.2.20 @@ -4906,8 +4892,8 @@ snapshots: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - postcss: 8.5.4 - postcss-scss: 4.0.9(postcss@8.5.4) + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) optionalDependencies: svelte: 4.2.20 @@ -4951,7 +4937,7 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@types/estree': 1.0.8 - acorn: 8.14.1 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 code-red: 1.0.4 @@ -4968,7 +4954,7 @@ snapshots: tinyexec@0.3.2: {} - tinypool@1.1.0: {} + tinypool@1.1.1: {} tinyqueue@3.0.0: {} @@ -4995,7 +4981,7 @@ snapshots: tslib: 1.14.1 typescript: 5.8.3 - tsx@4.19.4: + tsx@4.20.3: dependencies: esbuild: 0.25.5 get-tsconfig: 4.10.1 @@ -5016,8 +5002,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.21.0: {} - undici-types@7.8.0: {} unified@10.1.2: @@ -5094,7 +5078,7 @@ snapshots: unplugin@2.3.5: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 @@ -5135,9 +5119,9 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-imagetools@7.1.0(rollup@4.42.0): + vite-imagetools@7.1.0(rollup@4.44.0): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.42.0) + '@rollup/pluginutils': 5.2.0(rollup@4.44.0) imagetools-core: 7.1.0 sharp: 0.34.2 transitivePeerDependencies: @@ -5164,8 +5148,8 @@ snapshots: vite@5.4.19(@types/node@24.0.3): dependencies: esbuild: 0.21.5 - postcss: 8.5.4 - rollup: 4.42.0 + postcss: 8.5.6 + rollup: 4.44.0 optionalDependencies: '@types/node': 24.0.3 fsevents: 2.3.3 @@ -5174,6 +5158,10 @@ snapshots: optionalDependencies: vite: 5.4.19(@types/node@24.0.3) + vitefu@1.0.7(vite@5.4.19(@types/node@24.0.3)): + optionalDependencies: + vite: 5.4.19(@types/node@24.0.3) + vitest@2.1.9(@types/node@24.0.3): dependencies: '@vitest/expect': 2.1.9 @@ -5191,7 +5179,7 @@ snapshots: std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinypool: 1.1.0 + tinypool: 1.1.1 tinyrainbow: 1.2.0 vite: 5.4.19(@types/node@24.0.3) vite-node: 2.1.9(@types/node@24.0.3) diff --git a/src/lib/usage-logger.ts b/src/lib/usage-logger.ts new file mode 100644 index 000000000..b85e6a255 --- /dev/null +++ b/src/lib/usage-logger.ts @@ -0,0 +1,228 @@ +import { writeFileSync, existsSync } from 'fs' +import { join } from 'path' + +const USAGE_LOG_FILE = 'write-usage.log' + +interface UsageLogEntry { + [requestId: string]: { + timestamp: string // ISO format for lexical sorting + stepName: string + model: string + orgId?: string + usage: { + input_tokens: number + cache_creation_input_tokens: number + cache_read_input_tokens: number + output_tokens: number + service_tier: string + } + rateLimits: { + input_tokens_remaining: number + output_tokens_remaining: number + requests_remaining: number + input_tokens_limit: number + output_tokens_limit: number + requests_limit: number + } + durationMs: number + toolsUsed: boolean + webSearchCount?: number + } +} + +/** + * Log API usage information if write-usage.log file exists + * Uses JSON blob per line format for easy parsing + */ +export function logApiUsage( + requestId: string, + stepName: string, + model: string, + response: any, // Anthropic API response + headers: Record, // Response headers + durationMs: number, + toolsUsed: boolean = false, + webSearchCount: number = 0 +): void { + // Only log if the log file exists (opt-in) + if (!existsSync(USAGE_LOG_FILE)) { + return + } + + try { + const logEntry: UsageLogEntry = { + [requestId]: { + timestamp: new Date().toISOString(), + stepName, + model, + orgId: headers['anthropic-organization-id'], + usage: response.usage || { + input_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + output_tokens: 0, + service_tier: 'unknown' + }, + rateLimits: { + input_tokens_remaining: parseInt( + headers['anthropic-ratelimit-input-tokens-remaining'] || '0' + ), + output_tokens_remaining: parseInt( + headers['anthropic-ratelimit-output-tokens-remaining'] || '0' + ), + requests_remaining: parseInt(headers['anthropic-ratelimit-requests-remaining'] || '0'), + input_tokens_limit: parseInt(headers['anthropic-ratelimit-input-tokens-limit'] || '0'), + output_tokens_limit: parseInt(headers['anthropic-ratelimit-output-tokens-limit'] || '0'), + requests_limit: parseInt(headers['anthropic-ratelimit-requests-limit'] || '0') + }, + durationMs, + toolsUsed, + ...(webSearchCount > 0 && { webSearchCount }) + } + } + + // Append JSON blob as single line + const logLine = JSON.stringify(logEntry) + '\n' + writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' }) + } catch (error) { + // Silently fail to avoid breaking the API + console.warn('Failed to log API usage:', error) + } +} + +/** + * Wrap an Anthropic API promise to optionally add logging with rate limit headers + * If logging is disabled, returns the original response + * If logging is enabled, uses .withResponse() to capture headers and logs usage + */ +export function optionallyLogUsage( + originalPromise: any, // The anthropic.messages.create() promise + stepName: string, + model: string, + startTime: number, + toolsUsed: boolean = false, + webSearchCount: number = 0 +): Promise { + // If logging is disabled, return original promise unchanged + if (!existsSync(USAGE_LOG_FILE)) { + return originalPromise + } + + // If logging is enabled, use withResponse() to get headers + return originalPromise + .withResponse() + .then((responseWithMeta: any) => { + const response = responseWithMeta.data + const headers = responseWithMeta.response.headers + const durationMs = Date.now() - startTime + + // Log in background (don't block) + try { + const logEntry: UsageLogEntry = { + [response.id]: { + timestamp: new Date().toISOString(), + stepName, + model, + usage: response.usage || { + input_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + output_tokens: 0, + service_tier: 'unknown' + }, + rateLimits: { + input_tokens_remaining: parseInt( + headers.get('anthropic-ratelimit-input-tokens-remaining') || '0' + ), + output_tokens_remaining: parseInt( + headers.get('anthropic-ratelimit-output-tokens-remaining') || '0' + ), + requests_remaining: parseInt( + headers.get('anthropic-ratelimit-requests-remaining') || '0' + ), + input_tokens_limit: parseInt( + headers.get('anthropic-ratelimit-input-tokens-limit') || '0' + ), + output_tokens_limit: parseInt( + headers.get('anthropic-ratelimit-output-tokens-limit') || '0' + ), + requests_limit: parseInt(headers.get('anthropic-ratelimit-requests-limit') || '0') + // Check for potential web search rate limit headers (may not exist) + /* + web_search_remaining: + parseInt(headers.get('anthropic-ratelimit-web-search-remaining') || '0') || null, + web_search_limit: + parseInt(headers.get('anthropic-ratelimit-web-search-limit') || '0') || null, + // Also capture web search usage from response body if available + web_search_requests_used: response.usage?.server_tool_use?.web_search_requests || null + */ + }, + durationMs, + toolsUsed, + ...(webSearchCount > 0 && { webSearchCount }) + } + } + + // Append JSON blob as single line + const logLine = JSON.stringify(logEntry) + '\n' + writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' }) + } catch (error) { + // Silently fail to avoid breaking the API + console.warn('Failed to log API usage:', error) + } + + // Return the original response (not the withResponse wrapper) + return response + }) + .catch((error: any) => { + // Log errors with headers if possible + const durationMs = Date.now() - startTime + + try { + const errorId = `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const logEntry = { + [errorId]: { + timestamp: new Date().toISOString(), + stepName, + model, + error: error?.message || String(error), + durationMs, + toolsUsed, + // Try to get rate limits from error response if available + rateLimits: error?.headers + ? { + input_tokens_remaining: parseInt( + error.headers['anthropic-ratelimit-input-tokens-remaining'] || '0' + ), + output_tokens_remaining: parseInt( + error.headers['anthropic-ratelimit-output-tokens-remaining'] || '0' + ), + requests_remaining: parseInt( + error.headers['anthropic-ratelimit-requests-remaining'] || '0' + ), + input_tokens_limit: parseInt( + error.headers['anthropic-ratelimit-input-tokens-limit'] || '0' + ), + output_tokens_limit: parseInt( + error.headers['anthropic-ratelimit-output-tokens-limit'] || '0' + ), + requests_limit: parseInt( + error.headers['anthropic-ratelimit-requests-limit'] || '0' + ) + } + : null + } + } + + // Append JSON blob as single line + const logLine = JSON.stringify(logEntry) + '\n' + writeFileSync(USAGE_LOG_FILE, logLine, { flag: 'a' }) + } catch (logError) { + // Silently fail to avoid breaking the API + console.warn('Failed to log API error:', logError) + } + + // Rethrow the original error + throw error + }) +} diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 1fd2f9c92..54b24681e 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -1,9 +1,14 @@ import { error, json } from '@sveltejs/kit' import { env } from '$env/dynamic/private' import Anthropic from '@anthropic-ai/sdk' +import { optionallyLogUsage } from '$lib/usage-logger' // Safely access the API key, will be undefined if not set const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +// NEW: Add global toggle for web search functionality +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' // Default to true unless explicitly disabled +// NEW: Add configurable rate limiting for tool calls per step +const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') // Flag to track if API is available const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE @@ -16,14 +21,123 @@ if (!IS_API_AVAILABLE) { } // Define step types for server-side use -type StepName = 'research' | 'firstDraft' | 'firstCut' | 'firstEdit' | 'toneEdit' | 'finalEdit' +//CLAUDE CHANGE: Added userRevision step +type StepName = + | 'findTarget' + | 'webSearch' + | 'research' + | 'firstDraft' + | 'firstCut' + | 'firstEdit' + | 'toneEdit' + | 'finalEdit' + | 'userRevision' + +// Define workflow types +//CLAUDE CHANGE: Added workflow type 5 for revision +type WorkflowType = '1' | '2' | '3' | '4' | '5' + +//CLAUDE CHANGE: Added model configuration to step config interface +interface StepConfig { + toolsEnabled?: boolean // Whether this step can use tools + maxToolCalls?: number // Maximum tool calls for this step (overrides global) + description?: string // Enhanced description when tools are used + model?: string // Model to use for this step +} + +// ENHANCED: Extend workflow configuration to support step configs +type WorkflowConfig = { + steps: StepName[] + description: string + stepConfigs?: Record // NEW: Optional step-level configuration +} +// NEW: Define step-level tool configurations +//CLAUDE CHANGE: Updated step configurations with model specifications +const stepConfigs: Record = { + // Research-focused steps that benefit from web search - use Haiku for speed + findTarget: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Find possible targets (using web search)', + model: 'claude-3-5-haiku-latest' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)', + model: 'claude-3-5-haiku-latest' + }, + research: { + toolsEnabled: false, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs', + model: 'claude-3-7-sonnet-latest' + }, + // Text processing steps use Sonnet for quality + firstDraft: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + }, + firstCut: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + }, + firstEdit: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + }, + toneEdit: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + }, + finalEdit: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + }, + userRevision: { + toolsEnabled: false, + model: 'claude-3-7-sonnet-latest' + } +} + +//CLAUDE CHANGE: Added workflow 5 for email revision +const workflowConfigs: Record = { + '1': { + steps: ['findTarget'], + description: 'Find Target Only', + stepConfigs // NEW: Include step configurations + }, + '2': { + steps: ['webSearch', 'research'], + description: 'Web Search + Autofill', + stepConfigs // NEW: Include step configurations + }, + '3': { + steps: ['research'], + description: 'Autofill only', + stepConfigs // NEW: Include step configurations + }, + '4': { + steps: ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'], + description: 'Full Email Generation', + stepConfigs // NEW: Include step configurations + }, + '5': { + steps: ['userRevision'], + description: 'Email Revision', + stepConfigs + } +} // Server-side state management interface (not exposed to client) interface WriteState { step: StepName | 'complete' | 'start' // Current/completed step - userInput: string // Original input from form + workflowType: WorkflowType // Type of workflow being executed + userInput: string // Original input from form (cleaned, without prefix) email: string // Current email content information?: string // Processed information after research + //CLAUDE CHANGE: Added originalUserInput to store context from firstDraft step + originalUserInput?: string // Original user input from firstDraft step for context in subsequent steps completedSteps: Array<{ name: StepName durationSec: number @@ -43,6 +157,19 @@ export type ChatResponse = { information?: string // Processed information for form fields } +// Type safety by Claude +interface AnthropicResponse { + id: string + content: Array<{ + type: string + text?: string + name?: string + [key: string]: any + }> + stop_reason?: string + [key: string]: any +} + export type Message = { role: 'user' | 'assistant' | 'system' content: string @@ -52,6 +179,7 @@ const System_Prompts: { [id: string]: string } = {} System_Prompts['Basic'] = `You are a helpful AI assistant. Note: Some fields in the information may begin with a robot emoji (🤖). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.` + System_Prompts['Mail'] = ` What follows is the anatomy of a good email, a set of guidelines and criteria for writing a good mail. Each paragraph, except the last represents @@ -177,9 +305,67 @@ Example: Original: "Preferred communication style: undefined" Your output: "Preferred communication style: 🤖 Formal but approachable" +Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. + Output the full information, including your edits. Output nothing else. ` +System_Prompts['Target'] = ` +Please use your internet search capability to find individuals involved with AI safety who match the following description. + +For each person you find (aim for 3-5 people), please provide: +1. Name and current position +2. Why they're relevant to AI safety +3. Their organization +4. Brief note on their public stance on AI safety + +Do not tell the user what you are searching for. Only output the final product. + +Focus on finding People. NOT Organisations. +OUTPUT VERY VERY QUICKLY! YOU HAVE VERY LITTLE TIME! OTHERWISE IT'LL ALL BE WASTED! BE FAST!!! +` + +//Preface with '[Person's Name] = John Doe' etc. +System_Prompts['webSearch'] = ` +Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. + +Search for and provide: +1. Professional background (education, career history, notable positions) +2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) +3. Their public views on AI development and safety (with direct quotes where possible) +4. Recent activities related to technology policy or AI (last 6-12 months) +5. Communication style and key terms they use when discussing technology issues +6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) +7. Contact information (professional email or official channels if publicly available) + +Please only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. +Do not tell the user what you are searching for. Only output the final product. +OUTPUT VERY VERY QUICKLY! YOU HAVE VERY LITTLE TIME! OTHERWISE IT'LL ALL BE WASTED! BE FAST!!! +` + +//CLAUDE CHANGE: Added User_Revision system prompt with proper email writing context +System_Prompts['User_Revision'] = ` +You are helping to revise an email based on user feedback. You will be given: +1. The current email draft +2. User feedback and requested changes + +Your task is to: +- Apply the requested changes thoughtfully and professionally +- Maintain the overall structure and quality of the email +- Preserve the email's effectiveness while incorporating user preferences +- Keep the tone consistent unless specifically asked to change it +- Ensure all changes improve rather than detract from the message + +Focus on making precise, targeted improvements based on the user's specific feedback. If the feedback is unclear, make reasonable interpretations that improve the email's quality. + +Only output the revised email, nothing else. +` + +//BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. +//BE FAST! You do not have a lot of time to answer this query before it times out! +//ANSWER QUICKLY!!! +//` + // Only initialize the client if we have an API key const anthropic = IS_API_AVAILABLE ? new Anthropic({ @@ -191,16 +377,54 @@ export async function GET() { return json({ apiAvailable: IS_API_AVAILABLE }) } -// Helper function to call Claude API with timing +// Helper function to parse workflow type from user input +//CLAUDE CHANGE: Updated to handle workflow type 5 +function parseWorkflowType(userInput: string): { + workflowType: WorkflowType + cleanedInput: string +} { + const workflowMatch = userInput.match(/^\[([1-5])\](.*)$/s) + + if (workflowMatch) { + const workflowType = workflowMatch[1] as WorkflowType + const cleanedInput = workflowMatch[2].trim() + return { workflowType, cleanedInput } + } + + // Default to workflow 4 if no prefix is found + return { workflowType: '4', cleanedInput: userInput } +} +// NEW: Interface for tool use response content +interface ToolUseContent { + type: 'tool_use' + id: string + name: string + input: any +} + +// NEW: Interface for tool result content +interface ToolResultContent { + type: 'tool_result' + tool_use_id: string + content: string +} + +//CLAUDE CHANGE: Enhanced callClaude function to accept model parameter async function callClaude( stepName: string, promptNames: string[], - userContent: string + userContent: string, + toolsEnabled: boolean = false, // NEW: Optional parameter for tool usage + model?: string // CLAUDE CHANGE: Optional model parameter ): Promise<{ text: string; durationSec: number }> { const pencil = '✏️' + const search = '🔍' // NEW: Icon for tool usage const logPrefix = `${pencil} write:${stepName}` const startTime = Date.now() + //CLAUDE CHANGE: Determine model to use - from parameter, step config, or default + const modelToUse = model || stepConfigs[stepName as StepName]?.model || 'claude-3-5-haiku-latest' + console.time(`${logPrefix}`) try { @@ -212,73 +436,228 @@ async function callClaude( // Combine all the specified prompts const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') - const response = await anthropic.messages.create({ - model: 'claude-3-7-sonnet-20250219', - max_tokens: 4096, // Increased to handle all fields + // TEMP: Log the full request prompt for debugging + console.debug(`${logPrefix} system prompt:\n---\n${systemPrompt}\n---`) + console.debug(`${logPrefix} user content:\n---\n${userContent}\n---`) + //CLAUDE CHANGE: Log which model is being used + console.debug(`${logPrefix} using model: ${modelToUse}`) + + // NEW: Determine if tools should be included in this call + const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // NEW: Log tool usage status + if (shouldUseTools) { + console.log(`${search} ${logPrefix}: Tools enabled for this step`) + } + + // FIXED: Use correct web search tool definition matching API documentation + const tools = shouldUseTools + ? [ + { + type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs + name: 'web_search', + max_uses: 3 // ADDED: Limit searches per request + } + ] + : undefined + + // ENHANCED: Create API request with conditional tool support + const requestParams: any = { + model: modelToUse, //CLAUDE CHANGE: Use the determined model + max_tokens: 4096, system: systemPrompt, messages: [{ role: 'user', content: userContent }] - }) + } + + // NEW: Add tools to request if enabled + if (tools) { + requestParams.tools = tools + } - // Log the request ID at debug level - console.debug(`${logPrefix} requestId: ${response.id}`) + // FIXED: Implement proper tool execution loop + let currentMessages = [...requestParams.messages] + let finalText = '' + let toolCallCount = 0 + const maxCalls = Math.min( + MAX_TOOL_CALLS_PER_STEP, + stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP + ) - // Ensure the response content is text - if (response.content[0].type !== 'text') { - throw new Error(`Unexpected content type from API: ${response.content[0].type}`) + while (toolCallCount < maxCalls) { + // Create request with current message history + const currentRequest = { + ...requestParams, + messages: currentMessages + } + + const response = (await optionallyLogUsage( + anthropic.messages.create(currentRequest), + stepName, + modelToUse, //CLAUDE CHANGE: Pass the actual model being used + startTime, + shouldUseTools, + toolCallCount + )) as AnthropicResponse + + // Log the request ID at debug level + console.debug(`${logPrefix} requestId: ${response.id}`) + + // FIXED: Process response content properly + let hasToolUse = false + let textContent = '' + + for (const content of response.content) { + if (content.type === 'text') { + textContent += content.text + } else if (content.type === 'server_tool_use' && shouldUseTools) { + // FIXED: Handle server-side tool use (web search is executed automatically) + hasToolUse = true + toolCallCount++ + console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) + } else if (content.type === 'web_search_tool_result') { + // FIXED: Handle web search results (automatically provided by API) + console.log(`${search} ${logPrefix}: Received web search results`) + } + } + + // FIXED: Add assistant's response to conversation history + currentMessages.push({ + role: 'assistant', + content: response.content + }) + + // FIXED: Accumulate text content + finalText += textContent + + // FIXED: Break if no tool use or if we've hit limits + if (!hasToolUse || toolCallCount >= maxCalls) { + break + } + + // FIXED: If there was tool use, Claude might continue in the same turn + // Check if response has pause_turn stop reason + if (response.stop_reason === 'pause_turn') { + // Continue the conversation to let Claude finish its turn + continue + } else { + // Tool use complete, break the loop + break + } + } + + // FIXED: Ensure we have text content + if (!finalText) { + throw new Error('No text content received from Claude') } - const result = response.content[0].text const elapsed = (Date.now() - startTime) / 1000 // seconds + // ENHANCED: Log tool usage statistics + if (shouldUseTools && toolCallCount > 0) { + console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) + } + // Log the full response text at debug level - console.debug(`${logPrefix} full response:\n---\n${result}\n---`) - return { text: result, durationSec: elapsed } + console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) + + // Logging is handled by optionallyLogUsage wrapper + + return { text: finalText, durationSec: elapsed } + } catch (error) { + // ENHANCED: Better error handling for tool-related failures + const errorMessage = error instanceof Error ? error.message : String(error) + if (toolsEnabled && (errorMessage?.includes('tool') || errorMessage?.includes('search'))) { + console.warn( + `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, + errorMessage + ) + // Retry without tools on tool-related errors + return callClaude(stepName, promptNames, userContent, false, modelToUse) //CLAUDE CHANGE: Pass model in retry + } + throw error // Re-throw non-tool errors } finally { console.timeEnd(`${logPrefix}`) } } +// NEW: Function to get step description with tool awareness +function getStepDescription(stepName: StepName): string { + const stepConfig = stepConfigs[stepName] + const toolsWillBeUsed = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // Return enhanced description if tools are enabled and available + if (toolsWillBeUsed && stepConfig?.description) { + return stepConfig.description + } -// Define user-friendly step descriptions -const stepDescriptions: Record = { - research: 'Auto-fill missing user inputs', - firstDraft: 'Create initial draft', - firstCut: 'Remove unnecessary content', - firstEdit: 'Improve text flow', - toneEdit: 'Adjust tone and style', - finalEdit: 'Final polish' + // Fallback to standard descriptions + //CLAUDE CHANGE: Added userRevision to step descriptions + const stepDescriptions: Record = { + findTarget: 'Find possible targets', + webSearch: 'Research the target', + research: 'Auto-fill missing user inputs', + firstDraft: 'Create initial draft', + firstCut: 'Remove unnecessary content', + firstEdit: 'Improve text flow', + toneEdit: 'Adjust tone and style', + finalEdit: 'Final polish', + userRevision: 'Apply user feedback' + } + + return stepDescriptions[stepName] } // Function to generate a progress string from the state + function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = '✓' + const search = '🔍' // NEW: Icon for tool-enabled steps + + // Get workflow description + const workflowDescription = workflowConfigs[state.workflowType].description // Generate the progress string let lis = [] - // Completed steps - use descriptions instead of raw step names + // ENHANCED: Completed steps with tool usage indicators for (const step of state.completedSteps) { + const stepConfig = stepConfigs[step.name] + const usedTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const icon = usedTools ? `${search}${checkmark}` : checkmark + lis.push( - `
  • ${stepDescriptions[step.name]} (${step.durationSec.toFixed(1)}s) ${checkmark}
  • ` + `
  • ${getStepDescription(step.name)} (${step.durationSec.toFixed(1)}s) ${icon}
  • ` ) } - // Current step - only add if not already completed and state isn't complete + // ENHANCED: Current step with tool usage indicator if ( state.currentStep && state.step !== 'complete' && !state.completedSteps.some((s) => s.name === state.currentStep) ) { - lis.push(`
  • ${stepDescriptions[state.currentStep]} ${pencil}
  • `) + const stepConfig = stepConfigs[state.currentStep] + const willUseTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const icon = willUseTools ? `${search}${pencil}` : pencil + + lis.push(`
  • ${getStepDescription(state.currentStep)} ${icon}
  • `) } - // Remaining steps - filter out any that are in completed steps or current step + // ENHANCED: Remaining steps with tool usage preview const completedAndCurrentSteps = [...state.completedSteps.map((s) => s.name), state.currentStep] const filteredRemainingSteps = state.remainingSteps.filter( (step) => !completedAndCurrentSteps.includes(step) ) + lis = lis.concat( - filteredRemainingSteps.map((step) => `
  • ${stepDescriptions[step]}
  • `) + filteredRemainingSteps.map((step) => { + const stepConfig = stepConfigs[step] + const willUseTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const description = getStepDescription(step) + const indicator = willUseTools ? ` ${search}` : '' + + return `
  • ${description}${indicator}
  • ` + }) ) const listItems = lis.join('') @@ -286,31 +665,29 @@ function generateProgressString(state: WriteState): string { if (state.nextStep === null) { // Process is complete - return `Done (${totalTime.toFixed(1)}s):
      ${listItems}
    ` + return `${workflowDescription} - Done (${totalTime.toFixed(1)}s):
      ${listItems}
    ` } else { - return `Progress:
      ${listItems}
    ` + return `${workflowDescription} - Progress:
      ${listItems}
    ` } } // Initialize a new state for the step-by-step process function initializeState(userInput: string): WriteState { - const allSteps: StepName[] = [ - 'research', - 'firstDraft', - 'firstCut', - 'firstEdit', - 'toneEdit', - 'finalEdit' - ] + const { workflowType, cleanedInput } = parseWorkflowType(userInput) + const workflowSteps = workflowConfigs[workflowType].steps + + const firstStep = workflowSteps[0] + const remainingSteps = workflowSteps.slice(1) return { step: 'start', - userInput, + workflowType, + userInput: cleanedInput, email: '', completedSteps: [], - currentStep: 'research', // First step - remainingSteps: allSteps.slice(1), // All steps except research (which is current) - nextStep: 'research' + currentStep: firstStep, + remainingSteps, + nextStep: firstStep } } @@ -332,17 +709,66 @@ function prepareResponse(state: WriteState): ChatResponse { } // Define step handlers in a map for easy lookup +//CLAUDE CHANGE: Added userRevision step handler with proper email writing context const stepHandlers: Record< StepName, (state: WriteState) => Promise<{ text: string; durationSec: number }> > = { + // ENHANCED: Enable tools for target finding + findTarget: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.findTarget + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'findTarget', + ['Basic', 'Target'], + 'Hello! Please help me find a person to contact!' + System_Prompts['Information'], + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // ENHANCED: Enable tools for web search (this step is inherently search-based) + webSearch: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.webSearch + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'webSearch', + ['Basic', 'webSearch', 'Results'], + 'Hello! Please research this person!' + System_Prompts['Information'], + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text + + return result + }, + + // ENHANCED: Enable tools for research step research: async (state) => { System_Prompts['Information'] = state.userInput + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.research + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + const result = await callClaude( 'research', ['Basic', 'Mail', 'Information', 'Research'], - "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!" + "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", + toolsEnabled // NEW: Pass tool enablement flag ) state.information = result.text @@ -351,47 +777,88 @@ const stepHandlers: Record< return result }, + // UNCHANGED: Text processing steps remain without tools for performance firstDraft: async (state) => { + //CLAUDE CHANGE: Store the original user input for subsequent steps + state.originalUserInput = state.userInput + return await callClaude( 'firstDraft', ['Basic', 'Mail', 'First_Draft', 'Results'], - 'Hello! Please write an email draft using the following information. \n' + state.information + 'Hello! Please write an email draft using the following information. \n' + state.userInput + // NOTE: No toolsEnabled parameter = defaults to false ) }, firstCut: async (state) => { + //CLAUDE CHANGE: Include original user input context in subsequent steps + const contextualInput = state.originalUserInput + ? `Original Context:\n${state.originalUserInput}\n\nCurrent Email:\n${state.email}` + : `Current Email:\n${state.email}` + return await callClaude( 'firstCut', ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], - 'Hello! Please cut the following email draft. \n \n' + state.email + 'Hello! Please cut the following email draft. \n\n' + contextualInput + // NOTE: No toolsEnabled parameter = defaults to false ) }, firstEdit: async (state) => { + //CLAUDE CHANGE: Include original user input context in subsequent steps + const contextualInput = state.originalUserInput + ? `Original Context:\n${state.originalUserInput}\n\nCurrent Email:\n${state.email}` + : `Current Email:\n${state.email}` + return await callClaude( 'firstEdit', ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email + 'Hello! Please edit the following email draft. \n\n' + contextualInput + // NOTE: No toolsEnabled parameter = defaults to false ) }, toneEdit: async (state) => { + //CLAUDE CHANGE: Include original user input context in subsequent steps + const contextualInput = state.originalUserInput + ? `Original Context:\n${state.originalUserInput}\n\nCurrent Email:\n${state.email}` + : `Current Email:\n${state.email}` + return await callClaude( 'toneEdit', ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], - 'Hello! Please edit the tone of the following email draft. \n \n' + state.email + 'Hello! Please edit the tone of the following email draft. \n\n' + contextualInput + // NOTE: No toolsEnabled parameter = defaults to false ) }, finalEdit: async (state) => { + //CLAUDE CHANGE: Include original user input context in subsequent steps + const contextualInput = state.originalUserInput + ? `Original Context:\n${state.originalUserInput}\n\nCurrent Email:\n${state.email}` + : `Current Email:\n${state.email}` + return await callClaude( 'finalEdit', ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email + 'Hello! Please edit the following email draft. \n\n' + contextualInput + // NOTE: No toolsEnabled parameter = defaults to false + ) + }, + + //CLAUDE CHANGE: Added userRevision step handler with proper email writing context + userRevision: async (state) => { + // Prepare the revision request with current email and user feedback + const revisionRequest = `Current Email:\n\n${state.email}\n\nUser Feedback:\n\n${state.userInput}` + + return await callClaude( + 'userRevision', + ['Basic', 'Mail', 'User_Revision', 'Results'], + revisionRequest + // NOTE: No toolsEnabled parameter = defaults to false ) } } - // Process a specific step async function processStep(state: WriteState): Promise { if (!state.nextStep) { @@ -416,8 +883,8 @@ async function processStep(state: WriteState): Promise { const result = await stepHandler(state) - // Update email content (except for research step which updates information) - if (currentStep !== 'research') { + // Update email content (except for research-like steps which update information) + if (/*!['research', 'findTarget', 'webSearch']*/ !['research'].includes(currentStep)) { state.email = result.text } @@ -428,20 +895,13 @@ async function processStep(state: WriteState): Promise { durationSec: result.durationSec }) - // Set next step - const allSteps: StepName[] = [ - 'research', - 'firstDraft', - 'firstCut', - 'firstEdit', - 'toneEdit', - 'finalEdit' - ] - const currentIndex = allSteps.indexOf(currentStep) - - if (currentIndex !== -1 && currentIndex < allSteps.length - 1) { - state.nextStep = allSteps[currentIndex + 1] - state.currentStep = allSteps[currentIndex + 1] + // Set next step based on workflow configuration + const workflowSteps = workflowConfigs[state.workflowType].steps + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] } else { // Last step completed, mark as complete state.nextStep = null @@ -474,7 +934,10 @@ export async function POST({ fetch, request }) { // Continue an existing process try { state = JSON.parse(requestData.stateToken) as WriteState - console.log(`${pencil} write: Continuing from step ${state.step}`) + console.log( + `${pencil} write: Continuing from step ${state.step} (workflow ${state.workflowType})` + ) + //CLAUDE CHANGE: Debug logging to check if userInput is preserved } catch (error) { console.error('Error parsing state token:', error) return json({ @@ -497,12 +960,17 @@ export async function POST({ fetch, request }) { } as ChatResponse) } - // Initialize new state + // Initialize new state with workflow parsing state = initializeState(info) + console.log( + `${pencil} write: Detected workflow type ${state.workflowType}: ${workflowConfigs[state.workflowType].description}` + ) // For initial calls (no stateToken), return progress string without processing if (requestData.stateToken === undefined) { - return json(prepareResponse(state)) + const response = prepareResponse(state) + //CLAUDE CHANGE: Debug logging to check what's being returned + return json(response) } } diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index a864d3f5c..35bc9e718 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -23,18 +23,40 @@ // Use a unique localStorage key to avoid conflicts with other pages const STORAGE_KEY = 'email_writer_messages' + // CLAUDE CHANGE: Added storage key for form data + const FORM_DATA_STORAGE_KEY = 'email_writer_form_data' + // UPDATED: Storage key for collapsed sections + const COLLAPSED_SECTIONS_STORAGE_KEY = 'email_writer_collapsed_sections' let messages: Message[] = typeof localStorage !== 'undefined' ? JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') : [] // Array for form - let input_arr = new Array(35) let loading = false let apiAvailable = true // Default to true, will be updated after first API call const maxMessages = 20 + // UPDATED: Simple array-based state management for collapsed sections + let collapsedSections = { + form1: [false], // 1 section in Research form + form2: [false, true, true], // 3 sections in Target form + form3: [false], // 1 section in Message form + form4: [true, true, true], // 3 sections in MessageDetails form + //CLAUDE CHANGE: Added form5 for revision + form5: [false] // 1 section in Revision form + } + // Organizing the form questions into sections and subsections - const formSections: FieldSection[] = [ + const formSections_Target: FieldSection[] = [ + { + title: 'Researching a person', + subsections: [ + { + title: 'To research a person, fill out the following fields. Then, click on AI Help.', + questions: ["Person's Name", 'Current Role', 'Organization/Affiliation'] + } + ] + }, { title: 'Personal Context', subsections: [ @@ -92,27 +114,48 @@ ] } ] - }, + } + ] + + // Flatten the questions array for accessing by index + const paragraphText_Target: string[] = [] + formSections_Target.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_Target.push(question) + }) + }) + }) + + const formSections_Research: FieldSection[] = [ { - title: 'Information Needed About the Message', + title: 'Finding a target', subsections: [ { - title: 'Content Requirements', - questions: ['Specific outcome desired', 'Concrete action requested'] /* - questions: [ - 'Clear, singular objective', - 'Specific outcome desired', - 'Concrete action requested' - ]*/ - }, - { - title: 'Supporting Evidence', + title: + "Specify what sort of target you're looking for, and where. If you already have a target, skip this step.", questions: [ - 'Relevant facts', - 'Context for the request', - 'Potential impact or consequences' + "If you have certain institutions in mind, mention those. Otherwise, mention where you are and what sort of person you're looking for. Input does not carry over past finding a target." ] - }, + } + ] + } + ] + + // Flatten the questions array for accessing by index + const paragraphText_Research: string[] = [] + formSections_Research.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_Research.push(question) + }) + }) + }) + + const formSections_MessageDetails: FieldSection[] = [ + { + title: 'Message Details', + subsections: [ { title: 'Logical Structure', questions: [ @@ -164,25 +207,208 @@ } ] + // CLAUDE CHANGE: Fixed the population of paragraphText_MessageDetails - was incorrectly using formSections_Research + const paragraphText_MessageDetails: string[] = [] + formSections_MessageDetails.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_MessageDetails.push(question) + }) + }) + }) + + const formSections_Message: FieldSection[] = [ + { + title: 'What is your Message?', + subsections: [ + { + title: 'Content Requirements', + questions: ['Specific outcome desired'] /* + questions: [ + 'Clear, singular objective', + 'Specific outcome desired', + 'Concrete action requested' + ]*/ + }, + { + title: 'Supporting Evidence', + questions: ['Relevant facts', 'Context for the request'] + } + ] + } + ] + // Flatten the questions array for accessing by index - const paragraphText: string[] = [] - formSections.forEach((section) => { + const paragraphText_Message: string[] = [] + formSections_Message.forEach((section) => { section.subsections.forEach((subsection) => { subsection.questions.forEach((question) => { - paragraphText.push(question) + paragraphText_Message.push(question) }) }) }) + //CLAUDE CHANGE: Added Form5 for email revision with simplified single question + const formSections_Revision: FieldSection[] = [ + { + title: 'Email Revision Feedback', + subsections: [ + { + title: 'What changes would you like to make to the email?', + questions: [ + 'Describe any changes, improvements, or adjustments you would like to make to the generated email.' + ] + } + ] + } + ] + + //CLAUDE CHANGE: Flatten the revision questions array + const paragraphText_Revision: string[] = [] + formSections_Revision.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_Revision.push(question) + }) + }) + }) + + // UPDATED: Simplified section management functions + function toggleSection( + formId: 'form1' | 'form2' | 'form3' | 'form4' | 'form5', + sectionIndex: number + ) { + collapsedSections[formId][sectionIndex] = !collapsedSections[formId][sectionIndex] + collapsedSections = collapsedSections // Trigger reactivity + saveCollapsedState() + } + + function saveCollapsedState() { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify(collapsedSections)) + } + } + + function loadCollapsedState() { + if (typeof localStorage !== 'undefined') { + const saved = localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY) + if (saved) { + const savedState = JSON.parse(saved) + //CLAUDE CHANGE: Ensure form5 exists in loaded state + collapsedSections = { + form1: savedState.form1 || [false], + form2: savedState.form2 || [false, true, true], + form3: savedState.form3 || [false], + form4: savedState.form4 || [true, true, true], + form5: savedState.form5 || [false] + } + } else { + // Reset to default if no saved state + collapsedSections = { + form1: [false], + form2: [false, true, true], + form3: [false], + form4: [true, true, true], + //CLAUDE CHANGE: Added form5 default state + form5: [false] + } + } + } else { + collapsedSections = { + form1: [false], + form2: [false, true, true], + form3: [false], + form4: [true, true, true], + //CLAUDE CHANGE: Added form5 default state + form5: [false] + } + } + } + + // CLAUDE CHANGE: Added mapping functions to get correct arrays based on active form + function getCurrentInputArray(): string[] { + switch (activeForm) { + case 'form1': + return form1_input_arr + case 'form2': + return form2_input_arr + case 'form3': + return form3_input_arr + case 'form4': + return form4_input_arr + //CLAUDE CHANGE: Added form5 case + case 'form5': + return form5_input_arr + case 'form6': + return form2_input_arr.concat(form3_input_arr, form4_input_arr) + default: + return form2_input_arr + } + } + + function getCurrentQuestionArray(): string[] { + switch (activeForm) { + case 'form1': + return paragraphText_Research + case 'form2': + return paragraphText_Target + case 'form3': + return paragraphText_Message + case 'form4': + return paragraphText_MessageDetails + //CLAUDE CHANGE: Added form5 case + case 'form5': + return paragraphText_Revision + case 'form6': + let allText = paragraphText_Target.concat( + paragraphText_Message, + paragraphText_MessageDetails + ) + return allText + default: + return paragraphText_Target + } + } + function clear_arr(arr: string[]) { for (var i in arr) { arr[i] = '' } } + // CLAUDE CHANGE: Updated clear function to clear current form and reset to form1 function clear() { messages = [] localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)) + + // Clear all form arrays + clear_arr(form1_input_arr) + clear_arr(form2_input_arr) + clear_arr(form3_input_arr) + clear_arr(form4_input_arr) + //CLAUDE CHANGE: Added form5 clearing + clear_arr(form5_input_arr) + + // Clear form data from localStorage + localStorage.removeItem(FORM_DATA_STORAGE_KEY) + // UPDATED: Reset collapsed sections to default state + localStorage.removeItem(COLLAPSED_SECTIONS_STORAGE_KEY) + collapsedSections = { + form1: [false], + form2: [false, true, true], + form3: [false], + form4: [true, true, true], + //CLAUDE CHANGE: Added form5 default state + form5: [false] + } + + // Force Svelte to detect the changes + form1_input_arr = form1_input_arr + form2_input_arr = form2_input_arr + form3_input_arr = form3_input_arr + form4_input_arr = form4_input_arr + //CLAUDE CHANGE: Added form5 reactivity + form5_input_arr = form5_input_arr } function copy() { @@ -194,39 +420,134 @@ ) } - // Magic test function that populates three specific fields and starts the process - for demo purposes + // CLAUDE CHANGE: Updated runTest to work with currently active form and provide appropriate test data function runTest() { // Clear any existing chat clear() - // Clear all input fields - clear_arr(input_arr) + // Get current form arrays + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() - // Find index for specific fields based on their question text - const roleAuthorityIndex = paragraphText.findIndex( - (q) => q === 'Understanding their role and potential authority' - ) - const objectiveIndex = paragraphText.findIndex((q) => q === 'Concrete action requested') - const outcomeIndex = paragraphText.findIndex((q) => q === 'Specific outcome desired') + // Clear current input fields + clear_arr(currentInputArray) - input_arr[roleAuthorityIndex] = 'You are writing for a child of about thirteen years old.' - input_arr[objectiveIndex] = - 'Child understands that the current fast worldwide race to build AGI before we know how to make it safe could mean everybody dies. It sounds like science fiction, but experts are increasingly concerned. Of course the grown-ups should do something about this, but because it can be hard for them to admit to themselves that those they love could be in so much danger, then even once they hear about the problem they find reasons not to believe or act. Being sincere, kind and persistent with them that you need protecting can help everybody in the world.' - input_arr[outcomeIndex] = - "The child isn't too scared, but thinks this is a very important problem all their friends, family and teachers should know about and be actively working to solve." + // Provide test data based on active form + switch (activeForm) { + case 'form1': + // Test data for research form + if (currentInputArray.length > 0) { + currentInputArray[0] = + 'Local city council member or school board representative who handles education policy and child safety issues. Location: California, Los Angeles.' + } + break + + case 'form2': + // Test data for target/personal context form + const roleAuthorityIndex = currentQuestionArray.findIndex( + (q) => q === 'Understanding their role and potential authority' + ) + if (roleAuthorityIndex >= 0) { + currentInputArray[roleAuthorityIndex] = + 'You are writing for a local government official with decision-making authority over education and safety policies.' + } + break + + case 'form3': + // Test data for message form + const objectiveIndex = currentQuestionArray.findIndex( + (q) => q === 'Concrete action requested' + ) + const outcomeIndex = currentQuestionArray.findIndex((q) => q === 'Specific outcome desired') + + if (objectiveIndex >= 0) { + currentInputArray[objectiveIndex] = + 'Official takes action to ensure AI safety education and policies are implemented in local institutions.' + } + if (outcomeIndex >= 0) { + currentInputArray[outcomeIndex] = + 'Local community becomes informed about AI risks and appropriate safety measures are put in place.' + } + break + + case 'form4': + // Test data for message details form + const urgencyIndex = currentQuestionArray.findIndex((q) => q === 'Urgency of the request') + const toneIndex = currentQuestionArray.findIndex( + (q) => q === 'Balancing professionalism and approachability' + ) + + if (urgencyIndex >= 0) { + currentInputArray[urgencyIndex] = + 'High urgency due to rapidly advancing AI development timeline.' + } + if (toneIndex >= 0) { + currentInputArray[toneIndex] = + 'Professional but accessible tone that conveys seriousness without being alarmist.' + } + break + + //CLAUDE CHANGE: Added test data for form5 + case 'form5': + // Test data for revision form + if (currentInputArray.length > 0) { + currentInputArray[0] = + 'Make the tone more urgent and add a specific deadline for response. Also, make the call to action clearer and more direct.' + } + break + } sendMessage() } + // CLAUDE CHANGE: Updated sendMessage to work with currently active form only async function sendMessage() { + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() + let input = '' - for (var i in paragraphText) { - input = input + paragraphText[i] + ':\n' + input_arr[i] + '\n\n' + switch (activeForm) { + case 'form1': + input = input + '[1]' + break + + case 'form2': + input = input + '[2]' + break + + case 'form4': + input = input + '[3]' + break + + //CLAUDE CHANGE: Added form5 case for revision workflow with current email + case 'form5': + input = input + '[5]' + break + + case 'form6': + input = input + '[4]' + break } + + //CLAUDE CHANGE: For form5, add current email before the questions + if (activeForm === 'form5') { + const latestEmail = messages.filter((m) => m.role === 'assistant')[0]?.content || '' + if (latestEmail) { + input += `Current Email:\n${latestEmail}\n\n` + } + } + + for (let i = 0; i < currentQuestionArray.length; i++) { + input = + input + currentQuestionArray[i] + ':\n' + (currentInputArray[i] || 'undefined') + '\n\n' + } + messages = [...messages, { content: input, role: 'user' }] - clear_arr(input_arr) + // Clear current form fields + clear_arr(currentInputArray) loading = true + console.log(input) try { // First request - get initial progress message (no stateToken) @@ -239,6 +560,10 @@ }) const initialData = await initialResponse.json() + console.log('initialData: ', initialData.stateToken) + + //CLAUDE CHANGE: Clean up any incomplete progress messages before adding new one + messages = messages.filter((m) => m.role !== 'progress' || m.complete) // Add server-generated progress message with complete flag messages = [ @@ -259,6 +584,7 @@ }, 100) // Continue with the normal process, but pass the stateToken + console.log('initialData: ', initialData.stateToken) await processSteps(null, initialData.stateToken) } catch (error) { console.error('Error calling email API:', error) @@ -282,6 +608,7 @@ try { // Prepare the request body const requestBody = stateToken ? { stateToken } : inputMessages + console.log(requestBody) // Make the API call const response = await fetch('api/write', { @@ -294,12 +621,16 @@ // Process the response const data = await response.json() + console.log(data.information) // Update API availability apiAvailable = data.apiAvailable !== false // Show progress if available if (data.progressString) { + //CLAUDE CHANGE: Clean up incomplete progress messages before updating + messages = messages.filter((m) => m.role !== 'progress' || m.complete) + // Find existing progress message or create one const progressIndex = messages.findIndex((m) => m.role === 'progress') if (progressIndex >= 0) { @@ -332,23 +663,31 @@ } } - // Update form fields if we have information from the research step + // CLAUDE CHANGE: Updated auto-fill logic to work with currently active form if (data.information) { + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() + // Parse the information string to extract field values const lines = data.information.split('\n') let currentField = -1 for (let line of lines) { - // Look for field headers matching our paragraphText array - const fieldIndex = paragraphText.findIndex((text) => line.trim().startsWith(text + ':')) + // Look for field headers matching our current question array + const fieldIndex = currentQuestionArray.findIndex((text) => + line.trim().startsWith(text + ':') + ) if (fieldIndex >= 0) { currentField = fieldIndex } else if (currentField >= 0 && line.trim()) { // Only update when there's actual content and the field is empty const lineContent = line.trim() - if (lineContent && (!input_arr[currentField] || input_arr[currentField] === '')) { - input_arr[currentField] = lineContent + if ( + lineContent && + (!currentInputArray[currentField] || currentInputArray[currentField] === '') + ) { + currentInputArray[currentField] = lineContent } } } @@ -356,6 +695,8 @@ // Save messages to localStorage localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)) + // CLAUDE CHANGE: Also save form data + saveFormData() // If not complete, continue with the next step if (!data.complete && data.stateToken) { @@ -375,7 +716,41 @@ } } + // CLAUDE CHANGE: Added form data persistence functions + function saveFormData() { + const formData = { + form1_input_arr, + form2_input_arr, + form3_input_arr, + form4_input_arr, + //CLAUDE CHANGE: Added form5 to save data + form5_input_arr, + activeForm + } + localStorage.setItem(FORM_DATA_STORAGE_KEY, JSON.stringify(formData)) + } + + function loadFormData() { + const saved = localStorage.getItem(FORM_DATA_STORAGE_KEY) + if (saved) { + const formData = JSON.parse(saved) + form1_input_arr = formData.form1_input_arr || new Array(paragraphText_Research.length) + form2_input_arr = formData.form2_input_arr || new Array(paragraphText_Target.length) + form3_input_arr = formData.form3_input_arr || new Array(paragraphText_Message.length) + form4_input_arr = + formData.form4_input_arr || new Array(paragraphText_MessageDetails.length) + //CLAUDE CHANGE: Added form5 to load data + form5_input_arr = formData.form5_input_arr || new Array(paragraphText_Revision.length) + activeForm = formData.activeForm || 'form1' + } + } + onMount(async () => { + // CLAUDE CHANGE: Load form data on mount + loadFormData() + // UPDATED: Load collapsed state on mount + loadCollapsedState() + // Check API availability on component mount try { const response = await fetch('api/write') @@ -397,11 +772,35 @@ */ } - // Function to get the index of a question across all sections - function getQuestionIndex(question: string): number { - return paragraphText.findIndex((text) => text === question) + // FORM FUNCTIONS // + + // Add these variables for form toggling + let activeForm = 'form1' // Default active form + + // CLAUDE CHANGE: Updated setActiveForm to save form data when switching + function setActiveForm(formId: string) { + saveFormData() // Save current state before switching + activeForm = formId + saveFormData() // Save current state after switching to save activeForm + console.log(formId) + } + + function writeMail() { + setActiveForm('form6') + sendMessage() } + // UNTESTED AND GENERATED BY AI + // Create separate arrays for each form's inputs + //let input_arr = Array(formSections_Target.flatMap(s => s.subsections.flatMap(ss => ss.questions)).length).fill(''); + // CLAUDE CHANGE: Fixed form array initialization to use correct lengths + let form1_input_arr = Array(paragraphText_Research.length).fill('') + let form2_input_arr = Array(paragraphText_Target.length).fill('') + let form3_input_arr = Array(paragraphText_Message.length).fill('') + let form4_input_arr = Array(paragraphText_MessageDetails.length).fill('') + //CLAUDE CHANGE: Added form5 input array + let form5_input_arr = Array(paragraphText_Revision.length).fill('') + // Top of the page const title = `Write Email Content` const description = `This (beta!) webpage lets you write email content (with LLM assistance.)` @@ -428,47 +827,79 @@ {/if}

    - "Answer questions / fill fields after researching your target. Undefined fields will be + Answer questions / fill fields after researching your target. Undefined fields will be auto-filled. Check the generated email content carefully, as we're bound to make some - mistakes!" + mistakes!

    - The real user interface for entering inputs will surely be refined. For now, scan over the - thirty(!) imperfectly structured input fields at the end of the page, and fill in the ones - that seem the most important. See you back here when done! + You can switch tabs by click on any of the top 5 buttons. This will show new fields: you can + switch back and forth at any time, inputs are saved! If you clicked on "Autofill" and nothing + happened, switch back and forth to refresh the content. The button is grayed out when + unavailable.

    - You can then ask to write content. The AI assistant will auto-fill any fields you didn't - define, based on the ones you did, then proceed to craft an email over a number of steps. The - UX for this part is closer to something we would launch but please give the software team - further feedback! + First, fill in the topmost prompts. You can then ask to autofill content. The AI assistant + will auto-fill any fields you didn't define, based on the ones you did. The UX for this part + is still rough as this feature isn't universally available, so please give us feedback!

    - This is currently a very general writer. If you want it to write to your dad about puppies or - to Trump about how we need to accelerate AI development, it will. We would probably give it - more defaults and impose some restrictions in a truly public version. -

    -

    - For a very quick demo, you can use the button below - it fills in just three fields with - particular hardcoded values, and starts writing content. + To generate the entire email, click the "Write Mail" button. This will take from all input + fields and empty them!

    + +
    + + + + + + +
    +
    - - - +
    @@ -488,48 +919,248 @@
    -

    Here is the grab bag of input fields...

    - @@ -639,8 +1270,71 @@ background-color: var(--brand-subtle); } - button:active { + button.button.active { + background-color: var(--brand-subtle); + } + + /* UPDATED: Collapsible section styles */ + .section-container { + margin-bottom: 1rem; + border: 1px solid var(--text-subtle); + border-radius: 8px; + overflow: hidden; + } + + .section-header { + width: 100%; background-color: var(--brand); + border: none; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: normal; + } + + .section-header:hover { + background-color: var(--brand-subtle); + color: var(--bg); + } + + .section-header h1 { + margin: 0; + font-size: 1.5rem; + font-weight: bold; + } + + .chevron { + font-size: 1.2rem; + transition: transform 0.3s ease; + user-select: none; + } + + .chevron.collapsed { + transform: rotate(-90deg); + } + + .chevron.expanded { + transform: rotate(0deg); + } + + .section-content { + padding: 1rem; + border-top: 1px solid var(--text-subtle); + animation: expandSection 0.3s ease-out; + } + + @keyframes expandSection { + from { + opacity: 0; + max-height: 0; + } + to { + opacity: 1; + max-height: 1000px; + } } .message { @@ -667,6 +1361,20 @@ flex-direction: row; justify-content: flex-start; margin-right: auto; + width: 100%; + max-width: 100%; + height: 300px; /* Approximately 10 lines */ + overflow-y: auto; + } + + .assistant p { + padding: 10px; + border-radius: 10px; + /* Remove margin to prevent layout issues with fixed height */ + margin: 0; + /* Allow the paragraph to expand within the container */ + height: auto; + overflow-wrap: break-word; } :global(.progress) { diff --git a/svelte.config.js b/svelte.config.js index 9d56365f1..92cb8af73 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,7 +12,7 @@ import rehypeSlug from 'rehype-slug' import { locales } from './src/lib/paraglide/runtime.js' // Export configuration flags for use in build scripts -export const USE_EDGE_FUNCTIONS = true +export const USE_EDGE_FUNCTIONS = false /** @type {import('mdsvex').MdsvexOptions} */ const mdsvexOptions = { @@ -52,7 +52,8 @@ const config = { kit: { adapter: adapterPatchPrerendered( adapterNetlify({ - edge: USE_EDGE_FUNCTIONS + edge: USE_EDGE_FUNCTIONS, + split: true }) ), alias: {