From 5cac29e4548c60b92a4e7894482818efbcb13dd7 Mon Sep 17 00:00:00 2001 From: black_zero Date: Mon, 2 Mar 2026 10:08:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=80=E4=B8=8B=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E9=A3=8E=E6=A0=BC=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.zh-CN.md | 4 + index.html | 4 +- package-lock.json | 1962 +++++++++++++++++++++++++ src/app/App.tsx | 330 ++--- src/app/core/githubWriteback.ts | 607 -------- src/app/core/llmClassification.ts | 224 --- src/app/core/llmPromptResolver.ts | 160 -- src/app/core/purgeListLocal.ts | 18 + src/app/core/syncRepoListToGitHub.ts | 40 + src/app/core/types.ts | 23 - src/app/i18n/resources.ts | 446 +----- src/app/services/githubStarLists.ts | 58 + src/app/services/llmClient.ts | 65 - src/app/store/llmConfig.ts | 111 -- src/app/store/preferences.ts | 21 - src/app/styles/app.css | 697 ++++++--- src/app/styles/fonts.css | 2 +- src/app/ui/ApplyUpdatesModal.tsx | 313 ---- src/app/ui/AssignListModal.tsx | 26 +- src/app/ui/LlmClassificationModal.tsx | 331 ----- src/app/ui/ManageListsModal.tsx | 253 ++++ src/app/ui/SettingsModal.tsx | 337 +---- 22 files changed, 3082 insertions(+), 2950 deletions(-) create mode 100644 package-lock.json delete mode 100644 src/app/core/githubWriteback.ts delete mode 100644 src/app/core/llmClassification.ts delete mode 100644 src/app/core/llmPromptResolver.ts create mode 100644 src/app/core/purgeListLocal.ts create mode 100644 src/app/core/syncRepoListToGitHub.ts delete mode 100644 src/app/core/types.ts delete mode 100644 src/app/services/llmClient.ts delete mode 100644 src/app/store/llmConfig.ts delete mode 100644 src/app/ui/ApplyUpdatesModal.tsx delete mode 100644 src/app/ui/LlmClassificationModal.tsx create mode 100644 src/app/ui/ManageListsModal.tsx diff --git a/README.zh-CN.md b/README.zh-CN.md index 915ada3..4b8bbdb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,6 +4,10 @@ Star Manager 是一个本地优先(local-first)的 Web 应用,用于整理你的 GitHub Star 仓库与 Star Lists,并支持可选的 LLM 辅助分类。 +如果您想在不进行本地设置的情况下体验该应用,可以使用[在线部署](https://github-star-manager.blackzero.edu.kg)。 + + + ## 为什么使用 Star Manager 当 Star 仓库数量变多后,手工维护列表效率低且容易不一致。Star Manager 可以帮助你: diff --git a/index.html b/index.html index c353307..07792ff 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,8 @@ - - Star Manager + + GitHub星标管理面板
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8407b13 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1962 @@ +{ + "name": "star-manager", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "star-manager", + "version": "0.0.0", + "dependencies": { + "dexie": "^4.0.7", + "i18next": "^25.5.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-i18next": "^16.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.11", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.2", + "typescript": "~5.9.3", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dexie": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", + "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", + "license": "Apache-2.0" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.335", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", + "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-i18next": { + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.10.9", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/src/app/App.tsx b/src/app/App.tsx index a95346a..b5ce822 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -6,10 +6,9 @@ import { db } from "./data/db"; import { useLiveQuery } from "./data/useLiveQuery"; import { validatePat } from "./services/githubAuth"; import { usePreferenceStore } from "./store/preferences"; -import { ApplyUpdatesModal } from "./ui/ApplyUpdatesModal"; import { AssignListModal } from "./ui/AssignListModal"; import { FirstRunPrompt } from "./ui/FirstRunPrompt"; -import { LlmClassificationModal } from "./ui/LlmClassificationModal"; +import { ManageListsModal } from "./ui/ManageListsModal"; import { PatModal } from "./ui/PatModal"; import { SettingsModal } from "./ui/SettingsModal"; @@ -23,16 +22,10 @@ type RepoPreview = { updatedAt?: string; }; -type SyncStatusCode = "idle" | "running" | "completed" | "failed" | "ready" | "retrying"; - -type SyncMessage = { - key: string; - values?: Record; -}; - const previewRepos: RepoPreview[] = []; const previewLists: { id: string; name: string; count: number }[] = [ { id: "all", name: "", count: 0 }, + { id: "unclassified", name: "", count: 0 }, ]; export default function App() { @@ -42,19 +35,10 @@ export default function App() { const [activeList, setActiveList] = useState("all"); const [isPatModalOpen, setIsPatModalOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const [isClassificationOpen, setIsClassificationOpen] = useState(false); - const [isApplyUpdatesOpen, setIsApplyUpdatesOpen] = useState(false); + const [isManageListsOpen, setIsManageListsOpen] = useState(false); const [assignRepo, setAssignRepo] = useState<{ id: string; name: string } | null>(null); - const [syncStatus, setSyncStatus] = useState("idle"); - const [syncMessage, setSyncMessage] = useState({ - key: "app.sync.detail.connectPat", - }); - const [syncError, setSyncError] = useState(""); - const [syncStage, setSyncStage] = useState("idle"); - const [syncCurrent, setSyncCurrent] = useState(0); - const [syncTotal, setSyncTotal] = useState(0); - const [syncFailed, setSyncFailed] = useState(0); const [failedListIds, setFailedListIds] = useState([]); + const [listSidebarOpen, setListSidebarOpen] = useState(false); const [languageFilter, setLanguageFilter] = useState("all"); const [showUnlisted, setShowUnlisted] = useState(false); const [recentOnly, setRecentOnly] = useState(false); @@ -85,6 +69,7 @@ export default function App() { async () => { const listRows = await db.lists.toArray(); const repoListRows = await db.repoLists.toArray(); + const repoListMap = new Map(repoListRows.map((row) => [row.repoId, row.listIds])); const countMap = new Map(); for (const row of repoListRows) { for (const listId of row.listIds) { @@ -97,8 +82,18 @@ export default function App() { name: list.name, count: countMap.get(list.id) ?? 0, })); - const total = await db.repos.count(); - return [{ id: "all", name: "", count: total }, ...listCounts]; + const repoRows = await db.repos.toArray(); + let unclassifiedCount = 0; + for (const repo of repoRows) { + const ids = repoListMap.get(repo.id); + if (!ids || ids.length === 0) unclassifiedCount += 1; + } + const total = repoRows.length; + return [ + { id: "all", name: "", count: total }, + { id: "unclassified", name: "", count: unclassifiedCount }, + ...listCounts, + ]; }, [], previewLists @@ -141,11 +136,20 @@ export default function App() { const visibleRepos = useMemo(() => { if (activeList === "all") return repos; + if (activeList === "unclassified") return repos.filter((repo) => repo.tags.length === 0); const listName = lists.find((list) => list.id === activeList)?.name; if (!listName) return []; return repos.filter((repo) => repo.tags.includes(listName)); }, [activeList, repos, lists]); + const activeListLabel = useMemo(() => { + const item = lists.find((l) => l.id === activeList); + if (!item) return ""; + if (item.id === "all") return t("common.values.allStarred"); + if (item.id === "unclassified") return t("common.values.unclassified"); + return item.name; + }, [lists, activeList, t]); + const filteredRepos = useMemo(() => { const query = searchQuery.trim().toLowerCase(); const threshold = Date.now() - 1000 * 60 * 60 * 24 * 180; @@ -164,32 +168,42 @@ export default function App() { }); }, [visibleRepos, languageFilter, showUnlisted, recentOnly, searchQuery]); - const lastSyncText = useMemo(() => { - if (!preferences.lastSyncedAt) return ""; - const parsed = Date.parse(preferences.lastSyncedAt); - if (Number.isNaN(parsed)) return preferences.lastSyncedAt; - return new Date(parsed).toLocaleString(i18n.language); - }, [preferences.lastSyncedAt, i18n.language]); - - const syncDetail = useMemo(() => { - if (syncError) return syncError; - if (syncStatus === "running" || syncStatus === "retrying") { - const stageText = t(`progress.sync.${syncStage}`, { defaultValue: syncStage }); - return t("app.sync.detail.progress", { - stage: stageText, - current: syncCurrent, - total: syncTotal, - }); - } - return t(syncMessage.key, syncMessage.values); - }, [syncError, syncStatus, syncStage, syncCurrent, syncTotal, syncMessage, t]); - useEffect(() => { if (activeList === "all") return; const exists = lists.some((list) => list.id === activeList); if (!exists) setActiveList("all"); }, [activeList, lists]); + useEffect(() => { + if (!listSidebarOpen) return; + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") setListSidebarOpen(false); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [listSidebarOpen]); + + useEffect(() => { + if (!listSidebarOpen) return; + const mq = window.matchMedia("(max-width: 1100px)"); + if (!mq.matches) return; + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + }; + }, [listSidebarOpen]); + + useEffect(() => { + const mq = window.matchMedia("(min-width: 1101px)"); + const close = () => { + if (mq.matches) setListSidebarOpen(false); + }; + mq.addEventListener("change", close); + close(); + return () => mq.removeEventListener("change", close); + }, []); + return (
@@ -216,69 +230,108 @@ export default function App() { setIsPatModalOpen(true); return; } - setSyncStatus("running"); - setSyncError(""); - setSyncMessage({ key: "app.sync.detail.preparing" }); - setSyncStage("idle"); - setSyncCurrent(0); - setSyncTotal(0); - setSyncFailed(0); try { - const result = await syncFromGitHub({ token: preferences.patToken }, (progress) => { - setSyncStage(progress.stage); - setSyncCurrent(progress.current); - setSyncTotal(progress.total); - setSyncFailed(progress.failed); - }); - setSyncStatus("completed"); - setSyncMessage({ - key: "app.sync.detail.loaded", - values: { repos: result.repos, lists: result.lists }, - }); + const result = await syncFromGitHub({ token: preferences.patToken }); setFailedListIds(result.failedListIds); setLastSyncedAt(new Date().toISOString()); - if (result.failedListIds.length === 0) { - setSyncFailed(0); - } } catch (error) { - setSyncStatus("failed"); - setSyncError((error as Error).message || t("app.sync.detail.syncFailed")); + window.alert((error as Error).message || t("app.sync.detail.syncFailed")); } }} > {t("app.actions.syncStarLists")} + {failedListIds.length > 0 ? ( + + ) : null}
-
-

{t("app.sections.starLists")}

- {lists.length === 1 && lists[0].id === "all" ? ( -
-

{t("app.empty.noListsTitle")}

-

{t("app.empty.noListsHint")}

+ {listSidebarOpen ? ( +
setListSidebarOpen(false)} + /> + ) : null} +
+
+ +

{t("app.sections.starLists")}

+ +
+ {lists.filter((l) => l.id !== "all" && l.id !== "unclassified").length === 0 ? ( +

{t("app.empty.noListsHint")}

+ ) : null} + {lists.map((list) => ( +
{ + setActiveList(list.id); + setListSidebarOpen(false); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + setActiveList(list.id); + setListSidebarOpen(false); + } + }} + > + + {list.id === "all" + ? t("common.values.allStarred") + : list.id === "unclassified" + ? t("common.values.unclassified") + : list.name} + + {list.count}
- ) : ( - lists.map((list) => ( -
setActiveList(list.id)} - onKeyDown={(event) => { - if (event.key === "Enter") setActiveList(list.id); - }} - > - {list.id === "all" ? t("common.values.allStarred") : list.name} - {list.count} -
- )) - )} + ))}
-
+
+
+ +

{t("app.sections.repositories")}

{repos.length === 0 ? (
@@ -340,7 +393,7 @@ export default function App() {

{repo.name}

{repo.description || t("app.values.noDescription")}

-
+
{repo.tags.map((tag) => ( {tag} @@ -362,83 +415,6 @@ export default function App() {
)}
- -
-
-

{t("app.sections.syncStatus")}

-

- {t("common.labels.status")}: {t(`app.sync.status.${syncStatus}`, { defaultValue: syncStatus })} -

-

{syncDetail}

-

- {t("common.labels.stage")}: {t(`progress.sync.${syncStage}`, { defaultValue: syncStage })} -

-

- {t("common.labels.progress")}: {syncCurrent}/{syncTotal} · {t("common.labels.failed")}: {syncFailed} -

- {preferences.lastSyncedAt ? ( -

- {t("common.labels.lastSync")}: {lastSyncText} -

- ) : null} - {failedListIds.length > 0 ? ( - - ) : null} -
-
-

{t("app.sections.llmClassification")}

-

{t("app.sidebar.llmDesc")}

- - -
-
-

{t("app.sections.batchActions")}

-

{t("app.sidebar.batchDesc")}

- -
-
{!preferences.hasCompletedOnboarding ? ( @@ -460,25 +436,17 @@ export default function App() { try { const viewer = await validatePat(token); setPatToken(token, viewer.login); - setSyncStatus("ready"); - setSyncError(""); - setSyncMessage({ key: "app.sync.detail.tokenOk", values: { login: viewer.login } }); setIsPatModalOpen(false); } catch (error) { - setSyncStatus("failed"); - setSyncError((error as Error).message || t("app.sync.detail.tokenValidationFailed")); + window.alert((error as Error).message || t("app.sync.detail.tokenValidationFailed")); } }} /> setIsSettingsOpen(false)} /> - setIsClassificationOpen(false)} - /> - setIsApplyUpdatesOpen(false)} - token={preferences.patToken} + setIsManageListsOpen(false)} + patToken={preferences.patToken} onRequestToken={() => setIsPatModalOpen(true)} /> {assignRepo ? ( @@ -486,7 +454,9 @@ export default function App() { isOpen={Boolean(assignRepo)} repoId={assignRepo.id} repoName={assignRepo.name} + patToken={preferences.patToken} onClose={() => setAssignRepo(null)} + onRequestToken={() => setIsPatModalOpen(true)} /> ) : null} diff --git a/src/app/core/githubWriteback.ts b/src/app/core/githubWriteback.ts deleted file mode 100644 index 34b364f..0000000 --- a/src/app/core/githubWriteback.ts +++ /dev/null @@ -1,607 +0,0 @@ -import { db } from "../data/db"; -import type { GitHubConfig } from "../services/githubClient"; -import { - addStar, - buildRepoMembershipIndex, - createUserList, - detectStarListApi, - fetchStarLists, - getRepositoryMeta, - type UserListRef, - updateUserListsForItem, -} from "../services/githubStarLists"; -import { runWithConcurrency } from "./queues"; - -export type WritebackIssueReason = - | "empty_tags" - | "repo_not_found" - | "invalid_repo_name" - | "repo_meta_missing" - | "repo_meta_fetch_failed" - | "missing_list" - | "create_list_failed" - | "membership_scan_failed" - | "no_changes" - | "unstarred_requires_confirmation" - | "add_star_failed" - | "update_failed"; - -export type WritebackIssue = { - repoId?: string; - repoFullName?: string; - reason: WritebackIssueReason; - message: string; -}; - -export type WritebackCandidate = { - repoId: string; - repoFullName: string; - itemId: string; - viewerHasStarred: boolean; - targetTags: string[]; - targetListIds: string[]; - targetListNames: string[]; - currentListIds: string[]; - currentListNames: string[]; - finalListIds: string[]; - finalListNames: string[]; -}; - -export type WritebackPlan = { - totalClassifications: number; - validTaggedRepos: number; - actionableRepos: number; - unchangedRepos: number; - scanFailures: number; - missingLists: string[]; - createdLists: string[]; - unstarredActionableRepos: number; - skipped: WritebackIssue[]; - candidates: WritebackCandidate[]; -}; - -export type WritebackResult = { - plan: WritebackPlan; - applied: number; - failed: WritebackIssue[]; - skipped: WritebackIssue[]; - createdLists: string[]; - locallyUpdated: number; -}; - -export type WritebackProgress = { - stage: string; - current: number; - total: number; - failed: number; - skipped: number; -}; - -export type PlanWritebackOptions = { - createMissingLists?: boolean; - createMissingListsAsPrivate?: boolean; -}; - -export type ApplyWritebackOptions = { - autoStarForUnstarred?: boolean; - resolveAutoStarForUnstarred?: (unstarredCount: number) => boolean | Promise; - createMissingListsAsPrivate?: boolean; - reusePlan?: WritebackPlan | null; -}; - -type PreparedWriteback = { - plan: WritebackPlan; - listIdToName: Map; - createdListRefs: UserListRef[]; -}; - -const REPO_META_CONCURRENCY = 3; - -export async function planWriteback( - config: GitHubConfig, - options: PlanWritebackOptions = {}, - onProgress?: (progress: WritebackProgress) => void -): Promise { - const prepared = await prepareWriteback( - config, - options.createMissingLists ?? false, - options.createMissingListsAsPrivate, - onProgress - ); - return prepared.plan; -} - -export async function applyWriteback( - config: GitHubConfig, - options: ApplyWritebackOptions, - onProgress?: (progress: WritebackProgress) => void -): Promise { - const prepared = - options.reusePlan != null - ? { - plan: options.reusePlan, - listIdToName: new Map(), - createdListRefs: [], - } - : await prepareWriteback(config, true, options.createMissingListsAsPrivate, onProgress); - - return applyPreparedWriteback(config, prepared, options, onProgress); -} - -async function applyPreparedWriteback( - config: GitHubConfig, - prepared: PreparedWriteback, - options: ApplyWritebackOptions, - onProgress?: (progress: WritebackProgress) => void -): Promise { - const failed: WritebackIssue[] = []; - const skipped: WritebackIssue[] = [...prepared.plan.skipped]; - const successfulCandidates: WritebackCandidate[] = []; - let applied = 0; - let current = 0; - const total = prepared.plan.candidates.length; - let autoStarForUnstarred = options.autoStarForUnstarred ?? false; - - if (prepared.plan.unstarredActionableRepos > 0 && options.resolveAutoStarForUnstarred) { - autoStarForUnstarred = await options.resolveAutoStarForUnstarred( - prepared.plan.unstarredActionableRepos - ); - } - - emitProgress(onProgress, { - stage: "applying_updates", - current: 0, - total: total || 1, - failed: failed.length, - skipped: skipped.length, - }); - - for (const candidate of prepared.plan.candidates) { - if (!autoStarForUnstarred && !candidate.viewerHasStarred) { - skipped.push({ - repoId: candidate.repoId, - repoFullName: candidate.repoFullName, - reason: "unstarred_requires_confirmation", - message: "Repo is not starred and auto-star was declined.", - }); - current += 1; - emitProgress(onProgress, { - stage: "applying_updates", - current, - total: total || 1, - failed: failed.length, - skipped: skipped.length, - }); - continue; - } - - try { - if (autoStarForUnstarred && !candidate.viewerHasStarred) { - await addStar(config, candidate.itemId); - } - } catch (error) { - failed.push({ - repoId: candidate.repoId, - repoFullName: candidate.repoFullName, - reason: "add_star_failed", - message: `Failed to star repo before writeback: ${(error as Error).message || "unknown"}`, - }); - current += 1; - emitProgress(onProgress, { - stage: "applying_updates", - current, - total: total || 1, - failed: failed.length, - skipped: skipped.length, - }); - continue; - } - - try { - await updateUserListsForItem(config, candidate.itemId, candidate.finalListIds); - applied += 1; - successfulCandidates.push(candidate); - } catch (error) { - failed.push({ - repoId: candidate.repoId, - repoFullName: candidate.repoFullName, - reason: "update_failed", - message: `Failed to update repo lists: ${(error as Error).message || "unknown"}`, - }); - } - - current += 1; - emitProgress(onProgress, { - stage: "applying_updates", - current, - total: total || 1, - failed: failed.length, - skipped: skipped.length, - }); - } - - const locallyUpdated = await syncLocalWritebackState( - prepared, - successfulCandidates, - options.createMissingListsAsPrivate ?? false - ); - - return { - plan: prepared.plan, - applied, - failed, - skipped, - createdLists: prepared.plan.createdLists, - locallyUpdated, - }; -} - -async function prepareWriteback( - config: GitHubConfig, - createMissingLists: boolean, - createMissingListsAsPrivate = false, - onProgress?: (progress: WritebackProgress) => void -): Promise { - const skipped: WritebackIssue[] = []; - const createdLists: string[] = []; - const createdListRefs: UserListRef[] = []; - - emitProgress(onProgress, { - stage: "loading_local_classifications", - current: 0, - total: 1, - failed: 0, - skipped: 0, - }); - const [classificationRows, repoRows] = await Promise.all([db.classifications.toArray(), db.repos.toArray()]); - emitProgress(onProgress, { - stage: "loading_local_classifications", - current: 1, - total: 1, - failed: 0, - skipped: skipped.length, - }); - - const repoById = new Map(repoRows.map((repo) => [repo.id, repo])); - const repoTargetTags = new Map(); - const allTargetTags = new Set(); - - for (const row of classificationRows) { - const validTags = normalizeTags(row.tags); - if (validTags.length === 0) { - skipped.push({ - repoId: row.repoId, - reason: "empty_tags", - message: "Classification has no valid tags.", - }); - continue; - } - if (!repoById.has(row.repoId)) { - skipped.push({ - repoId: row.repoId, - reason: "repo_not_found", - message: "Repo is not present in local database.", - }); - continue; - } - repoTargetTags.set(row.repoId, validTags); - for (const tag of validTags) { - allTargetTags.add(tag); - } - } - - emitProgress(onProgress, { - stage: "fetching_star_lists", - current: 0, - total: 1, - failed: 0, - skipped: skipped.length, - }); - const listApi = await detectStarListApi(config); - const remoteLists = await fetchStarLists(config, listApi); - const listNameToId = new Map(remoteLists.map((list) => [list.name, list.id])); - const listIdToName = new Map(remoteLists.map((list) => [list.id, list.name])); - emitProgress(onProgress, { - stage: "fetching_star_lists", - current: 1, - total: 1, - failed: 0, - skipped: skipped.length, - }); - - const missingLists: string[] = []; - for (const tag of allTargetTags) { - if (!listNameToId.has(tag)) { - missingLists.push(tag); - } - } - - if (createMissingLists && missingLists.length > 0) { - let current = 0; - emitProgress(onProgress, { - stage: "creating_missing_lists", - current: 0, - total: missingLists.length, - failed: 0, - skipped: skipped.length, - }); - for (const listName of missingLists) { - try { - const created = await createUserList(config, listName, createMissingListsAsPrivate, ""); - listNameToId.set(created.name, created.id); - listIdToName.set(created.id, created.name); - createdLists.push(created.name); - createdListRefs.push(created); - } catch (error) { - skipped.push({ - reason: "create_list_failed", - message: `Failed to create list "${listName}": ${(error as Error).message || "unknown"}`, - }); - } - current += 1; - emitProgress(onProgress, { - stage: "creating_missing_lists", - current, - total: missingLists.length, - failed: 0, - skipped: skipped.length, - }); - } - } - - const resolvableRepoIds = Array.from(repoTargetTags.keys()); - const repoMetaByRepoId = new Map(); - let repoMetaCurrent = 0; - - emitProgress(onProgress, { - stage: "resolving_repositories", - current: 0, - total: resolvableRepoIds.length || 1, - failed: 0, - skipped: skipped.length, - }); - - const repoMetaTasks = resolvableRepoIds.map((repoId) => async () => { - const repo = repoById.get(repoId); - if (!repo) return; - const parsed = parseRepoFullName(repo.fullName); - if (!parsed) { - skipped.push({ - repoId, - repoFullName: repo.fullName, - reason: "invalid_repo_name", - message: `Invalid fullName format: ${repo.fullName}`, - }); - repoMetaCurrent += 1; - emitProgress(onProgress, { - stage: "resolving_repositories", - current: repoMetaCurrent, - total: resolvableRepoIds.length || 1, - failed: 0, - skipped: skipped.length, - }); - return; - } - - try { - const meta = await getRepositoryMeta(config, parsed.owner, parsed.name); - if (!meta) { - skipped.push({ - repoId, - repoFullName: repo.fullName, - reason: "repo_meta_missing", - message: "Repository metadata cannot be loaded from GitHub.", - }); - } else { - repoMetaByRepoId.set(repoId, { - itemId: meta.id, - viewerHasStarred: meta.viewerHasStarred, - repoFullName: repo.fullName, - }); - } - } catch (error) { - skipped.push({ - repoId, - repoFullName: repo.fullName, - reason: "repo_meta_fetch_failed", - message: `Repository metadata request failed: ${(error as Error).message || "unknown"}`, - }); - } - - repoMetaCurrent += 1; - emitProgress(onProgress, { - stage: "resolving_repositories", - current: repoMetaCurrent, - total: resolvableRepoIds.length || 1, - failed: 0, - skipped: skipped.length, - }); - }); - await runWithConcurrency(repoMetaTasks, REPO_META_CONCURRENCY); - - const targetRepoIds = new Set(); - for (const meta of repoMetaByRepoId.values()) { - targetRepoIds.add(meta.itemId); - } - - emitProgress(onProgress, { - stage: "scanning_existing_memberships", - current: 0, - total: listNameToId.size || 1, - failed: 0, - skipped: skipped.length, - }); - const repoMembershipResult = await buildRepoMembershipIndex( - config, - Array.from(listNameToId.values()), - targetRepoIds, - (progress) => { - emitProgress(onProgress, { - stage: "scanning_existing_memberships", - current: progress.current, - total: progress.total || 1, - failed: 0, - skipped: skipped.length, - }); - } - ); - const repoMembershipIndex = repoMembershipResult.index; - const scanFailures = repoMembershipResult.failedListIds.length; - for (const failedListId of repoMembershipResult.failedListIds) { - skipped.push({ - reason: "membership_scan_failed", - message: `Membership scan failed for list ${listIdToName.get(failedListId) || failedListId}.`, - }); - } - - const candidates: WritebackCandidate[] = []; - let unchangedRepos = 0; - let unstarredActionableRepos = 0; - - for (const [repoId, tags] of repoTargetTags.entries()) { - const meta = repoMetaByRepoId.get(repoId); - if (!meta) continue; - - const targetListIds: string[] = []; - let hasMissingList = false; - for (const tag of tags) { - const listId = listNameToId.get(tag); - if (!listId) { - hasMissingList = true; - break; - } - targetListIds.push(listId); - } - if (hasMissingList) { - skipped.push({ - repoId, - repoFullName: meta.repoFullName, - reason: "missing_list", - message: "One or more target lists do not exist on GitHub.", - }); - continue; - } - - const currentSet = repoMembershipIndex.get(meta.itemId) ?? new Set(); - const finalSet = new Set([...currentSet, ...targetListIds]); - const changed = finalSet.size !== currentSet.size; - - if (!changed) { - unchangedRepos += 1; - skipped.push({ - repoId, - repoFullName: meta.repoFullName, - reason: "no_changes", - message: "Repo already contains all target lists.", - }); - continue; - } - - if (!meta.viewerHasStarred) { - unstarredActionableRepos += 1; - } - - candidates.push({ - repoId, - repoFullName: meta.repoFullName, - itemId: meta.itemId, - viewerHasStarred: meta.viewerHasStarred, - targetTags: tags, - targetListIds: uniqueArray(targetListIds), - targetListNames: uniqueArray( - targetListIds.map((id) => listIdToName.get(id)).filter((name): name is string => Boolean(name)) - ), - currentListIds: Array.from(currentSet), - currentListNames: Array.from(currentSet) - .map((id) => listIdToName.get(id)) - .filter((name): name is string => Boolean(name)), - finalListIds: Array.from(finalSet), - finalListNames: Array.from(finalSet) - .map((id) => listIdToName.get(id)) - .filter((name): name is string => Boolean(name)), - }); - } - - return { - plan: { - totalClassifications: classificationRows.length, - validTaggedRepos: repoTargetTags.size, - actionableRepos: candidates.length, - unchangedRepos, - scanFailures, - missingLists, - createdLists, - unstarredActionableRepos, - skipped, - candidates, - }, - listIdToName, - createdListRefs, - }; -} - -async function syncLocalWritebackState( - prepared: PreparedWriteback, - successfulCandidates: WritebackCandidate[], - createMissingListsAsPrivate: boolean -): Promise { - const updates = successfulCandidates.map((candidate) => ({ - repoId: candidate.repoId, - listIds: candidate.finalListIds, - })); - - await db.transaction("rw", db.lists, db.repoLists, async () => { - if (prepared.createdListRefs.length > 0) { - await db.lists.bulkPut( - prepared.createdListRefs.map((list) => ({ - id: list.id, - name: list.name, - description: "", - isPrivate: createMissingListsAsPrivate, - })) - ); - } - - for (const update of updates) { - await db.repoLists.put(update); - } - }); - - return updates.length; -} - -function normalizeTags(tags: string[]): string[] { - const out: string[] = []; - const seen = new Set(); - - for (const raw of tags) { - const tag = raw.trim(); - if (!tag) continue; - if (tag.toLowerCase() === "unclassified") continue; - const key = tag.toLowerCase(); - if (seen.has(key)) continue; - seen.add(key); - out.push(tag); - } - return out; -} - -function parseRepoFullName(fullName: string): { owner: string; name: string } | null { - const parts = fullName.split("/"); - if (parts.length !== 2) return null; - const owner = parts[0].trim(); - const name = parts[1].trim(); - if (!owner || !name) return null; - return { owner, name }; -} - -function uniqueArray(items: string[]): string[] { - return Array.from(new Set(items)); -} - -function emitProgress( - onProgress: ((progress: WritebackProgress) => void) | undefined, - progress: WritebackProgress -) { - onProgress?.(progress); -} diff --git a/src/app/core/llmClassification.ts b/src/app/core/llmClassification.ts deleted file mode 100644 index c3a0c60..0000000 --- a/src/app/core/llmClassification.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { LLMConfig } from "../services/llmClient"; -import { requestCompletion } from "../services/llmClient"; -import { resolvePrompts } from "./llmPromptResolver"; -import { runWithConcurrency } from "./queues"; - -export type RepoForClassification = { - id: string; - fullName: string; - description: string; - topics: string[]; - language: string | null; - readmeExcerpt: string; - existingLists?: string[]; -}; - -export type ClassificationProgress = { - stage: string; - current: number; - total: number; -}; - -export type ClassificationResult = { - repoTags: Record; - compressedTags: string[]; - tagMap: Record; - runId: string; -}; - -export type ClassificationOptions = { - strictSingleTag?: boolean; - pass1Prompt?: string; - pass1StrictPrompt?: string; - pass2Prompt?: string; - useExistingLists?: boolean; - allowNewTagsWithExistingLists?: boolean; - language?: "zh" | "en"; -}; - -const DEFAULT_PASS2_PROMPT = - "You consolidate tag lists by merging synonyms and near-duplicates. Output only JSON mapping each original tag to a compressed tag. Do not invent new tags beyond the provided list."; - -export async function runTwoStageClassification( - config: LLMConfig, - repos: RepoForClassification[], - options: ClassificationOptions = {}, - onProgress?: (progress: ClassificationProgress) => void -): Promise { - const mode = { - strictSingleTag: options.strictSingleTag ?? false, - useExistingLists: options.useExistingLists ?? false, - allowNewTagsWithExistingLists: options.allowNewTagsWithExistingLists !== false, - language: options.language ?? "en", - } as const; - const prompts = resolvePrompts(config, mode); - const repoTags: Record = {}; - let completed = 0; - const runId = createRunId(); - - const tasks = repos.map((repo) => async () => { - const tags = await classifyRepo(config, repo, { - strictSingleTag: mode.strictSingleTag, - systemPrompt: prompts.selectedPass1, - useExistingLists: mode.useExistingLists, - allowNewTagsWithExistingLists: mode.allowNewTagsWithExistingLists, - }); - completed += 1; - onProgress?.({ stage: "classifying_repos", current: completed, total: repos.length }); - return { id: repo.id, tags }; - }); - - const results = await runWithConcurrency(tasks, 3); - const allTags: string[] = []; - for (const result of results) { - repoTags[result.id] = result.tags; - allTags.push(...result.tags); - } - - const uniqueTags = normalizeTags(allTags); - onProgress?.({ stage: "compressing_tags", current: 0, total: 1 }); - const tagMap = await compressTags(config, uniqueTags, prompts.pass2); - const compressedTags = Array.from(new Set(Object.values(tagMap))); - - for (const repoId of Object.keys(repoTags)) { - repoTags[repoId] = repoTags[repoId].map((tag) => tagMap[tag] ?? tag); - } - - onProgress?.({ stage: "compressing_tags", current: 1, total: 1 }); - return { repoTags, compressedTags, tagMap, runId }; -} - -async function classifyRepo( - config: LLMConfig, - repo: RepoForClassification, - options: { - strictSingleTag: boolean; - systemPrompt: string; - useExistingLists?: boolean; - allowNewTagsWithExistingLists?: boolean; - } -): Promise { - const userContent = [ - `Name: ${repo.fullName}`, - `Description: ${repo.description || ""}`, - `Topics: ${repo.topics.join(", ") || ""}`, - `Language: ${repo.language || ""}`, - `README: ${repo.readmeExcerpt.slice(0, 800)}`, - options.useExistingLists - ? `Existing Lists: ${repo.existingLists && repo.existingLists.length > 0 ? repo.existingLists.join(", ") : "(none)"}` - : "", - ] - .filter(Boolean) - .join("\n"); - - const response = await requestCompletion(config, [ - { role: "system", content: options.systemPrompt }, - { role: "user", content: userContent }, - ]); - - const tags = parseTags(response, options.strictSingleTag); - if (options.useExistingLists && options.allowNewTagsWithExistingLists === false) { - const existing = new Set((repo.existingLists || []).map((tag) => tag.toLowerCase())); - return tags.filter((tag) => existing.has(tag.toLowerCase())); - } - if (tags.length === 0) { - return ["Unclassified"]; - } - return tags; -} - -async function compressTags( - config: LLMConfig, - tags: string[], - pass2Prompt?: string -): Promise> { - if (tags.length === 0) return {}; - if (tags.length === 1) return { [tags[0]]: tags[0] }; - - const userContent = JSON.stringify({ tags }); - try { - const response = await requestCompletion(config, [ - { role: "system", content: pass2Prompt || DEFAULT_PASS2_PROMPT }, - { role: "user", content: userContent }, - ]); - return parseTagMap(response, tags); - } catch { - return Object.fromEntries(tags.map((tag) => [tag, tag])); - } -} - -function parseTags(response: string, strictSingleTag: boolean): string[] { - const trimmed = response.trim(); - if (!trimmed) return []; - - if (trimmed.startsWith("[")) { - try { - const parsed = JSON.parse(trimmed) as string[]; - if (Array.isArray(parsed)) { - const normalized = normalizeTags(parsed); - return strictSingleTag ? normalized.slice(0, 1) : normalized; - } - } catch { - // fall through - } - } - - const parts = trimmed - .split(/[,\n]/) - .map((part) => part.replace(/^[-*\d.\s]+/, "").trim()) - .filter(Boolean); - const normalized = normalizeTags(parts); - return strictSingleTag ? normalized.slice(0, 1) : normalized; -} - -function parseTagMap(response: string, originals: string[]): Record { - const fallback = Object.fromEntries(originals.map((tag) => [tag, tag])); - const raw = extractJson(response); - if (!raw) return fallback; - - try { - const parsed = JSON.parse(raw) as Record | Array<{ from: string; to: string }>; - const map: Record = { ...fallback }; - if (Array.isArray(parsed)) { - for (const entry of parsed) { - if (!entry?.from || !entry?.to) continue; - map[entry.from.trim()] = entry.to.trim(); - } - return map; - } - for (const [key, value] of Object.entries(parsed)) { - if (!key || !value) continue; - map[key.trim()] = value.trim(); - } - return map; - } catch { - return fallback; - } -} - -function extractJson(text: string): string | null { - const start = text.indexOf("{"); - const end = text.lastIndexOf("}"); - if (start === -1 || end === -1 || end <= start) return null; - return text.slice(start, end + 1); -} - -function normalizeTags(tags: string[]): string[] { - const seen = new Set(); - const result: string[] = []; - for (const tag of tags) { - const cleaned = tag.trim(); - if (!cleaned) continue; - const key = cleaned.toLowerCase(); - if (seen.has(key)) continue; - seen.add(key); - result.push(cleaned); - } - return result; -} - -function createRunId(): string { - const stamp = new Date().toISOString().replace(/[:.]/g, "-"); - const random = Math.random().toString(36).slice(2, 8); - return `run-${stamp}-${random}`; -} diff --git a/src/app/core/llmPromptResolver.ts b/src/app/core/llmPromptResolver.ts deleted file mode 100644 index 541ab4f..0000000 --- a/src/app/core/llmPromptResolver.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { LLMConfig } from "../services/llmClient"; - -export type PromptLanguage = "zh" | "en"; - -export type PromptFieldKey = - | "pass1Prompt" - | "pass1StrictPrompt" - | "pass2Prompt" - | "pass1PromptWithExisting" - | "pass1PromptNoNewWithExisting" - | "pass1StrictPromptWithExisting" - | "pass1StrictNoNewWithExisting" - | "pass2PromptWithExisting" - | "pass1PromptZh" - | "pass1StrictPromptZh" - | "pass2PromptZh" - | "pass1PromptWithExistingZh" - | "pass1PromptNoNewWithExistingZh" - | "pass1StrictPromptWithExistingZh" - | "pass1StrictNoNewWithExistingZh" - | "pass2PromptWithExistingZh"; - -type PromptFieldSet = { - pass1: PromptFieldKey; - pass1Strict: PromptFieldKey; - pass2: PromptFieldKey; -}; - -export type PromptMode = { - useExistingLists: boolean; - allowNewTagsWithExistingLists: boolean; - strictSingleTag: boolean; - language: PromptLanguage; -}; - -export type ResolvedPrompts = { - pass1: string; - pass1Strict: string; - pass2: string; - selectedPass1: string; - activeKeys: PromptFieldSet; - inactiveKeys: PromptFieldKey[]; -}; - -const DEFAULT_PASS1_PROMPT = - "You label GitHub repositories with 1-3 concise tags. Output only comma-separated tags."; -const DEFAULT_PASS1_STRICT_PROMPT = - "You label GitHub repositories with exactly 1 concise tag. Output only the tag text."; -const DEFAULT_PASS2_PROMPT = - "You consolidate tag lists by merging synonyms and near-duplicates. Output only JSON mapping each original tag to a compressed tag. Do not invent new tags beyond the provided list."; - -const EN_PROMPT_FIELDS: PromptFieldKey[] = [ - "pass1Prompt", - "pass1StrictPrompt", - "pass2Prompt", - "pass1PromptWithExisting", - "pass1PromptNoNewWithExisting", - "pass1StrictPromptWithExisting", - "pass1StrictNoNewWithExisting", - "pass2PromptWithExisting", -]; - -const ZH_PROMPT_FIELDS: PromptFieldKey[] = [ - "pass1PromptZh", - "pass1StrictPromptZh", - "pass2PromptZh", - "pass1PromptWithExistingZh", - "pass1PromptNoNewWithExistingZh", - "pass1StrictPromptWithExistingZh", - "pass1StrictNoNewWithExistingZh", - "pass2PromptWithExistingZh", -]; - -export function resolvePrompts(config: LLMConfig, mode: PromptMode): ResolvedPrompts { - const activeKeys = resolvePromptKeys(mode); - const pass1 = readPrompt(config, activeKeys.pass1, DEFAULT_PASS1_PROMPT); - const pass1Strict = readPrompt(config, activeKeys.pass1Strict, DEFAULT_PASS1_STRICT_PROMPT); - const pass2 = readPrompt(config, activeKeys.pass2, DEFAULT_PASS2_PROMPT); - const selectedPass1 = mode.strictSingleTag ? pass1Strict : pass1; - const inactiveKeys = getPromptFields(mode.language).filter( - (key) => - key !== activeKeys.pass1 && key !== activeKeys.pass1Strict && key !== activeKeys.pass2 - ); - - return { - pass1, - pass1Strict, - pass2, - selectedPass1, - activeKeys, - inactiveKeys, - }; -} - -function resolvePromptKeys(mode: PromptMode): PromptFieldSet { - if (mode.language === "zh") { - return resolveZhPromptKeys(mode); - } - return resolveEnPromptKeys(mode); -} - -function resolveEnPromptKeys(mode: PromptMode): PromptFieldSet { - if (!mode.useExistingLists) { - return { - pass1: "pass1Prompt", - pass1Strict: "pass1StrictPrompt", - pass2: "pass2Prompt", - }; - } - - if (mode.allowNewTagsWithExistingLists) { - return { - pass1: "pass1PromptWithExisting", - pass1Strict: "pass1StrictPromptWithExisting", - pass2: "pass2PromptWithExisting", - }; - } - - return { - pass1: "pass1PromptNoNewWithExisting", - pass1Strict: "pass1StrictNoNewWithExisting", - pass2: "pass2PromptWithExisting", - }; -} - -function resolveZhPromptKeys(mode: PromptMode): PromptFieldSet { - if (!mode.useExistingLists) { - return { - pass1: "pass1PromptZh", - pass1Strict: "pass1StrictPromptZh", - pass2: "pass2PromptZh", - }; - } - - if (mode.allowNewTagsWithExistingLists) { - return { - pass1: "pass1PromptWithExistingZh", - pass1Strict: "pass1StrictPromptWithExistingZh", - pass2: "pass2PromptWithExistingZh", - }; - } - - return { - pass1: "pass1PromptNoNewWithExistingZh", - pass1Strict: "pass1StrictNoNewWithExistingZh", - pass2: "pass2PromptWithExistingZh", - }; -} - -function getPromptFields(language: PromptLanguage): PromptFieldKey[] { - return language === "zh" ? ZH_PROMPT_FIELDS : EN_PROMPT_FIELDS; -} - -function readPrompt(config: LLMConfig, key: PromptFieldKey, fallback: string): string { - const value = config[key]; - if (typeof value === "string" && value.trim()) { - return value; - } - return fallback; -} diff --git a/src/app/core/purgeListLocal.ts b/src/app/core/purgeListLocal.ts new file mode 100644 index 0000000..9298b72 --- /dev/null +++ b/src/app/core/purgeListLocal.ts @@ -0,0 +1,18 @@ +import { db } from "../data/db"; + +/** Removes a list from `db.lists` and strips its id from every `repoLists` row. */ +export async function purgeListFromLocalStores(listId: string): Promise { + await db.transaction("rw", db.lists, db.repoLists, async () => { + await db.lists.delete(listId); + const rows = await db.repoLists.toArray(); + for (const row of rows) { + if (!row.listIds.includes(listId)) continue; + const next = row.listIds.filter((id) => id !== listId); + if (next.length === 0) { + await db.repoLists.delete(row.repoId); + } else { + await db.repoLists.put({ repoId: row.repoId, listIds: next }); + } + } + }); +} diff --git a/src/app/core/syncRepoListToGitHub.ts b/src/app/core/syncRepoListToGitHub.ts new file mode 100644 index 0000000..175d79d --- /dev/null +++ b/src/app/core/syncRepoListToGitHub.ts @@ -0,0 +1,40 @@ +import { db } from "../data/db"; +import type { GitHubConfig } from "../services/githubClient"; +import { addStar, getRepositoryMeta, updateUserListsForItem } from "../services/githubStarLists"; + +function parseRepoFullName(fullName: string): { owner: string; name: string } | null { + const parts = fullName.split("/"); + if (parts.length !== 2) return null; + const owner = parts[0].trim(); + const name = parts[1].trim(); + if (!owner || !name) return null; + return { owner, name }; +} + +/** + * Pushes the selected Star List IDs for a repo to GitHub (replaces list membership for that starred item). + * Stars the repo first if the viewer has not starred it yet. + */ +export async function syncRepoListAssignmentToGitHub( + config: GitHubConfig, + repoId: string, + listIds: string[] +): Promise { + const repo = await db.repos.get(repoId); + if (!repo) { + throw new Error("Repository not found in local database."); + } + const parsed = parseRepoFullName(repo.fullName); + if (!parsed) { + throw new Error(`Invalid repository name: ${repo.fullName}`); + } + const meta = await getRepositoryMeta(config, parsed.owner, parsed.name); + if (!meta) { + throw new Error("Could not load repository from GitHub."); + } + const uniqueListIds = Array.from(new Set(listIds.map((id) => id.trim()).filter(Boolean))); + if (!meta.viewerHasStarred) { + await addStar(config, meta.id); + } + await updateUserListsForItem(config, meta.id, uniqueListIds); +} diff --git a/src/app/core/types.ts b/src/app/core/types.ts deleted file mode 100644 index a0438fd..0000000 --- a/src/app/core/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type RepoInfo = { - id: string; - fullName: string; - description: string; - url: string; - topics: string[]; - language: string | null; - updatedAt: string; - readmeExcerpt: string; -}; - -export type StarList = { - id: string; - name: string; - description: string; - isPrivate: boolean; -}; - -export type ClassificationResult = { - repoId: string; - listName: string; - confidence: number; -}; diff --git a/src/app/i18n/resources.ts b/src/app/i18n/resources.ts index 49198f6..5f22db2 100644 --- a/src/app/i18n/resources.ts +++ b/src/app/i18n/resources.ts @@ -31,17 +31,13 @@ export const resources = { app: { subtitle: "Organize GitHub Star Lists with confidence.", actions: { - applyUpdates: "Apply Updates", assignList: "Assign List", - configureLlm: "Configure LLM", connectPat: "Connect PAT", + manageLists: "Manage lists", retryFailedLists: "Retry failed lists ({{count}})", - runClassification: "Run Classification", syncStarLists: "Sync Star Lists", }, sections: { - batchActions: "Batch Actions", - llmClassification: "LLM Classification", repositories: "Repositories", starLists: "Star Lists", syncStatus: "Sync Status", @@ -81,18 +77,18 @@ export const resources = { noFilteredTitle: "No repositories match the current filters.", noFilteredHint: "Try clearing filters or sync your Star Lists first.", }, - sidebar: { - llmDesc: "Configure your OpenAI-compatible endpoint to auto-label repos.", - batchDesc: "Apply optimized Star Lists back to GitHub with one click.", - }, values: { noDescription: "No description", }, + nav: { + openStarLists: "Categories", + closeListSidebar: "Close categories", + }, }, onboarding: { title: "README Fetching", description1: - "We can fetch README snippets to improve classification quality. This may increase GitHub API usage and takes longer to sync.", + "We can fetch README snippets during sync. This may increase GitHub API usage and takes longer.", description2: "Would you like to enable README fetching now? You can change this later in Settings.", skip: "Skip for now", @@ -108,14 +104,12 @@ export const resources = { settings: { closeAria: "Close settings", title: "Settings", - description: "Manage tokens, LLM configuration, README fetching, and local cache.", + description: "Manage tokens, README fetching, and local cache.", sections: { language: "Language", localCache: "Local Cache", - llmConfiguration: "LLM Configuration", patManagement: "PAT Management", readmeFetching: "README Fetching", - writeback: "Writeback", }, language: { appLanguage: "App language", @@ -137,122 +131,43 @@ export const resources = { validationFailed: "PAT validation failed ({{name}}): {{message}}", cleared: "PAT cleared.", }, - llm: { - useExistingLists: "Use existing Star Lists as context", - allowNewTags: "Allow new tags when using existing lists", - currentMode: "Current mode", - modeBase: "Base mode: classify without existing Star Lists context.", - modeExisting: - "Existing lists mode: prefer existing lists and allow new tags.", - modeExistingOnly: - "Existing-only mode: classify with existing lists only, no new tags.", - baseUrl: "Base URL", - apiKey: "API Key", - model: "Model", - temperature: "Temperature", - maxTokens: "Max Tokens", - activePass1: "Pass 1 System Prompt (active mode)", - activePass1Strict: "Pass 1 Strict System Prompt (active mode)", - activePass2: "Pass 2 System Prompt (active mode)", - advancedSettings: "Advanced prompt settings ({{count}})", - advancedHelp: - "Edit prompts for non-active modes. These values are kept but hidden from the main view.", - testLlm: "Test LLM", - testing: "Testing...", - resetLlm: "Reset LLM", - missingBaseUrl: "Missing base URL.", - missingApiKey: "Missing API key.", - testOk: "OK in {{elapsed}}ms · {{response}}", - testError: "Error ({{name}}) in {{elapsed}}ms: {{message}}", - cleared: "LLM config cleared.", - promptLabels: { - pass1Prompt: "Pass 1 Base Prompt", - pass1StrictPrompt: "Pass 1 Strict Base Prompt", - pass2Prompt: "Pass 2 Base Prompt", - pass1PromptWithExisting: "Pass 1 Existing Context Prompt", - pass1PromptNoNewWithExisting: "Pass 1 Existing-Only Prompt", - pass1StrictPromptWithExisting: - "Pass 1 Strict Existing Context Prompt", - pass1StrictNoNewWithExisting: "Pass 1 Strict Existing-Only Prompt", - pass2PromptWithExisting: "Pass 2 Existing Context Prompt", - pass1PromptZh: "Pass 1 Base Prompt (ZH)", - pass1StrictPromptZh: "Pass 1 Strict Base Prompt (ZH)", - pass2PromptZh: "Pass 2 Base Prompt (ZH)", - pass1PromptWithExistingZh: "Pass 1 Existing Context Prompt (ZH)", - pass1PromptNoNewWithExistingZh: - "Pass 1 Existing-Only Prompt (ZH)", - pass1StrictPromptWithExistingZh: - "Pass 1 Strict Existing Context Prompt (ZH)", - pass1StrictNoNewWithExistingZh: - "Pass 1 Strict Existing-Only Prompt (ZH)", - pass2PromptWithExistingZh: "Pass 2 Existing Context Prompt (ZH)", - }, - }, readme: { enable: "Enable README snippets during sync", }, - writeback: { - refreshBeforeApply: "Refresh GitHub data before apply", - refreshHelp: - "When enabled, Apply Updates recalculates the writeback plan using latest GitHub state.", - }, cache: { help: "Clear IndexedDB data to reset your local cache.", clearButton: "Clear Local Cache", cleared: "Local cache cleared. Reloading...", }, }, - classification: { - closeAria: "Close classification", - title: "LLM Classification", - description: - "Run a two-stage classification. Stage 2 only compresses tags to save tokens.", - sections: { - diffView: "Diff View", - preview: "Preview", - runStatus: "Run Status", - }, - actions: { - run: "Run Classification", - running: "Running...", - }, - labels: { - compareAgainst: "Compare against", - current: "Current", - existing: "Existing", - previous: "Previous", - selectRun: "Select run", - }, - options: { - existingStarLists: "Existing Star Lists", - previousRun: "Previous run", - selectRunPlaceholder: "Select a run", - reposSuffix: "repos", - testSuffix: "test", - singleSuffix: "single", - }, - toggles: { - strictSingleTag: "Strict single tag (exactly 1 per repo)", - testMode: "Test mode (first 5 repos)", - }, + listManage: { + closeAria: "Close list management", + title: "Manage Star Lists", + intro: + "Create, rename, or delete lists on GitHub. Changes apply to your account and update the local cache.", + createSection: "New list", + editSection: "Edit list", + existingSection: "Your lists", + nameLabel: "Name", + descriptionLabel: "Description", + isPrivate: "Private list", + create: "Create", + save: "Save", + cancelEdit: "Cancel edit", + edit: "Edit", + delete: "Delete", + reposColumn: "Repos", + actionsColumn: "Actions", + deleteConfirm: + "Delete list \"{{name}}\" on GitHub? Starred repositories stay starred; only this list is removed.", + empty: "No lists yet. Create one above, or sync to import existing lists.", + needPat: "Connect a GitHub PAT first.", + nameRequired: "Name is required.", + saveFailed: "Could not save the list.", + deleteFailed: "Could not delete the list.", status: { - noResults: "No classification results yet.", - noSavedRuns: "No saved runs yet.", - noTagDiff: "No tag differences found.", - noDiffFromExisting: "No differences from existing lists.", - }, - errors: { - missingConfig: "LLM config is missing.", - noRepos: "No repos available. Sync your Star Lists first.", - classificationFailed: "Classification failed", - }, - stage: { - idle: "Idle", - preparing: "Preparing", - classifying_repos: "Classifying repos", - compressing_tags: "Compressing tags", - completed: "Completed", - failed: "Failed", + creating: "Creating...", + saving: "Saving...", }, }, assignList: { @@ -267,81 +182,7 @@ export const resources = { saved: "Saved.", saveFailed: "Save failed.", saving: "Saving...", - }, - }, - apply: { - closeAria: "Close apply updates", - title: "Apply Updates", - description: "Write current classification results back to GitHub Star Lists.", - sections: { - applyResult: "Apply Result", - issues: "Issues", - planSummary: "Plan Summary", - plannedRepoChanges: "Planned Repo Changes", - progress: "Progress", - runControls: "Run Controls", - }, - controls: { - refreshBeforeApply: "Refresh GitHub data before apply", - refreshOn: - "Apply will re-plan against latest GitHub state.", - refreshOff: - "Apply will use the current preview plan without re-planning.", - applyToGithub: "Apply to GitHub", - applying: "Applying...", - }, - labels: { - actionable: "Actionable", - after: "After", - applied: "Applied", - createdLists: "Created Lists", - current: "Current", - failed: "Failed", - locallyUpdated: "Local updated", - missingLists: "Missing Lists", - skipped: "Skipped", - unchanged: "Unchanged", - unstarred: "Unstarred", - validTagged: "Valid Tagged", - }, - status: { - noChangesDetected: "No repo list changes detected.", - scanFailures: - "Membership scan partial failures: {{count}}. Preview may be partially incomplete.", - }, - errors: { - missingPat: "GitHub PAT is missing. Please connect PAT first.", - previewNotReady: - "Preview is not ready yet. Please wait for plan generation to finish.", - previewFailed: "Preview failed.", - applyFailed: "Apply failed.", - }, - confirmUnstarred: - "Found {{count}} unstarred repos that require list updates.\n\nClick OK to auto-star and continue.\nClick Cancel to skip those repos and continue.", - stage: { - idle: "Idle", - preparing_preview: "Preparing preview", - preparing_writeback: "Preparing writeback", - loading_local_classifications: "Loading local classifications", - fetching_star_lists: "Fetching Star Lists", - creating_missing_lists: "Creating missing lists", - resolving_repositories: "Resolving repositories", - scanning_existing_memberships: "Scanning existing memberships", - applying_updates: "Applying updates", - }, - issueReasons: { - empty_tags: "Empty tags", - repo_not_found: "Repo not found", - invalid_repo_name: "Invalid repo name", - repo_meta_missing: "Repo metadata missing", - repo_meta_fetch_failed: "Repo metadata fetch failed", - missing_list: "Missing list", - create_list_failed: "Create list failed", - membership_scan_failed: "Membership scan failed", - no_changes: "No changes", - unstarred_requires_confirmation: "Unstarred requires confirmation", - add_star_failed: "Add star failed", - update_failed: "Update failed", + needPat: "Connect a GitHub PAT first to sync Star Lists to GitHub.", }, }, progress: { @@ -358,7 +199,7 @@ export const resources = { "zh-CN": { translation: { common: { - appName: "Star Manager", + appName: "GitHub星标管理面板", actions: { cancel: "取消", clear: "清空", @@ -387,17 +228,13 @@ export const resources = { app: { subtitle: "更安心地管理 GitHub Star Lists。", actions: { - applyUpdates: "应用更新", assignList: "分配列表", - configureLlm: "配置 LLM", connectPat: "连接 PAT", + manageLists: "管理列表", retryFailedLists: "重试失败列表({{count}})", - runClassification: "运行分类", syncStarLists: "同步 Star Lists", }, sections: { - batchActions: "批量操作", - llmClassification: "LLM 分类", repositories: "仓库", starLists: "Star 列表", syncStatus: "同步状态", @@ -437,18 +274,18 @@ export const resources = { noFilteredTitle: "当前筛选条件下没有匹配仓库。", noFilteredHint: "尝试清空筛选,或先同步 Star 列表。", }, - sidebar: { - llmDesc: "配置兼容 OpenAI 的接口,自动为仓库打标签。", - batchDesc: "一键将优化后的 Star Lists 写回 GitHub。", - }, values: { noDescription: "暂无描述", }, + nav: { + openStarLists: "分类", + closeListSidebar: "关闭分类侧栏", + }, }, onboarding: { title: "README 抓取", description1: - "我们可以抓取 README 片段来提升分类质量,但会增加 GitHub API 调用并延长同步时间。", + "同步时可抓取 README 片段,会增加 GitHub API 调用并延长同步时间。", description2: "是否现在启用 README 抓取?你也可以稍后在设置中修改。", skip: "暂不启用", enable: "启用 README 抓取", @@ -463,14 +300,12 @@ export const resources = { settings: { closeAria: "关闭设置", title: "设置", - description: "管理 Token、LLM 配置、README 抓取和本地缓存。", + description: "管理 Token、README 抓取和本地缓存。", sections: { language: "语言", localCache: "本地缓存", - llmConfiguration: "LLM 配置", patManagement: "PAT 管理", readmeFetching: "README 抓取", - writeback: "写回", }, language: { appLanguage: "界面语言", @@ -492,114 +327,41 @@ export const resources = { validationFailed: "PAT 校验失败({{name}}):{{message}}", cleared: "PAT 已清除。", }, - llm: { - useExistingLists: "使用已有 Star Lists 作为上下文", - allowNewTags: "使用已有列表时允许新增标签", - currentMode: "当前模式", - modeBase: "基础模式:不参考已有 Star Lists 进行分类。", - modeExisting: "已有列表模式:优先使用已有列表,并允许新增标签。", - modeExistingOnly: "仅已有列表模式:仅使用已有列表,不新增标签。", - baseUrl: "Base URL", - apiKey: "API Key", - model: "模型", - temperature: "温度", - maxTokens: "最大 Tokens", - activePass1: "Pass 1 系统 Prompt(当前模式)", - activePass1Strict: "Pass 1 严格系统 Prompt(当前模式)", - activePass2: "Pass 2 系统 Prompt(当前模式)", - advancedSettings: "高级 Prompt 设置({{count}})", - advancedHelp: "可编辑非当前模式的 Prompt,这些值会保留但不在主视图展示。", - testLlm: "测试 LLM", - testing: "测试中...", - resetLlm: "重置 LLM", - missingBaseUrl: "缺少 Base URL。", - missingApiKey: "缺少 API Key。", - testOk: "{{elapsed}}ms 内成功 · {{response}}", - testError: "错误({{name}}),耗时 {{elapsed}}ms:{{message}}", - cleared: "LLM 配置已清除。", - promptLabels: { - pass1Prompt: "Pass 1 基础 Prompt", - pass1StrictPrompt: "Pass 1 严格基础 Prompt", - pass2Prompt: "Pass 2 基础 Prompt", - pass1PromptWithExisting: "Pass 1 含已有上下文 Prompt", - pass1PromptNoNewWithExisting: "Pass 1 仅已有列表 Prompt", - pass1StrictPromptWithExisting: "Pass 1 严格含已有上下文 Prompt", - pass1StrictNoNewWithExisting: "Pass 1 严格仅已有列表 Prompt", - pass2PromptWithExisting: "Pass 2 含已有上下文 Prompt", - pass1PromptZh: "Pass 1 基础 Prompt(中文)", - pass1StrictPromptZh: "Pass 1 严格基础 Prompt(中文)", - pass2PromptZh: "Pass 2 基础 Prompt(中文)", - pass1PromptWithExistingZh: "Pass 1 含已有上下文 Prompt(中文)", - pass1PromptNoNewWithExistingZh: "Pass 1 仅已有列表 Prompt(中文)", - pass1StrictPromptWithExistingZh: - "Pass 1 严格含已有上下文 Prompt(中文)", - pass1StrictNoNewWithExistingZh: "Pass 1 严格仅已有列表 Prompt(中文)", - pass2PromptWithExistingZh: "Pass 2 含已有上下文 Prompt(中文)", - }, - }, readme: { enable: "同步时启用 README 片段", }, - writeback: { - refreshBeforeApply: "应用前刷新 GitHub 数据", - refreshHelp: "启用后,Apply Updates 会基于最新 GitHub 状态重新生成写回计划。", - }, cache: { help: "清空 IndexedDB 数据以重置本地缓存。", clearButton: "清空本地缓存", cleared: "本地缓存已清空,正在刷新...", }, }, - classification: { - closeAria: "关闭分类", - title: "LLM 分类", - description: "执行两阶段分类。第 2 阶段仅压缩标签以节省 token。", - sections: { - diffView: "差异视图", - preview: "预览", - runStatus: "运行状态", - }, - actions: { - run: "运行分类", - running: "运行中...", - }, - labels: { - compareAgainst: "对比基准", - current: "当前", - existing: "已有", - previous: "上次", - selectRun: "选择运行记录", - }, - options: { - existingStarLists: "已有 Star Lists", - previousRun: "历史运行", - selectRunPlaceholder: "选择一条运行记录", - reposSuffix: "个仓库", - testSuffix: "测试", - singleSuffix: "单标签", - }, - toggles: { - strictSingleTag: "严格单标签(每仓库仅 1 个)", - testMode: "测试模式(仅前 5 个仓库)", - }, + listManage: { + closeAria: "关闭列表管理", + title: "管理 Star 列表", + intro: "在 GitHub 上新建、重命名或删除列表。更改会应用到你的账号并更新本地缓存。", + createSection: "新建列表", + editSection: "编辑列表", + existingSection: "已有列表", + nameLabel: "名称", + descriptionLabel: "描述", + isPrivate: "私有列表", + create: "创建", + save: "保存", + cancelEdit: "取消编辑", + edit: "编辑", + delete: "删除", + reposColumn: "仓库数", + actionsColumn: "操作", + deleteConfirm: "确认在 GitHub 上删除列表「{{name}}」?不会取消收藏仓库,仅删除该列表。", + empty: "暂无列表。可在上方新建,或先同步导入已有列表。", + needPat: "请先连接 GitHub PAT。", + nameRequired: "名称不能为空。", + saveFailed: "保存列表失败。", + deleteFailed: "删除列表失败。", status: { - noResults: "暂无分类结果。", - noSavedRuns: "暂无保存的运行记录。", - noTagDiff: "未发现标签差异。", - noDiffFromExisting: "与已有列表无差异。", - }, - errors: { - missingConfig: "LLM 配置不完整。", - noRepos: "没有可分类仓库,请先同步 Star Lists。", - classificationFailed: "分类失败", - }, - stage: { - idle: "空闲", - preparing: "准备中", - classifying_repos: "正在分类仓库", - compressing_tags: "正在压缩标签", - completed: "已完成", - failed: "失败", + creating: "创建中...", + saving: "保存中...", }, }, assignList: { @@ -614,77 +376,7 @@ export const resources = { saved: "已保存。", saveFailed: "保存失败。", saving: "保存中...", - }, - }, - apply: { - closeAria: "关闭应用更新", - title: "应用更新", - description: "将当前分类结果写回到 GitHub Star Lists。", - sections: { - applyResult: "应用结果", - issues: "问题", - planSummary: "计划摘要", - plannedRepoChanges: "计划中的仓库变更", - progress: "进度", - runControls: "运行控制", - }, - controls: { - refreshBeforeApply: "应用前刷新 GitHub 数据", - refreshOn: "应用时会基于最新 GitHub 状态重新规划。", - refreshOff: "应用时会直接使用当前预览计划,不再重新规划。", - applyToGithub: "应用到 GitHub", - applying: "应用中...", - }, - labels: { - actionable: "可执行", - after: "变更后", - applied: "已应用", - createdLists: "新建列表", - current: "当前", - failed: "失败", - locallyUpdated: "本地已更新", - missingLists: "缺失列表", - skipped: "跳过", - unchanged: "未变化", - unstarred: "未收藏", - validTagged: "有效标签仓库", - }, - status: { - noChangesDetected: "未检测到仓库列表变更。", - scanFailures: "列表成员扫描部分失败:{{count}}。预览可能不完整。", - }, - errors: { - missingPat: "缺少 GitHub PAT,请先连接 PAT。", - previewNotReady: "预览尚未就绪,请等待计划生成完成。", - previewFailed: "预览失败。", - applyFailed: "应用失败。", - }, - confirmUnstarred: - "发现 {{count}} 个未收藏仓库需要更新列表。\n\n点击“确定”将自动收藏并继续。\n点击“取消”将跳过这些仓库并继续。", - stage: { - idle: "空闲", - preparing_preview: "正在准备预览", - preparing_writeback: "正在准备写回", - loading_local_classifications: "正在加载本地分类", - fetching_star_lists: "正在拉取 Star 列表", - creating_missing_lists: "正在创建缺失列表", - resolving_repositories: "正在解析仓库", - scanning_existing_memberships: "正在扫描已有成员关系", - applying_updates: "正在应用更新", - }, - issueReasons: { - empty_tags: "标签为空", - repo_not_found: "仓库不存在", - invalid_repo_name: "仓库名格式无效", - repo_meta_missing: "仓库元数据缺失", - repo_meta_fetch_failed: "获取仓库元数据失败", - missing_list: "目标列表缺失", - create_list_failed: "创建列表失败", - membership_scan_failed: "成员扫描失败", - no_changes: "无变更", - unstarred_requires_confirmation: "未收藏需确认", - add_star_failed: "自动收藏失败", - update_failed: "更新失败", + needPat: "请先连接 GitHub PAT,才能将 Star 列表同步到 GitHub。", }, }, progress: { diff --git a/src/app/services/githubStarLists.ts b/src/app/services/githubStarLists.ts index 600c1ae..f364a71 100644 --- a/src/app/services/githubStarLists.ts +++ b/src/app/services/githubStarLists.ts @@ -95,6 +95,20 @@ type CreateUserListResponse = { } | null; }; +type UpdateUserListResponse = { + updateUserList: { + list: { + id: string; + name: string; + description?: string | null; + } | null; + } | null; +}; + +type DeleteUserListResponse = { + deleteUserList: { clientMutationId: string | null } | null; +}; + type RepositoryMetaResponse = { repository: RepositoryMeta | null; }; @@ -334,6 +348,50 @@ export async function updateUserListsForItem( await ghGraphql(config, query, { input: { itemId, listIds } }); } +export async function updateUserListOnGitHub( + config: GitHubConfig, + listId: string, + patch: { name?: string; description?: string; isPrivate?: boolean } +): Promise<{ id: string; name: string; description: string }> { + const input: Record = { listId }; + if (patch.name !== undefined) input.name = patch.name; + if (patch.description !== undefined) input.description = patch.description; + if (patch.isPrivate !== undefined) input.isPrivate = patch.isPrivate; + + const query = ` + mutation($input: UpdateUserListInput!) { + updateUserList(input: $input) { + list { + id + name + description + } + } + } + `; + const data = await ghGraphql(config, query, { input }); + const list = data.updateUserList?.list; + if (!list) { + throw new Error("Failed to update list"); + } + return { + id: list.id, + name: list.name, + description: list.description ?? "", + }; +} + +export async function deleteUserListOnGitHub(config: GitHubConfig, listId: string): Promise { + const query = ` + mutation($input: DeleteUserListInput!) { + deleteUserList(input: $input) { + clientMutationId + } + } + `; + await ghGraphql(config, query, { input: { listId } }); +} + export async function buildRepoMembershipIndex( config: GitHubConfig, listIds: string[], diff --git a/src/app/services/llmClient.ts b/src/app/services/llmClient.ts deleted file mode 100644 index 6d05ef4..0000000 --- a/src/app/services/llmClient.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type LLMConfig = { - baseUrl: string; - apiKey: string; - model: string; - temperature: number; - maxTokens: number; - pass1Prompt?: string; - pass1StrictPrompt?: string; - pass2Prompt?: string; - pass1PromptZh?: string; - pass1StrictPromptZh?: string; - pass2PromptZh?: string; - pass1PromptWithExisting?: string; - pass1PromptNoNewWithExisting?: string; - pass1StrictPromptWithExisting?: string; - pass1StrictNoNewWithExisting?: string; - pass1PromptWithExistingZh?: string; - pass1PromptNoNewWithExistingZh?: string; - pass1StrictPromptWithExistingZh?: string; - pass1StrictNoNewWithExistingZh?: string; - pass2PromptWithExisting?: string; - pass2PromptWithExistingZh?: string; -}; - -export type LLMMessage = { - role: "system" | "user" | "assistant"; - content: string; -}; - -export async function requestCompletion( - config: LLMConfig, - messages: LLMMessage[], - signal?: AbortSignal -): Promise { - const response = await fetch(`${config.baseUrl}/chat/completions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.apiKey}`, - }, - body: JSON.stringify({ - model: config.model, - temperature: config.temperature, - max_tokens: config.maxTokens, - messages, - }), - signal, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(text || `LLM request failed: ${response.status}`); - } - - const data = (await response.json()) as { - choices?: { message?: { content?: string } }[]; - }; - - const content = data.choices?.[0]?.message?.content?.trim(); - if (!content) { - throw new Error("LLM response missing content"); - } - - return content; -} diff --git a/src/app/store/llmConfig.ts b/src/app/store/llmConfig.ts deleted file mode 100644 index b2c0b7f..0000000 --- a/src/app/store/llmConfig.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useCallback, useSyncExternalStore } from "react"; -import type { LLMConfig } from "../services/llmClient"; - -type LlmConfigState = { - config: LLMConfig; -}; - -const STORAGE_KEY = "star-manager.llm-config"; - -const defaultConfig: LLMConfig = { - baseUrl: "https://api.openai.com/v1", - apiKey: "", - model: "gpt-4o-mini", - temperature: 0.3, - maxTokens: 120, - pass1Prompt: "You label GitHub repositories with 1-3 concise tags. Output only comma-separated tags.", - pass1StrictPrompt: "You label GitHub repositories with exactly 1 concise tag. Output only the tag text.", - pass2Prompt: - "You consolidate tag lists by merging synonyms and near-duplicates. Output only JSON mapping each original tag to a compressed tag. Do not invent new tags beyond the provided list.", - pass1PromptZh: - "你将 GitHub 仓库标注为 1-3 个简洁标签,仅输出逗号分隔的标签。", - pass1StrictPromptZh: "你将 GitHub 仓库标注为且仅标注 1 个简洁标签,仅输出标签文本。", - pass2PromptZh: - "你需要合并标签中的同义词或近似词,输出仅包含原始标签到压缩标签的 JSON 映射。不要创造列表外的新标签。", - pass1PromptWithExisting: - "You label GitHub repositories with 1-3 concise tags. Prefer existing Star Lists when they fit, but you may add a new tag if needed. Output only comma-separated tags.", - pass1PromptNoNewWithExisting: - "You label GitHub repositories with 1-3 concise tags. You must select from the existing Star Lists only. If none fit, output nothing. Output only comma-separated tags.", - pass1StrictPromptWithExisting: - "You label GitHub repositories with exactly 1 concise tag. Prefer existing Star Lists when they fit. Output only the tag text.", - pass1StrictNoNewWithExisting: - "You must select exactly 1 tag from the existing Star Lists. If none fit, output nothing. Output only the tag text.", - pass1PromptWithExistingZh: - "你将 GitHub 仓库标注为 1-3 个简洁标签,优先使用已有 Star Lists,如有必要可新增标签,仅输出逗号分隔的标签。", - pass1PromptNoNewWithExistingZh: - "你将 GitHub 仓库标注为 1-3 个简洁标签,只能从已有 Star Lists 中选择。如果没有合适标签,请输出空内容,仅输出逗号分隔的标签。", - pass1StrictPromptWithExistingZh: - "你将 GitHub 仓库标注为且仅标注 1 个简洁标签,优先使用已有 Star Lists,仅输出标签文本。", - pass1StrictNoNewWithExistingZh: - "你必须从已有 Star Lists 中选择且仅选择 1 个标签。如果没有合适的标签,请输出空内容,仅输出标签文本。", - pass2PromptWithExisting: - "You consolidate tag lists by merging synonyms and near-duplicates. Output only JSON mapping each original tag to a compressed tag. Do not invent new tags beyond the provided list.", - pass2PromptWithExistingZh: - "你需要合并标签中的同义词或近似词,输出仅包含原始标签到压缩标签的 JSON 映射。不要创造列表外的新标签。", -}; - -let cached = readFromStorage(); -const listeners = new Set<() => void>(); - -function emitChange() { - for (const listener of listeners) { - listener(); - } -} - -function readFromStorage(): LlmConfigState { - if (typeof window === "undefined") { - return { config: { ...defaultConfig } }; - } - try { - const raw = window.localStorage.getItem(STORAGE_KEY); - if (!raw) return { config: { ...defaultConfig } }; - const parsed = JSON.parse(raw) as Partial; - return { - config: { - ...defaultConfig, - ...parsed, - }, - }; - } catch { - return { config: { ...defaultConfig } }; - } -} - -function persist(next: LlmConfigState) { - cached = next; - if (typeof window !== "undefined") { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next.config)); - } - emitChange(); -} - -export function useLlmConfigStore() { - const state = useSyncExternalStore( - (onStoreChange) => { - listeners.add(onStoreChange); - return () => listeners.delete(onStoreChange); - }, - () => cached, - () => cached - ); - - const setConfig = useCallback((config: LLMConfig) => { - persist({ config }); - }, []); - - const updateConfig = useCallback((partial: Partial) => { - persist({ config: { ...cached.config, ...partial } }); - }, []); - - const clearConfig = useCallback(() => { - persist({ config: { ...defaultConfig, apiKey: "" } }); - }, []); - - return { - config: state.config, - setConfig, - updateConfig, - clearConfig, - }; -} diff --git a/src/app/store/preferences.ts b/src/app/store/preferences.ts index 722f976..428186b 100644 --- a/src/app/store/preferences.ts +++ b/src/app/store/preferences.ts @@ -7,9 +7,6 @@ type Preferences = { patToken: string; viewerLogin: string; lastSyncedAt: string; - useExistingListsForClassification: boolean; - allowNewTagsWithExistingLists: boolean; - refreshBeforeApply: boolean; uiLanguage: UiLanguagePreference; }; @@ -21,9 +18,6 @@ const defaultPreferences: Preferences = { patToken: "", viewerLogin: "", lastSyncedAt: "", - useExistingListsForClassification: false, - allowNewTagsWithExistingLists: true, - refreshBeforeApply: true, uiLanguage: "auto", }; @@ -87,18 +81,6 @@ export function usePreferenceStore() { persist({ ...cached, lastSyncedAt: timestamp }); }, []); - const setUseExistingListsForClassification = useCallback((value: boolean) => { - persist({ ...cached, useExistingListsForClassification: value }); - }, []); - - const setAllowNewTagsWithExistingLists = useCallback((value: boolean) => { - persist({ ...cached, allowNewTagsWithExistingLists: value }); - }, []); - - const setRefreshBeforeApply = useCallback((value: boolean) => { - persist({ ...cached, refreshBeforeApply: value }); - }, []); - const setUiLanguage = useCallback((value: UiLanguagePreference) => { persist({ ...cached, uiLanguage: value }); }, []); @@ -109,9 +91,6 @@ export function usePreferenceStore() { markOnboarded, setPatToken, setLastSyncedAt, - setUseExistingListsForClassification, - setAllowNewTagsWithExistingLists, - setRefreshBeforeApply, setUiLanguage, }; } diff --git a/src/app/styles/app.css b/src/app/styles/app.css index ede6864..cbdde1e 100644 --- a/src/app/styles/app.css +++ b/src/app/styles/app.css @@ -1,10 +1,22 @@ :root { color-scheme: light; - font-family: "Space Grotesk", system-ui, -apple-system, sans-serif; - line-height: 1.4; + --font-body: "Source Serif 4", Georgia, "Times New Roman", serif; + --font-ui: "Source Sans 3", system-ui, -apple-system, sans-serif; + --radius-sm: 3px; + --radius-md: 4px; + --radius-lg: 6px; + --doc-bg: #f8fafc; + --doc-surface: #ffffff; + --doc-border: #e2e8f0; + --doc-text: #1e293b; + --doc-muted: #64748b; + --doc-accent: #2563eb; + --doc-accent-soft: rgba(37, 99, 235, 0.08); + font-family: var(--font-body); + line-height: 1.55; font-weight: 400; - background-color: #f5f1e8; - color: #1e1b16; + background-color: var(--doc-bg); + color: var(--doc-text); } * { @@ -14,8 +26,8 @@ body { margin: 0; min-height: 100vh; - background: radial-gradient(circle at 20% 10%, #f8efe0 0%, #f5f1e8 45%, #efe7db 100%); - font-family: "Space Grotesk", system-ui, -apple-system, sans-serif; + background: var(--doc-bg); + font-family: var(--font-body); } #root { @@ -32,114 +44,183 @@ body { display: flex; align-items: center; justify-content: space-between; - padding: 28px 40px; - border-bottom: 1px solid rgba(30, 27, 22, 0.08); + padding: 20px 40px; + border-bottom: 1px solid var(--doc-border); + background: var(--doc-surface); + font-family: var(--font-ui); } .brand { display: flex; align-items: center; - gap: 16px; + gap: 14px; } .brand-icon { - width: 44px; - height: 44px; - border-radius: 12px; - background: #f0b429; + width: 40px; + height: 40px; + border-radius: var(--radius-lg); + background: var(--doc-accent-soft); + border: 1px solid var(--doc-border); display: grid; place-items: center; font-weight: 600; - color: #1e1b16; - box-shadow: 0 12px 24px rgba(240, 180, 41, 0.2); + font-family: var(--font-ui); + font-size: 15px; + color: var(--doc-accent); } .brand-title { - font-size: 22px; - letter-spacing: 0.4px; + font-size: 1.125rem; + font-weight: 600; + font-family: var(--font-ui); + letter-spacing: -0.01em; margin: 0; } .brand-subtitle { - margin: 4px 0 0; - font-size: 13px; - color: rgba(30, 27, 22, 0.6); + margin: 2px 0 0; + font-size: 0.8125rem; + font-family: var(--font-body); + color: var(--doc-muted); } .header-actions { display: flex; + flex-wrap: wrap; align-items: center; + justify-content: flex-end; gap: 12px; } .button { - border: 1px solid rgba(30, 27, 22, 0.2); - background: #ffffff; - padding: 10px 16px; - border-radius: 999px; - font-size: 13px; + border: 1px solid var(--doc-border); + background: var(--doc-surface); + padding: 8px 14px; + border-radius: var(--radius-md); + font-size: 0.8125rem; + font-family: var(--font-ui); + font-weight: 500; cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.button:hover { + background: #f1f5f9; } .button.primary { - background: #1e1b16; - color: #f5f1e8; - border-color: transparent; - box-shadow: 0 8px 20px rgba(30, 27, 22, 0.25); + background: var(--doc-accent); + color: #ffffff; + border-color: var(--doc-accent); +} + +.button.primary:hover { + background: #1d4ed8; + border-color: #1d4ed8; } .button:active { - transform: scale(0.98); + transform: none; } .app-main { display: grid; - grid-template-columns: 280px 1fr 320px; - gap: 24px; - padding: 32px 40px 48px; + grid-template-columns: 280px 1fr; + gap: 0; + padding: 0; flex: 1; + border-top: 1px solid var(--doc-border); +} + +.app-main > .panel:first-of-type { + border-right: 1px solid var(--doc-border); +} + +@media (min-width: 1101px) { + .list-sidebar-backdrop { + display: none !important; + } + + .list-sidebar-close-btn { + display: none !important; + } + + .repo-panel-toolbar { + display: none !important; + } } .panel { - background: rgba(255, 255, 255, 0.8); - border-radius: 24px; - padding: 20px; - box-shadow: 0 12px 40px rgba(30, 27, 22, 0.08); - backdrop-filter: blur(12px); + background: var(--doc-surface); + border-radius: 0; + padding: 24px 28px 40px; + box-shadow: none; + backdrop-filter: none; +} + +.panel-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.panel-heading h2 { + margin: 0; } .panel h2 { - font-size: 15px; - margin: 0 0 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.6875rem; + font-family: var(--font-ui); + font-weight: 600; + margin: 0 0 16px; + color: var(--doc-muted); text-transform: uppercase; - letter-spacing: 1.2px; + letter-spacing: 0.06em; +} + +.panel-heading h2 { + margin-bottom: 0; } .list-item { display: flex; align-items: center; justify-content: space-between; - padding: 10px 12px; - border-radius: 12px; - margin-bottom: 8px; - background: #fffdf8; - border: 1px solid rgba(30, 27, 22, 0.08); - font-size: 14px; + padding: 8px 10px; + border-radius: var(--radius-md); + margin-bottom: 4px; + background: var(--doc-bg); + border: 1px solid transparent; + font-size: 0.875rem; + font-family: var(--font-ui); } .list-item.active { - border-color: #f0b429; - box-shadow: 0 6px 16px rgba(240, 180, 41, 0.2); + border-color: var(--doc-accent); + background: var(--doc-accent-soft); + color: var(--doc-text); +} + +.list-panel-hint { + margin: 0 0 12px; } .repo-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } +@media (min-width: 1101px) { + .repo-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } +} + .filters { display: flex; flex-wrap: wrap; @@ -155,16 +236,18 @@ body { } .filter-group label { - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); } .filter-group select { - padding: 8px 10px; - border-radius: 10px; - border: 1px solid rgba(30, 27, 22, 0.2); - background: #fffdf8; - font-size: 12px; + padding: 6px 8px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + background: var(--doc-surface); + font-size: 0.75rem; + font-family: var(--font-ui); } .filter-group.search { @@ -172,11 +255,12 @@ body { } .filter-group.search input { - padding: 8px 10px; - border-radius: 10px; - border: 1px solid rgba(30, 27, 22, 0.2); - background: #fffdf8; - font-size: 12px; + padding: 6px 8px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + background: var(--doc-surface); + font-size: 0.75rem; + font-family: var(--font-ui); min-width: 220px; } @@ -184,83 +268,139 @@ body { display: inline-flex; align-items: center; gap: 6px; - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); } .repo-card { - background: #fffdf8; - border-radius: 16px; - padding: 16px; - border: 1px solid rgba(30, 27, 22, 0.08); + background: var(--doc-surface); + border-radius: var(--radius-lg); + padding: 14px 16px; + border: 1px solid var(--doc-border); display: flex; flex-direction: column; - gap: 10px; + gap: 8px; min-height: 180px; + min-width: 0; + max-width: 100%; + overflow: hidden; } .repo-meta { display: flex; justify-content: space-between; - font-size: 12px; - color: rgba(30, 27, 22, 0.65); + align-items: baseline; + gap: 8px; + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); + min-width: 0; +} + +.repo-meta span { + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; } .repo-title { font-weight: 600; - font-size: 15px; + font-size: 0.9375rem; + font-family: var(--font-ui); margin: 0; + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; } .repo-desc { - font-size: 13px; - color: rgba(30, 27, 22, 0.65); + font-size: 0.8125rem; + color: var(--doc-muted); margin: 0; + flex: 1; + min-width: 0; + overflow-wrap: break-word; + word-break: break-word; +} + +.repo-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} + +.repo-card .button { + max-width: 100%; + box-sizing: border-box; } .tag { display: inline-flex; align-items: center; - padding: 4px 10px; - border-radius: 999px; - font-size: 12px; - background: rgba(240, 180, 41, 0.18); - color: #1e1b16; - margin-right: 6px; + padding: 2px 8px; + border-radius: var(--radius-sm); + font-size: 0.6875rem; + font-family: var(--font-ui); + font-weight: 500; + background: var(--doc-accent-soft); + color: var(--doc-accent); + border: 1px solid rgba(37, 99, 235, 0.2); + margin-right: 0; + max-width: 100%; + overflow-wrap: anywhere; } -.sidebar-meta { +.list-manage-table { + border: 1px solid var(--doc-border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.list-manage-head, +.list-manage-row { + display: grid; + grid-template-columns: 1fr 72px minmax(140px, auto); + gap: 8px; + align-items: center; + padding: 8px 10px; + font-size: 0.8125rem; + font-family: var(--font-ui); +} + +.list-manage-head { + font-weight: 600; + color: var(--doc-muted); + background: var(--doc-bg); + border-bottom: 1px solid var(--doc-border); +} + +.list-manage-row { + border-bottom: 1px solid var(--doc-border); +} + +.list-manage-row:last-child { + border-bottom: none; +} + +.list-manage-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-manage-actions { display: flex; - flex-direction: column; - gap: 12px; -} - -.meta-card { - background: #fffdf8; - border-radius: 16px; - padding: 16px; - border: 1px solid rgba(30, 27, 22, 0.08); -} - -.meta-card .button { - margin-top: 12px; -} - -.meta-title { - margin: 0 0 8px; - font-size: 14px; -} - -.meta-desc { - margin: 0; - font-size: 12px; - color: rgba(30, 27, 22, 0.6); + flex-wrap: wrap; + gap: 6px; + justify-content: flex-end; } .drawer { position: fixed; inset: 0; - background: rgba(30, 27, 22, 0.4); + background: rgba(15, 23, 42, 0.4); display: grid; place-items: center; z-index: 40; @@ -268,11 +408,13 @@ body { .drawer-panel { width: min(560px, 92vw); - background: #fffdf8; - border-radius: 20px; + background: var(--doc-surface); + border-radius: var(--radius-lg); padding: 24px; - box-shadow: 0 18px 50px rgba(30, 27, 22, 0.2); + border: 1px solid var(--doc-border); + box-shadow: 0 16px 48px rgba(15, 23, 42, 0.12); position: relative; + font-family: var(--font-body); } .drawer-panel.settings { @@ -288,17 +430,24 @@ body { overflow-y: auto; padding-right: 8px; padding-bottom: 16px; - border-radius: 16px; + border-radius: var(--radius-md); } .drawer-panel h3 { margin: 0 0 8px; + font-family: var(--font-ui); + font-weight: 600; + font-size: 1.0625rem; +} + +.drawer-panel h4 { + font-family: var(--font-ui); } .drawer-panel p { margin: 0 0 16px; - font-size: 14px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.875rem; + color: var(--doc-muted); } .drawer-panel p + p { @@ -308,12 +457,12 @@ body { .settings-section { margin-top: 20px; padding-top: 14px; - border-top: 1px solid rgba(30, 27, 22, 0.08); + border-top: 1px solid var(--doc-border); } .settings-section h4 { margin: 0 0 10px; - font-size: 14px; + font-size: 0.875rem; } .settings-actions { @@ -338,44 +487,47 @@ body { } .helper-text { - font-size: 12px; - color: rgba(30, 27, 22, 0.6); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); margin-top: 8px; } .prompt-mode-card { - border: 1px solid rgba(240, 180, 41, 0.35); - background: rgba(240, 180, 41, 0.12); - border-radius: 12px; + border: 1px solid var(--doc-border); + background: var(--doc-bg); + border-radius: var(--radius-lg); padding: 10px 12px; margin: 10px 0 12px; } .prompt-mode-title { margin: 0; - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); font-weight: 600; } .prompt-mode-value { margin: 4px 0 0; - font-size: 12px; - color: rgba(30, 27, 22, 0.72); + font-size: 0.75rem; + color: var(--doc-text); } .advanced-prompts { margin-top: 10px; padding: 10px 12px; - border: 1px dashed rgba(30, 27, 22, 0.2); - border-radius: 12px; - background: rgba(255, 255, 255, 0.6); + border: 1px dashed var(--doc-border); + border-radius: var(--radius-lg); + background: var(--doc-bg); } .advanced-prompts summary { cursor: pointer; - font-size: 12px; - color: rgba(30, 27, 22, 0.75); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-text); font-weight: 600; list-style: none; } @@ -391,9 +543,9 @@ body { } .writeback-summary-item { - border: 1px solid rgba(30, 27, 22, 0.12); - border-radius: 10px; - background: #fffdf8; + border: 1px solid var(--doc-border); + border-radius: var(--radius-md); + background: var(--doc-bg); padding: 10px; display: flex; flex-direction: column; @@ -401,16 +553,18 @@ body { } .writeback-summary-label { - font-size: 11px; - color: rgba(30, 27, 22, 0.62); + font-size: 0.6875rem; + font-family: var(--font-ui); + color: var(--doc-muted); text-transform: uppercase; - letter-spacing: 0.4px; + letter-spacing: 0.04em; } .writeback-summary-value { - font-size: 16px; + font-size: 1rem; + font-family: var(--font-ui); font-weight: 600; - color: #1e1b16; + color: var(--doc-text); } .writeback-issues { @@ -422,9 +576,9 @@ body { } .writeback-issue { - border: 1px solid rgba(30, 27, 22, 0.12); - border-radius: 10px; - background: #fffdf8; + border: 1px solid var(--doc-border); + border-radius: var(--radius-md); + background: var(--doc-bg); padding: 8px 10px; display: flex; flex-direction: column; @@ -432,15 +586,16 @@ body { } .writeback-issue-reason { - font-size: 11px; - color: rgba(30, 27, 22, 0.6); + font-size: 0.6875rem; + font-family: var(--font-ui); + color: var(--doc-muted); text-transform: uppercase; - letter-spacing: 0.4px; + letter-spacing: 0.04em; } .writeback-issue-msg { - font-size: 12px; - color: rgba(30, 27, 22, 0.76); + font-size: 0.75rem; + color: var(--doc-text); } .assign-list-grid { @@ -454,12 +609,13 @@ body { display: flex; align-items: center; gap: 8px; - border: 1px solid rgba(30, 27, 22, 0.12); - border-radius: 10px; - background: #fffdf8; + border: 1px solid var(--doc-border); + border-radius: var(--radius-md); + background: var(--doc-bg); padding: 8px 10px; - font-size: 13px; - color: rgba(30, 27, 22, 0.78); + font-size: 0.8125rem; + font-family: var(--font-ui); + color: var(--doc-text); } .writeback-preview-list { @@ -471,14 +627,15 @@ body { } .writeback-preview-item { - border: 1px solid rgba(30, 27, 22, 0.12); - border-radius: 10px; - background: #fffdf8; + border: 1px solid var(--doc-border); + border-radius: var(--radius-md); + background: var(--doc-bg); padding: 10px; } .writeback-preview-title { - font-size: 13px; + font-size: 0.8125rem; + font-family: var(--font-ui); font-weight: 600; margin-bottom: 6px; } @@ -486,8 +643,8 @@ body { .writeback-preview-row { display: flex; gap: 8px; - font-size: 12px; - color: rgba(30, 27, 22, 0.72); + font-size: 0.75rem; + color: var(--doc-muted); margin-top: 4px; } @@ -503,14 +660,15 @@ body { } .preview-card { - border: 1px solid rgba(30, 27, 22, 0.1); - border-radius: 12px; + border: 1px solid var(--doc-border); + border-radius: var(--radius-lg); padding: 12px; - background: #fffdf8; + background: var(--doc-bg); } .preview-title { - font-size: 13px; + font-size: 0.8125rem; + font-family: var(--font-ui); font-weight: 600; margin-bottom: 8px; } @@ -519,8 +677,8 @@ body { display: flex; flex-wrap: wrap; gap: 6px; - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + color: var(--doc-muted); } .diff-grid { @@ -530,17 +688,17 @@ body { } .diff-card { - border: 1px solid rgba(30, 27, 22, 0.1); - border-radius: 12px; + border: 1px solid var(--doc-border); + border-radius: var(--radius-lg); padding: 12px; - background: #fffdf8; + background: var(--doc-bg); } .diff-row { display: flex; gap: 8px; - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + color: var(--doc-muted); margin-top: 6px; } @@ -550,12 +708,12 @@ body { } .empty-state { - padding: 18px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.7); - border: 1px dashed rgba(30, 27, 22, 0.2); - font-size: 13px; - color: rgba(30, 27, 22, 0.7); + padding: 16px; + border-radius: var(--radius-lg); + background: var(--doc-bg); + border: 1px dashed var(--doc-border); + font-size: 0.8125rem; + color: var(--doc-muted); } .drawer-actions { @@ -572,37 +730,181 @@ body { } .input-row input { - padding: 10px 12px; - border-radius: 10px; - border: 1px solid rgba(30, 27, 22, 0.2); - font-size: 13px; + padding: 8px 10px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + font-size: 0.8125rem; + font-family: var(--font-ui); + background: var(--doc-surface); } .input-row textarea { - padding: 10px 12px; - border-radius: 10px; - border: 1px solid rgba(30, 27, 22, 0.2); - font-size: 12px; + padding: 8px 10px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + font-size: 0.75rem; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + background: var(--doc-surface); resize: vertical; } .input-row select { padding: 8px 10px; - border-radius: 10px; - border: 1px solid rgba(30, 27, 22, 0.2); - background: #fffdf8; - font-size: 12px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + background: var(--doc-surface); + font-size: 0.75rem; + font-family: var(--font-ui); } .input-row label { - font-size: 12px; - color: rgba(30, 27, 22, 0.7); + font-size: 0.75rem; + font-family: var(--font-ui); + color: var(--doc-muted); } @media (max-width: 1100px) { + .app-header { + padding: 14px 16px; + } + .app-main { grid-template-columns: 1fr; + border-top: none; + } + + .app-main > .panel--star-lists { + border-right: none; + height: 0; + min-height: 0; + padding: 0; + margin: 0; + overflow: visible; + } + + .panel.panel--star-lists { + position: fixed; + left: 0; + top: 0; + height: 100vh; + height: 100dvh; + width: min(300px, 86vw); + z-index: 200; + margin: 0; + padding: 16px 14px 24px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + box-shadow: 4px 0 24px rgba(15, 23, 42, 0.12); + transform: translateX(-100%); + transition: transform 0.22s ease; + border-right: 1px solid var(--doc-border); + } + + .panel.panel--star-lists.is-open { + transform: translateX(0); + } + + .list-sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 150; + background: rgba(15, 23, 42, 0.45); + cursor: pointer; + } + + .panel-heading--list-sidebar { + flex-wrap: nowrap; + align-items: center; + } + + .panel-heading--list-sidebar h2 { + flex: 1; + min-width: 0; + } + + .list-sidebar-close-btn { + display: grid; + place-items: center; + width: 32px; + height: 32px; + padding: 0; + flex-shrink: 0; + border: 1px solid var(--doc-border); + background: var(--doc-surface); + border-radius: var(--radius-md); + cursor: pointer; + font-size: 1rem; + line-height: 1; + font-family: var(--font-ui); + color: var(--doc-text); + } + + .repo-panel-toolbar { + margin-bottom: 12px; + } + + .list-sidebar-open-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + justify-content: flex-start; + } + + .list-sidebar-open-icon { + font-size: 1rem; + opacity: 0.85; + } + + .list-sidebar-open-text { + font-weight: 600; + } + + .list-sidebar-active-chip { + margin-left: auto; + font-size: 0.75rem; + font-weight: 600; + color: var(--doc-accent); + max-width: 46%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .panel--repos > h2 { + margin-bottom: 12px; + } + + .panel.panel--repos { + padding: 14px 10px 28px; + } + + .repo-grid { + grid-template-columns: 1fr; + gap: 12px; + min-width: 0; + } + + .repo-card { + min-height: 0; + padding: 12px 12px; + } + + .filters { + gap: 10px; + } + + .filter-group.search { + margin-left: 0; + flex: 1 1 100%; + max-width: 100%; + } + + .filter-group.search input { + min-width: 0; + width: 100%; + max-width: 100%; } .writeback-summary-grid { @@ -611,18 +913,19 @@ body { } .panel-close { position: absolute; - top: -22px; - right: -22px; - width: 36px; - height: 36px; - border-radius: 999px; - border: 1px solid rgba(30, 27, 22, 0.2); - background: #fffdf8; + top: -18px; + right: -18px; + width: 32px; + height: 32px; + border-radius: var(--radius-md); + border: 1px solid var(--doc-border); + background: var(--doc-surface); cursor: pointer; - font-size: 16px; + font-size: 14px; + font-family: var(--font-ui); display: grid; place-items: center; - box-shadow: 0 10px 24px rgba(30, 27, 22, 0.12); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.1); z-index: 5; } diff --git a/src/app/styles/fonts.css b/src/app/styles/fonts.css index b0273be..5c8fa73 100644 --- a/src/app/styles/fonts.css +++ b/src/app/styles/fonts.css @@ -1 +1 @@ -@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&display=swap"); diff --git a/src/app/ui/ApplyUpdatesModal.tsx b/src/app/ui/ApplyUpdatesModal.tsx deleted file mode 100644 index f167340..0000000 --- a/src/app/ui/ApplyUpdatesModal.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - applyWriteback, - planWriteback, - type WritebackPlan, - type WritebackProgress, - type WritebackResult, -} from "../core/githubWriteback"; -import { usePreferenceStore } from "../store/preferences"; - -type ApplyUpdatesModalProps = { - isOpen: boolean; - onClose: () => void; - token: string; - onRequestToken: () => void; -}; - -const EMPTY_PROGRESS: WritebackProgress = { - stage: "idle", - current: 0, - total: 0, - failed: 0, - skipped: 0, -}; - -export function ApplyUpdatesModal({ - isOpen, - onClose, - token, - onRequestToken, -}: ApplyUpdatesModalProps) { - const { t } = useTranslation(); - const { preferences, setRefreshBeforeApply } = usePreferenceStore(); - const [plan, setPlan] = useState(null); - const [result, setResult] = useState(null); - const [progress, setProgress] = useState(EMPTY_PROGRESS); - const [isPlanning, setIsPlanning] = useState(false); - const [isApplying, setIsApplying] = useState(false); - const [error, setError] = useState(""); - const requestSeqRef = useRef(0); - - const summary = useMemo(() => { - const activePlan = result?.plan ?? plan; - if (!activePlan) return null; - return { - actionableRepos: activePlan.actionableRepos, - unchangedRepos: activePlan.unchangedRepos, - validTaggedRepos: activePlan.validTaggedRepos, - unstarredRepos: activePlan.unstarredActionableRepos, - missingLists: activePlan.missingLists.length, - createdLists: activePlan.createdLists.length, - }; - }, [plan, result]); - - useEffect(() => { - if (!isOpen) { - requestSeqRef.current += 1; - setPlan(null); - setResult(null); - setProgress(EMPTY_PROGRESS); - setIsPlanning(false); - setIsApplying(false); - setError(""); - } - }, [isOpen]); - - useEffect(() => { - if (!isOpen) return; - const trimmedToken = token.trim(); - if (!trimmedToken) { - setPlan(null); - setResult(null); - setProgress(EMPTY_PROGRESS); - setIsPlanning(false); - setError(t("apply.errors.missingPat")); - return; - } - - const requestId = ++requestSeqRef.current; - setIsPlanning(true); - setError(""); - setResult(null); - setProgress({ - stage: "preparing_preview", - current: 0, - total: 1, - failed: 0, - skipped: 0, - }); - - planWriteback({ token: trimmedToken }, {}, setProgress) - .then((nextPlan) => { - if (requestId !== requestSeqRef.current) return; - setPlan(nextPlan); - }) - .catch((err) => { - if (requestId !== requestSeqRef.current) return; - setError((err as Error).message || t("apply.errors.previewFailed")); - }) - .finally(() => { - if (requestId !== requestSeqRef.current) return; - setIsPlanning(false); - }); - }, [isOpen, token, t]); - - if (!isOpen) return null; - - const ensureToken = (): boolean => { - if (token.trim()) return true; - setError(t("apply.errors.missingPat")); - onRequestToken(); - return false; - }; - - const handleApply = async () => { - if (!ensureToken()) return; - if (!preferences.refreshBeforeApply && !plan) { - setError(t("apply.errors.previewNotReady")); - return; - } - const requestId = ++requestSeqRef.current; - setIsApplying(true); - setError(""); - setResult(null); - setProgress({ - stage: "preparing_writeback", - current: 0, - total: 1, - failed: 0, - skipped: 0, - }); - - try { - const nextResult = await applyWriteback( - { token: token.trim() }, - { - createMissingListsAsPrivate: false, - reusePlan: preferences.refreshBeforeApply ? null : plan, - resolveAutoStarForUnstarred: (unstarredCount) => - window.confirm(t("apply.confirmUnstarred", { count: unstarredCount })), - }, - setProgress - ); - if (requestId !== requestSeqRef.current) return; - setResult(nextResult); - setPlan(nextResult.plan); - } catch (err) { - if (requestId !== requestSeqRef.current) return; - setError((err as Error).message || t("apply.errors.applyFailed")); - } finally { - if (requestId !== requestSeqRef.current) return; - setIsApplying(false); - } - }; - - const disabled = isPlanning || isApplying; - const issues = result ? [...result.failed, ...result.skipped] : (plan?.skipped ?? []); - const changePreviewRows = (result?.plan.candidates ?? plan?.candidates ?? []).slice(0, 80); - - return ( -
-
- -
-

{t("apply.title")}

-

{t("apply.description")}

- -
-

{t("apply.sections.runControls")}

- -

- {preferences.refreshBeforeApply - ? t("apply.controls.refreshOn") - : t("apply.controls.refreshOff")} -

-
- -
-
- -
-

{t("apply.sections.progress")}

-

- {t("common.labels.stage")}: {t(`apply.stage.${progress.stage}`, { defaultValue: progress.stage })} -

-

- {t("common.labels.progress")}: {progress.current}/{progress.total} · {t("apply.labels.failed")}: {progress.failed} · {t("apply.labels.skipped")}: {progress.skipped} -

-
- - {summary ? ( -
-

{t("apply.sections.planSummary")}

-
-
- {t("apply.labels.actionable")} - {summary.actionableRepos} -
-
- {t("apply.labels.unchanged")} - {summary.unchangedRepos} -
-
- {t("apply.labels.validTagged")} - {summary.validTaggedRepos} -
-
- {t("apply.labels.unstarred")} - {summary.unstarredRepos} -
-
- {t("apply.labels.missingLists")} - {summary.missingLists} -
-
- {t("apply.labels.createdLists")} - {summary.createdLists} -
-
- {plan?.scanFailures ? ( -

- {t("apply.status.scanFailures", { count: plan.scanFailures })} -

- ) : null} -
- ) : null} - - {changePreviewRows.length > 0 ? ( -
-

{t("apply.sections.plannedRepoChanges")}

-
- {changePreviewRows.map((row) => ( -
-
{row.repoFullName}
-
- {t("apply.labels.current")} - {row.currentListNames.join(", ") || t("common.values.none")} -
-
- {t("apply.labels.after")} - {row.finalListNames.join(", ") || t("common.values.none")} -
-
- ))} -
-
- ) : plan ? ( -
-

{t("apply.sections.plannedRepoChanges")}

-

{t("apply.status.noChangesDetected")}

-
- ) : null} - - {result ? ( -
-

{t("apply.sections.applyResult")}

-

- {t("apply.labels.applied")}: {result.applied} -

-

- {t("apply.labels.locallyUpdated")}: {result.locallyUpdated} -

-

- {t("apply.labels.failed")}: {result.failed.length} -

-

- {t("apply.labels.skipped")}: {result.skipped.length} -

-
- ) : null} - - {issues.length > 0 ? ( -
-

{t("apply.sections.issues")}

-
- {issues.slice(0, 60).map((issue, index) => ( -
- - {t(`apply.issueReasons.${issue.reason}`, { defaultValue: issue.reason })} - - - {issue.repoFullName ? `${issue.repoFullName}: ` : ""} - {issue.message} - -
- ))} -
-
- ) : null} - - {error ? ( -

- {t("common.labels.error")}: {error} -

- ) : null} -
-
-
- ); -} diff --git a/src/app/ui/AssignListModal.tsx b/src/app/ui/AssignListModal.tsx index f1357af..cd7c2c5 100644 --- a/src/app/ui/AssignListModal.tsx +++ b/src/app/ui/AssignListModal.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { setRepoListMembership } from "../core/repoListAssignments"; +import { syncRepoListAssignmentToGitHub } from "../core/syncRepoListToGitHub"; import { db } from "../data/db"; import { useLiveQuery } from "../data/useLiveQuery"; @@ -8,10 +9,19 @@ type AssignListModalProps = { isOpen: boolean; repoId: string; repoName: string; + patToken: string; onClose: () => void; + onRequestToken?: () => void; }; -export function AssignListModal({ isOpen, repoId, repoName, onClose }: AssignListModalProps) { +export function AssignListModal({ + isOpen, + repoId, + repoName, + patToken, + onClose, + onRequestToken, +}: AssignListModalProps) { const { t } = useTranslation(); const [selectedListIds, setSelectedListIds] = useState([]); const [isSaving, setIsSaving] = useState(false); @@ -48,15 +58,21 @@ export function AssignListModal({ isOpen, repoId, repoName, onClose }: AssignLis }; const handleSave = async () => { + if (!patToken.trim()) { + setStatus(t("assignList.status.needPat")); + onRequestToken?.(); + return; + } setIsSaving(true); setStatus(""); try { + await syncRepoListAssignmentToGitHub({ token: patToken }, repoId, selectedListIds); await setRepoListMembership(repoId, selectedListIds); - setStatus(t("assignList.status.saved")); - } catch (error) { - setStatus((error as Error).message || t("assignList.status.saveFailed")); - } finally { setIsSaving(false); + onClose(); + } catch (error) { + setIsSaving(false); + setStatus((error as Error).message || t("assignList.status.saveFailed")); } }; diff --git a/src/app/ui/LlmClassificationModal.tsx b/src/app/ui/LlmClassificationModal.tsx deleted file mode 100644 index ca908c7..0000000 --- a/src/app/ui/LlmClassificationModal.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { runTwoStageClassification } from "../core/llmClassification"; -import { db } from "../data/db"; -import { useLiveQuery } from "../data/useLiveQuery"; -import { normalizeAppLanguage, toPromptLanguage } from "../i18n/language"; -import { useLlmConfigStore } from "../store/llmConfig"; -import { usePreferenceStore } from "../store/preferences"; - -type LlmClassificationModalProps = { - isOpen: boolean; - onClose: () => void; -}; - -export function LlmClassificationModal({ isOpen, onClose }: LlmClassificationModalProps) { - const { t, i18n } = useTranslation(); - const { config } = useLlmConfigStore(); - const { preferences } = usePreferenceStore(); - const [stage, setStage] = useState("idle"); - const [progress, setProgress] = useState({ current: 0, total: 0 }); - const [isRunning, setIsRunning] = useState(false); - const [error, setError] = useState(""); - const [isTestMode, setIsTestMode] = useState(false); - const [strictSingleTag, setStrictSingleTag] = useState(false); - const [selectedRunId, setSelectedRunId] = useState(null); - const [compareMode, setCompareMode] = useState<"existing" | "run">("existing"); - - const repos = useLiveQuery( - async () => db.repos.toArray(), - [], - [] - ); - - const classifications = useLiveQuery(async () => db.classifications.toArray(), [], []); - const lists = useLiveQuery(async () => db.lists.toArray(), [], []); - const repoLists = useLiveQuery(async () => db.repoLists.toArray(), [], []); - - const runs = useLiveQuery( - async () => db.classificationRuns.orderBy("createdAt").reverse().toArray(), - [], - [] - ); - - const selectedRunTags = useLiveQuery( - async () => { - if (!selectedRunId) return []; - return db.classificationTags.where("runId").equals(selectedRunId).toArray(); - }, - [selectedRunId], - [] - ); - - const previewRows = useMemo(() => { - const map = new Map(classifications.map((item) => [item.repoId, item.tags])); - return repos.slice(0, 12).map((repo) => ({ - id: repo.id, - name: repo.fullName, - tags: map.get(repo.id) ?? [], - })); - }, [repos, classifications]); - - const existingListMap = useMemo(() => { - const listMap = new Map(lists.map((list) => [list.id, list.name])); - const out = new Map(); - for (const row of repoLists) { - const names = row.listIds.map((listId) => listMap.get(listId)).filter(Boolean) as string[]; - out.set(row.repoId, names); - } - return out; - }, [lists, repoLists]); - - const diffRows = useMemo(() => { - if (compareMode === "run" && !selectedRunId) return []; - const runMap = new Map(selectedRunTags.map((item) => [item.repoId, item.tags])); - const currentMap = new Map(classifications.map((item) => [item.repoId, item.tags])); - const rows = repos.map((repo) => { - const current = currentMap.get(repo.id) ?? []; - const existing = existingListMap.get(repo.id) ?? []; - const previous = compareMode === "run" ? runMap.get(repo.id) ?? [] : existing; - const changed = previous.join("|") !== current.join("|"); - return { - id: repo.id, - name: repo.fullName, - previous, - current, - existing, - changed, - }; - }); - return rows.filter((row) => row.changed).slice(0, 30); - }, [repos, classifications, selectedRunTags, existingListMap, compareMode, selectedRunId]); - - if (!isOpen) return null; - - const handleRun = async () => { - if (!config.apiKey || !config.baseUrl) { - setError(t("classification.errors.missingConfig")); - return; - } - if (repos.length === 0) { - setError(t("classification.errors.noRepos")); - return; - } - const repoBatch = isTestMode ? repos.slice(0, 5) : repos; - const promptLanguage = toPromptLanguage( - normalizeAppLanguage(i18n.resolvedLanguage || i18n.language) - ); - setError(""); - setIsRunning(true); - setStage("preparing"); - setProgress({ current: 0, total: repoBatch.length }); - try { - const result = await runTwoStageClassification( - config, - repoBatch.map((repo) => ({ - id: repo.id, - fullName: repo.fullName, - description: repo.description, - topics: repo.topics, - language: repo.language, - readmeExcerpt: repo.readmeExcerpt, - existingLists: existingListMap.get(repo.id) ?? [], - })), - { - strictSingleTag, - pass1Prompt: config.pass1Prompt, - pass1StrictPrompt: config.pass1StrictPrompt, - pass2Prompt: config.pass2Prompt, - useExistingLists: preferences.useExistingListsForClassification, - allowNewTagsWithExistingLists: preferences.allowNewTagsWithExistingLists, - language: promptLanguage, - }, - (p) => { - setStage(p.stage); - setProgress({ current: p.current, total: p.total }); - } - ); - - await db.transaction( - "rw", - db.classifications, - db.tagCompression, - db.classificationRuns, - db.classificationTags, - async () => { - await db.classifications.clear(); - await db.tagCompression.clear(); - const now = new Date().toISOString(); - const rows = Object.entries(result.repoTags).map(([repoId, tags]) => ({ - repoId, - tags, - lastRunAt: now, - })); - await db.classifications.bulkPut(rows); - await db.tagCompression.bulkPut( - Object.entries(result.tagMap).map(([tag, compressedTag]) => ({ - tag, - compressedTag, - })) - ); - await db.classificationRuns.add({ - id: result.runId, - createdAt: now, - repoCount: repoBatch.length, - strictSingleTag, - testMode: isTestMode, - pass1Prompt: config.pass1Prompt || "", - pass1StrictPrompt: config.pass1StrictPrompt || "", - pass2Prompt: config.pass2Prompt || "", - }); - await db.classificationTags.bulkPut( - Object.entries(result.repoTags).map(([repoId, tags]) => ({ - id: `${result.runId}:${repoId}`, - runId: result.runId, - repoId, - tags, - })) - ); - } - ); - setStage("completed"); - setSelectedRunId(result.runId); - } catch (err) { - setError((err as Error).message || t("classification.errors.classificationFailed")); - setStage("failed"); - } finally { - setIsRunning(false); - } - }; - - return ( -
-
- -
-

{t("classification.title")}

-

{t("classification.description")}

- -
-

{t("classification.sections.runStatus")}

-

- {t("common.labels.stage")}: {t(`classification.stage.${stage}`, { defaultValue: stage })} -

-

- {t("common.labels.progress")}: {progress.current}/{progress.total} -

- - - {error ? ( -

- {t("common.labels.error")}: {error} -

- ) : null} -
- -
-
- -
-

{t("classification.sections.preview")}

- {previewRows.length === 0 ? ( -

{t("classification.status.noResults")}

- ) : ( -
- {previewRows.map((row) => ( -
-
{row.name}
-
- {row.tags.length === 0 - ? t("common.values.unclassified") - : row.tags.map((tag) => ( - - {tag} - - ))} -
-
- ))} -
- )} -
- -
-

{t("classification.sections.diffView")}

-
- - -
- {compareMode === "run" ? ( - runs.length === 0 ? ( -

{t("classification.status.noSavedRuns")}

- ) : ( -
- - -
- ) - ) : null} - {compareMode === "run" && selectedRunId && diffRows.length === 0 ? ( -

{t("classification.status.noTagDiff")}

- ) : null} - {compareMode === "existing" && diffRows.length === 0 ? ( -

{t("classification.status.noDiffFromExisting")}

- ) : null} - {diffRows.length > 0 ? ( -
- {diffRows.map((row) => ( -
-
{row.name}
-
- {t("classification.labels.existing")} - {row.existing.join(", ") || t("common.values.none")} -
- {compareMode === "run" ? ( -
- {t("classification.labels.previous")} - {row.previous.join(", ") || t("common.values.unclassified")} -
- ) : null} -
- {t("classification.labels.current")} - {row.current.join(", ") || t("common.values.unclassified")} -
-
- ))} -
- ) : null} -
-
-
-
- ); -} diff --git a/src/app/ui/ManageListsModal.tsx b/src/app/ui/ManageListsModal.tsx new file mode 100644 index 0000000..d08f903 --- /dev/null +++ b/src/app/ui/ManageListsModal.tsx @@ -0,0 +1,253 @@ +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { purgeListFromLocalStores } from "../core/purgeListLocal"; +import { db } from "../data/db"; +import { useLiveQuery } from "../data/useLiveQuery"; +import { + createUserList, + deleteUserListOnGitHub, + updateUserListOnGitHub, +} from "../services/githubStarLists"; + +type ManageListsModalProps = { + isOpen: boolean; + onClose: () => void; + patToken: string; + onRequestToken?: () => void; +}; + +export function ManageListsModal({ + isOpen, + onClose, + patToken, + onRequestToken, +}: ManageListsModalProps) { + const { t } = useTranslation(); + const listRows = useLiveQuery(async () => db.lists.orderBy("name").toArray(), [], []); + const repoListRows = useLiveQuery(async () => db.repoLists.toArray(), [], []); + + const countByList = useMemo(() => { + const map = new Map(); + for (const row of repoListRows) { + for (const id of row.listIds) { + map.set(id, (map.get(id) ?? 0) + 1); + } + } + return map; + }, [repoListRows]); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [isPrivate, setIsPrivate] = useState(false); + const [editingId, setEditingId] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + if (!isOpen) return; + setError(""); + setEditingId(null); + setName(""); + setDescription(""); + setIsPrivate(false); + }, [isOpen]); + + const startEdit = (listId: string) => { + const row = listRows.find((l) => l.id === listId); + if (!row) return; + setEditingId(listId); + setName(row.name); + setDescription(row.description ?? ""); + setIsPrivate(row.isPrivate); + setError(""); + }; + + const cancelEdit = () => { + setEditingId(null); + setName(""); + setDescription(""); + setIsPrivate(false); + setError(""); + }; + + const requirePat = (): boolean => { + if (patToken.trim()) return true; + setError(t("listManage.needPat")); + onRequestToken?.(); + return false; + }; + + const handleSubmit = async () => { + if (!requirePat()) return; + const trimmed = name.trim(); + if (!trimmed) { + setError(t("listManage.nameRequired")); + return; + } + setBusy(true); + setError(""); + try { + if (editingId) { + const updated = await updateUserListOnGitHub( + { token: patToken }, + editingId, + { + name: trimmed, + description: description.trim(), + isPrivate, + } + ); + await db.lists.put({ + id: updated.id, + name: updated.name, + description: updated.description, + isPrivate, + }); + cancelEdit(); + } else { + const ref = await createUserList({ token: patToken }, trimmed, isPrivate, description.trim()); + await db.lists.put({ + id: ref.id, + name: ref.name, + description: description.trim(), + isPrivate, + }); + setName(""); + setDescription(""); + setIsPrivate(false); + } + } catch (e) { + setError((e as Error).message || t("listManage.saveFailed")); + } finally { + setBusy(false); + } + }; + + const handleDelete = async (listId: string, listName: string) => { + if (!requirePat()) return; + if ( + !window.confirm(t("listManage.deleteConfirm", { name: listName })) + ) { + return; + } + setBusy(true); + setError(""); + try { + await deleteUserListOnGitHub({ token: patToken }, listId); + await purgeListFromLocalStores(listId); + if (editingId === listId) cancelEdit(); + } catch (e) { + setError((e as Error).message || t("listManage.deleteFailed")); + } finally { + setBusy(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ +
+

{t("listManage.title")}

+

{t("listManage.intro")}

+ +
+

{editingId ? t("listManage.editSection") : t("listManage.createSection")}

+
+ + setName(e.target.value)} + disabled={busy} + /> +
+
+ +