diff --git a/package-lock.json b/package-lock.json index c92ef63c..1269ba99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "the-agent-web-app", - "version": "5.4.0", + "version": "5.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "the-agent-web-app", - "version": "5.4.0", + "version": "5.5.0", "dependencies": { "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", @@ -30,7 +30,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.2", @@ -39,7 +39,7 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@tailwindcss/postcss": "^4.2.2", - "@types/node": "^25.5.0", + "@types/node": "^25.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", @@ -55,13 +55,13 @@ "postcss-import": "^16.1.1", "postcss-nesting": "^14.0.0", "postcss-theme-ui": "^0.10.0", - "shadcn": "^4.1.1", + "shadcn": "^4.1.2", "stylelint": "17.6.0", "stylelint-config-standard": "^40.0.0", "stylelint-config-tailwindcss": "^1.0.1", "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", - "typescript-eslint": "^8.57.2", + "typescript-eslint": "^8.58.0", "vite": "^8.0.2" } }, @@ -3973,9 +3973,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4040,20 +4040,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", - "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/type-utils": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4063,9 +4063,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.2", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -4079,16 +4079,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", - "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "engines": { @@ -4100,18 +4100,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -4122,18 +4122,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4144,9 +4144,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -4157,21 +4157,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4182,13 +4182,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -4200,21 +4200,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4224,7 +4224,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { @@ -4251,13 +4251,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -4280,16 +4280,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4300,17 +4300,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -7353,9 +7353,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -8753,9 +8753,9 @@ } }, "node_modules/react-router": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", - "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -8775,12 +8775,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", - "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", "license": "MIT", "dependencies": { - "react-router": "7.13.2" + "react-router": "7.14.0" }, "engines": { "node": ">=20.0.0" @@ -9113,9 +9113,9 @@ "license": "ISC" }, "node_modules/shadcn": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.1.1.tgz", - "integrity": "sha512-nBj+7LYC9kzV9v9QmRPpoOhfW4KctJVQejywdAt/K+K+z4RYlJOcO2a4AaF7elrRWkfCbgXeGK02liV0KB9HvQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.1.2.tgz", + "integrity": "sha512-qNQcCavkbYsgBj+X09tF2bTcwRd8abR880bsFkDU2kMqceMCLAm5c+cLg7kWDhfh1H9g08knpQ5ZEf6y/co16g==", "dev": true, "license": "MIT", "dependencies": { @@ -10031,16 +10031,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", - "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2" + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10051,7 +10051,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index ba96a26c..9663d2ce 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "the-agent-web-app", "private": true, - "version": "5.5.0", + "version": "5.5.2", "type": "module", "scripts": { "clean": "rm -rf dist .vite node_modules/.vite", @@ -39,7 +39,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.13.2", + "react-router-dom": "^7.14.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.2", @@ -48,7 +48,7 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@tailwindcss/postcss": "^4.2.2", - "@types/node": "^25.5.0", + "@types/node": "^25.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", @@ -64,13 +64,13 @@ "postcss-import": "^16.1.1", "postcss-nesting": "^14.0.0", "postcss-theme-ui": "^0.10.0", - "shadcn": "^4.1.1", + "shadcn": "^4.1.2", "stylelint": "17.6.0", "stylelint-config-standard": "^40.0.0", "stylelint-config-tailwindcss": "^1.0.1", "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", - "typescript-eslint": "^8.57.2", + "typescript-eslint": "^8.58.0", "vite": "^8.0.2" } } diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index 31f779c4..57ab9da3 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -100,7 +100,9 @@ "show_scope_and_filters": "عرض النطاق والمرشحات", "hide_scope_and_filters": "إخفاء النطاق والمرشحات", "include_sponsored_label": "تضمين المستخدمين المرعيين", - "exclude_self_label": "استبعاد استخدامي" + "exclude_self_label": "استبعاد استخدامي", + "include_transfers_label": "تضمين التحويلات", + "only_transfers_label": "عرض التحويلات فقط" }, "stats": { "total_records": "إجمالي السجلات", @@ -145,6 +147,18 @@ "status_completed": "مكتمل", "status_failed": "فشل" }, + "transfer": { + "to": "إلى", + "from": "من", + "note": "ملاحظة", + "card_title": "تحويل الرصيد", + "button": "تحويل الرصيد", + "handle_label": "اسم المستخدم أو هاتف المستلم", + "amount_label": "المبلغ", + "amount_placeholder": "1.00", + "note_label": "ملاحظة (اختياري)", + "note_placeholder": "سبب التحويل…" + }, "load_more": "تحميل المزيد", "credit_balance": "الأرصدة التي أملكها: {balance}" }, @@ -259,6 +273,7 @@ "where_is_my_key": "أين يوجد مفتاح {providerName} الخاص بي؟", "configure_intelligence": "تكوين الذكاء", "configure_ai_providers": "تكوين مزودي الذكاء الاصطناعي", + "configure_access_keys": "تهيئة مفاتيح الوصول", "intelligence_presets": { "label": "وضع الذكاء", "lowest_price": "أقل تكلفة", @@ -346,7 +361,6 @@ "audio_transcription_failed": "فشل في تحويل الصوت إلى نص", "announcement_not_received": "لم يتم استلام إعلان الذكاء الاصطناعي", "user_limit_reached": "تم الوصول إلى الحد الأقصى للمستخدمين", - "insufficient_credits": "أرصدة غير كافية", "unsupported_chat_type": "نوع دردشة غير مدعوم", "unsupported_provider": "مزود غير مدعوم", "missing_chat_context": "سياق الدردشة غير متاح", @@ -364,6 +378,12 @@ "profile_connect_failed": "فشل في ربط الملف الشخصي", "unexpected_error": "حدث خطأ غير متوقع", "policy_acceptance_revocation_forbidden": "لا يمكن إلغاء قبول السياسات", + "invalid_transfer_amount": "مبلغ التحويل أقل من الحد الأدنى (1.0 رصيد)", + "self_transfer_not_allowed": "لا يمكنك تحويل الأرصدة لنفسك", + "insufficient_credits": "أرصدة غير كافية لهذه العملية", + "sponsored_user_transfer_not_allowed": "لا يمكن للمستخدمين المدعومين إرسال أو استقبال تحويلات الأرصدة", + "transfer_recipient_not_found": "لم يتم العثور على مستلم التحويل على هذه المنصة", + "transfer_failed": "فشل تحويل الأرصدة بسبب خطأ داخلي", "waitlist_account_not_active": "حسابك غير نشط بعد", "waitlist_invited_policies_required": "يرجى قبول السياسات لتفعيل حسابك المدعو" }, @@ -434,6 +454,10 @@ "title": "X / تويتر", "description": "الوصول لقراءة وتحليل محتوى تويتر." }, + "credit_transfer": { + "title": "تحويل الأرصدة", + "description": "أداة داخلية لتحويل الأرصدة بين المستخدمين." + }, "deprecated": { "title": "مُهمل", "description": "هذه الأداة أو الميزة مُهملة وقد تُزال في الإصدارات المستقبلية." diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 95949cf9..dca37942 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -100,7 +100,9 @@ "show_scope_and_filters": "Bereich und Filter anzeigen", "hide_scope_and_filters": "Umfang und Filter ausblenden", "include_sponsored_label": "Gesponserte Benutzer einbeziehen", - "exclude_self_label": "Meine Nutzung ausschließen" + "exclude_self_label": "Meine Nutzung ausschließen", + "include_transfers_label": "Überweisungen einbeziehen", + "only_transfers_label": "Nur Überweisungen anzeigen" }, "stats": { "total_records": "Gesamt Einträge", @@ -145,6 +147,18 @@ "status_completed": "Abgeschlossen", "status_failed": "Fehlgeschlagen" }, + "transfer": { + "to": "An", + "from": "Von", + "note": "Notiz", + "card_title": "Guthaben überweisen", + "button": "Guthaben überweisen", + "handle_label": "Benutzername oder Telefonnummer des Empfängers", + "amount_label": "Betrag", + "amount_placeholder": "1,00", + "note_label": "Notiz (optional)", + "note_placeholder": "Grund der Überweisung…" + }, "load_more": "Mehr laden", "credit_balance": "Guthaben, die ich besitze: {balance}" }, @@ -259,6 +273,7 @@ "where_is_my_key": "Wo ist mein {providerName}-Schlüssel?", "configure_intelligence": "Intelligenz konfigurieren", "configure_ai_providers": "KI-Anbieter konfigurieren", + "configure_access_keys": "Zugriffsschlüssel konfigurieren", "tools": { "not_configured_with_prefix": "– Konfigurieren", "select_tool": "Tool auswählen", @@ -326,6 +341,10 @@ "title": "X / Twitter", "description": "Zugang zum Lesen und Analysieren von Twitter-Inhalten." }, + "credit_transfer": { + "title": "Guthabenüberweisung", + "description": "Internes Tool zur Überweisung von Guthaben zwischen Benutzern." + }, "deprecated": { "title": "Veraltet", "description": "Dieses Tool oder diese Funktion ist veraltet und kann in zukünftigen Versionen entfernt werden." @@ -465,7 +484,6 @@ "audio_transcription_failed": "Audio-Transkription fehlgeschlagen", "announcement_not_received": "KI-Ankündigung nicht erhalten", "user_limit_reached": "Benutzerlimit erreicht", - "insufficient_credits": "Unzureichendes Guthaben", "unsupported_chat_type": "Nicht unterstützter Chat-Typ", "unsupported_provider": "Nicht unterstützter Anbieter", "missing_chat_context": "Chat-Kontext ist nicht verfügbar", @@ -483,6 +501,12 @@ "profile_connect_failed": "Profilverbindung fehlgeschlagen", "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten", "policy_acceptance_revocation_forbidden": "Die Annahme der Richtlinien kann nicht widerrufen werden", + "invalid_transfer_amount": "Überweisungsbetrag liegt unter dem Minimum (1,0 Guthaben)", + "self_transfer_not_allowed": "Sie können kein Guthaben an sich selbst überweisen", + "insufficient_credits": "Nicht genügend Guthaben für diese Aktion", + "sponsored_user_transfer_not_allowed": "Gesponserte Benutzer können keine Guthabenüberweisungen senden oder empfangen", + "transfer_recipient_not_found": "Überweisungsempfänger auf dieser Plattform nicht gefunden", + "transfer_failed": "Guthabenüberweisung aufgrund eines internen Fehlers fehlgeschlagen", "waitlist_account_not_active": "Ihr Konto ist noch nicht aktiv", "waitlist_invited_policies_required": "Bitte akzeptieren Sie die Richtlinien, um Ihr eingeladenes Konto zu aktivieren" }, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index d0babd4a..3eb8e8c9 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -100,7 +100,9 @@ "show_scope_and_filters": "Show Scope & Filters", "hide_scope_and_filters": "Hide Scope & Filters", "include_sponsored_label": "Include sponsored users", - "exclude_self_label": "Exclude my usage" + "exclude_self_label": "Exclude my usage", + "include_transfers_label": "Include transfers", + "only_transfers_label": "Show only transfers" }, "stats": { "total_records": "Total Records", @@ -145,6 +147,18 @@ "status_completed": "Completed", "status_failed": "Failed" }, + "transfer": { + "to": "To", + "from": "From", + "note": "Note", + "card_title": "Transfer Credits", + "button": "Transfer Credits", + "handle_label": "Username or phone of the recipient", + "amount_label": "Amount", + "amount_placeholder": "1.00", + "note_label": "Note (optional)", + "note_placeholder": "Reason for transfer…" + }, "load_more": "Load more", "credit_balance": "Credits I own: {balance}" }, @@ -259,6 +273,7 @@ "where_is_my_key": "Where is my {providerName} key?", "configure_intelligence": "Configure intelligence", "configure_ai_providers": "Configure AI providers", + "configure_access_keys": "Configure access keys", "tools": { "not_configured_with_prefix": "– Configure", "select_tool": "Select a tool", @@ -326,6 +341,10 @@ "title": "X / Twitter", "description": "Access to read and analyze Twitter content." }, + "credit_transfer": { + "title": "Credit Transfer", + "description": "Internal tool for transferring credits between users." + }, "deprecated": { "title": "Deprecated", "description": "This tool or feature is deprecated and may be removed in future versions." @@ -423,7 +442,6 @@ "audio_transcription_failed": "Audio transcription failed", "announcement_not_received": "AI announcement not received", "user_limit_reached": "User limit reached", - "insufficient_credits": "Insufficient credits", "unsupported_chat_type": "Unsupported chat type", "unsupported_provider": "Unsupported provider", "missing_chat_context": "Chat context is not available", @@ -441,6 +459,12 @@ "profile_connect_failed": "Profile connection failed", "unexpected_error": "An unexpected error occurred", "policy_acceptance_revocation_forbidden": "Policy acceptance cannot be revoked", + "invalid_transfer_amount": "Transfer amount is below the minimum (1.0 credits)", + "self_transfer_not_allowed": "You cannot transfer credits to yourself", + "insufficient_credits": "Insufficient credits for this operation", + "sponsored_user_transfer_not_allowed": "Sponsored users cannot send or receive credit transfers", + "transfer_recipient_not_found": "Transfer recipient not found on this platform", + "transfer_failed": "Credit transfer failed due to an internal error", "waitlist_account_not_active": "Your account is not yet active", "waitlist_invited_policies_required": "Please accept the policies to activate your invited account" }, diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 6e75e6f5..12b5ee17 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -108,6 +108,18 @@ "status_completed": "Completado", "status_failed": "Fallido" }, + "transfer": { + "to": "A", + "from": "De", + "note": "Nota", + "card_title": "Transferir créditos", + "button": "Transferir créditos", + "handle_label": "Nombre de usuario o teléfono del destinatario", + "amount_label": "Cantidad", + "amount_placeholder": "1,00", + "note_label": "Nota (opcional)", + "note_placeholder": "Motivo de la transferencia…" + }, "load_more": "Cargar más", "credit_balance": "Créditos que poseo: {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "Mostrar alcance y filtros", "hide_scope_and_filters": "Ocultar alcance y filtros", "include_sponsored_label": "Incluir usuarios patrocinados", - "exclude_self_label": "Excluir mi uso" + "exclude_self_label": "Excluir mi uso", + "include_transfers_label": "Incluir transferencias", + "only_transfers_label": "Mostrar solo transferencias" }, "stats": { "total_records": "Total de registros", @@ -259,6 +273,7 @@ "where_is_my_key": "¿Dónde está mi clave de {providerName}?", "configure_intelligence": "Configurar inteligencia", "configure_ai_providers": "Configurar proveedores de IA", + "configure_access_keys": "Configurar las claves de acceso", "intelligence_presets": { "label": "Modo de Inteligencia", "lowest_price": "Costo más bajo", @@ -346,7 +361,6 @@ "audio_transcription_failed": "Error en la transcripción de audio", "announcement_not_received": "Anuncio de IA no recibido", "user_limit_reached": "Límite de usuarios alcanzado", - "insufficient_credits": "Créditos insuficientes", "unsupported_chat_type": "Tipo de chat no compatible", "unsupported_provider": "Proveedor no compatible", "missing_chat_context": "El contexto del chat no está disponible", @@ -364,6 +378,12 @@ "profile_connect_failed": "Error al conectar perfil", "unexpected_error": "Ocurrió un error inesperado", "policy_acceptance_revocation_forbidden": "La aceptación de políticas no puede revocarse", + "invalid_transfer_amount": "El monto de transferencia está por debajo del mínimo (1,0 créditos)", + "self_transfer_not_allowed": "No puedes transferir créditos a ti mismo", + "insufficient_credits": "Créditos insuficientes para esta operación", + "sponsored_user_transfer_not_allowed": "Los usuarios patrocinados no pueden enviar ni recibir transferencias de créditos", + "transfer_recipient_not_found": "Destinatario de la transferencia no encontrado en esta plataforma", + "transfer_failed": "La transferencia de créditos falló debido a un error interno", "waitlist_account_not_active": "Tu cuenta aún no está activa", "waitlist_invited_policies_required": "Acepta las políticas para activar tu cuenta invitada" }, @@ -480,6 +500,10 @@ "title": "X / Twitter", "description": "Acceso para leer y analizar contenido de Twitter." }, + "credit_transfer": { + "title": "Transferencia de créditos", + "description": "Herramienta interna para transferir créditos entre usuarios." + }, "deprecated": { "title": "Obsoleto", "description": "Esta herramienta o función está obsoleta y puede ser eliminada en versiones futuras." diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index e4dc6392..3ac64016 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -108,6 +108,18 @@ "status_completed": "Terminé", "status_failed": "Échoué" }, + "transfer": { + "to": "À", + "from": "De", + "note": "Note", + "card_title": "Transférer des crédits", + "button": "Transférer des crédits", + "handle_label": "Nom d'utilisateur ou téléphone du destinataire", + "amount_label": "Montant", + "amount_placeholder": "1,00", + "note_label": "Note (facultatif)", + "note_placeholder": "Motif du transfert…" + }, "load_more": "Charger plus", "credit_balance": "Crédits que je possède : {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "Afficher la portée et les filtres", "hide_scope_and_filters": "Masquer la portée et les filtres", "include_sponsored_label": "Inclure les utilisateurs sponsorisés", - "exclude_self_label": "Exclure mon utilisation" + "exclude_self_label": "Exclure mon utilisation", + "include_transfers_label": "Inclure les transferts", + "only_transfers_label": "Afficher uniquement les transferts" }, "stats": { "total_records": "Total d'enregistrements", @@ -259,6 +273,7 @@ "where_is_my_key": "Où est ma clé {providerName} ?", "configure_intelligence": "Configurer l'intelligence", "configure_ai_providers": "Configurer les fournisseurs d'IA", + "configure_access_keys": "Configurer les clés d'accès", "tools": { "not_configured_with_prefix": "– Configurer", "select_tool": "Sélectionner un outil", @@ -326,6 +341,10 @@ "title": "X / Twitter", "description": "Accès pour lire et analyser le contenu Twitter." }, + "credit_transfer": { + "title": "Transfert de crédits", + "description": "Outil interne pour transférer des crédits entre utilisateurs." + }, "deprecated": { "title": "Obsolète", "description": "Cet outil ou cette fonctionnalité est obsolète et pourrait être supprimé dans les versions futures." @@ -465,7 +484,6 @@ "audio_transcription_failed": "Échec de la transcription audio", "announcement_not_received": "Annonce de l'IA non reçue", "user_limit_reached": "Limite d'utilisateurs atteinte", - "insufficient_credits": "Crédits insuffisants", "unsupported_chat_type": "Type de discussion non pris en charge", "unsupported_provider": "Fournisseur non pris en charge", "missing_chat_context": "Le contexte de la discussion n'est pas disponible", @@ -483,6 +501,12 @@ "profile_connect_failed": "Échec de la connexion du profil", "unexpected_error": "Une erreur inattendue s'est produite", "policy_acceptance_revocation_forbidden": "L'acceptation des politiques ne peut pas être révoquée", + "invalid_transfer_amount": "Le montant du transfert est inférieur au minimum (1,0 crédit)", + "self_transfer_not_allowed": "Vous ne pouvez pas transférer des crédits à vous-même", + "insufficient_credits": "Crédits insuffisants pour cette opération", + "sponsored_user_transfer_not_allowed": "Les utilisateurs sponsorisés ne peuvent pas envoyer ou recevoir des transferts de crédits", + "transfer_recipient_not_found": "Destinataire du transfert introuvable sur cette plateforme", + "transfer_failed": "Le transfert de crédits a échoué en raison d'une erreur interne", "waitlist_account_not_active": "Votre compte n'est pas encore actif", "waitlist_invited_policies_required": "Veuillez accepter les politiques pour activer votre compte invité" }, diff --git a/src/assets/i18n/hi.json b/src/assets/i18n/hi.json index 4614d43b..e141ac42 100644 --- a/src/assets/i18n/hi.json +++ b/src/assets/i18n/hi.json @@ -108,6 +108,18 @@ "status_completed": "पूर्ण", "status_failed": "विफल" }, + "transfer": { + "to": "को", + "from": "से", + "note": "नोट", + "card_title": "क्रेडिट ट्रांसफर करें", + "button": "क्रेडिट ट्रांसफर करें", + "handle_label": "प्राप्तकर्ता का उपयोगकर्ता नाम या फ़ोन", + "amount_label": "राशि", + "amount_placeholder": "1.00", + "note_label": "नोट (वैकल्पिक)", + "note_placeholder": "ट्रांसफर का कारण…" + }, "load_more": "और लोड करें", "credit_balance": "मेरे पास क्रेडिट: {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "दायरा और फ़िल्टर दिखाएं", "hide_scope_and_filters": "दायरा और फ़िल्टर छुपाएं", "include_sponsored_label": "प्रायोजित उपयोगकर्ताओं को शामिल करें", - "exclude_self_label": "मेरे उपयोग को बाहर करें" + "exclude_self_label": "मेरे उपयोग को बाहर करें", + "include_transfers_label": "ट्रांसफर शामिल करें", + "only_transfers_label": "केवल ट्रांसफर दिखाएं" }, "stats": { "total_records": "कुल रिकॉर्ड", @@ -259,6 +273,7 @@ "where_is_my_key": "मेरा {providerName} कुंजी कहाँ है?", "configure_intelligence": "बुद्धिमत्ता कॉन्फ़िगर करें", "configure_ai_providers": "AI प्रदाता कॉन्फ़िगर करें", + "configure_access_keys": "एक्सेस कुंजियाँ कॉन्फ़िगर करें", "intelligence_presets": { "label": "इंटेलिजेंस मोड", "lowest_price": "सबसे कम लागत", @@ -346,7 +361,6 @@ "audio_transcription_failed": "ऑडियो ट्रांसक्रिप्शन में विफल", "announcement_not_received": "AI घोषणा प्राप्त नहीं हुई", "user_limit_reached": "उपयोगकर्ता सीमा पूर्ण हो गई", - "insufficient_credits": "अपर्याप्त क्रेडिट", "unsupported_chat_type": "असमर्थित चैट प्रकार", "unsupported_provider": "असमर्थित प्रदाता", "missing_chat_context": "चैट संदर्भ उपलब्ध नहीं है", @@ -364,6 +378,12 @@ "profile_connect_failed": "प्रोफाइल कनेक्शन में विफल", "unexpected_error": "एक अप्रत्याशित त्रुटि हुई", "policy_acceptance_revocation_forbidden": "नीति स्वीकृति को रद्द नहीं किया जा सकता", + "invalid_transfer_amount": "ट्रांसफर राशि न्यूनतम से कम है (1.0 क्रेडिट)", + "self_transfer_not_allowed": "आप अपने आप को क्रेडिट ट्रांसफर नहीं कर सकते", + "insufficient_credits": "इस ऑपरेशन के लिए अपर्याप्त क्रेडिट", + "sponsored_user_transfer_not_allowed": "प्रायोजित उपयोगकर्ता क्रेडिट ट्रांसफर नहीं भेज या प्राप्त कर सकते", + "transfer_recipient_not_found": "इस प्लेटफ़ॉर्म पर ट्रांसफर प्राप्तकर्ता नहीं मिला", + "transfer_failed": "आंतरिक त्रुटि के कारण क्रेडिट ट्रांसफर विफल हुआ", "waitlist_account_not_active": "आपका खाता अभी सक्रिय नहीं है", "waitlist_invited_policies_required": "अपने आमंत्रित खाते को सक्रिय करने के लिए नीतियाँ स्वीकार करें" }, @@ -434,6 +454,10 @@ "title": "X / Twitter", "description": "Twitter सामग्री पढ़ने और विश्लेषण करने के लिए पहुंच।" }, + "credit_transfer": { + "title": "क्रेडिट ट्रांसफर", + "description": "उपयोगकर्ताओं के बीच क्रेडिट ट्रांसफर करने के लिए आंतरिक उपकरण।" + }, "deprecated": { "title": "अप्रचलित", "description": "यह टूल या सुविधा अप्रचलित है और भविष्य के संस्करणों में हटाया जा सकता है।" diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 206f36e0..56da7d8c 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -108,6 +108,18 @@ "status_completed": "Completato", "status_failed": "Fallito" }, + "transfer": { + "to": "A", + "from": "Da", + "note": "Nota", + "card_title": "Trasferisci crediti", + "button": "Trasferisci crediti", + "handle_label": "Nome utente o telefono del destinatario", + "amount_label": "Importo", + "amount_placeholder": "1,00", + "note_label": "Nota (facoltativo)", + "note_placeholder": "Motivo del trasferimento…" + }, "load_more": "Carica altro", "credit_balance": "Crediti che possiedo: {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "Mostra ambito e filtri", "hide_scope_and_filters": "Nascondi ambito e filtri", "include_sponsored_label": "Includi utenti sponsorizzati", - "exclude_self_label": "Escludi il mio utilizzo" + "exclude_self_label": "Escludi il mio utilizzo", + "include_transfers_label": "Includi trasferimenti", + "only_transfers_label": "Mostra solo trasferimenti" }, "stats": { "total_records": "Totale record", @@ -259,6 +273,7 @@ "where_is_my_key": "Dove si trova la mia chiave {providerName}?", "configure_intelligence": "Configura intelligenza", "configure_ai_providers": "Configura provider AI", + "configure_access_keys": "Configura le chiavi di accesso", "intelligence_presets": { "label": "Modalità di Intelligenza", "lowest_price": "Costo più basso", @@ -346,7 +361,6 @@ "audio_transcription_failed": "Trascrizione audio non riuscita", "announcement_not_received": "Annuncio IA non ricevuto", "user_limit_reached": "Limite utenti raggiunto", - "insufficient_credits": "Crediti insufficienti", "unsupported_chat_type": "Tipo di chat non supportato", "unsupported_provider": "Provider non supportato", "missing_chat_context": "Il contesto della chat non è disponibile", @@ -364,6 +378,12 @@ "profile_connect_failed": "Connessione del profilo non riuscita", "unexpected_error": "Si è verificato un errore imprevisto", "policy_acceptance_revocation_forbidden": "L'accettazione delle politiche non può essere revocata", + "invalid_transfer_amount": "L'importo del trasferimento è inferiore al minimo (1,0 crediti)", + "self_transfer_not_allowed": "Non puoi trasferire crediti a te stesso", + "insufficient_credits": "Crediti insufficienti per questa operazione", + "sponsored_user_transfer_not_allowed": "Gli utenti sponsorizzati non possono inviare o ricevere trasferimenti di crediti", + "transfer_recipient_not_found": "Destinatario del trasferimento non trovato su questa piattaforma", + "transfer_failed": "Trasferimento crediti fallito a causa di un errore interno", "waitlist_account_not_active": "Il tuo account non è ancora attivo", "waitlist_invited_policies_required": "Accetta le politiche per attivare il tuo account invitato" }, @@ -480,6 +500,10 @@ "title": "X / Twitter", "description": "Accesso per leggere e analizzare i contenuti di Twitter." }, + "credit_transfer": { + "title": "Trasferimento crediti", + "description": "Strumento interno per trasferire crediti tra utenti." + }, "deprecated": { "title": "Obsoleto", "description": "Questo strumento o funzionalità è obsoleto e potrebbe essere rimosso nelle versioni future." diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 8e6b2d2d..d84c75fa 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -108,6 +108,18 @@ "status_completed": "Выполнено", "status_failed": "Ошибка" }, + "transfer": { + "to": "Кому", + "from": "От", + "note": "Примечание", + "card_title": "Перевести кредиты", + "button": "Перевести кредиты", + "handle_label": "Имя пользователя или телефон получателя", + "amount_label": "Сумма", + "amount_placeholder": "1,00", + "note_label": "Примечание (необязательно)", + "note_placeholder": "Причина перевода…" + }, "load_more": "Загрузить ещё", "credit_balance": "Кредиты, которыми я владею: {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "Показать область и фильтры", "hide_scope_and_filters": "Скрыть область и фильтры", "include_sponsored_label": "Включить спонсируемых пользователей", - "exclude_self_label": "Исключить моё использование" + "exclude_self_label": "Исключить моё использование", + "include_transfers_label": "Включить переводы", + "only_transfers_label": "Показать только переводы" }, "stats": { "total_records": "Всего записей", @@ -259,6 +273,7 @@ "where_is_my_key": "Где мой ключ {providerName}?", "configure_intelligence": "Настроить интеллект", "configure_ai_providers": "Настроить AI-провайдеры", + "configure_access_keys": "Настроить ключи доступа", "intelligence_presets": { "label": "Режим интеллекта", "lowest_price": "Наименьшая стоимость", @@ -346,7 +361,6 @@ "audio_transcription_failed": "Не удалось расшифровать аудио", "announcement_not_received": "Объявление ИИ не получено", "user_limit_reached": "Достигнут лимит пользователей", - "insufficient_credits": "Недостаточно кредитов", "unsupported_chat_type": "Неподдерживаемый тип чата", "unsupported_provider": "Неподдерживаемый провайдер", "missing_chat_context": "Контекст чата недоступен", @@ -364,6 +378,12 @@ "profile_connect_failed": "Не удалось подключить профиль", "unexpected_error": "Произошла неожиданная ошибка", "policy_acceptance_revocation_forbidden": "Принятие политик не может быть отозвано", + "invalid_transfer_amount": "Сумма перевода ниже минимума (1,0 кредит)", + "self_transfer_not_allowed": "Нельзя перевести кредиты самому себе", + "insufficient_credits": "Недостаточно кредитов для этой операции", + "sponsored_user_transfer_not_allowed": "Спонсируемые пользователи не могут отправлять или получать переводы кредитов", + "transfer_recipient_not_found": "Получатель перевода не найден на этой платформе", + "transfer_failed": "Перевод кредитов не удался из-за внутренней ошибки", "waitlist_account_not_active": "Ваш аккаунт ещё не активен", "waitlist_invited_policies_required": "Примите политики для активации приглашённого аккаунта" }, @@ -434,6 +454,10 @@ "title": "X / Twitter", "description": "Доступ для чтения и анализа контента Twitter." }, + "credit_transfer": { + "title": "Перевод кредитов", + "description": "Внутренний инструмент для перевода кредитов между пользователями." + }, "deprecated": { "title": "Устаревший", "description": "Этот инструмент или функция устарели и могут быть удалены в будущих версиях." diff --git a/src/assets/i18n/sr.json b/src/assets/i18n/sr.json index a0e0b8e4..a62e3220 100644 --- a/src/assets/i18n/sr.json +++ b/src/assets/i18n/sr.json @@ -100,7 +100,9 @@ "show_scope_and_filters": "Prikaži opseg i filtere", "hide_scope_and_filters": "Sakrij opseg i filtere", "include_sponsored_label": "Uključi sponzorisane korisnike", - "exclude_self_label": "Isključi moje korišćenje" + "exclude_self_label": "Isključi moje korišćenje", + "include_transfers_label": "Uključi transfere", + "only_transfers_label": "Prikaži samo transfere" }, "stats": { "total_records": "Ukupno zapisa", @@ -145,6 +147,18 @@ "status_completed": "Uspešno", "status_failed": "Neuspešno" }, + "transfer": { + "to": "Za", + "from": "Od", + "note": "Napomena", + "card_title": "Prenos kredita", + "button": "Prenos kredita", + "handle_label": "Korisničko ime ili telefon primaoca", + "amount_label": "Iznos", + "amount_placeholder": "1,00", + "note_label": "Napomena (opciono)", + "note_placeholder": "Razlog prenosa…" + }, "load_more": "Učitaj još", "credit_balance": "Krediti koje posedujem: {balance}" }, @@ -259,6 +273,7 @@ "where_is_my_key": "Gde je moj {providerName} ključ?", "configure_intelligence": "Podesi inteligenciju", "configure_ai_providers": "Podesi AI provajdere", + "configure_access_keys": "Podesi pristupne ključeve", "intelligence_presets": { "label": "Režim inteligencije", "lowest_price": "Najniža cena", @@ -346,7 +361,6 @@ "audio_transcription_failed": "Transkripcija zvuka nije uspela", "announcement_not_received": "AI obaveštenje nije primljeno", "user_limit_reached": "Dostignut je limit korisnika", - "insufficient_credits": "Nedovoljno kredita", "unsupported_chat_type": "Nepodržana vrsta četa", "unsupported_provider": "Nepodržan provajder", "missing_chat_context": "Kontekst četa nije dostupan", @@ -364,6 +378,12 @@ "profile_connect_failed": "Povezivanje profila nije uspelo", "unexpected_error": "Došlo je do neočekivane greške", "policy_acceptance_revocation_forbidden": "Prihvatanje politika se ne može opozvati", + "invalid_transfer_amount": "Iznos transfera je ispod minimuma (1,0 kredit)", + "self_transfer_not_allowed": "Ne možete preneti kredite sebi", + "insufficient_credits": "Nedovoljno kredita za ovu operaciju", + "sponsored_user_transfer_not_allowed": "Sponzorisani korisnici ne mogu slati ili primati transfere kredita", + "transfer_recipient_not_found": "Primalac transfera nije pronađen na ovoj platformi", + "transfer_failed": "Transfer kredita nije uspeo zbog interne greške", "waitlist_account_not_active": "Vaš nalog još nije aktivan", "waitlist_invited_policies_required": "Prihvatite politike da biste aktivirali pozvani nalog" }, @@ -480,6 +500,10 @@ "title": "X / Twitter", "description": "Pristup za čitanje i analizu Twitter sadržaja." }, + "credit_transfer": { + "title": "Transfer kredita", + "description": "Interni alat za transfer kredita između korisnika." + }, "deprecated": { "title": "Zastarelo", "description": "Ovaj alat ili funkcija je zastarela i može biti uklonjena u budućim verzijama." diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json index 5c6b3836..336103b9 100644 --- a/src/assets/i18n/tr.json +++ b/src/assets/i18n/tr.json @@ -108,6 +108,18 @@ "status_completed": "Tamamlandı", "status_failed": "Başarısız" }, + "transfer": { + "to": "Kime", + "from": "Kimden", + "note": "Not", + "card_title": "Kredi Transfer Et", + "button": "Kredi Transfer Et", + "handle_label": "Alıcının kullanıcı adı veya telefonu", + "amount_label": "Tutar", + "amount_placeholder": "1,00", + "note_label": "Not (isteğe bağlı)", + "note_placeholder": "Transfer nedeni…" + }, "load_more": "Daha fazla yükle", "credit_balance": "Sahip olduğum krediler: {balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "Kapsam ve filtreleri göster", "hide_scope_and_filters": "Kapsam ve filtreleri gizle", "include_sponsored_label": "Sponsorlu kullanıcıları dahil et", - "exclude_self_label": "Kullanımımı hariç tut" + "exclude_self_label": "Kullanımımı hariç tut", + "include_transfers_label": "Transferleri dahil et", + "only_transfers_label": "Sadece transferleri göster" }, "stats": { "total_records": "Toplam Kayıt", @@ -259,6 +273,7 @@ "where_is_my_key": "{providerName} anahtarım nerede?", "configure_intelligence": "Zeka yapılandır", "configure_ai_providers": "AI sağlayıcılarını yapılandır", + "configure_access_keys": "Erişim anahtarlarını yapılandır", "intelligence_presets": { "label": "Zeka Modu", "lowest_price": "En düşük maliyet", @@ -346,7 +361,6 @@ "audio_transcription_failed": "Ses transkripsionu başarısız", "announcement_not_received": "AI duyurusu alınamadı", "user_limit_reached": "Kullanıcı limitine ulaşıldı", - "insufficient_credits": "Yetersiz kredi", "unsupported_chat_type": "Desteklenmeyen sohbet türü", "unsupported_provider": "Desteklenmeyen sağlayıcı", "missing_chat_context": "Sohbet bağlamı mevcut değil", @@ -364,6 +378,12 @@ "profile_connect_failed": "Profil bağlantısı başarısız", "unexpected_error": "Beklenmeyen bir hata oluştu", "policy_acceptance_revocation_forbidden": "Politika kabulü iptal edilemez", + "invalid_transfer_amount": "Transfer tutarı minimumun altında (1,0 kredi)", + "self_transfer_not_allowed": "Kendinize kredi transfer edemezsiniz", + "insufficient_credits": "Bu işlem için yetersiz kredi", + "sponsored_user_transfer_not_allowed": "Sponsorlu kullanıcılar kredi transferi gönderemez veya alamaz", + "transfer_recipient_not_found": "Transfer alıcısı bu platformda bulunamadı", + "transfer_failed": "Kredi transferi dahili bir hata nedeniyle başarısız oldu", "waitlist_account_not_active": "Hesabınız henüz aktif değil", "waitlist_invited_policies_required": "Davet hesabınızı etkinleştirmek için politikaları kabul edin" }, @@ -434,6 +454,10 @@ "title": "X / Twitter", "description": "Twitter içeriğini okumak ve analiz etmek için erişim." }, + "credit_transfer": { + "title": "Kredi Transferi", + "description": "Kullanıcılar arasında kredi transferi için dahili araç." + }, "deprecated": { "title": "Kullanımdan Kaldırıldı", "description": "Bu araç veya özellik kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılabilir." diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json index e66d9827..fcfc3784 100644 --- a/src/assets/i18n/zh.json +++ b/src/assets/i18n/zh.json @@ -108,6 +108,18 @@ "status_completed": "已完成", "status_failed": "失败" }, + "transfer": { + "to": "发送至", + "from": "来自", + "note": "备注", + "card_title": "转账积分", + "button": "转账积分", + "handle_label": "收款人的用户名或电话", + "amount_label": "金额", + "amount_placeholder": "1.00", + "note_label": "备注(可选)", + "note_placeholder": "转账原因…" + }, "load_more": "加载更多", "credit_balance": "我拥有的积分:{balance}", "filters": { @@ -135,7 +147,9 @@ "show_scope_and_filters": "显示范围和筛选器", "hide_scope_and_filters": "隐藏范围和筛选器", "include_sponsored_label": "包括赞助用户", - "exclude_self_label": "排除我的使用" + "exclude_self_label": "排除我的使用", + "include_transfers_label": "包含转账", + "only_transfers_label": "仅显示转账" }, "stats": { "total_records": "总记录数", @@ -259,6 +273,7 @@ "where_is_my_key": "我的 {providerName} 密钥在哪里?", "configure_intelligence": "配置智能", "configure_ai_providers": "配置 AI 提供商", + "configure_access_keys": "配置访问密钥", "intelligence_presets": { "label": "智能模式", "lowest_price": "最低成本", @@ -346,7 +361,6 @@ "audio_transcription_failed": "音频转录失败", "announcement_not_received": "未收到AI公告", "user_limit_reached": "已达到用户限制", - "insufficient_credits": "积分不足", "unsupported_chat_type": "不支持的聊天类型", "unsupported_provider": "不支持的提供商", "missing_chat_context": "聊天上下文不可用", @@ -364,6 +378,12 @@ "profile_connect_failed": "配置文件连接失败", "unexpected_error": "发生意外错误", "policy_acceptance_revocation_forbidden": "无法撤销政策接受", + "invalid_transfer_amount": "转账金额低于最低限额(1.0积分)", + "self_transfer_not_allowed": "不能向自己转账积分", + "insufficient_credits": "积分不足,无法完成此操作", + "sponsored_user_transfer_not_allowed": "被赞助的用户无法发送或接收积分转账", + "transfer_recipient_not_found": "在此平台上未找到转账接收者", + "transfer_failed": "由于内部错误,积分转账失败", "waitlist_account_not_active": "您的账户尚未激活", "waitlist_invited_policies_required": "请接受政策以激活您的受邀账户" }, @@ -480,6 +500,10 @@ "title": "X / Twitter", "description": "访问以读取和分析Twitter内容。" }, + "credit_transfer": { + "title": "积分转账", + "description": "用于用户之间转移积分的内部工具。" + }, "deprecated": { "title": "已弃用", "description": "此工具或功能已弃用,可能会在未来版本中移除。" diff --git a/src/assets/logo-monochrome-vector.svg b/src/assets/logo-monochrome-vector.svg new file mode 100644 index 00000000..5d189bf8 --- /dev/null +++ b/src/assets/logo-monochrome-vector.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/AdvancedToolsPanel.tsx b/src/components/AdvancedToolsPanel.tsx index f6345cac..0b5d022f 100644 --- a/src/components/AdvancedToolsPanel.tsx +++ b/src/components/AdvancedToolsPanel.tsx @@ -12,6 +12,7 @@ import { Euro, Bitcoin, Bird, + ArrowLeftRight, BookOpenText, BrainCog, Image, @@ -89,6 +90,7 @@ const getToolGroupCategory = (toolType: ToolType): ToolGroupCategory => { api_fiat_exchange: "integrations", api_crypto_exchange: "integrations", api_twitter: "integrations", + credit_transfer: "integrations", deprecated: "integrations", }; return categoryMap[toolType]; @@ -180,6 +182,7 @@ const AdvancedToolsPanel: React.FC = ({ api_fiat_exchange: "integrations", api_crypto_exchange: "integrations", api_twitter: "integrations", + credit_transfer: "integrations", } as Record) .filter(([, cat]) => cat === category) .map(([type]) => type as ToolType); @@ -206,6 +209,7 @@ const AdvancedToolsPanel: React.FC = ({ api_fiat_exchange: Euro, api_crypto_exchange: Bitcoin, api_twitter: Bird, + credit_transfer: ArrowLeftRight, }; return iconMap[toolType]; }; diff --git a/src/components/PlatformHandleInput.tsx b/src/components/PlatformHandleInput.tsx new file mode 100644 index 00000000..5bdf6638 --- /dev/null +++ b/src/components/PlatformHandleInput.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import PlatformDropdown from "@/components/PlatformDropdown"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Platform } from "@/lib/platform"; +import { cn } from "@/lib/utils"; +import { t } from "@/lib/translations"; + +interface PlatformHandleInputProps { + label: string; + selectedPlatform: Platform; + onPlatformChange: (platform: Platform) => void; + platformHandle: string; + onPlatformHandleChange: (value: string) => void; + disabled?: boolean; + onKeyboardConfirm?: () => void; + className?: string; +} + +const getPlatformPlaceholder = ( + platform: Platform, + disabled: boolean, +): string => { + if (disabled) return "—"; + switch (platform) { + case Platform.TELEGRAM: + return t("sponsorship.platform_handle_placeholder_telegram"); + case Platform.WHATSAPP: + return t("sponsorship.platform_handle_placeholder_whatsapp"); + default: + return t("sponsorship.platform_handle_placeholder"); + } +}; + +const PlatformHandleInput: React.FC = ({ + label, + selectedPlatform, + onPlatformChange, + platformHandle, + onPlatformHandleChange, + disabled = false, + onKeyboardConfirm, + className, +}) => { + return ( + + + {label} + + + + onPlatformHandleChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && onKeyboardConfirm) { + onKeyboardConfirm(); + } + }} + /> + + + ); +}; + +export default PlatformHandleInput; diff --git a/src/components/ProviderIcon.tsx b/src/components/ProviderIcon.tsx index c7de05be..f1d48b71 100644 --- a/src/components/ProviderIcon.tsx +++ b/src/components/ProviderIcon.tsx @@ -8,6 +8,7 @@ import CoinMarketCapLogo from "@/assets/svg/coinmarketcap-white.svg"; import ReplicateLogo from "@/assets/svg/replicate-white.svg"; import XLogo from "@/assets/svg/x-logo-white.svg"; import XAILogo from "@/assets/svg/x-ai-logo-white.svg"; +import InternalLogo from "@/assets/logo-monochrome-vector.svg"; import PlatformIcon from "@/components/PlatformIcon"; import { Platform } from "@/lib/platform"; @@ -33,6 +34,7 @@ const ProviderIcon: React.FC = ({ replicate: ReplicateLogo, x: XLogo, x_ai: XAILogo, + internal: InternalLogo, }; return logoMap[id] || null; }; diff --git a/src/components/UsageFilters.tsx b/src/components/UsageFilters.tsx index 6291b950..ac7a0c26 100644 --- a/src/components/UsageFilters.tsx +++ b/src/components/UsageFilters.tsx @@ -34,6 +34,10 @@ interface UsageFiltersProps { onIncludeSponsoredChange: (value: boolean) => void; excludeSelf: boolean; onExcludeSelfChange: (value: boolean) => void; + includeTransfers: boolean; + onIncludeTransfersChange: (value: boolean) => void; + onlyTransfers: boolean; + onOnlyTransfersChange: (value: boolean) => void; stats: UsageAggregatesResponse; disabled?: boolean; isExpanded?: boolean; @@ -53,6 +57,10 @@ const UsageFilters: React.FC = ({ onIncludeSponsoredChange, excludeSelf, onExcludeSelfChange, + includeTransfers, + onIncludeTransfersChange, + onlyTransfers, + onOnlyTransfersChange, stats, disabled = false, isExpanded: externalIsExpanded, @@ -95,19 +103,6 @@ const UsageFilters: React.FC = ({ {t("usage.filters.scope_section")} - onTimeRangeChange(value as TimeRange)} - options={timeRangeOptions.map((opt) => ({ - ...opt, - disabled: opt.value === timeRange, - }))} - disabled={disabled} - placeholder={disabled ? "—" : t("usage.filters.time_range_all")} - className="-mt-2" - /> - = ({ disabled={disabled} /> )} + + { + onIncludeTransfersChange(value); + if (!value) onOnlyTransfersChange(false); + }} + disabled={disabled || onlyTransfers} + /> + + {includeTransfers && ( + + )} @@ -132,6 +148,19 @@ const UsageFilters: React.FC = ({ {t("usage.filters.filters_section")} + onTimeRangeChange(value as TimeRange)} + options={timeRangeOptions.map((opt) => ({ + ...opt, + disabled: opt.value === timeRange, + }))} + disabled={disabled} + placeholder={disabled ? "—" : t("usage.filters.time_range_all")} + className="-mt-2" + /> + = ({ isSingleItem, currentUserId, chats, - sponsorships, locale, }) => { // Border/rounded classes @@ -67,6 +64,11 @@ const UsageRecordCard: React.FC = ({ const isSponsoredByOthersForMe = normalizedPayerId !== normalizedUserId && normalizedUserId === normalizedCurrentId; const isSponsoredByMeForOthers = normalizedPayerId === normalizedCurrentId && normalizedUserId !== normalizedCurrentId; + const isTransfer = record.tool_purpose === "credit_transfer"; + const normalizedCounterpartId = record.counterpart_id?.replace(/-/g, "") ?? ""; + const isTransferReceived = isTransfer && normalizedCounterpartId === normalizedCurrentId; + const isTransferSent = isTransfer && !isTransferReceived; + // Get translated purpose title const getPurposeTitle = (): string => { const key = `tools.types.${record.tool_purpose}.title` as TranslationKey; @@ -84,14 +86,8 @@ const UsageRecordCard: React.FC = ({ let displayName = t("usage.context_ids.user_me"); if (!isCurrentUserRecord) { - displayName = t("usage.context_ids.user_sponsored"); - const sponsorship = sponsorships?.find( - (s) => s.user_id_hex.replace(/-/g, "") === normalizedRecordId - ); - - if (sponsorship) { - displayName = sponsorship.full_name || sponsorship.platform_handle || displayName; - } + const owner = record.participant_details?.owner; + displayName = owner?.full_name || owner?.handle || t("usage.context_ids.user_sponsored"); } return ( @@ -107,6 +103,23 @@ const UsageRecordCard: React.FC = ({ ); }; + const middleTruncateId = (id: string): string => { + if (id.length <= 16) return id; + return `${id.slice(0, 8)}…${id.slice(-6)}`; + }; + + const getOwnerDisplay = (): string => { + const owner = record.participant_details?.owner; + if (!owner) return "—"; + return owner.full_name || owner.handle || middleTruncateId(owner.user_id); + }; + + const getCounterpartDisplay = (): string | null => { + const cp = record.participant_details?.counterpart; + if (!cp) return null; + return cp.full_name || cp.handle || middleTruncateId(cp.user_id); + }; + // Get chat display - try to find chat name from chats list const getChatDisplay = (): string => { if (!record.chat_id) { @@ -126,7 +139,7 @@ const UsageRecordCard: React.FC = ({ // Format runtime const formatRuntime = (seconds: number): string => { - return `${seconds.toFixed(1)}s`; + return `${seconds.toFixed(2)}s`; }; // Format credits @@ -179,12 +192,30 @@ const UsageRecordCard: React.FC = ({ - - {record.tool.name} - - - {getPurposeTitle()} - + {isTransfer ? ( + <> + + {getPurposeTitle()} + + + {isTransferSent ? ( + + ) : ( + + )} + {getOwnerDisplay()} + + > + ) : ( + <> + + {record.tool.name} + + + {getPurposeTitle()} + + > + )} @@ -192,7 +223,14 @@ const UsageRecordCard: React.FC = ({ "flex items-center min-w-[4rem] justify-end", !isSponsoredByOthersForMe && "space-x-1" )}> - {isSponsoredByOthersForMe ? ( + {isTransferReceived ? ( + <> + + + +{formatCredits(record.total_cost_credits)} + + > + ) : isSponsoredByOthersForMe ? ( ) : record.is_failed ? ( <> @@ -391,12 +429,30 @@ const UsageRecordCard: React.FC = ({ {getUserDisplayName()} - - - {t("usage.context_ids.chat_label")} - - {getChatDisplay()} - + {isTransfer && getCounterpartDisplay() && ( + + + {isTransferSent ? t("usage.transfer.to") : t("usage.transfer.from")} + + {getCounterpartDisplay()} + + )} + {(!isTransfer || record.chat_id) && ( + + + {t("usage.context_ids.chat_label")} + + {getChatDisplay()} + + )} + {isTransfer && record.note && ( + + + {t("usage.transfer.note")} + + {record.note} + + )} {record.remote_runtime_seconds != null && ( diff --git a/src/hooks/useUserSettings.ts b/src/hooks/useUserSettings.ts index 4b94d7f5..b874cb17 100644 --- a/src/hooks/useUserSettings.ts +++ b/src/hooks/useUserSettings.ts @@ -5,6 +5,7 @@ import { setCachedSettings, areSettingsCached, clearUserSettingsCache, + subscribeToCacheInvalidation, } from "@/services/user-settings-cache"; interface UseUserSettingsResult { @@ -66,6 +67,13 @@ export const useUserSettings = ( fetchSettings(); }, [fetchSettings]); + // Re-fetch when any instance invalidates the cache + useEffect(() => { + return subscribeToCacheInvalidation(() => { + fetchSettings(true); + }); + }, [fetchSettings]); + const refreshSettings = useCallback(async () => { if (userId) { clearUserSettingsCache(userId); diff --git a/src/lib/api-error.ts b/src/lib/api-error.ts index 3d047014..87ae11b8 100644 --- a/src/lib/api-error.ts +++ b/src/lib/api-error.ts @@ -58,6 +58,10 @@ export function getErrorTranslationKey(errorCode: number): TranslationKey | null case 1029: return "error_codes.malformed_user_id"; case 1030: return "error_codes.malformed_chat_id"; case 1031: return "error_codes.policy_acceptance_revocation_forbidden"; + case 1032: return "error_codes.invalid_transfer_amount"; + case 1033: return "error_codes.self_transfer_not_allowed"; + case 1034: return "error_codes.insufficient_credits"; + case 1035: return "error_codes.sponsored_user_transfer_not_allowed"; case 2001: return "error_codes.user_not_found"; case 2002: return "error_codes.chat_not_found"; case 2003: return "error_codes.attachment_not_found"; @@ -70,6 +74,7 @@ export function getErrorTranslationKey(errorCode: number): TranslationKey | null case 2010: return "error_codes.token_not_found"; case 2011: return "error_codes.unknown_command"; case 2012: return "error_codes.no_authorized_chats"; + case 2013: return "error_codes.transfer_recipient_not_found"; case 3001: return "error_codes.not_chat_admin"; case 3002: return "error_codes.not_target_user"; case 3003: return "error_codes.not_developer"; @@ -93,7 +98,7 @@ export function getErrorTranslationKey(errorCode: number): TranslationKey | null case 5011: return "error_codes.audio_transcription_failed"; case 5012: return "error_codes.announcement_not_received"; case 6001: return "error_codes.user_limit_reached"; - case 6002: return "error_codes.insufficient_credits"; + case 6002: return "error_codes.insufficient_credits"; // legacy, moved to 1034 case 7001: return "error_codes.unsupported_chat_type"; case 7002: return "error_codes.unsupported_provider"; case 7003: return "error_codes.missing_chat_context"; @@ -109,6 +114,7 @@ export function getErrorTranslationKey(errorCode: number): TranslationKey | null case 8011: return "error_codes.sponsorship_operation_failed"; case 8012: return "error_codes.unsponsor_self_failed"; case 8013: return "error_codes.profile_connect_failed"; + case 8014: return "error_codes.transfer_failed"; case 8999: return "error_codes.unexpected_error"; default: return null; } diff --git a/src/lib/translation-keys.ts b/src/lib/translation-keys.ts index f7940cfb..5daf52c6 100644 --- a/src/lib/translation-keys.ts +++ b/src/lib/translation-keys.ts @@ -98,6 +98,8 @@ export type TranslationKey = | "usage.filters.hide_scope_and_filters" | "usage.filters.include_sponsored_label" | "usage.filters.exclude_self_label" + | "usage.filters.include_transfers_label" + | "usage.filters.only_transfers_label" | "usage.stats.total_records" | "usage.stats.total_cost" | "usage.stats.total_runtime" @@ -133,6 +135,16 @@ export type TranslationKey = | "usage.context_ids.status_label" | "usage.context_ids.status_completed" | "usage.context_ids.status_failed" + | "usage.transfer.to" + | "usage.transfer.from" + | "usage.transfer.note" + | "usage.transfer.card_title" + | "usage.transfer.button" + | "usage.transfer.handle_label" + | "usage.transfer.amount_label" + | "usage.transfer.amount_placeholder" + | "usage.transfer.note_label" + | "usage.transfer.note_placeholder" | "usage.load_more" | "usage.credit_balance" | "purchases.page_title" @@ -230,6 +242,7 @@ export type TranslationKey = | "where_is_my_key" | "configure_intelligence" | "configure_ai_providers" + | "configure_access_keys" | "tools.not_configured_with_prefix" | "tools.select_tool" | "tools.no_tools_available" @@ -261,6 +274,8 @@ export type TranslationKey = | "tools.types.api_crypto_exchange.description" | "tools.types.api_twitter.title" | "tools.types.api_twitter.description" + | "tools.types.credit_transfer.title" + | "tools.types.credit_transfer.description" | "tools.types.deprecated.title" | "tools.types.deprecated.description" | "intelligence_warnings.no_access_message" @@ -347,7 +362,6 @@ export type TranslationKey = | "error_codes.audio_transcription_failed" | "error_codes.announcement_not_received" | "error_codes.user_limit_reached" - | "error_codes.insufficient_credits" | "error_codes.unsupported_chat_type" | "error_codes.unsupported_provider" | "error_codes.missing_chat_context" @@ -365,6 +379,12 @@ export type TranslationKey = | "error_codes.profile_connect_failed" | "error_codes.unexpected_error" | "error_codes.policy_acceptance_revocation_forbidden" + | "error_codes.invalid_transfer_amount" + | "error_codes.self_transfer_not_allowed" + | "error_codes.insufficient_credits" + | "error_codes.sponsored_user_transfer_not_allowed" + | "error_codes.transfer_recipient_not_found" + | "error_codes.transfer_failed" | "error_codes.waitlist_account_not_active" | "error_codes.waitlist_invited_policies_required" | "features.header" diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 55b4773d..4c661e6d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -64,7 +64,7 @@ export class PageError { public static fromApiError( apiError: ApiError, isBlocker: boolean = false, - showGenericAppendix: boolean = true + showGenericAppendix: boolean = false ) { const translationKey = getErrorTranslationKey(apiError.errorCode) || "errors.unknown"; const variables = translationKey === "errors.unknown" diff --git a/src/pages/AccessSettingsPage.tsx b/src/pages/AccessSettingsPage.tsx index 67dd2de6..3a04db43 100644 --- a/src/pages/AccessSettingsPage.tsx +++ b/src/pages/AccessSettingsPage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from "react"; import { useParams } from "react-router-dom"; import BaseSettingsPage from "@/pages/BaseSettingsPage"; import { toast } from "sonner"; +import { ApiError } from "@/lib/api-error"; import { PageError, buildSponsoredBlockerError } from "@/lib/utils"; import { t } from "@/lib/translations"; import WarningBanner from "@/components/WarningBanner"; @@ -13,6 +14,7 @@ import { getSettingsFieldName, buildChangedPayload, areSettingsChanged, + hasAnyApiKey, } from "@/services/user-settings-service"; import { useUserSettings } from "@/hooks/useUserSettings"; import { @@ -80,18 +82,25 @@ const AccessSettingsPage: React.FC = () => { rawToken: accessToken.raw, }); console.info("Fetched external tools!", externalTools); + const visibleProviders = externalTools.providers.filter( + (p) => p.definition.id !== "internal", + ); setExternalToolProviders( - externalTools.providers.map((p) => p.definition), + visibleProviders.map((p) => p.definition), ); const statusMap = new Map(); - externalTools.providers.forEach((p) => { + visibleProviders.forEach((p) => { statusMap.set(p.definition.id, p.is_configured); }); setProviderConfigStatus(statusMap); hasLoadedOnce.current = true; } catch (err) { console.error("Error fetching data!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } finally { setIsLoadingState(false); } @@ -201,19 +210,26 @@ const AccessSettingsPage: React.FC = () => { }); updateSettingsCache(userSettings!); + const updatedVisibleProviders = updatedExternalTools.providers.filter( + (p) => p.definition.id !== "internal", + ); setExternalToolProviders( - updatedExternalTools.providers.map((p) => p.definition), + updatedVisibleProviders.map((p) => p.definition), ); // Update provider configuration status const statusMap = new Map(); - updatedExternalTools.providers.forEach((p) => { + updatedVisibleProviders.forEach((p) => { statusMap.set(p.definition.id, p.is_configured); }); setProviderConfigStatus(statusMap); toast(t("saved")); } catch (saveError) { console.error("Error saving settings!", saveError); - setError(PageError.simple("errors.save_failed")); + setError( + saveError instanceof ApiError + ? PageError.fromApiError(saveError) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -227,24 +243,12 @@ const AccessSettingsPage: React.FC = () => { const botName = import.meta.env.VITE_APP_NAME_SHORT; - // Check if any API keys are configured in local state - const hasAnyApiKey = !!( - userSettings?.open_ai_key || - userSettings?.anthropic_key || - userSettings?.google_ai_key || - userSettings?.perplexity_key || - userSettings?.replicate_key || - userSettings?.rapid_api_key || - userSettings?.coinmarketcap_key || - userSettings?.x_key || - userSettings?.x_ai_key - ); - - // Check if user has credits const hasCredits = (userSettings?.credit_balance ?? 0) > 0; - - // Show warning only if user has credits AND API keys AND hasn't dismissed it - const showCreditsWarning = hasCredits && hasAnyApiKey && !isWarningDismissed; + const showCreditsWarning = + hasCredits && + !!userSettings && + hasAnyApiKey(userSettings) && + !isWarningDismissed; const handleRemoveAllApiKeys = () => { if (!userSettings) return; diff --git a/src/pages/BaseSettingsPage.tsx b/src/pages/BaseSettingsPage.tsx index 6b1c8a35..831cd871 100644 --- a/src/pages/BaseSettingsPage.tsx +++ b/src/pages/BaseSettingsPage.tsx @@ -219,7 +219,7 @@ const BaseSettingsPage = forwardRef( className={cn( "sticky top-0 z-30 transition-all duration-200 ease-in-out", isActionBarSticky - ? "glass-dark-static rounded-b-2xl py-3 px-4 -mx-4 sm:-mx-6 lg:-mx-8 shadow-lg shadow-black/40" + ? "glass-dark-static rounded-2xl py-3 px-4 -mx-4 sm:-mx-6 lg:-mx-8 shadow-lg shadow-black/40" : "", )} > diff --git a/src/pages/ChatSettingsPage.tsx b/src/pages/ChatSettingsPage.tsx index b28a363f..13f2e5f5 100644 --- a/src/pages/ChatSettingsPage.tsx +++ b/src/pages/ChatSettingsPage.tsx @@ -9,6 +9,7 @@ import type { } from "@/services/chat-settings-service"; import SettingSelector from "@/components/SettingSelector"; import { toast } from "sonner"; +import { ApiError } from "@/lib/api-error"; import { PageError } from "@/lib/utils"; import { t } from "@/lib/translations"; import { @@ -69,7 +70,11 @@ const ChatSettingsPage: React.FC = () => { setRemoteSettings(settings); } catch (err) { console.error("Error fetching data!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } finally { setIsLoadingState(false); } @@ -111,7 +116,11 @@ const ChatSettingsPage: React.FC = () => { toast(t("saved")); } catch (saveError) { console.error("Error saving settings!", saveError); - setError(PageError.simple("errors.save_failed")); + setError( + saveError instanceof ApiError + ? PageError.fromApiError(saveError) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } diff --git a/src/pages/IntelligenceSettingsPage.tsx b/src/pages/IntelligenceSettingsPage.tsx index ad70d8ab..4dc8c0c8 100644 --- a/src/pages/IntelligenceSettingsPage.tsx +++ b/src/pages/IntelligenceSettingsPage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import BaseSettingsPage from "@/pages/BaseSettingsPage"; import { toast } from "sonner"; +import { ApiError } from "@/lib/api-error"; import { PageError, buildSponsoredBlockerError } from "@/lib/utils"; import { t } from "@/lib/translations"; import AdvancedToolsPanel from "@/components/AdvancedToolsPanel"; @@ -127,7 +128,11 @@ const IntelligenceSettingsPage: React.FC = () => { }); } catch (saveError) { console.error("Error saving settings!", saveError); - setError(PageError.simple("errors.save_failed")); + setError( + saveError instanceof ApiError + ? PageError.fromApiError(saveError) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -270,22 +275,26 @@ const IntelligenceSettingsPage: React.FC = () => { t("intelligence_presets.custom_description")} - - - {t("detailed_tool_choices")} - - + {selectedPreset === "custom" && ( + <> + + + {t("detailed_tool_choices")} + + + > + )} > ) : !isLoadingState ? ( diff --git a/src/pages/LinkedProfilesPage.tsx b/src/pages/LinkedProfilesPage.tsx index 19de199e..241bf68b 100644 --- a/src/pages/LinkedProfilesPage.tsx +++ b/src/pages/LinkedProfilesPage.tsx @@ -86,7 +86,11 @@ const LinkedProfilesPage: React.FC = () => { setConnectKey(response.connect_key); } catch (err) { console.error("Error fetching connect key!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } finally { setIsLoadingState(false); } diff --git a/src/pages/SponsorshipsPage.tsx b/src/pages/SponsorshipsPage.tsx index 028d5de7..4b93081d 100644 --- a/src/pages/SponsorshipsPage.tsx +++ b/src/pages/SponsorshipsPage.tsx @@ -16,6 +16,7 @@ import { Clock, } from "lucide-react"; import BaseSettingsPage from "@/pages/BaseSettingsPage"; +import { ApiError } from "@/lib/api-error"; import { PageError, cn, cleanUsername } from "@/lib/utils"; import { toast } from "sonner"; import { t } from "@/lib/translations"; @@ -29,11 +30,9 @@ import { import { Platform } from "@/lib/platform"; import { usePageSession } from "@/hooks/usePageSession"; import { useUserSettings } from "@/hooks/useUserSettings"; -import PlatformDropdown from "@/components/PlatformDropdown"; +import PlatformHandleInput from "@/components/PlatformHandleInput"; import PlatformIcon from "@/components/PlatformIcon"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { fetchExternalTools } from "@/services/external-tools-service"; const SponsorshipsPage: React.FC = () => { @@ -116,7 +115,11 @@ const SponsorshipsPage: React.FC = () => { setHasApiKeysConfigured(hasConfiguredProviders); } catch (err) { console.error("Error fetching data!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } finally { setIsLoadingState(false); } @@ -160,7 +163,11 @@ const SponsorshipsPage: React.FC = () => { toast(t("saved")); } catch (err) { console.error("Error saving sponsorship!", err); - setError(PageError.simple("errors.save_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -191,7 +198,11 @@ const SponsorshipsPage: React.FC = () => { toast(t("saved")); } catch (err) { console.error("Error saving sponsorship!", err); - setError(PageError.simple("errors.save_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -213,7 +224,11 @@ const SponsorshipsPage: React.FC = () => { await refreshSettings(); } catch (err) { console.error("Error saving sponsorship!", err); - setError(PageError.simple("errors.save_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -314,20 +329,6 @@ const SponsorshipsPage: React.FC = () => { const shouldShowCancelButton = isEditing && !userSettings?.is_sponsored; - // Get platform-specific placeholder - const getPlatformPlaceholder = (): string => { - if (error?.isBlocker) return "—"; - - switch (selectedPlatform) { - case Platform.TELEGRAM: - return t("sponsorship.platform_handle_placeholder_telegram"); - case Platform.WHATSAPP: - return t("sponsorship.platform_handle_placeholder_whatsapp"); - default: - return t("sponsorship.platform_handle_placeholder"); - } - }; - const getSponsorshipStatusLabel = (sponsorship: SponsorshipResponse): string => { return sponsorship.accepted_at ? t("sponsorship.details.accepted") @@ -366,33 +367,22 @@ const SponsorshipsPage: React.FC = () => { > ) : isEditing ? ( - <> - {/* New sponsorship input with platform dropdown */} - - - {t("sponsorship.platform_handle_label")} - - - - setPlatformHandle(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !error?.isBlocker) { - handleSaveSponsorship(); - } - }} - /> - - - > + + { + if (!error?.isBlocker) { + handleSaveSponsorship(); + } + }} + /> + ) : ( <> {/* Sponsorships List */} diff --git a/src/pages/UsagePage.tsx b/src/pages/UsagePage.tsx index 4d405197..29c1ed79 100644 --- a/src/pages/UsagePage.tsx +++ b/src/pages/UsagePage.tsx @@ -1,21 +1,24 @@ import React, { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; -import { ChartNoAxesCombined, BadgeCent, ShoppingCart } from "lucide-react"; +import { ChartNoAxesCombined, BadgeCent, ShoppingCart, ArrowRightLeft } from "lucide-react"; import BaseSettingsPage from "@/pages/BaseSettingsPage"; -import { PageError, buildSponsoredBlockerError } from "@/lib/utils"; +import { ApiError } from "@/lib/api-error"; +import { PageError, buildSponsoredBlockerError, cleanUsername } from "@/lib/utils"; +import { toast } from "sonner"; import { t } from "@/lib/translations"; import { fetchUsageRecords, fetchUsageStats, + createTransfer, UsageRecord, UsageAggregatesResponse, } from "@/services/usage-service"; import { fetchProducts, Product } from "@/services/purchase-service"; +import { Platform } from "@/lib/platform"; import ProductPickerDialog from "@/components/ProductPickerDialog"; -import { - fetchUserSponsorships, - SponsorshipResponse, -} from "@/services/sponsorships-service"; +import PlatformHandleInput from "@/components/PlatformHandleInput"; +import SettingInput from "@/components/SettingInput"; +import SettingTextarea from "@/components/SettingTextarea"; import { usePageSession } from "@/hooks/usePageSession"; import { useChats } from "@/hooks/useChats"; import { useUserSettings } from "@/hooks/useUserSettings"; @@ -38,7 +41,7 @@ const UsagePage: React.FC = () => { const { chats } = useChats(accessToken?.decoded?.sub, accessToken?.raw); - const { userSettings } = useUserSettings( + const { userSettings, refreshSettings } = useUserSettings( user_id, accessToken?.raw, ); @@ -60,7 +63,6 @@ const UsagePage: React.FC = () => { }, [user_id]); const [usageRecords, setUsageRecords] = useState([]); - const [sponsorships, setSponsorships] = useState([]); const [stats, setStats] = useState(null); const [expandedItems, setExpandedItems] = useState>(new Set()); const [hasMore, setHasMore] = useState(false); @@ -72,8 +74,19 @@ const UsagePage: React.FC = () => { const [selectedProvider, setSelectedProvider] = useState("all"); const [includeSponsored, setIncludeSponsored] = useState(true); const [excludeSelf, setExcludeSelf] = useState(false); + const [includeTransfers, setIncludeTransfers] = useState(true); + const [onlyTransfers, setOnlyTransfers] = useState(false); const [filtersExpanded, setFiltersExpanded] = useState(false); + const [dataRefreshCounter, setDataRefreshCounter] = useState(0); + + const [isTransferring, setIsTransferring] = useState(false); + const [transferPlatform, setTransferPlatform] = useState(Platform.TELEGRAM); + const [transferHandle, setTransferHandle] = useState(""); + const [transferAmount, setTransferAmount] = useState(""); + const [transferNote, setTransferNote] = useState(""); + const [isTransferSaving, setIsTransferSaving] = useState(false); + const currentInterfaceLanguage = INTERFACE_LANGUAGES.find((lang) => lang.isoCode === lang_iso_code) || DEFAULT_LANGUAGE; @@ -107,7 +120,7 @@ const UsagePage: React.FC = () => { const dateRange = getDateRange(timeRange); - const [records, statsData, sponsorshipsData] = await Promise.all([ + const [records, statsData] = await Promise.all([ fetchUsageRecords({ apiBaseUrl, user_id, @@ -115,6 +128,8 @@ const UsagePage: React.FC = () => { limit: RECORDS_PER_PAGE + 1, include_sponsored: includeSponsored, exclude_self: excludeSelf, + include_transfers: includeTransfers, + only_transfers: onlyTransfers, tool_id: selectedTool !== "all" ? selectedTool : undefined, purpose: selectedPurpose !== "all" ? selectedPurpose : undefined, provider_id: selectedProvider !== "all" ? selectedProvider : undefined, @@ -127,25 +142,20 @@ const UsagePage: React.FC = () => { rawToken: accessToken.raw, include_sponsored: includeSponsored, exclude_self: excludeSelf, + include_transfers: includeTransfers, + only_transfers: onlyTransfers, tool_id: selectedTool !== "all" ? selectedTool : undefined, purpose: selectedPurpose !== "all" ? selectedPurpose : undefined, provider_id: selectedProvider !== "all" ? selectedProvider : undefined, start_date: dateRange.start?.toISOString(), end_date: dateRange.end?.toISOString(), }), - fetchUserSponsorships({ - apiBaseUrl, - resource_id: user_id, - rawToken: accessToken.raw, - }), ]); console.info("Fetched usage records!", records.length); console.info("Fetched usage stats!", statsData); - console.info("Fetched sponsorships!", sponsorshipsData); setStats(statsData); - setSponsorships(sponsorshipsData.sponsorships); if (records.length > RECORDS_PER_PAGE) { setUsageRecords(records.slice(0, RECORDS_PER_PAGE)); @@ -160,7 +170,11 @@ const UsagePage: React.FC = () => { } } catch (err) { console.error("Error fetching data!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } }; fetchData(); @@ -175,6 +189,9 @@ const UsagePage: React.FC = () => { selectedProvider, includeSponsored, excludeSelf, + includeTransfers, + onlyTransfers, + dataRefreshCounter, ]); const getDateRange = (range: TimeRange): { start: Date | null; end: Date | null } => { @@ -238,6 +255,8 @@ const UsagePage: React.FC = () => { limit: RECORDS_PER_PAGE + 1, include_sponsored: includeSponsored, exclude_self: excludeSelf, + include_transfers: includeTransfers, + only_transfers: onlyTransfers, tool_id: selectedTool !== "all" ? selectedTool : undefined, purpose: selectedPurpose !== "all" ? selectedPurpose : undefined, provider_id: selectedProvider !== "all" ? selectedProvider : undefined, @@ -257,7 +276,11 @@ const UsagePage: React.FC = () => { } } catch (err) { console.error("Error loading more records!", err); - setError(PageError.simple("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err) + : PageError.simple("errors.fetch_failed"), + ); } finally { setIsLoadingMore(false); } @@ -271,6 +294,62 @@ const UsagePage: React.FC = () => { setShopOpen(true); }; + const handleStartTransfer = () => { + setIsTransferring(true); + setTransferHandle(""); + setTransferAmount(""); + setTransferNote(""); + setTransferPlatform(Platform.TELEGRAM); + }; + + const handleCancelTransfer = () => { + setIsTransferring(false); + setTransferHandle(""); + setTransferAmount(""); + setTransferNote(""); + setTransferPlatform(Platform.TELEGRAM); + }; + + const parsedTransferAmount = parseFloat(transferAmount); + const isTransferValid = + cleanUsername(transferHandle).length > 0 && + !isNaN(parsedTransferAmount) && + parsedTransferAmount >= 1.0; + + const handleSaveTransfer = async () => { + if (!isTransferValid || !user_id || !accessToken) return; + + setIsTransferSaving(true); + setError(null); + try { + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; + await createTransfer({ + apiBaseUrl, + user_id, + rawToken: accessToken.raw, + payload: { + platform: transferPlatform, + platform_handle: cleanUsername(transferHandle), + amount: parsedTransferAmount, + note: transferNote.trim() || undefined, + }, + }); + handleCancelTransfer(); + setDataRefreshCounter((c) => c + 1); + await refreshSettings(); + toast(t("saved")); + } catch (err) { + console.error("Error transferring credits!", err); + setError( + err instanceof ApiError + ? PageError.fromApiError(err) + : PageError.simple("errors.save_failed"), + ); + } finally { + setIsTransferSaving(false); + } + }; + return ( <> { /> } - actionButtonText={t("purchases.buy_credits")} - isContentLoading={isLoadingState} - externalError={error} - onExternalErrorDismiss={() => setError(null)} - > - {userSettings && ( - - - {t("usage.credit_balance", { balance: "" }).trim()} - - - {userSettings.credit_balance.toFixed(2)} - - + cardTitle={isTransferring ? t("usage.transfer.card_title") : t("usage.card_title")} + onActionClicked={isTransferring ? handleSaveTransfer : handleBuyMore} + actionDisabled={isTransferring ? !isTransferValid : false} + actionIcon={isTransferring ? undefined : } + actionButtonText={isTransferring ? t("save") : t("purchases.buy_credits")} + showSecondaryButton={!isTransferring} + onSecondaryClicked={handleStartTransfer} + secondaryIcon={} + secondaryTooltipText={t("usage.transfer.button")} + showCancelButton={isTransferring} + onCancelClicked={handleCancelTransfer} + isContentLoading={isLoadingState || isTransferSaving} + externalError={error} + onExternalErrorDismiss={() => setError(null)} + > + {isTransferring ? ( + + { + if (!error?.isBlocker && isTransferValid) { + handleSaveTransfer(); + } + }} + /> + { + if (!error?.isBlocker && isTransferValid) { + handleSaveTransfer(); + } + }} + /> + - )} - - - - {stats && } - - {stats && ( - - )} - - - {usageRecords.length === 0 ? ( - - - - {t("usage.no_records_found")} - - - ) : ( - <> - - - {usageRecords.map((record, index) => { - const isExpanded = expandedItems.has(index); - const toggleExpanded = () => { - const newExpandedItems = new Set(expandedItems); - if (isExpanded) { - newExpandedItems.delete(index); - } else { - newExpandedItems.add(index); - } - setExpandedItems(newExpandedItems); - }; - - return ( - - ); - })} + ) : ( + <> + {userSettings && ( + + + {t("usage.credit_balance", { balance: "" }).trim()} + + + {userSettings.credit_balance.toFixed(2)} + + - - {hasMore && ( - - - {isLoadingMore ? t("loading_placeholder") : t("usage.load_more")} - + )} + + + + {stats && } + + {stats && ( + + )} + + + {usageRecords.length === 0 ? ( + + + + {t("usage.no_records_found")} + + ) : ( + <> + + + {usageRecords.map((record, index) => { + const isExpanded = expandedItems.has(index); + const toggleExpanded = () => { + const newExpandedItems = new Set(expandedItems); + if (isExpanded) { + newExpandedItems.delete(index); + } else { + newExpandedItems.add(index); + } + setExpandedItems(newExpandedItems); + }; + + return ( + + ); + })} + + + {hasMore && ( + + + {isLoadingMore ? t("loading_placeholder") : t("usage.load_more")} + + + )} + > )} - > - )} - + + > + )} > ); diff --git a/src/pages/UserSettingsPage.tsx b/src/pages/UserSettingsPage.tsx index 72bfbf8d..3d70d088 100644 --- a/src/pages/UserSettingsPage.tsx +++ b/src/pages/UserSettingsPage.tsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import BaseSettingsPage from "@/pages/BaseSettingsPage"; import { t } from "@/lib/translations"; import { usePageSession } from "@/hooks/usePageSession"; +import { ApiError } from "@/lib/api-error"; import { PageError } from "@/lib/utils"; import { toast } from "sonner"; import { ChevronsRight } from "lucide-react"; @@ -12,11 +13,8 @@ import { UserSettings, buildChangedPayload, areSettingsChanged, + hasAnyApiKey, } from "@/services/user-settings-service"; -import { - fetchExternalTools, - ExternalToolProviderResponse, -} from "@/services/external-tools-service"; import { useNavigation } from "@/hooks/useNavigation"; import SettingTextarea from "@/components/SettingTextarea"; import SettingInput from "@/components/SettingInput"; @@ -30,16 +28,12 @@ const UserSettingsPage: React.FC = () => { const { error, accessToken, isLoadingState, setError, setIsLoadingState } = usePageSession(); - const { navigateToAccess, navigateToIntelligence } = useNavigation(); + const { navigateToAccess, navigateToIntelligence, navigateToPurchases } = useNavigation(); const [userSettings, setUserSettings] = useState(null); const [remoteSettings, setRemoteSettings] = useState( null ); - const [externalToolProviders, setExternalToolProviders] = useState< - ExternalToolProviderResponse[] - >([]); - const botName = import.meta.env.VITE_APP_NAME_SHORT; // Fetch user settings and external tools when session is ready @@ -51,26 +45,21 @@ const UserSettingsPage: React.FC = () => { setError(null); try { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; - const [settings, externalTools] = await Promise.all([ - fetchUserSettings({ - apiBaseUrl, - user_id, - rawToken: accessToken.raw, - }), - fetchExternalTools({ - apiBaseUrl, - user_id, - rawToken: accessToken.raw, - }), - ]); + const settings = await fetchUserSettings({ + apiBaseUrl, + user_id, + rawToken: accessToken.raw, + }); console.info("Fetched settings!", settings); - console.info("Fetched external tools!", externalTools); setUserSettings(settings); setRemoteSettings(settings); - setExternalToolProviders(externalTools.providers); } catch (err) { console.error("Error fetching data!", err); - setError(PageError.blocker("errors.fetch_failed")); + setError( + err instanceof ApiError + ? PageError.fromApiError(err, true) + : PageError.blocker("errors.fetch_failed"), + ); } finally { setIsLoadingState(false); } @@ -121,7 +110,11 @@ const UserSettingsPage: React.FC = () => { toast(t("saved")); } catch (saveError) { console.error("Error saving settings!", saveError); - setError(PageError.simple("errors.save_failed")); + setError( + saveError instanceof ApiError + ? PageError.fromApiError(saveError) + : PageError.simple("errors.save_failed"), + ); } finally { setIsLoadingState(false); } @@ -211,19 +204,20 @@ const UserSettingsPage: React.FC = () => { className="w-full sm:w-auto" /> - {/* Provider configuration link */} + {/* Navigation links based on user setup status */} {(() => { - if (externalToolProviders.length === 0) return null; + if (!userSettings) return null; - const firstUnconfiguredProvider = externalToolProviders.find( - (p) => !p.is_configured - ); - const allConfigured = !firstUnconfiguredProvider; + const hasApiKeys = hasAnyApiKey(userSettings); + const hasCredits = (userSettings.credit_balance ?? 0) > 0; - if (allConfigured) { - // All providers configured - show Customize Intelligence link + const linkClass = + "underline underline-offset-3 decoration-accent-amber/70 text-accent-amber/70 hover:text-accent-amber cursor-pointer"; + const rowClass = "flex items-center gap-2 text-sm text-muted-foreground"; + + if (hasApiKeys || hasCredits) { return ( - + { @@ -231,35 +225,44 @@ const UserSettingsPage: React.FC = () => { navigateToIntelligence(user_id, lang_iso_code); } }} - className="underline underline-offset-3 decoration-accent-amber/70 text-accent-amber/70 hover:text-accent-amber cursor-pointer" + className={linkClass} > {t("configure_intelligence")} ); - } else { - // Has unconfigured providers - show Configure AI providers link - return ( - + } + + return ( + + + + { + if (user_id && lang_iso_code) { + navigateToPurchases(user_id, lang_iso_code); + } + }} + className={linkClass} + > + {t("purchases.buy_credits")} + + + { - if (user_id && lang_iso_code && firstUnconfiguredProvider) { - // Store provider ID in sessionStorage so Access page can scroll to it - sessionStorage.setItem( - "scrollToProvider", - firstUnconfiguredProvider.definition.id - ); + if (user_id && lang_iso_code) { navigateToAccess(user_id, lang_iso_code); } }} - className="underline underline-offset-3 decoration-accent-amber/70 text-accent-amber/70 hover:text-accent-amber cursor-pointer" + className={linkClass} > - {t("configure_ai_providers")} + {t("configure_access_keys")} - ); - } + + ); })()} diff --git a/src/services/external-tools-service.ts b/src/services/external-tools-service.ts index a21b7741..48c0221c 100644 --- a/src/services/external-tools-service.ts +++ b/src/services/external-tools-service.ts @@ -15,6 +15,7 @@ export type ToolType = | "api_fiat_exchange" | "api_crypto_exchange" | "api_twitter" + | "credit_transfer" | "deprecated"; export interface CostEstimate { diff --git a/src/services/usage-service.ts b/src/services/usage-service.ts index d5523907..48615ded 100644 --- a/src/services/usage-service.ts +++ b/src/services/usage-service.ts @@ -1,7 +1,21 @@ import { request } from "@/services/networking"; import { ExternalTool, ToolType } from "@/services/external-tools-service"; +import { Platform } from "@/lib/platform"; import { parseApiError } from "@/lib/api-error"; +export interface ParticipantInfo { + user_id: string; + full_name: string | null; + platform: string | null; + handle: string | null; +} + +export interface ParticipantDetails { + payer: ParticipantInfo; + owner: ParticipantInfo; + counterpart: ParticipantInfo | null; +} + export interface UsageRecord { user_id: string; payer_id: string; @@ -24,6 +38,9 @@ export interface UsageRecord { output_image_sizes?: string[]; input_image_sizes?: string[]; is_failed: boolean; + participant_details?: ParticipantDetails; + counterpart_id?: string; + note?: string; } export interface AggregateStats { @@ -63,6 +80,8 @@ export interface UsageRecordsParams { end_date?: string; exclude_self?: boolean; include_sponsored?: boolean; + include_transfers?: boolean; + only_transfers?: boolean; tool_id?: string; purpose?: string; provider_id?: string; @@ -76,6 +95,8 @@ export interface UsageStatsParams { end_date?: string; exclude_self?: boolean; include_sponsored?: boolean; + include_transfers?: boolean; + only_transfers?: boolean; tool_id?: string; purpose?: string; provider_id?: string; @@ -102,6 +123,8 @@ export async function fetchUsageRecords({ end_date, exclude_self, include_sponsored, + include_transfers, + only_transfers, tool_id, purpose, provider_id, @@ -117,6 +140,8 @@ export async function fetchUsageRecords({ end_date, exclude_self, include_sponsored, + include_transfers, + only_transfers, tool_id, purpose, provider_id, @@ -142,6 +167,8 @@ export async function fetchUsageStats({ end_date, exclude_self, include_sponsored, + include_transfers, + only_transfers, tool_id, purpose, provider_id, @@ -155,6 +182,8 @@ export async function fetchUsageStats({ end_date, exclude_self, include_sponsored, + include_transfers, + only_transfers, tool_id, purpose, provider_id, @@ -172,6 +201,46 @@ export async function fetchUsageStats({ return response.json(); } +export interface CreditTransferPayload { + platform: Platform; + platform_handle: string; + amount: number; + note?: string; +} + +export interface StatusResponse { + status: "OK"; +} + +export async function createTransfer({ + apiBaseUrl, + user_id, + rawToken, + payload, +}: { + apiBaseUrl: string; + user_id: string; + rawToken: string; + payload: CreditTransferPayload; +}): Promise { + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${rawToken}`, + }; + const response = await request( + `${apiBaseUrl}/user/${user_id}/transfers`, + { + method: "POST", + headers, + body: JSON.stringify(payload), + } + ); + if (!response.ok) { + throw await parseApiError(response); + } + return response.json(); +} + export type TimeRange = | "all" | "10min" diff --git a/src/services/user-settings-cache.ts b/src/services/user-settings-cache.ts index 64a57bda..a984d414 100644 --- a/src/services/user-settings-cache.ts +++ b/src/services/user-settings-cache.ts @@ -8,6 +8,18 @@ interface CacheEntry { // In-memory cache: persists during the session, clears on page reload/hard refresh const userSettingsCache = new Map(); +type CacheListener = () => void; +const cacheListeners = new Set(); + +export function subscribeToCacheInvalidation(listener: CacheListener): () => void { + cacheListeners.add(listener); + return () => cacheListeners.delete(listener); +} + +function notifyListeners(): void { + cacheListeners.forEach((listener) => listener()); +} + // TTL (time-to-live) in milliseconds. Set to 0 to disable automatic expiry. // 5 minutes seems reasonable to keep credit balance relatively fresh while avoiding excessive calls const CACHE_TTL_MS = 5 * 60 * 1000; @@ -45,6 +57,7 @@ export function setCachedSettings(userId: string, settings: UserSettings): void */ export function clearUserSettingsCache(userId: string): void { userSettingsCache.delete(userId); + notifyListeners(); } /** diff --git a/src/services/user-settings-service.ts b/src/services/user-settings-service.ts index 9d9b8c28..282f27dd 100644 --- a/src/services/user-settings-service.ts +++ b/src/services/user-settings-service.ts @@ -166,6 +166,10 @@ export function buildChangedPayload( return payload; } +export function hasAnyApiKey(userSettings: UserSettings): boolean { + return MASKED_FIELDS.some((field) => !!userSettings[field]); +} + export function areSettingsChanged( userSettings: UserSettings, remoteSettings: UserSettings,
- {t("usage.no_records_found")} -
+ {t("usage.no_records_found")} +