From db217ddf8646f1cc71db030f54097ff1b7de9802 Mon Sep 17 00:00:00 2001 From: Yiwei Gong Date: Tue, 30 Dec 2025 11:58:48 +0800 Subject: [PATCH 01/12] add office target --- webapp/_webapp/bun.lock | 6 + webapp/_webapp/package-lock.json | 667 ++++++++++++++++++++++++++++++- webapp/_webapp/package.json | 4 +- webapp/_webapp/src/office.tsx | 9 + webapp/_webapp/vite.config.ts | 4 + 5 files changed, 680 insertions(+), 10 deletions(-) create mode 100644 webapp/_webapp/src/office.tsx diff --git a/webapp/_webapp/bun.lock b/webapp/_webapp/bun.lock index 6851b64..d871c82 100644 --- a/webapp/_webapp/bun.lock +++ b/webapp/_webapp/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "webapp", @@ -12,6 +13,7 @@ "@heroui/react": "^2.7.9", "@iconify/react": "^6.0.0", "@lukemorales/query-key-factory": "^1.3.4", + "@r2wc/react-to-web-component": "^2.1.0", "@tanstack/react-query": "^5.79.0", "@types/diff": "^8.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -412,6 +414,10 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@r2wc/core": ["@r2wc/core@1.3.0", "", {}, "sha512-aPBnND92Itl+SWWbWyyxdFFF0+RqKB6dptGHEdiPB8ZvnHWHlVzOfEvbEcyUYGtB6HBdsfkVuBiaGYyBFVTzVQ=="], + + "@r2wc/react-to-web-component": ["@r2wc/react-to-web-component@2.1.0", "", { "dependencies": { "@r2wc/core": "^1.3.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-m/PzgUOEiL1HxmvfP5LgBLqB7sHeRj+d1QAeZklwS4OEI2HUU+xTpT3hhJipH5DQoFInDqDTfe0lNFFKcrqk4w=="], + "@react-aria/breadcrumbs": ["@react-aria/breadcrumbs@3.5.24", "", { "dependencies": { "@react-aria/i18n": "^3.12.9", "@react-aria/link": "^3.8.1", "@react-aria/utils": "^3.29.0", "@react-types/breadcrumbs": "^3.7.13", "@react-types/shared": "^3.29.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-CRheGyyM8afPJvDHLXn/mmGG/WAr/z2LReK3DlPdxVKcsOn7g3NIRxAcAIAJQlDLdOiu1SXHiZe6uu2jPhHrxA=="], "@react-aria/button": ["@react-aria/button@3.13.1", "", { "dependencies": { "@react-aria/interactions": "^3.25.1", "@react-aria/toolbar": "3.0.0-beta.16", "@react-aria/utils": "^3.29.0", "@react-stately/toggle": "^3.8.4", "@react-types/button": "^3.12.1", "@react-types/shared": "^3.29.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-E49qcbBRgofXYfWbli50bepWVNtQBq7qewL9XsX7nHkwPPUe1IRwJOnWZqYMgwwhUBOXfnsR6/TssiXqZsrJdw=="], diff --git a/webapp/_webapp/package-lock.json b/webapp/_webapp/package-lock.json index 595bf82..aefcb68 100644 --- a/webapp/_webapp/package-lock.json +++ b/webapp/_webapp/package-lock.json @@ -11,9 +11,12 @@ "@buf/googleapis_googleapis.bufbuild_es": "^2.2.3-20250211200939-546238c53f73.1", "@bufbuild/protobuf": "^2.5.1", "@capacitor-community/apple-sign-in": "^7.0.1", + "@grafana/faro-web-sdk": "^2.0.2", + "@grafana/faro-web-tracing": "^2.0.2", "@heroui/react": "^2.7.9", "@iconify/react": "^6.0.0", "@lukemorales/query-key-factory": "^1.3.4", + "@r2wc/react-to-web-component": "^2.1.0", "@tanstack/react-query": "^5.79.0", "@types/diff": "^8.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -38,6 +41,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.37.1", "@eslint/js": "^9.28.0", + "@grafana/faro-rollup-plugin": "^0.7.0", "@types/chrome": "^0.0.326", "@types/codemirror": "^5.60.16", "@types/events": "^3.0.3", @@ -91,7 +95,8 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz", "integrity": "sha512-lut4UTvKL8tqtend0UDu7R79/n9jA7Jtxf77RNPbxtmWqfWI4qQ9bTjf7KCS4vfqLmpQbuHr1ciqJumAgJODdw==", - "license": "(Apache-2.0 AND BSD-3-Clause)" + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true }, "node_modules/@capacitor-community/apple-sign-in": { "version": "7.0.1", @@ -782,6 +787,70 @@ "tslib": "^2.8.0" } }, + "node_modules/@grafana/faro-bundlers-shared": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@grafana/faro-bundlers-shared/-/faro-bundlers-shared-0.7.0.tgz", + "integrity": "sha512-wNc9ktcZK4CwRmQ/FRB/SbI4ThnkaekGOQuPq6r/Oz+urDjrUpjjZ3cC+KpP47OcWYAuLaIisrt092mZ/Rm+6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ansis": "^4.0.0", + "tar": "^7.1.0" + } + }, + "node_modules/@grafana/faro-core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@grafana/faro-core/-/faro-core-2.1.0.tgz", + "integrity": "sha512-Pc9U2gWH8I1AVeXKLK667NF8N3Nwa1AcAAV3/KKG7C0ZDoy6kY5lXOMY930lzqFtuaji0B2tidayYxrfDmicXA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/otlp-transformer": "^0.208.0" + } + }, + "node_modules/@grafana/faro-rollup-plugin": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@grafana/faro-rollup-plugin/-/faro-rollup-plugin-0.7.1.tgz", + "integrity": "sha512-+Z6wyqJw5HX6OSktgDxTjGQ+pdx6vsT70mbBX24k95kHV0xbefMt7MSHxJ+NIarLy0SQSTnaIofUhiR47L7lUQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grafana/faro-bundlers-shared": "^0.7.0", + "cross-fetch": "^4.0.0", + "magic-string": "^0.30.5", + "rollup": "^4.22.4" + } + }, + "node_modules/@grafana/faro-web-sdk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@grafana/faro-web-sdk/-/faro-web-sdk-2.1.0.tgz", + "integrity": "sha512-ozutA3rN1h+JLphXGOz3B0jb81ew0lBHJEXg47EvWozyZtwtsMWTAX+4PP63Sm5P/BMO/Jm3rt3LtOORXnsPPg==", + "license": "Apache-2.0", + "dependencies": { + "@grafana/faro-core": "^2.1.0", + "ua-parser-js": "^1.0.32", + "web-vitals": "^5.0.3" + } + }, + "node_modules/@grafana/faro-web-tracing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@grafana/faro-web-tracing/-/faro-web-tracing-2.1.0.tgz", + "integrity": "sha512-S8Pkq+mU2LjRuyzbRvY2eSFJn3i9cJTN7dzmiv0CQJHEpzFcCMsTEdY4UhrdO3moPIF5qipl3RJvb6fCfOaDLw==", + "license": "Apache-2.0", + "dependencies": { + "@grafana/faro-web-sdk": "^2.1.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-fetch": "^0.208.0", + "@opentelemetry/instrumentation-xml-http-request": "^0.208.0", + "@opentelemetry/otlp-transformer": "^0.208.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.32.0" + } + }, "node_modules/@heroui/accordion": { "version": "2.2.17", "resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.17.tgz", @@ -1908,6 +1977,7 @@ "resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.16.tgz", "integrity": "sha512-kk8XQsejHv4/vZBm7936D9+YkKV/meUp2tY49auS0wLsrGOQ2vvBKiwzQ0r+ibTvSNyCe5SX9tLfEGVgaGIY7g==", "license": "MIT", + "peer": true, "dependencies": { "@heroui/react-utils": "2.1.10", "@heroui/system-rsc": "2.3.14", @@ -2001,6 +2071,7 @@ "resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.16.tgz", "integrity": "sha512-XWRr1MJNBGIESxOCgPgQMq3gt8VfWoYzDnBpIdIHjSlin+4oK8LRqLsP6CeVTpGSwv6lurQk11jVKY6MTI7JTw==", "license": "MIT", + "peer": true, "dependencies": { "@heroui/shared-utils": "2.1.9", "clsx": "^1.2.1", @@ -2479,6 +2550,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -2512,9 +2596,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2582,6 +2666,243 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz", + "integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", + "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.208.0.tgz", + "integrity": "sha512-zgStoUfNF1xH9bCq539k1aeieKxPiAvBo5gKipQ9fIt+eJsFvqGcSzrrDX+OYgpIPW/IVNgWBoOw6zVmKwgNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/sdk-trace-web": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-xml-http-request": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-xml-http-request/-/instrumentation-xml-http-request-0.208.0.tgz", + "integrity": "sha512-me0knebxJxnzis73p5/ZQgdLNG6nsUXMsDR/dZk+BPOiNyd3Me9ye2wVM06JlcLA54w4JESw6wMTNi4lMhowFQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/sdk-trace-web": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.2.0.tgz", + "integrity": "sha512-x/LHsDBO3kfqaFx5qSzBljJ5QHsRXrvS4MybBDy1k7Svidb8ZyIPudWVzj3s5LpPkYZIgi9e+7tdsNCnptoelw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2592,6 +2913,89 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@r2wc/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@r2wc/core/-/core-1.3.0.tgz", + "integrity": "sha512-aPBnND92Itl+SWWbWyyxdFFF0+RqKB6dptGHEdiPB8ZvnHWHlVzOfEvbEcyUYGtB6HBdsfkVuBiaGYyBFVTzVQ==", + "license": "MIT" + }, + "node_modules/@r2wc/react-to-web-component": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@r2wc/react-to-web-component/-/react-to-web-component-2.1.0.tgz", + "integrity": "sha512-m/PzgUOEiL1HxmvfP5LgBLqB7sHeRj+d1QAeZklwS4OEI2HUU+xTpT3hhJipH5DQoFInDqDTfe0lNFFKcrqk4w==", + "license": "MIT", + "dependencies": { + "@r2wc/core": "^1.3.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.24", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.24.tgz", @@ -4517,6 +4921,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz", "integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -4527,6 +4932,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz", "integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.79.0" }, @@ -4660,7 +5066,6 @@ "version": "22.15.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4672,6 +5077,7 @@ "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4755,6 +5161,7 @@ "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", @@ -4994,8 +5401,8 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5003,6 +5410,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5057,6 +5473,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -5205,6 +5631,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5324,6 +5751,22 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -5427,6 +5870,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5464,7 +5917,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5678,6 +6130,7 @@ "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6060,6 +6513,7 @@ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.15.0.tgz", "integrity": "sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==", "license": "MIT", + "peer": true, "dependencies": { "motion-dom": "^12.15.0", "motion-utils": "^12.12.1", @@ -6316,6 +6770,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -6338,6 +6793,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.1.tgz", + "integrity": "sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6610,6 +7077,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6628,6 +7101,16 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-to-jsx": { "version": "7.7.6", "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.6.tgz", @@ -6714,6 +7197,25 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.15.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.15.0.tgz", @@ -6733,7 +7235,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -6772,6 +7273,27 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7060,6 +7582,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7227,6 +7750,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7285,6 +7832,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7294,6 +7842,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7395,6 +7944,19 @@ "node": ">=8.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -7842,6 +8404,23 @@ "node": ">=14.0.0" } }, + "node_modules/tar": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -7901,6 +8480,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7930,6 +8510,13 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -7974,6 +8561,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8005,6 +8593,32 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -8016,7 +8630,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -8139,6 +8752,7 @@ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -8229,6 +8843,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8243,6 +8858,30 @@ "dev": true, "license": "MIT" }, + "node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8356,6 +8995,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", diff --git a/webapp/_webapp/package.json b/webapp/_webapp/package.json index 42fb631..f386f80 100644 --- a/webapp/_webapp/package.json +++ b/webapp/_webapp/package.json @@ -9,6 +9,7 @@ "build": "tsc -b && npm run _build:default && npm run _build:background && npm run _build:intermediate && npm run _build:settings && npm run _build:popup", "_build": "vite build", "_build:default": "VITE_CONFIG=default npm run _build", + "_build:office": "VITE_CONFIG=office npm run _build", "_build:background": "VITE_CONFIG=background npm run _build", "_build:intermediate": "VITE_CONFIG=intermediate npm run _build", "_build:settings": "VITE_CONFIG=settings npm run _build", @@ -29,6 +30,7 @@ "@heroui/react": "^2.7.9", "@iconify/react": "^6.0.0", "@lukemorales/query-key-factory": "^1.3.4", + "@r2wc/react-to-web-component": "^2.1.0", "@tanstack/react-query": "^5.79.0", "@types/diff": "^8.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -74,4 +76,4 @@ "typescript-eslint": "^8.33.0", "vite": "^6.3.5" } -} +} \ No newline at end of file diff --git a/webapp/_webapp/src/office.tsx b/webapp/_webapp/src/office.tsx new file mode 100644 index 0000000..c1ec646 --- /dev/null +++ b/webapp/_webapp/src/office.tsx @@ -0,0 +1,9 @@ +const Greeting = () => { + return

Hello, World!

+} + +import r2wc from "@r2wc/react-to-web-component" + +const WebGreeting = r2wc(Greeting) + +customElements.define("web-greeting", WebGreeting) diff --git a/webapp/_webapp/vite.config.ts b/webapp/_webapp/vite.config.ts index ed3b393..26647fa 100644 --- a/webapp/_webapp/vite.config.ts +++ b/webapp/_webapp/vite.config.ts @@ -82,6 +82,10 @@ const configs: Record = { draft.build.copyPublicDir = true; draft.plugins.push(generateManifestPlugin()); }), + office: generateConfig("./src/office.tsx", "office", (draft) => { + draft.build.emptyOutDir = true; + draft.build.outDir = "dist/office"; + }), background: generateConfig("./src/background.ts", "background"), intermediate: generateConfig("./src/intermediate.ts", "intermediate"), settings: generateConfig("./src/views/extension-settings/app.tsx", "settings"), From de183972ba0a9b580c4dfe23b86925da67b73ac8 Mon Sep 17 00:00:00 2001 From: Yiwei Gong Date: Tue, 30 Dec 2025 12:14:38 +0800 Subject: [PATCH 02/12] add webcomponent --- webapp/_webapp/package.json | 2 +- webapp/_webapp/src/office.tsx | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/webapp/_webapp/package.json b/webapp/_webapp/package.json index f386f80..e18df26 100644 --- a/webapp/_webapp/package.json +++ b/webapp/_webapp/package.json @@ -76,4 +76,4 @@ "typescript-eslint": "^8.33.0", "vite": "^6.3.5" } -} \ No newline at end of file +} diff --git a/webapp/_webapp/src/office.tsx b/webapp/_webapp/src/office.tsx index c1ec646..ec0b328 100644 --- a/webapp/_webapp/src/office.tsx +++ b/webapp/_webapp/src/office.tsx @@ -1,9 +1,8 @@ -const Greeting = () => { - return

Hello, World!

-} - -import r2wc from "@r2wc/react-to-web-component" +import r2wc from "@r2wc/react-to-web-component"; -const WebGreeting = r2wc(Greeting) +const Greeting = () => { + return

Hello, World!

; +}; -customElements.define("web-greeting", WebGreeting) +const PaperdebuggerOffice = r2wc(Greeting); +customElements.define("paperdebugger-office", PaperdebuggerOffice); From 72d83240622ba737bec1666384cbfe533168bf70 Mon Sep 17 00:00:00 2001 From: Yiwei Gong Date: Tue, 30 Dec 2025 12:44:51 +0800 Subject: [PATCH 03/12] build web component --- webapp/_webapp/src/office.tsx | 8 ------ webapp/_webapp/src/views/office/app.tsx | 24 ++++++++++++++++ webapp/_webapp/src/views/office/providers.tsx | 28 +++++++++++++++++++ webapp/_webapp/vite.config.ts | 2 +- 4 files changed, 53 insertions(+), 9 deletions(-) delete mode 100644 webapp/_webapp/src/office.tsx create mode 100644 webapp/_webapp/src/views/office/app.tsx create mode 100644 webapp/_webapp/src/views/office/providers.tsx diff --git a/webapp/_webapp/src/office.tsx b/webapp/_webapp/src/office.tsx deleted file mode 100644 index ec0b328..0000000 --- a/webapp/_webapp/src/office.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import r2wc from "@r2wc/react-to-web-component"; - -const Greeting = () => { - return

Hello, World!

; -}; - -const PaperdebuggerOffice = r2wc(Greeting); -customElements.define("paperdebugger-office", PaperdebuggerOffice); diff --git a/webapp/_webapp/src/views/office/app.tsx b/webapp/_webapp/src/views/office/app.tsx new file mode 100644 index 0000000..8370028 --- /dev/null +++ b/webapp/_webapp/src/views/office/app.tsx @@ -0,0 +1,24 @@ +import r2wc from "@r2wc/react-to-web-component"; +import { MainDrawer } from ".."; +import { useConversationUiStore } from "../../stores/conversation/conversation-ui-store"; +import { useEffect } from "react"; +import { Providers } from "./providers"; + +import "../../index.css"; + +const PaperDebugger = () => { + const { setDisplayMode, setIsOpen, isOpen } = useConversationUiStore(); + useEffect(() => { + setDisplayMode("right-fixed"); + setIsOpen(true); + }, [setIsOpen, isOpen]); + + return ( + + + + ); +}; + +const PaperdebuggerOffice = r2wc(PaperDebugger); +customElements.define("paperdebugger-office", PaperdebuggerOffice); diff --git a/webapp/_webapp/src/views/office/providers.tsx b/webapp/_webapp/src/views/office/providers.tsx new file mode 100644 index 0000000..c33d651 --- /dev/null +++ b/webapp/_webapp/src/views/office/providers.tsx @@ -0,0 +1,28 @@ +import { HeroUIProvider, ToastProvider } from "@heroui/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +export function Providers({ children }: { children: React.ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ( + + + + {children} + + + ); +} diff --git a/webapp/_webapp/vite.config.ts b/webapp/_webapp/vite.config.ts index 26647fa..5512013 100644 --- a/webapp/_webapp/vite.config.ts +++ b/webapp/_webapp/vite.config.ts @@ -82,7 +82,7 @@ const configs: Record = { draft.build.copyPublicDir = true; draft.plugins.push(generateManifestPlugin()); }), - office: generateConfig("./src/office.tsx", "office", (draft) => { + office: generateConfig("./src/views/office/app.tsx", "office", (draft) => { draft.build.emptyOutDir = true; draft.build.outDir = "dist/office"; }), From 4e778bdb0928c11cfc0468702e711d38ebe8267f Mon Sep 17 00:00:00 2001 From: Yiwei Gong Date: Tue, 30 Dec 2025 12:52:21 +0800 Subject: [PATCH 04/12] add fullscreen --- .../conversation/conversation-ui-store.ts | 1 + webapp/_webapp/src/views/index.tsx | 19 +++++++++++++++++-- webapp/_webapp/src/views/office/app.tsx | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts index 6a97156..59c2b69 100644 --- a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts +++ b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts @@ -8,6 +8,7 @@ export const DISPLAY_MODES = [ { key: "floating", label: "Floating" }, { key: "right-fixed", label: "Right Fixed" }, { key: "bottom-fixed", label: "Bottom Fixed" }, + { key: "fullscreen", label: "Full Screen" }, ] as const; export type DisplayMode = (typeof DISPLAY_MODES)[number]["key"]; diff --git a/webapp/_webapp/src/views/index.tsx b/webapp/_webapp/src/views/index.tsx index ff0e191..fa96323 100644 --- a/webapp/_webapp/src/views/index.tsx +++ b/webapp/_webapp/src/views/index.tsx @@ -182,6 +182,16 @@ export const MainDrawer = () => { enableResizing: { left: false, right: false, top: true, bottom: false }, disableDragging: true, }; + } else if (displayMode === "fullscreen") { + rndProps = { + default: { x: 0, y: 0, width: windowSize.width, height: windowSize.height }, + position: { x: 0, y: 0 }, + size: { width: windowSize.width, height: windowSize.height }, + minWidth: windowSize.width, + minHeight: windowSize.height, + enableResizing: { left: false, right: false, top: false, bottom: false }, + disableDragging: true, + }; } const handleResize = useCallback( @@ -278,8 +288,13 @@ export const MainDrawer = () => { }, }} > - - + + {displayMode !== "fullscreen" && } , diff --git a/webapp/_webapp/src/views/office/app.tsx b/webapp/_webapp/src/views/office/app.tsx index 8370028..09133fb 100644 --- a/webapp/_webapp/src/views/office/app.tsx +++ b/webapp/_webapp/src/views/office/app.tsx @@ -9,7 +9,7 @@ import "../../index.css"; const PaperDebugger = () => { const { setDisplayMode, setIsOpen, isOpen } = useConversationUiStore(); useEffect(() => { - setDisplayMode("right-fixed"); + setDisplayMode("fullscreen"); setIsOpen(true); }, [setIsOpen, isOpen]); From cbcfe9d55c5497082b269a4cbcd81616f3ec08da Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Thu, 8 Jan 2026 19:46:39 +0800 Subject: [PATCH 05/12] adapter layer --- webapp/_webapp/src/adapters/context.tsx | 40 ++++ webapp/_webapp/src/adapters/index.ts | 16 ++ .../_webapp/src/adapters/overleaf-adapter.ts | 141 ++++++++++++++ webapp/_webapp/src/adapters/types.ts | 76 ++++++++ webapp/_webapp/src/adapters/word-adapter.ts | 179 ++++++++++++++++++ .../_webapp/src/components/text-patches.tsx | 53 +++++- webapp/_webapp/src/intermediate.ts | 23 ++- webapp/_webapp/src/libs/oauth.ts | 51 +++-- webapp/_webapp/src/views/office/app.tsx | 79 +++++++- 9 files changed, 628 insertions(+), 30 deletions(-) create mode 100644 webapp/_webapp/src/adapters/context.tsx create mode 100644 webapp/_webapp/src/adapters/index.ts create mode 100644 webapp/_webapp/src/adapters/overleaf-adapter.ts create mode 100644 webapp/_webapp/src/adapters/types.ts create mode 100644 webapp/_webapp/src/adapters/word-adapter.ts diff --git a/webapp/_webapp/src/adapters/context.tsx b/webapp/_webapp/src/adapters/context.tsx new file mode 100644 index 0000000..74abe0d --- /dev/null +++ b/webapp/_webapp/src/adapters/context.tsx @@ -0,0 +1,40 @@ +/** + * Adapter Context + * + * Provides the DocumentAdapter to React components via context. + * This allows UI components to access document operations without knowing the platform. + */ + +import { createContext, useContext, type ReactNode } from "react"; +import type { DocumentAdapter } from "./types"; + +const AdapterContext = createContext(null); + +interface AdapterProviderProps { + adapter: DocumentAdapter; + children: ReactNode; +} + +export function AdapterProvider({ adapter, children }: AdapterProviderProps) { + return {children}; +} + +/** + * Hook to access the document adapter + * @throws Error if used outside of AdapterProvider + */ +export function useAdapter(): DocumentAdapter { + const adapter = useContext(AdapterContext); + if (!adapter) { + throw new Error("useAdapter must be used within an AdapterProvider"); + } + return adapter; +} + +/** + * Hook to safely access the document adapter (returns null if not available) + */ +export function useAdapterOptional(): DocumentAdapter | null { + return useContext(AdapterContext); +} + diff --git a/webapp/_webapp/src/adapters/index.ts b/webapp/_webapp/src/adapters/index.ts new file mode 100644 index 0000000..d3e3c45 --- /dev/null +++ b/webapp/_webapp/src/adapters/index.ts @@ -0,0 +1,16 @@ +/** + * Adapters module entry point + * + * Re-exports all adapter-related types, contexts, and implementations. + */ + +// Types +export type { DocumentAdapter, SelectionInfo, AdapterProps } from "./types"; + +// Context and hooks +export { AdapterProvider, useAdapter, useAdapterOptional } from "./context"; + +// Implementations +export { OverleafAdapter, getOverleafAdapter } from "./overleaf-adapter"; +export { WordAdapter, createWordAdapter } from "./word-adapter"; + diff --git a/webapp/_webapp/src/adapters/overleaf-adapter.ts b/webapp/_webapp/src/adapters/overleaf-adapter.ts new file mode 100644 index 0000000..09ef158 --- /dev/null +++ b/webapp/_webapp/src/adapters/overleaf-adapter.ts @@ -0,0 +1,141 @@ +/** + * OverleafAdapter + * + * Document adapter implementation for Overleaf/browser environment. + * Uses CodeMirror editor APIs to interact with the document. + */ + +import { EditorView } from "@codemirror/view"; +import type { DocumentAdapter, SelectionInfo } from "./types"; +import { getCodeMirrorView, getProjectId } from "../libs/helpers"; + +export class OverleafAdapter implements DocumentAdapter { + readonly platform = "overleaf" as const; + + private getEditorView(): EditorView | null { + return getCodeMirrorView(); + } + + isReady(): boolean { + return this.getEditorView() !== null; + } + + async getFullText(): Promise { + const view = this.getEditorView(); + if (!view) { + return ""; + } + return view.state.doc.toString(); + } + + async getSelection(): Promise { + const view = this.getEditorView(); + if (!view) { + return null; + } + + const selection = view.state.selection.main; + if (selection.empty) { + return null; + } + + const text = view.state.sliceDoc(selection.from, selection.to); + const doc = view.state.doc; + + // Get surrounding context + const contextBefore = doc.sliceString(Math.max(0, selection.from - 100), selection.from); + const contextAfter = doc.sliceString(selection.to, Math.min(doc.length, selection.to + 100)); + const surroundingText = `${contextBefore}[SELECTED_TEXT_START]${text}[SELECTED_TEXT_END]${contextAfter}`; + + // Create a range identifier that can be used later + const rangeId = JSON.stringify({ from: selection.from, to: selection.to }); + + return { + text, + surroundingText, + rangeId, + }; + } + + async insertText(text: string, location: "cursor" | "start" | "end" = "cursor"): Promise { + const view = this.getEditorView(); + if (!view) { + throw new Error("Editor not available"); + } + + let pos: number; + switch (location) { + case "start": + pos = 0; + break; + case "end": + pos = view.state.doc.length; + break; + case "cursor": + default: + pos = view.state.selection.main.head; + break; + } + + view.dispatch({ + changes: { from: pos, insert: text }, + selection: { anchor: pos + text.length }, + }); + } + + async replaceSelection(text: string, rangeId?: string): Promise { + const view = this.getEditorView(); + if (!view) { + throw new Error("Editor not available"); + } + + let from: number; + let to: number; + + if (rangeId) { + try { + const range = JSON.parse(rangeId); + from = range.from; + to = range.to; + } catch { + // If rangeId is invalid, use current selection + from = view.state.selection.main.from; + to = view.state.selection.main.to; + } + } else { + from = view.state.selection.main.from; + to = view.state.selection.main.to; + } + + view.dispatch({ + changes: { from, to, insert: text }, + selection: { anchor: from + text.length }, + }); + } + + onSelectionChange(callback: (selection: SelectionInfo | null) => void): () => void { + const handler = () => { + this.getSelection().then(callback); + }; + + document.addEventListener("selectionchange", handler); + return () => { + document.removeEventListener("selectionchange", handler); + }; + } + + getDocumentId(): string { + return getProjectId(); + } +} + +// Singleton instance for browser environment +let overleafAdapterInstance: OverleafAdapter | null = null; + +export function getOverleafAdapter(): OverleafAdapter { + if (!overleafAdapterInstance) { + overleafAdapterInstance = new OverleafAdapter(); + } + return overleafAdapterInstance; +} + diff --git a/webapp/_webapp/src/adapters/types.ts b/webapp/_webapp/src/adapters/types.ts new file mode 100644 index 0000000..1c3967b --- /dev/null +++ b/webapp/_webapp/src/adapters/types.ts @@ -0,0 +1,76 @@ +/** + * DocumentAdapter Interface + * + * Platform-agnostic interface for document operations. + * Implementations: + * - OverleafAdapter: For Overleaf/browser environment + * - WordAdapter: For Microsoft Word Office Add-in + * + * The React UI should only depend on this interface, never on platform-specific APIs. + */ + +export interface SelectionInfo { + /** The selected text content */ + text: string; + /** Optional surrounding text for context (with markers like [SELECTED_TEXT_START] and [SELECTED_TEXT_END]) */ + surroundingText?: string; + /** Platform-specific range identifier for later operations */ + rangeId?: string; +} + +export interface DocumentAdapter { + /** + * Platform identifier for conditional UI rendering + */ + readonly platform: "overleaf" | "word" | "browser"; + + /** + * Get the full document text + */ + getFullText(): Promise; + + /** + * Get the currently selected text and its context + */ + getSelection(): Promise; + + /** + * Insert text at the current cursor position + * @param text - Text to insert + * @param location - Optional: 'cursor' (default), 'start', or 'end' + */ + insertText(text: string, location?: "cursor" | "start" | "end"): Promise; + + /** + * Replace the current selection with new text + * @param text - Text to replace with + * @param rangeId - Optional range identifier from getSelection() + */ + replaceSelection(text: string, rangeId?: string): Promise; + + /** + * Subscribe to selection change events + * @param callback - Called when selection changes + * @returns Cleanup function to unsubscribe + */ + onSelectionChange?(callback: (selection: SelectionInfo | null) => void): () => void; + + /** + * Check if the adapter is ready/connected + */ + isReady(): boolean; + + /** + * Optional: Get project/document identifier + */ + getDocumentId?(): string; +} + +/** + * Type for adapter props passed to Web Component + */ +export interface AdapterProps { + adapter?: DocumentAdapter; + displayMode?: "floating" | "bottom-fixed" | "right-fixed" | "fullscreen"; +} + diff --git a/webapp/_webapp/src/adapters/word-adapter.ts b/webapp/_webapp/src/adapters/word-adapter.ts new file mode 100644 index 0000000..b14dbe1 --- /dev/null +++ b/webapp/_webapp/src/adapters/word-adapter.ts @@ -0,0 +1,179 @@ +/** + * WordAdapter + * + * Document adapter implementation for Microsoft Word Office Add-in. + * Uses Office JavaScript API to interact with the document. + * + * Note: This adapter is designed to be used in the Web Component build + * and injected by the Office Add-in host application. + */ + +import type { DocumentAdapter, SelectionInfo } from "./types"; + +// Type declarations for Office JS (will be available at runtime) +declare const Word: { + run: (callback: (context: WordContext) => Promise) => Promise; + InsertLocation: { + start: string; + end: string; + before: string; + after: string; + replace: string; + }; +}; + +interface WordContext { + document: { + body: WordBody; + getSelection(): WordRange; + }; + sync(): Promise; +} + +interface WordBody { + text: string; + insertText(text: string, location: string): WordRange; + load(properties: string): void; +} + +interface WordRange { + text: string; + insertText(text: string, location: string): WordRange; + load(properties: string): void; +} + +export class WordAdapter implements DocumentAdapter { + readonly platform = "word" as const; + + private _ready = false; + + constructor() { + // Check if Office is available + this._ready = typeof Word !== "undefined"; + } + + isReady(): boolean { + return this._ready; + } + + async getFullText(): Promise { + if (!this.isReady()) { + throw new Error("Word API not available"); + } + + return Word.run(async (context) => { + const body = context.document.body; + body.load("text"); + await context.sync(); + return body.text; + }); + } + + async getSelection(): Promise { + if (!this.isReady()) { + return null; + } + + try { + return await Word.run(async (context) => { + const selection = context.document.getSelection(); + selection.load("text"); + await context.sync(); + + if (!selection.text || selection.text.trim() === "") { + return null; + } + + // Note: Word API doesn't provide easy access to surrounding context + // We can potentially expand the range in future iterations + return { + text: selection.text, + surroundingText: selection.text, // Simplified for now + rangeId: undefined, // Word ranges are transient, can't easily serialize + }; + }); + } catch (error) { + console.error("Error getting Word selection:", error); + return null; + } + } + + async insertText(text: string, location: "cursor" | "start" | "end" = "cursor"): Promise { + if (!this.isReady()) { + throw new Error("Word API not available"); + } + + await Word.run(async (context) => { + if (location === "start") { + context.document.body.insertText(text, Word.InsertLocation.start); + } else if (location === "end") { + context.document.body.insertText(text, Word.InsertLocation.end); + } else { + // Insert at cursor (current selection) + const selection = context.document.getSelection(); + selection.insertText(text, Word.InsertLocation.replace); + } + await context.sync(); + }); + } + + async replaceSelection(text: string, _rangeId?: string): Promise { + if (!this.isReady()) { + throw new Error("Word API not available"); + } + + // Note: rangeId is not used for Word as ranges are transient + // We always operate on the current selection + await Word.run(async (context) => { + const selection = context.document.getSelection(); + selection.insertText(text, Word.InsertLocation.replace); + await context.sync(); + }); + } + + onSelectionChange(callback: (selection: SelectionInfo | null) => void): () => void { + // Word doesn't have a built-in selection change event in the same way + // We can use Office.context.document.addHandlerAsync for some events + // For now, implement polling as a fallback + let intervalId: ReturnType | null = null; + let lastSelection: string | null = null; + + const checkSelection = async () => { + try { + const selection = await this.getSelection(); + const currentText = selection?.text ?? null; + + if (currentText !== lastSelection) { + lastSelection = currentText; + callback(selection); + } + } catch { + // Ignore errors during polling + } + }; + + // Poll every 500ms (not ideal but works for MVP) + intervalId = setInterval(checkSelection, 500); + // Initial check + checkSelection(); + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + } + + getDocumentId(): string { + // Word documents don't have a simple project ID like Overleaf + // Could potentially use Office.context.document.url or similar + return "word-document"; + } +} + +// Factory function to create WordAdapter +// This will be called by the Office Add-in host +export function createWordAdapter(): WordAdapter { + return new WordAdapter(); +} + diff --git a/webapp/_webapp/src/components/text-patches.tsx b/webapp/_webapp/src/components/text-patches.tsx index e757424..615153b 100644 --- a/webapp/_webapp/src/components/text-patches.tsx +++ b/webapp/_webapp/src/components/text-patches.tsx @@ -1,10 +1,11 @@ import { Button } from "@heroui/react"; import { useCallback, useEffect, useRef, useState } from "react"; import { diffWords } from "diff"; -import { applyChanges, getProjectId } from "../libs/helpers"; +import { getProjectId } from "../libs/helpers"; import { useSelectionStore } from "../stores/selection-store"; import googleAnalytics from "../libs/google-analytics"; import { useAuthStore } from "../stores/auth-store"; +import { useAdapterOptional } from "../adapters"; type TextPatchesProps = { attachment?: string; @@ -15,6 +16,7 @@ type TextPatchesProps = { export function TextPatches({ attachment, children }: TextPatchesProps) { const { user } = useAuthStore(); const { selectionRange, setSelectionRange } = useSelectionStore(); + const adapter = useAdapterOptional(); const preRef = useRef(null); const [insertBtnText, setInsertBtnText] = useState("Insert"); @@ -32,16 +34,42 @@ export function TextPatches({ attachment, children }: TextPatchesProps) { } }, [preRef]); - const applyText = useCallback(() => { - if (preRef.current && selectionRange) { - applyChanges(preRef.current.innerText, selectionRange); - setSelectionRange(null); - setInsertBtnText("Applied!"); + const applyText = useCallback(async () => { + if (!preRef.current) return; + + const textToInsert = preRef.current.innerText; + + try { + // Prefer adapter-based insertion if available + if (adapter && adapter.isReady()) { + await adapter.replaceSelection(textToInsert); + setSelectionRange(null); + setInsertBtnText("Applied!"); + setTimeout(() => { + setInsertBtnText("Insert"); + }, 1500); + return; + } + + // Fallback to legacy Range-based insertion for Overleaf + if (selectionRange) { + const newText = document.createTextNode(textToInsert); + selectionRange.deleteContents(); + selectionRange.insertNode(newText); + setSelectionRange(null); + setInsertBtnText("Applied!"); + setTimeout(() => { + setInsertBtnText("Insert"); + }, 1500); + } + } catch (error) { + console.error("Failed to apply text:", error); + setInsertBtnText("Failed!"); setTimeout(() => { setInsertBtnText("Insert"); }, 1500); } - }, [preRef, selectionRange, setSelectionRange]); + }, [preRef, selectionRange, setSelectionRange, adapter]); // Process children to handle newlines let processedChildren = children; @@ -86,6 +114,11 @@ export function TextPatches({ attachment, children }: TextPatchesProps) { setMappedNodes(diffElements); }, [preRef, attachment]); + // Determine if insert button should be enabled + // With adapter: always enabled if adapter is ready + // Without adapter: only enabled if selectionRange exists (legacy Overleaf behavior) + const canInsert = (adapter && adapter.isReady()) || !!selectionRange; + return (
@@ -135,11 +168,11 @@ export function TextPatches({ attachment, children }: TextPatchesProps) {
         
-      
+      {!isWord && (
+        
+          
+
View onboarding guide
+
Learn how to use PaperDebugger effectively
+
+ +
+ )}
Status
@@ -47,21 +52,25 @@ export const AccountSettings = () => {
User
-
-
0 ? "bg-primary-500" : "bg-red-500", - )} - >
- Session -
-
-
0 ? "bg-primary-500" : "bg-red-500")} - >
- GCLB -
+ {!isWord && ( + <> +
+
0 ? "bg-primary-500" : "bg-red-500", + )} + >
+ Session +
+
+
0 ? "bg-primary-500" : "bg-red-500")} + >
+ GCLB +
+ + )}
From 09e0ba749d650e9742bf3c49c826ef0690db1e2d Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Mon, 12 Jan 2026 14:35:37 +0800 Subject: [PATCH 10/12] fix: adapters and storage-adapter --- ...verleaf-adapter.ts => document-adapter.ts} | 0 webapp/_webapp/src/adapters/index.ts | 17 ++- .../_webapp/src/adapters/storage-adapter.ts | 102 ++++++++++++++++ webapp/_webapp/src/adapters/types.ts | 39 +++++- webapp/_webapp/src/libs/storage.ts | 115 +++--------------- webapp/_webapp/src/stores/auth-store.ts | 35 ++++-- webapp/_webapp/src/views/office/app.tsx | 37 +++++- 7 files changed, 227 insertions(+), 118 deletions(-) rename webapp/_webapp/src/adapters/{overleaf-adapter.ts => document-adapter.ts} (100%) create mode 100644 webapp/_webapp/src/adapters/storage-adapter.ts diff --git a/webapp/_webapp/src/adapters/overleaf-adapter.ts b/webapp/_webapp/src/adapters/document-adapter.ts similarity index 100% rename from webapp/_webapp/src/adapters/overleaf-adapter.ts rename to webapp/_webapp/src/adapters/document-adapter.ts diff --git a/webapp/_webapp/src/adapters/index.ts b/webapp/_webapp/src/adapters/index.ts index c0abd7f..66fd071 100644 --- a/webapp/_webapp/src/adapters/index.ts +++ b/webapp/_webapp/src/adapters/index.ts @@ -8,11 +8,22 @@ */ // Types -export type { DocumentAdapter, SelectionInfo, AdapterProps } from "./types"; +export type { + DocumentAdapter, + SelectionInfo, + AdapterProps, + StorageAdapter, +} from "./types"; // Context and hooks export { AdapterProvider, useAdapter, useAdapterOptional } from "./context"; -// Implementations -export { OverleafAdapter, getOverleafAdapter } from "./overleaf-adapter"; +// Document Adapter Implementations +export { OverleafAdapter, getOverleafAdapter } from "./document-adapter"; +// Storage Adapter Implementations +export { + LocalStorageAdapter, + MemoryStorageAdapter, + createStorageAdapter, +} from "./storage-adapter"; \ No newline at end of file diff --git a/webapp/_webapp/src/adapters/storage-adapter.ts b/webapp/_webapp/src/adapters/storage-adapter.ts new file mode 100644 index 0000000..a4a2da6 --- /dev/null +++ b/webapp/_webapp/src/adapters/storage-adapter.ts @@ -0,0 +1,102 @@ +/** + * Storage Adapter Implementations + * + * Browser-specific storage implementations for the webapp. + * - LocalStorageAdapter: Uses browser localStorage + * - MemoryStorageAdapter: In-memory fallback when localStorage is unavailable + */ + +import type { StorageAdapter } from "./types"; + +/** + * LocalStorage adapter for browser environments + */ +export class LocalStorageAdapter implements StorageAdapter { + getItem(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + console.warn("[Storage] localStorage.getItem failed for key:", key); + return null; + } + } + + setItem(key: string, value: string): void { + try { + localStorage.setItem(key, value); + } catch (e) { + console.warn("[Storage] localStorage.setItem failed for key:", key, e); + } + } + + removeItem(key: string): void { + try { + localStorage.removeItem(key); + } catch (e) { + console.warn("[Storage] localStorage.removeItem failed for key:", key, e); + } + } + + clear(): void { + try { + localStorage.clear(); + } catch (e) { + console.warn("[Storage] localStorage.clear failed", e); + } + } + + keys(): string[] { + try { + return Object.keys(localStorage); + } catch { + return []; + } + } +} + +/** + * In-memory storage adapter (fallback when no storage is available) + */ +export class MemoryStorageAdapter implements StorageAdapter { + private _store: Map = new Map(); + + getItem(key: string): string | null { + return this._store.get(key) ?? null; + } + + setItem(key: string, value: string): void { + this._store.set(key, value); + } + + removeItem(key: string): void { + this._store.delete(key); + } + + clear(): void { + this._store.clear(); + } + + keys(): string[] { + return Array.from(this._store.keys()); + } +} + +/** + * Create storage adapter for browser environment + */ +export function createStorageAdapter(type?: "localStorage" | "memory"): StorageAdapter { + if (type === "memory") { + return new MemoryStorageAdapter(); + } + + // Default: try localStorage, fallback to memory + try { + const testKey = "__storage_test__"; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return new LocalStorageAdapter(); + } catch { + console.warn("[Storage] localStorage not available, falling back to memory storage"); + return new MemoryStorageAdapter(); + } +} diff --git a/webapp/_webapp/src/adapters/types.ts b/webapp/_webapp/src/adapters/types.ts index 1c3967b..9323ec6 100644 --- a/webapp/_webapp/src/adapters/types.ts +++ b/webapp/_webapp/src/adapters/types.ts @@ -1,14 +1,29 @@ /** - * DocumentAdapter Interface + * Adapter Type Definitions * - * Platform-agnostic interface for document operations. - * Implementations: - * - OverleafAdapter: For Overleaf/browser environment - * - WordAdapter: For Microsoft Word Office Add-in + * Platform-agnostic interfaces for document and storage operations. * - * The React UI should only depend on this interface, never on platform-specific APIs. + * ⚠️ SOURCE OF TRUTH - This file is synced to office-addin + * + * When modifying this file, run the sync script in office-addin: + * cd ../paperdebugger-office-addin && ./scripts/sync-types.sh + * + * Document Adapter Implementations: + * - OverleafAdapter: For Overleaf/browser environment (webapp) + * - WordAdapter: For Microsoft Word Office Add-in (office-addin) + * + * Storage Adapter Implementations: + * - LocalStorageAdapter: For browser localStorage (webapp) + * - MemoryStorageAdapter: For in-memory fallback (webapp) + * - OfficeRoamingAdapter: For Office roaming settings (office-addin) + * + * The React UI should only depend on these interfaces, never on platform-specific APIs. */ +// ============================================================================ +// Document Adapter Types +// ============================================================================ + export interface SelectionInfo { /** The selected text content */ text: string; @@ -74,3 +89,15 @@ export interface AdapterProps { displayMode?: "floating" | "bottom-fixed" | "right-fixed" | "fullscreen"; } +// ============================================================================ +// Storage Adapter Types +// ============================================================================ + +export interface StorageAdapter { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; + clear(): void; + /** Get all keys in storage */ + keys(): string[]; +} diff --git a/webapp/_webapp/src/libs/storage.ts b/webapp/_webapp/src/libs/storage.ts index 5e8b15d..9cd9c6b 100644 --- a/webapp/_webapp/src/libs/storage.ts +++ b/webapp/_webapp/src/libs/storage.ts @@ -1,7 +1,8 @@ /** * Storage Abstraction Layer * - * Provides a unified interface for storage using localStorage. + * Provides a unified interface for storage using the adapter pattern. + * Adapter implementations are in src/adapters/storage-adapter.ts * * Usage: * import { storage } from './storage'; @@ -9,107 +10,16 @@ * const value = storage.getItem('key'); */ -export interface StorageAdapter { - getItem(key: string): string | null; - setItem(key: string, value: string): void; - removeItem(key: string): void; - clear(): void; - /** Get all keys in storage */ - keys(): string[]; -} - -/** - * LocalStorage adapter for browser environments - */ -export class LocalStorageAdapter implements StorageAdapter { - getItem(key: string): string | null { - try { - return localStorage.getItem(key); - } catch { - console.warn("[Storage] localStorage.getItem failed for key:", key); - return null; - } - } - - setItem(key: string, value: string): void { - try { - localStorage.setItem(key, value); - } catch (e) { - console.warn("[Storage] localStorage.setItem failed for key:", key, e); - } - } - - removeItem(key: string): void { - try { - localStorage.removeItem(key); - } catch (e) { - console.warn("[Storage] localStorage.removeItem failed for key:", key, e); - } - } - - clear(): void { - try { - localStorage.clear(); - } catch (e) { - console.warn("[Storage] localStorage.clear failed", e); - } - } - - keys(): string[] { - try { - return Object.keys(localStorage); - } catch { - return []; - } - } -} - -/** - * In-memory storage adapter (fallback when no storage is available) - */ -export class MemoryStorageAdapter implements StorageAdapter { - private _store: Map = new Map(); - - getItem(key: string): string | null { - return this._store.get(key) ?? null; - } - - setItem(key: string, value: string): void { - this._store.set(key, value); - } - - removeItem(key: string): void { - this._store.delete(key); - } +// Re-export types and implementations from adapters +export type { StorageAdapter } from "../adapters/types"; +export { + LocalStorageAdapter, + MemoryStorageAdapter, + createStorageAdapter, +} from "../adapters/storage-adapter"; - clear(): void { - this._store.clear(); - } - - keys(): string[] { - return Array.from(this._store.keys()); - } -} - -/** - * Create storage adapter for browser environment - */ -export function createStorageAdapter(type?: "localStorage" | "memory"): StorageAdapter { - if (type === "memory") { - return new MemoryStorageAdapter(); - } - - // Default: try localStorage, fallback to memory - try { - const testKey = "__storage_test__"; - localStorage.setItem(testKey, testKey); - localStorage.removeItem(testKey); - return new LocalStorageAdapter(); - } catch { - console.warn("[Storage] localStorage not available, falling back to memory storage"); - return new MemoryStorageAdapter(); - } -} +import type { StorageAdapter } from "../adapters/types"; +import { createStorageAdapter } from "../adapters/storage-adapter"; // Global storage instance let _storageInstance: StorageAdapter | null = null; @@ -119,8 +29,10 @@ let _storageInstance: StorageAdapter | null = null; */ export function getStorage(): StorageAdapter { if (!_storageInstance) { + console.log("[Storage] Creating default storage adapter (no custom adapter set)"); _storageInstance = createStorageAdapter(); } + console.log("[Storage] getStorage returning:", _storageInstance?.constructor?.name ?? "unknown"); return _storageInstance; } @@ -129,6 +41,7 @@ export function getStorage(): StorageAdapter { * Useful for testing or when host environment provides a custom adapter */ export function setStorage(adapter: StorageAdapter): void { + console.log("[Storage] setStorage called, adapter type:", adapter?.constructor?.name ?? "unknown"); _storageInstance = adapter; } diff --git a/webapp/_webapp/src/stores/auth-store.ts b/webapp/_webapp/src/stores/auth-store.ts index 8bd8294..d667971 100644 --- a/webapp/_webapp/src/stores/auth-store.ts +++ b/webapp/_webapp/src/stores/auth-store.ts @@ -4,6 +4,7 @@ import { User } from "../pkg/gen/apiclient/user/v1/user_pb"; import apiclient, { apiclientV2 } from "../libs/apiclient"; import { logout as apiLogout, getUser } from "../query/api"; import { logInfo } from "../libs/logger"; +import { storage } from "../libs/storage"; const LOCAL_STORAGE_KEY = { TOKEN: "pd.auth.token", @@ -25,6 +26,13 @@ export interface AuthStore { refreshToken: string; setRefreshToken: (refreshToken: string) => void; + + /** + * Initialize store from storage. + * Must be called after storage adapter is set (e.g., after Office.onReady). + * This reloads token/refreshToken from the configured storage adapter. + */ + initFromStorage: () => void; } export const useAuthStore = create((set, get) => ({ @@ -50,9 +58,9 @@ export const useAuthStore = create((set, get) => ({ logout: async () => { const { refreshToken } = get(); - localStorage.removeItem(LOCAL_STORAGE_KEY.USER); - localStorage.removeItem(LOCAL_STORAGE_KEY.TOKEN); - localStorage.removeItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN); + storage.removeItem(LOCAL_STORAGE_KEY.USER); + storage.removeItem(LOCAL_STORAGE_KEY.TOKEN); + storage.removeItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN); try { await Promise.all([apiLogout({ refreshToken })]); logInfo("logged out"); @@ -64,15 +72,28 @@ export const useAuthStore = create((set, get) => ({ set({ user: null, token: "", refreshToken: "" }); }, - token: localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN) ?? "", + // Initial values are empty - will be populated by initFromStorage() + // This ensures we don't read from storage before the adapter is set + token: "", setToken: (token) => { - localStorage.setItem(LOCAL_STORAGE_KEY.TOKEN, token); + storage.setItem(LOCAL_STORAGE_KEY.TOKEN, token); set({ token }); }, - refreshToken: localStorage.getItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN) ?? "", + refreshToken: "", setRefreshToken: (refreshToken) => { - localStorage.setItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN, refreshToken ?? ""); + storage.setItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN, refreshToken ?? ""); set({ refreshToken }); }, + + initFromStorage: () => { + const token = storage.getItem(LOCAL_STORAGE_KEY.TOKEN) ?? ""; + const refreshToken = storage.getItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN) ?? ""; + console.log("[AuthStore] initFromStorage:", { + hasToken: !!token, + tokenLength: token.length, + hasRefreshToken: !!refreshToken + }); + set({ token, refreshToken }); + }, })); diff --git a/webapp/_webapp/src/views/office/app.tsx b/webapp/_webapp/src/views/office/app.tsx index 01b1604..6b59f02 100644 --- a/webapp/_webapp/src/views/office/app.tsx +++ b/webapp/_webapp/src/views/office/app.tsx @@ -1,12 +1,15 @@ import r2wc from "@r2wc/react-to-web-component"; import { MainDrawer } from ".."; import { useConversationUiStore } from "../../stores/conversation/conversation-ui-store"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Providers } from "./providers"; import { AdapterProvider, type DocumentAdapter, + type StorageAdapter, } from "../../adapters"; +import { setStorage as setGlobalStorage } from "../../libs/storage"; +import { useAuthStore } from "../../stores/auth-store"; import "../../index.css"; @@ -36,6 +39,15 @@ export function unregisterAdapter(id: string): void { adapterRegistry.delete(id); } +/** + * Set the storage adapter to be used by PaperDebugger + * This MUST be called before the component mounts to ensure auth state is properly loaded + * @param adapter - A StorageAdapter implementation (e.g., OfficeRoamingAdapter) + */ +export function setStorage(adapter: StorageAdapter): void { + setGlobalStorage(adapter); +} + // Expose registration functions globally for cross-bundle access if (typeof window !== "undefined") { (window as unknown as { __pdAdapterRegistry: typeof adapterRegistry }). @@ -44,10 +56,24 @@ if (typeof window !== "undefined") { __pdRegisterAdapter = registerAdapter; (window as unknown as { __pdUnregisterAdapter: typeof unregisterAdapter }). __pdUnregisterAdapter = unregisterAdapter; + (window as unknown as { __pdSetStorage: typeof setStorage }). + __pdSetStorage = setStorage; } const PaperDebugger = ({ displayMode = "fullscreen", adapterId }: PaperDebuggerProps) => { const { setDisplayMode, setIsOpen, isOpen } = useConversationUiStore(); + const { initFromStorage, login } = useAuthStore(); + const [isInitialized, setIsInitialized] = useState(false); + + // Initialize auth from storage on mount + // This must happen before the main UI renders to restore login state + useEffect(() => { + // Re-initialize auth store from storage (storage adapter should be set by host before component mounts) + initFromStorage(); + // Attempt to login with restored tokens + login(); + setIsInitialized(true); + }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setDisplayMode(displayMode); @@ -78,6 +104,15 @@ const PaperDebugger = ({ displayMode = "fullscreen", adapterId }: PaperDebuggerP ); } + // Wait for initialization to complete before rendering main UI + if (!isInitialized) { + return ( +
+ Loading... +
+ ); + } + return ( From 43837538f1eacbea160b5ff0a375f1c49e6f59ea Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Mon, 12 Jan 2026 14:58:46 +0800 Subject: [PATCH 11/12] feat: selection --- webapp/_webapp/src/views/office/app.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/webapp/_webapp/src/views/office/app.tsx b/webapp/_webapp/src/views/office/app.tsx index 6b59f02..c9863c3 100644 --- a/webapp/_webapp/src/views/office/app.tsx +++ b/webapp/_webapp/src/views/office/app.tsx @@ -7,9 +7,11 @@ import { AdapterProvider, type DocumentAdapter, type StorageAdapter, + type SelectionInfo, } from "../../adapters"; import { setStorage as setGlobalStorage } from "../../libs/storage"; import { useAuthStore } from "../../stores/auth-store"; +import { useSelectionStore } from "../../stores/selection-store"; import "../../index.css"; @@ -48,6 +50,25 @@ export function setStorage(adapter: StorageAdapter): void { setGlobalStorage(adapter); } +/** + * Set the current selection from external host (e.g., Office Add-in) + * @param selection - The selection info, or null to clear + */ +export function setSelection(selection: SelectionInfo | null): void { + const store = useSelectionStore.getState(); + if (selection) { + store.setLastSelectedText(selection.text); + store.setLastSurroundingText(selection.surroundingText ?? null); + store.setSelectedText(selection.text); + store.setSurroundingText(selection.surroundingText ?? null); + } else { + store.setLastSelectedText(null); + store.setLastSurroundingText(null); + store.setSelectedText(null); + store.setSurroundingText(null); + } +} + // Expose registration functions globally for cross-bundle access if (typeof window !== "undefined") { (window as unknown as { __pdAdapterRegistry: typeof adapterRegistry }). @@ -58,6 +79,8 @@ if (typeof window !== "undefined") { __pdUnregisterAdapter = unregisterAdapter; (window as unknown as { __pdSetStorage: typeof setStorage }). __pdSetStorage = setStorage; + (window as unknown as { __pdSetSelection: typeof setSelection }). + __pdSetSelection = setSelection; } const PaperDebugger = ({ displayMode = "fullscreen", adapterId }: PaperDebuggerProps) => { From 0119be3d8c9f2275f580efb4f4bba6539c4eedfd Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Mon, 12 Jan 2026 19:46:38 +0800 Subject: [PATCH 12/12] feat: replace all storage by storage-adapter-layer --- webapp/_webapp/src/devtool/app.tsx | 17 ++--- webapp/_webapp/src/intermediate.ts | 38 +++++++++--- webapp/_webapp/src/libs/apiclient.ts | 40 ++++++++---- webapp/_webapp/src/libs/helpers.ts | 10 +-- webapp/_webapp/src/stores/devtool-store.ts | 27 ++++++-- webapp/_webapp/src/stores/setting-store.ts | 62 +++++++++++++++---- webapp/_webapp/src/views/office/app.tsx | 16 +++-- .../sections/real-developer-tools.tsx | 59 ++++++++++++------ 8 files changed, 197 insertions(+), 72 deletions(-) diff --git a/webapp/_webapp/src/devtool/app.tsx b/webapp/_webapp/src/devtool/app.tsx index 577c4d5..5a7f0c7 100644 --- a/webapp/_webapp/src/devtool/app.tsx +++ b/webapp/_webapp/src/devtool/app.tsx @@ -3,32 +3,33 @@ import { useAuthStore } from "../stores/auth-store"; import { useCallback, useEffect, useState } from "react"; import { getCookies } from "../intermediate"; import { TooltipArea } from "./tooltip"; +import { storage } from "../libs/storage"; const App = () => { const { token, refreshToken, setToken, setRefreshToken } = useAuthStore(); - const [projectId, setProjectId] = useState(localStorage.getItem("pd.projectId") ?? ""); - const [overleafSession, setOverleafSession] = useState(localStorage.getItem("pd.auth.overleafSession") ?? ""); - const [gclb, setGclb] = useState(localStorage.getItem("pd.auth.gclb") ?? ""); + const [projectId, setProjectId] = useState(storage.getItem("pd.projectId") ?? ""); + const [overleafSession, setOverleafSession] = useState(storage.getItem("pd.auth.overleafSession") ?? ""); + const [gclb, setGclb] = useState(storage.getItem("pd.auth.gclb") ?? ""); useEffect(() => { getCookies(window.location.hostname).then((cookies) => { - setOverleafSession(cookies.session ?? localStorage.getItem("pd.auth.overleafSession") ?? ""); - setGclb(cookies.gclb ?? localStorage.getItem("pd.auth.gclb") ?? ""); + setOverleafSession(cookies.session ?? storage.getItem("pd.auth.overleafSession") ?? ""); + setGclb(cookies.gclb ?? storage.getItem("pd.auth.gclb") ?? ""); }); }, []); const setProjectId_ = useCallback((projectId: string) => { - localStorage.setItem("pd.projectId", projectId); + storage.setItem("pd.projectId", projectId); setProjectId(projectId); }, []); const setOverleafSession_ = useCallback((overleafSession: string) => { - localStorage.setItem("pd.auth.overleafSession", overleafSession); + storage.setItem("pd.auth.overleafSession", overleafSession); setOverleafSession(overleafSession); }, []); const setGclb_ = useCallback((gclb: string) => { - localStorage.setItem("pd.auth.gclb", gclb); + storage.setItem("pd.auth.gclb", gclb); setGclb(gclb); }, []); diff --git a/webapp/_webapp/src/intermediate.ts b/webapp/_webapp/src/intermediate.ts index ba6ab6e..c191044 100644 --- a/webapp/_webapp/src/intermediate.ts +++ b/webapp/_webapp/src/intermediate.ts @@ -14,6 +14,7 @@ */ import { HANDLER_NAMES } from "./shared/constants"; import { v4 as uuidv4 } from "uuid"; +import { storage } from "./libs/storage"; const REQUEST_TIMEOUT_MS = 5000; @@ -119,8 +120,8 @@ if (import.meta.env.DEV) { // eslint-disable-next-line @typescript-eslint/no-unused-vars getCookies = async (_domain: string) => { return { - session: localStorage.getItem("pd.auth.overleafSession") ?? "", - gclb: localStorage.getItem("pd.auth.gclb") ?? "", + session: storage.getItem("pd.auth.overleafSession") ?? "", + gclb: storage.getItem("pd.auth.gclb") ?? "", }; }; } else if (isExtensionEnvironment) { @@ -147,7 +148,7 @@ export { getUrl }; // ============================================================================ // getOrCreateSessionId - Get or create analytics session ID -// Office Add-in: Use sessionStorage instead of chrome.storage.session +// Office Add-in: Use sessionStorage with in-memory fallback // ============================================================================ let getOrCreateSessionId: () => Promise; if (isExtensionEnvironment) { @@ -155,12 +156,33 @@ if (isExtensionEnvironment) { } else { const SESSION_EXPIRATION_IN_MIN = 30; const SESSION_STORAGE_KEY = "pd.sessionData"; + + // In-memory fallback for environments where sessionStorage is not available + let inMemorySessionData: { session_id: string; timestamp: number } | null = null; + + // Helper to safely access sessionStorage + const getSessionStorageItem = (key: string): string | null => { + try { + return sessionStorage.getItem(key); + } catch { + return null; + } + }; + + const setSessionStorageItem = (key: string, value: string): void => { + try { + sessionStorage.setItem(key, value); + } catch { + // Ignore errors - we have in-memory fallback + } + }; + getOrCreateSessionId = async () => { const currentTimeInMs = Date.now(); - const storedData = sessionStorage.getItem(SESSION_STORAGE_KEY); + const storedData = getSessionStorageItem(SESSION_STORAGE_KEY); let sessionData: { session_id: string; timestamp: number } | null = storedData ? JSON.parse(storedData) - : null; + : inMemorySessionData; if (sessionData && sessionData.timestamp) { const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000; @@ -168,7 +190,8 @@ if (isExtensionEnvironment) { sessionData = null; } else { sessionData.timestamp = currentTimeInMs; - sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(sessionData)); + setSessionStorageItem(SESSION_STORAGE_KEY, JSON.stringify(sessionData)); + inMemorySessionData = sessionData; } } if (!sessionData) { @@ -176,7 +199,8 @@ if (isExtensionEnvironment) { session_id: currentTimeInMs.toString(), timestamp: currentTimeInMs, }; - sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(sessionData)); + setSessionStorageItem(SESSION_STORAGE_KEY, JSON.stringify(sessionData)); + inMemorySessionData = sessionData; } return sessionData.session_id; }; diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index 0552f4c..0105ef9 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -5,6 +5,7 @@ import { GetUserResponseSchema } from "../pkg/gen/apiclient/user/v1/user_pb"; import { EventEmitter } from "events"; import { ErrorCode, ErrorSchema } from "../pkg/gen/apiclient/shared/v1/shared_pb"; import { errorToast } from "./toasts"; +import { storage } from "./storage"; // Exhaustive type check helper - will cause compile error if a case is not handled const assertNever = (x: never): never => { @@ -43,10 +44,10 @@ class ApiClient { this.axiosInstance.defaults.baseURL = `${baseURL}/_pd/api/${apiVersion}`; switch (apiVersion) { case "v1": - localStorage.setItem(API_VERSION_STORAGE_KEYS.v1, this.axiosInstance.defaults.baseURL); + storage.setItem(API_VERSION_STORAGE_KEYS.v1, this.axiosInstance.defaults.baseURL); break; case "v2": - localStorage.setItem(API_VERSION_STORAGE_KEYS.v2, this.axiosInstance.defaults.baseURL); + storage.setItem(API_VERSION_STORAGE_KEYS.v2, this.axiosInstance.defaults.baseURL); break; default: assertNever(apiVersion); // Compile error if a new version is added but not handled @@ -213,26 +214,41 @@ const DEFAULT_ENDPOINT = `${process.env.PD_API_ENDPOINT || "http://localhost:300 const LOCAL_STORAGE_KEY_V1 = "pd.devtool.endpoint"; const LOCAL_STORAGE_KEY_V2 = "pd.devtool.endpoint.v2"; -// Create apiclient instance with endpoint from localStorage or default -export const getEndpointFromLocalStorage = () => { +// Create apiclient instance with endpoint from storage or default +export const getEndpointFromStorage = () => { let endpoint = ""; try { - endpoint = localStorage.getItem(LOCAL_STORAGE_KEY_V1) || DEFAULT_ENDPOINT; + endpoint = storage.getItem(LOCAL_STORAGE_KEY_V1) || DEFAULT_ENDPOINT; } catch { - // Fallback if localStorage is not available (e.g., in SSR) + // Fallback if storage is not available endpoint = DEFAULT_ENDPOINT; } return endpoint.replace("/_pd/api/v1", "").replace("/_pd/api/v2", ""); // compatible with old endpoint }; +/** + * @deprecated Use getEndpointFromStorage instead + */ +export const getEndpointFromLocalStorage = getEndpointFromStorage; + export const resetApiClientEndpoint = () => { - localStorage.removeItem(LOCAL_STORAGE_KEY_V1); - localStorage.removeItem(LOCAL_STORAGE_KEY_V2); - apiclient.updateBaseURL(getEndpointFromLocalStorage(), "v1"); - apiclientV2.updateBaseURL(getEndpointFromLocalStorage(), "v2"); + storage.removeItem(LOCAL_STORAGE_KEY_V1); + storage.removeItem(LOCAL_STORAGE_KEY_V2); + apiclient.updateBaseURL(getEndpointFromStorage(), "v1"); + apiclientV2.updateBaseURL(getEndpointFromStorage(), "v2"); +}; + +/** + * Reinitialize API client endpoints from storage. + * Call this after the storage adapter has been set. + */ +export const initApiClientFromStorage = () => { + const endpoint = getEndpointFromStorage(); + apiclient.updateBaseURL(endpoint, "v1"); + apiclientV2.updateBaseURL(endpoint, "v2"); }; -const apiclient = new ApiClient(getEndpointFromLocalStorage(), "v1"); -export const apiclientV2 = new ApiClient(getEndpointFromLocalStorage(), "v2"); +const apiclient = new ApiClient(getEndpointFromStorage(), "v1"); +export const apiclientV2 = new ApiClient(getEndpointFromStorage(), "v2"); export default apiclient; diff --git a/webapp/_webapp/src/libs/helpers.ts b/webapp/_webapp/src/libs/helpers.ts index 1b7016a..3b50366 100644 --- a/webapp/_webapp/src/libs/helpers.ts +++ b/webapp/_webapp/src/libs/helpers.ts @@ -1,4 +1,6 @@ import { EditorView } from "@codemirror/view"; +import { storage } from "./storage"; +import { storage } from "./storage"; export async function onElementAppeared(selector: string, callback: (element: Element) => void) { const element = document.querySelector(selector); @@ -54,7 +56,7 @@ export function applyChanges(changes: string, range: Range): boolean { export function getProjectId() { if (import.meta.env.DEV) { - return localStorage.getItem("pd.projectId") ?? ""; + return storage.getItem("pd.projectId") ?? ""; } const match = window.location.pathname.match(/\/project\/([a-zA-Z0-9]+)/); return match ? match[1] : ""; @@ -171,7 +173,7 @@ export function generateSHA1Hash(inputString: string): string { return result.map((b) => b.toString(16).padStart(2, "0")).join(""); } -// --- Overleaf Comments Clicked LocalStorage --- +// --- Overleaf Comments Clicked Storage --- const OVERLEAF_COMMENTS_CLICKED_PREFIX = "pd.overleaf_comments_clicked."; const MAX_CLICKED_COMMENTS = 200; @@ -179,7 +181,7 @@ export function getClickedOverleafComments(projectId: string): string[] { if (!projectId) return []; const key = OVERLEAF_COMMENTS_CLICKED_PREFIX + projectId; try { - const raw = localStorage.getItem(key); + const raw = storage.getItem(key); if (!raw) return []; const arr = JSON.parse(raw); if (Array.isArray(arr)) return arr; @@ -200,7 +202,7 @@ export function addClickedOverleafComment(projectId: string, messageId: string) if (arr.length > MAX_CLICKED_COMMENTS) { arr = arr.slice(arr.length - MAX_CLICKED_COMMENTS); } - localStorage.setItem(key, JSON.stringify(arr)); + storage.setItem(key, JSON.stringify(arr)); } export function hasClickedOverleafComment(projectId: string, messageId: string): boolean { diff --git a/webapp/_webapp/src/stores/devtool-store.ts b/webapp/_webapp/src/stores/devtool-store.ts index a95e620..8789f0b 100644 --- a/webapp/_webapp/src/stores/devtool-store.ts +++ b/webapp/_webapp/src/stores/devtool-store.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { storage } from "../libs/storage"; export const localStorageKey = { showTool: "pd.devtool.showTool", @@ -7,6 +8,12 @@ export const localStorageKey = { } as const; interface DevtoolStore { + /** + * Initialize devtool settings from storage. + * Must be called after storage adapter is set (e.g., after Office.onReady). + */ + initFromStorage: () => void; + showTool: boolean; setShowTool: (showTool: boolean) => void; @@ -18,21 +25,29 @@ interface DevtoolStore { } export const useDevtoolStore = create((set) => ({ - showTool: JSON.parse(localStorage.getItem(localStorageKey.showTool) || "false"), + initFromStorage: () => { + const showTool = JSON.parse(storage.getItem(localStorageKey.showTool) || "false"); + const slowStreamingMode = JSON.parse(storage.getItem(localStorageKey.slowStreamingMode) || "false"); + const alwaysSyncProject = JSON.parse(storage.getItem(localStorageKey.alwaysSyncProject) || "false"); + set({ showTool, slowStreamingMode, alwaysSyncProject }); + }, + + // Initial values are defaults - will be populated by initFromStorage() + showTool: false, setShowTool: (showTool: boolean) => { - localStorage.setItem(localStorageKey.showTool, JSON.stringify(showTool)); + storage.setItem(localStorageKey.showTool, JSON.stringify(showTool)); set({ showTool }); }, - slowStreamingMode: JSON.parse(localStorage.getItem(localStorageKey.slowStreamingMode) || "false"), + slowStreamingMode: false, setSlowStreamingMode: (slowStreamingMode: boolean) => { - localStorage.setItem(localStorageKey.slowStreamingMode, JSON.stringify(slowStreamingMode)); + storage.setItem(localStorageKey.slowStreamingMode, JSON.stringify(slowStreamingMode)); set({ slowStreamingMode }); }, - alwaysSyncProject: JSON.parse(localStorage.getItem(localStorageKey.alwaysSyncProject) || "false"), + alwaysSyncProject: false, setAlwaysSyncProject: (alwaysSyncProject: boolean) => { - localStorage.setItem(localStorageKey.alwaysSyncProject, JSON.stringify(alwaysSyncProject)); + storage.setItem(localStorageKey.alwaysSyncProject, JSON.stringify(alwaysSyncProject)); set({ alwaysSyncProject }); }, })); diff --git a/webapp/_webapp/src/stores/setting-store.ts b/webapp/_webapp/src/stores/setting-store.ts index 92f02ae..ffd80bc 100644 --- a/webapp/_webapp/src/stores/setting-store.ts +++ b/webapp/_webapp/src/stores/setting-store.ts @@ -3,6 +3,17 @@ import { getSettings, resetSettings, updateSettings } from "../query/api"; import { Settings, UpdateSettingsRequest } from "../pkg/gen/apiclient/user/v1/user_pb"; import { PlainMessage } from "../query/types"; import { logError } from "../libs/logger"; +import { storage } from "../libs/storage"; + +// Storage keys for local UI settings +const LOCAL_STORAGE_KEY = { + ENABLE_USER_DEVELOPER_TOOLS: "pd.devtool.enabled", + CONVERSATION_MODE: "pd.devtool.conversationMode", + DISABLE_LINE_WRAP: "pd.lineWrap.enabled", + MINIMALIST_MODE: "pd.ui.minimalistMode", + HIDE_AVATAR: "pd.ui.hideAvatar", + ALLOW_OUT_OF_BOUNDS: "pd.ui.allowOutOfBounds", +}; export interface SettingStore { settings: PlainMessage | null; @@ -13,6 +24,13 @@ export interface SettingStore { updateSettings: (newSettings: Partial>) => Promise; resetSettings: () => Promise; + /** + * Initialize local UI settings from storage. + * Must be called after storage adapter is set (e.g., after Office.onReady). + * This reloads local UI settings from the configured storage adapter. + */ + initLocalSettings: () => void; + enableUserDeveloperTools: boolean; // Not actual developer tools setEnableUserDeveloperTools: (enable: boolean) => void; @@ -125,39 +143,59 @@ export const useSettingStore = create()((set, get) => ({ } }, - enableUserDeveloperTools: localStorage.getItem("pd.devtool.enabled") === "true" || false, + initLocalSettings: () => { + const enableUserDeveloperTools = storage.getItem(LOCAL_STORAGE_KEY.ENABLE_USER_DEVELOPER_TOOLS) === "true"; + const conversationMode = (storage.getItem(LOCAL_STORAGE_KEY.CONVERSATION_MODE) as "debug" | "normal") || "normal"; + const disableLineWrap = storage.getItem(LOCAL_STORAGE_KEY.DISABLE_LINE_WRAP) === "true"; + const minimalistMode = storage.getItem(LOCAL_STORAGE_KEY.MINIMALIST_MODE) === "true"; + const hideAvatar = storage.getItem(LOCAL_STORAGE_KEY.HIDE_AVATAR) === "true"; + const allowOutOfBounds = storage.getItem(LOCAL_STORAGE_KEY.ALLOW_OUT_OF_BOUNDS) === "true"; + + set({ + enableUserDeveloperTools, + conversationMode, + disableLineWrap, + minimalistMode, + hideAvatar, + allowOutOfBounds, + }); + }, + + // Initial values are defaults - will be populated by initLocalSettings() + // This ensures we don't read from storage before the adapter is set + enableUserDeveloperTools: false, setEnableUserDeveloperTools: (enable: boolean) => { - localStorage.setItem("pd.devtool.enabled", enable.toString()); + storage.setItem(LOCAL_STORAGE_KEY.ENABLE_USER_DEVELOPER_TOOLS, enable.toString()); set({ enableUserDeveloperTools: enable }); }, - conversationMode: (localStorage.getItem("pd.devtool.conversationMode") as "debug" | "normal") || "normal", + conversationMode: "normal", setConversationMode: (mode: "debug" | "normal") => { - localStorage.setItem("pd.devtool.conversationMode", mode); + storage.setItem(LOCAL_STORAGE_KEY.CONVERSATION_MODE, mode); set({ conversationMode: mode }); }, - disableLineWrap: localStorage.getItem("pd.lineWrap.enabled") === "true" || false, + disableLineWrap: false, setDisableLineWrap: (enable: boolean) => { - localStorage.setItem("pd.lineWrap.enabled", enable.toString()); + storage.setItem(LOCAL_STORAGE_KEY.DISABLE_LINE_WRAP, enable.toString()); set({ disableLineWrap: enable }); }, - minimalistMode: localStorage.getItem("pd.ui.minimalistMode") === "true" || false, + minimalistMode: false, setMinimalistMode: (enable: boolean) => { - localStorage.setItem("pd.ui.minimalistMode", enable.toString()); + storage.setItem(LOCAL_STORAGE_KEY.MINIMALIST_MODE, enable.toString()); set({ minimalistMode: enable }); }, - hideAvatar: localStorage.getItem("pd.ui.hideAvatar") === "true" || false, + hideAvatar: false, setHideAvatar: (enable: boolean) => { - localStorage.setItem("pd.ui.hideAvatar", enable.toString()); + storage.setItem(LOCAL_STORAGE_KEY.HIDE_AVATAR, enable.toString()); set({ hideAvatar: enable }); }, - allowOutOfBounds: localStorage.getItem("pd.ui.allowOutOfBounds") === "true" || false, + allowOutOfBounds: false, setAllowOutOfBounds: (enable: boolean) => { - localStorage.setItem("pd.ui.allowOutOfBounds", enable.toString()); + storage.setItem(LOCAL_STORAGE_KEY.ALLOW_OUT_OF_BOUNDS, enable.toString()); set({ allowOutOfBounds: enable }); }, })); diff --git a/webapp/_webapp/src/views/office/app.tsx b/webapp/_webapp/src/views/office/app.tsx index c9863c3..a804d4e 100644 --- a/webapp/_webapp/src/views/office/app.tsx +++ b/webapp/_webapp/src/views/office/app.tsx @@ -12,6 +12,8 @@ import { import { setStorage as setGlobalStorage } from "../../libs/storage"; import { useAuthStore } from "../../stores/auth-store"; import { useSelectionStore } from "../../stores/selection-store"; +import { useSettingStore } from "../../stores/setting-store"; +import { useDevtoolStore } from "../../stores/devtool-store"; import "../../index.css"; @@ -85,14 +87,20 @@ if (typeof window !== "undefined") { const PaperDebugger = ({ displayMode = "fullscreen", adapterId }: PaperDebuggerProps) => { const { setDisplayMode, setIsOpen, isOpen } = useConversationUiStore(); - const { initFromStorage, login } = useAuthStore(); + const { initFromStorage: initAuthFromStorage, login } = useAuthStore(); + const { initLocalSettings } = useSettingStore(); + const { initFromStorage: initDevtoolFromStorage } = useDevtoolStore(); const [isInitialized, setIsInitialized] = useState(false); - // Initialize auth from storage on mount - // This must happen before the main UI renders to restore login state + // Initialize stores from storage on mount + // This must happen before the main UI renders to restore login state and settings useEffect(() => { // Re-initialize auth store from storage (storage adapter should be set by host before component mounts) - initFromStorage(); + initAuthFromStorage(); + // Re-initialize local UI settings from storage + initLocalSettings(); + // Re-initialize devtool settings from storage + initDevtoolFromStorage(); // Attempt to login with restored tokens login(); setIsInitialized(true); diff --git a/webapp/_webapp/src/views/settings/sections/real-developer-tools.tsx b/webapp/_webapp/src/views/settings/sections/real-developer-tools.tsx index 8c6b79a..a9fbf0e 100644 --- a/webapp/_webapp/src/views/settings/sections/real-developer-tools.tsx +++ b/webapp/_webapp/src/views/settings/sections/real-developer-tools.tsx @@ -6,6 +6,14 @@ import { SettingsSectionContainer } from "./components"; import { SettingsSectionTitle } from "./components"; import { SettingItem } from "../setting-items"; import { localStorageKey, useDevtoolStore } from "../../../stores/devtool-store"; +import { storage } from "../../../libs/storage"; + +// Keys to preserve during reset +const PRESERVED_KEY_PREFIXES = [ + "pd.auth.", + "pd.devtool.", + "pd.projectId", +]; export const RealDeveloperTools = () => { const { @@ -77,9 +85,9 @@ export const RealDeveloperTools = () => { />
-
Reset localStorage and reload
+
Reset storage and reload
- Reset all user-configurable localStorage of the app except the:
+ Reset all user-configurable storage of the app except:
pd.projectId
pd.auth.* @@ -93,23 +101,36 @@ export const RealDeveloperTools = () => { color="secondary" radius="full" onPress={() => { - const refreshToken = localStorage.getItem("pd.auth.refreshToken"); - const token = localStorage.getItem("pd.auth.token"); - const gclb = localStorage.getItem("pd.auth.gclb"); - const overleafSession = localStorage.getItem("pd.auth.overleafSession"); - const projectId = localStorage.getItem("pd.projectId"); - const keys = Object.values(localStorageKey); - const values = keys.map((key) => localStorage.getItem(key)); - localStorage.clear(); - - localStorage.setItem("pd.auth.refreshToken", refreshToken || ""); - localStorage.setItem("pd.auth.token", token || ""); - localStorage.setItem("pd.auth.gclb", gclb || ""); - localStorage.setItem("pd.auth.overleafSession", overleafSession || ""); - localStorage.setItem("pd.projectId", projectId || ""); - - keys.forEach((key, index) => { - localStorage.setItem(key, values[index] || ""); + // Get all keys from storage + const allKeys = storage.keys(); + + // Identify keys to preserve (auth, devtool, projectId) + const keysToPreserve = allKeys.filter((key) => + PRESERVED_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) + ); + + // Save values of keys to preserve + const preservedValues: Record = {}; + keysToPreserve.forEach((key) => { + preservedValues[key] = storage.getItem(key); + }); + + // Also preserve devtool store keys + const devtoolKeys = Object.values(localStorageKey); + devtoolKeys.forEach((key) => { + if (!preservedValues[key]) { + preservedValues[key] = storage.getItem(key); + } + }); + + // Clear all storage + storage.clear(); + + // Restore preserved values + Object.entries(preservedValues).forEach(([key, value]) => { + if (value !== null) { + storage.setItem(key, value); + } }); window.location.reload();