From c305872ddc4c453f2a9f2ada4011432db989c2ba Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 13 Feb 2026 18:33:21 +0100 Subject: [PATCH] feat(coding-agent): added real-time OAuth provider authentication validation with status indicators - Added real-time OAuth provider authentication validation with visual status indicators (checking, valid, invalid). - Added `validateAuth` and `requestRender` options to OAuthSelectorComponent for custom authentication validation and UI refresh callbacks. - Changed OAuth provider selector to display live authentication status instead of static login state. - Updated logout flow to refresh OAuth provider authentication state before showing selector. --- bun.lock | 199 +++--- packages/ai/CHANGELOG.md | 8 + packages/ai/src/utils/oauth/index.ts | 36 +- packages/ai/src/utils/oauth/perplexity.ts | 217 ------- packages/coding-agent/CHANGELOG.md | 9 + .../src/modes/components/oauth-selector.ts | 117 +++- .../modes/controllers/selector-controller.ts | 40 +- packages/stats/package.json | 17 +- packages/stats/src/client/App.tsx | 613 ++---------------- .../src/client/components/ChartsContainer.tsx | 185 ++++++ .../stats/src/client/components/Header.tsx | 48 ++ .../src/client/components/ModelsTable.tsx | 343 ++++++++++ .../src/client/components/RequestDetail.tsx | 210 +++--- .../src/client/components/RequestList.tsx | 149 ++--- .../stats/src/client/components/StatCard.tsx | 32 - .../stats/src/client/components/StatsGrid.tsx | 88 +++ packages/stats/src/client/index.tsx | 1 + packages/stats/src/client/styles.css | 277 ++++++++ packages/stats/src/server.ts | 71 +- packages/stats/tailwind.config.js | 40 ++ 20 files changed, 1537 insertions(+), 1163 deletions(-) create mode 100644 packages/stats/src/client/components/ChartsContainer.tsx create mode 100644 packages/stats/src/client/components/Header.tsx create mode 100644 packages/stats/src/client/components/ModelsTable.tsx delete mode 100644 packages/stats/src/client/components/StatCard.tsx create mode 100644 packages/stats/src/client/components/StatsGrid.tsx create mode 100644 packages/stats/src/client/styles.css create mode 100644 packages/stats/tailwind.config.js diff --git a/bun.lock b/bun.lock index 877ce35ca..2c9e414ad 100644 --- a/bun.lock +++ b/bun.lock @@ -131,16 +131,19 @@ "dependencies": { "@oh-my-pi/pi-ai": "workspace:*", "@oh-my-pi/pi-utils": "workspace:*", + "chart.js": "4.5.1", "date-fns": "^4.1.0", "lucide-react": "^0.563.0", "react": "^19.2.4", + "react-chartjs-2": "5.3.1", "react-dom": "^19.2.4", - "recharts": "^3.7.0", }, "devDependencies": { "@types/bun": "^1.3.9", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", + "postcss": "8.5.6", + "tailwindcss": "3", }, }, "packages/swarm-extension": { @@ -185,6 +188,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.74.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -299,10 +304,26 @@ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@mistralai/mistralai": ["@mistralai/mistralai@1.14.0", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-6zaj2f2LCd37cRpBvCgctkDbXtYBlAC85p+u4uU/726zjtsI+sdVH34qRzkm9iE3tRb8BoaiI0/P7TD+uMvLLQ=="], "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oh-my-pi/omp-stats": ["@oh-my-pi/omp-stats@workspace:packages/stats"], "@oh-my-pi/pi-agent-core": ["@oh-my-pi/pi-agent-core@workspace:packages/agent"], @@ -345,8 +366,6 @@ "@puppeteer/browsers": ["@puppeteer/browsers@2.12.1", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-fXa6uXLxfslBlus3MEpW8S6S9fe5RwmAE5Gd8u3krqOwnkZJV3/lQJiY3LaFdTctLLqJtyMgEUGkbDnRNf6vbQ=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], "@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="], @@ -441,10 +460,6 @@ "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -455,24 +470,6 @@ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], @@ -485,8 +482,6 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260213.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260213.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260213.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260213.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260213.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260213.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260213.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260213.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-TZ/0Tv954jRpn5IHQJZQF0gaNmw//ZTvkgr3nC2H8u9t4GxUhIk5vfAHVFgdl8JnNGbl7gZks37FVsEwXjdHqg=="], @@ -523,6 +518,12 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], @@ -551,6 +552,8 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -567,8 +570,14 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -577,8 +586,6 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], @@ -603,46 +610,28 @@ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], - "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], "devtools-protocol": ["devtools-protocol@0.0.1566079", "", {}, "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -669,8 +658,6 @@ "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], @@ -693,10 +680,14 @@ "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -717,6 +708,10 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], @@ -731,6 +726,8 @@ "glob": ["glob@13.0.3", "", { "dependencies": { "minimatch": "^10.2.0", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -741,6 +738,8 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -755,20 +754,24 @@ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-network-error": ["is-network-error@1.3.0", "", {}, "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -779,6 +782,8 @@ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -797,6 +802,8 @@ "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], @@ -817,6 +824,8 @@ "marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -837,8 +846,12 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], @@ -849,8 +862,12 @@ "node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -879,6 +896,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -889,6 +908,24 @@ "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], @@ -905,36 +942,38 @@ "puppeteer-core": ["puppeteer-core@24.37.3", "", { "dependencies": { "@puppeteer/browsers": "2.12.1", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-fokQ8gv+hNgsRWqVuP5rUjGp+wzV5aMTP3fcm8ekNabmLGlJdFHas1OdMscAH9Gzq4Qcf7cfI/Pe6wEcAqQhqg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], - - "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], - - "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -965,6 +1004,8 @@ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], @@ -985,8 +1026,14 @@ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + "tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], @@ -995,7 +1042,9 @@ "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -1009,6 +1058,8 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1025,12 +1076,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="], @@ -1071,7 +1118,7 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1087,6 +1134,8 @@ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -1099,6 +1148,8 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b2fa40203..f0182d10a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog ## [Unreleased] +### Changed + +- Perplexity OAuth token refresh now uses JWT expiry extraction instead of Socket.IO RPC, improving reliability when server is unreachable +- Removed Socket.IO client implementation for Perplexity token refresh; tokens are now validated using embedded JWT expiry claims + +### Removed + +- Removed `refreshPerplexityToken` export; token refresh is now handled internally via JWT expiry detection ## [12.0.0] - 2026-02-12 diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 01c2d1b3b..eb0a47791 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -9,7 +9,6 @@ import { refreshAntigravityToken } from "./google-antigravity"; import { refreshGoogleCloudToken } from "./google-gemini-cli"; import { refreshKimiToken } from "./kimi"; import { refreshOpenAICodexToken } from "./openai-codex"; -import { refreshPerplexityToken } from "./perplexity"; import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types"; /** @@ -56,7 +55,7 @@ export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex"; // OpenCode (API key) export { loginOpenCode } from "./opencode"; // Perplexity -export { loginPerplexity, refreshPerplexityToken } from "./perplexity"; +export { loginPerplexity } from "./perplexity"; export * from "./types"; // Z.AI (API key) export { loginZai } from "./zai"; @@ -104,8 +103,6 @@ export async function refreshOAuthToken( newCredentials = await refreshCursorToken(credentials.refresh); break; case "perplexity": - newCredentials = await refreshPerplexityToken(credentials.refresh); - break; case "opencode": case "zai": case "minimax-code": @@ -120,6 +117,20 @@ export async function refreshOAuthToken( return newCredentials; } +function getPerplexityJwtExpiryMs(token: string): number | undefined { + const parts = token.split("."); + if (parts.length !== 3) return undefined; + const payload = parts[1]; + if (!payload) return undefined; + try { + const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: unknown }; + if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) return undefined; + return decoded.exp * 1000 - 5 * 60_000; + } catch { + return undefined; + } +} + /** * Get API key for a provider from OAuth credentials. * Automatically refreshes expired tokens. @@ -138,11 +149,28 @@ export async function getOAuthApiKey( return null; } + if (provider === "perplexity") { + const normalizedExpires = + creds.expires > 0 && creds.expires < 10_000_000_000 ? creds.expires * 1000 : creds.expires; + const jwtExpiry = getPerplexityJwtExpiryMs(creds.access); + const expires = jwtExpiry && jwtExpiry > normalizedExpires ? jwtExpiry : normalizedExpires; + if (expires !== creds.expires) { + creds = { ...creds, expires }; + } + } + // Refresh if expired if (Date.now() >= creds.expires) { try { creds = await refreshOAuthToken(provider, creds); } catch { + if (provider === "perplexity") { + const jwtExpiry = getPerplexityJwtExpiryMs(creds.access); + if (jwtExpiry && Date.now() < jwtExpiry) { + const fallbackCredentials = { ...creds, expires: jwtExpiry }; + return { newCredentials: fallbackCredentials, apiKey: fallbackCredentials.access }; + } + } throw new Error(`Failed to refresh OAuth token for ${provider}`); } } diff --git a/packages/ai/src/utils/oauth/perplexity.ts b/packages/ai/src/utils/oauth/perplexity.ts index 20f3c892f..1f80864b0 100644 --- a/packages/ai/src/utils/oauth/perplexity.ts +++ b/packages/ai/src/utils/oauth/perplexity.ts @@ -16,195 +16,9 @@ import { $env } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import type { OAuthController, OAuthCredentials } from "./types"; -const WS_URL = "wss://www.perplexity.ai/socket.io/?EIO=4&transport=websocket"; const API_VERSION = "2.18"; const NATIVE_APP_BUNDLE = "ai.perplexity.mac"; const APP_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0"; -const CONNECT_TIMEOUT_MS = 10_000; -const RPC_TIMEOUT_MS = 30_000; - -// --------------------------------------------------------------------------- -// Socket.IO v4 client (Engine.IO v4 transport: WebSocket only) -// --------------------------------------------------------------------------- - -/** - * Minimal Socket.IO v4 client over Engine.IO v4 WebSocket transport. - * - * Supports: - * - Authenticated namespace connect (`40{...auth}`) - * - Fire-and-forget events (Socket.IO EVENT, type `42`) - * - Request-reply RPCs via ack pattern (EVENT with ack id → ACK response) - * - Engine.IO ping/pong keepalive - */ -class PerplexitySocket { - #ws: WebSocket; - #nextAckId = 0; - #pendingAcks = new Map void; reject: (e: Error) => void; timer: Timer }>(); - #ready: Promise; - #messageHandler?: (data: string) => void; - - constructor(auth?: Record) { - this.#ws = new WebSocket(WS_URL, { - headers: { - "User-Agent": "PerplexityApp", - "X-App-ApiVersion": API_VERSION, - }, - } as unknown as string[]); - - const { promise, resolve, reject } = Promise.withResolvers(); - this.#ready = promise; - - const timeout = setTimeout(() => reject(new Error("Socket.IO connection timeout")), CONNECT_TIMEOUT_MS); - let phase = 0; - - this.#ws.onmessage = ev => { - const data = String(ev.data); - - // Engine.IO ping → pong - if (data === "2") { - this.#ws.send("3"); - return; - } - - // Phase 0: Engine.IO OPEN packet → send Socket.IO CONNECT - if (data.startsWith("0{") && phase === 0) { - phase = 1; - const payload = auth ? `40${JSON.stringify(auth)}` : "40"; - this.#ws.send(payload); - return; - } - - // Phase 1: Socket.IO CONNECT acknowledgment - if (data.startsWith("40") && phase === 1) { - clearTimeout(timeout); - phase = 2; - this.#installRpcHandler(); - resolve(); - return; - } - - // Socket.IO CONNECT_ERROR (type 44) - if (data.startsWith("44")) { - clearTimeout(timeout); - const body = data.slice(2); - reject(new Error(`Socket.IO connect error: ${body}`)); - } - }; - - this.#ws.onerror = () => { - clearTimeout(timeout); - reject(new Error("WebSocket error")); - }; - this.#ws.onclose = () => { - clearTimeout(timeout); - reject(new Error("WebSocket closed during handshake")); - }; - } - - #installRpcHandler(): void { - this.#ws.onmessage = ev => { - const data = String(ev.data); - - if (data === "2") { - this.#ws.send("3"); - return; - } - - // Forward to external listener if set - this.#messageHandler?.(data); - - // Socket.IO ACK: 43 - if (data.startsWith("43")) { - const match = data.match(/^43(\d+)([\s\S]*)/); - if (!match) return; - - const ackId = Number.parseInt(match[1], 10); - const pending = this.#pendingAcks.get(ackId); - if (!pending) return; - - this.#pendingAcks.delete(ackId); - clearTimeout(pending.timer); - - let payload: unknown; - try { - payload = match[2] ? JSON.parse(match[2]) : undefined; - } catch { - pending.reject(new Error(`Malformed ACK payload for ack ${ackId}`)); - return; - } - - // ACK payload is a JSON array; unwrap first element - const result = Array.isArray(payload) ? (payload[0] as Record) : payload; - - if (result && typeof result === "object" && "error" in result) { - const r = result as Record; - const msg = (r.error_message ?? r.error_code ?? r.error ?? "RPC error") as string; - pending.reject(new Error(msg)); - } else { - pending.resolve(result); - } - } - }; - - this.#ws.onclose = () => { - for (const p of this.#pendingAcks.values()) { - clearTimeout(p.timer); - p.reject(new Error("Socket closed")); - } - this.#pendingAcks.clear(); - }; - } - - /** Wait for the connection handshake to complete. */ - ready(): Promise { - return this.#ready; - } - - /** Set an external message listener (for debugging or event handling). */ - onMessage(handler: (data: string) => void): void { - this.#messageHandler = handler; - } - - /** Fire-and-forget event (Socket.IO EVENT, no ack). */ - emit(event: string, data: unknown): void { - this.#ws.send(`42${JSON.stringify([event, data])}`); - } - - /** Emit event and wait for ack response (request-reply RPC). */ - emitWithAck(event: string, data: unknown, timeoutMs = RPC_TIMEOUT_MS): Promise { - const id = this.#nextAckId++; - const { promise, resolve, reject } = Promise.withResolvers(); - - const timer = setTimeout(() => { - this.#pendingAcks.delete(id); - reject(new Error(`RPC timeout: ${event}`)); - }, timeoutMs); - - this.#pendingAcks.set(id, { - resolve: resolve as (v: unknown) => void, - reject, - timer, - }); - - // Socket.IO EVENT with ack: 42["event", data] - this.#ws.send(`42${id}${JSON.stringify([event, data])}`); - return promise; - } - - /** Close the socket and reject all pending RPCs. */ - close(): void { - for (const p of this.#pendingAcks.values()) { - clearTimeout(p.timer); - p.reject(new Error("Socket closed")); - } - this.#pendingAcks.clear(); - try { - this.#ws.close(); - } catch { - // Ignore close errors - } - } -} // --------------------------------------------------------------------------- // JWT helpers @@ -384,34 +198,3 @@ export async function loginPerplexity(ctrl: OAuthController): Promise { - const socket = new PerplexitySocket({ jwt: currentJwt }); - try { - await socket.ready(); - - const response = await socket.emitWithAck>("refreshJWT", { jwt: currentJwt }); - - // Response field is `perplexity_jwt` or `jwt` - const newJwt = - typeof response?.perplexity_jwt === "string" - ? response.perplexity_jwt - : typeof response?.jwt === "string" - ? response.jwt - : undefined; - - if (!newJwt) { - throw new Error("No JWT in refreshJWT response"); - } - - return jwtToCredentials(newJwt); - } finally { - socket.close(); - } -} diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9c5ddce5c..2c804cded 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog ## [Unreleased] +### Added + +- Added real-time authentication validation in OAuth provider selector with visual status indicators (checking, valid, invalid) +- Added `validateAuth` and `requestRender` options to OAuthSelectorComponent for custom authentication validation and UI refresh callbacks + +### Changed + +- Changed OAuth provider selector to display live authentication status instead of static login state +- Changed logout flow to refresh OAuth provider authentication state before showing selector ## [12.1.0] - 2026-02-13 diff --git a/packages/coding-agent/src/modes/components/oauth-selector.ts b/packages/coding-agent/src/modes/components/oauth-selector.ts index 864c4e958..6602805a9 100644 --- a/packages/coding-agent/src/modes/components/oauth-selector.ts +++ b/packages/coding-agent/src/modes/components/oauth-selector.ts @@ -3,9 +3,8 @@ import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui"; import { theme } from "../../modes/theme/theme"; import type { AuthStorage } from "../../session/auth-storage"; import { DynamicBorder } from "./dynamic-border"; - /** - * Component that renders an OAuth provider selector + * Component that renders an OAuth provider selector. */ export class OAuthSelectorComponent extends Container { #listContainer: Container; @@ -16,62 +15,139 @@ export class OAuthSelectorComponent extends Container { #onSelectCallback: (providerId: string) => void; #onCancelCallback: () => void; #statusMessage: string | undefined; - + #validateAuthCallback?: (providerId: string) => Promise; + #requestRenderCallback?: () => void; + #authState: Map = new Map(); + #spinnerFrame: number = 0; + #spinnerInterval?: NodeJS.Timeout; + #validationGeneration: number = 0; constructor( mode: "login" | "logout", authStorage: AuthStorage, onSelect: (providerId: string) => void, onCancel: () => void, + options?: { + validateAuth?: (providerId: string) => Promise; + requestRender?: () => void; + }, ) { super(); - this.#mode = mode; this.#authStorage = authStorage; this.#onSelectCallback = onSelect; this.#onCancelCallback = onCancel; - + this.#validateAuthCallback = options?.validateAuth; + this.#requestRenderCallback = options?.requestRender; // Load all OAuth providers this.#loadProviders(); - - // Add top border this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); - // Add title const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:"; this.addChild(new TruncatedText(theme.bold(title))); this.addChild(new Spacer(1)); - // Create list container this.#listContainer = new Container(); this.addChild(this.#listContainer); - this.addChild(new Spacer(1)); - // Add bottom border this.addChild(new DynamicBorder()); - // Initial render this.#updateList(); + this.#startValidation(); } + stopValidation(): void { + this.#validationGeneration += 1; + this.#stopSpinner(); + } #loadProviders(): void { this.#allProviders = getOAuthProviders(); } + #startValidation(): void { + if (!this.#validateAuthCallback) return; + const generation = this.#validationGeneration + 1; + this.#validationGeneration = generation; + + let pending = 0; + for (const provider of this.#allProviders) { + if (!this.#authStorage.hasAuth(provider.id)) { + this.#authState.delete(provider.id); + continue; + } + this.#authState.set(provider.id, "checking"); + pending += 1; + void this.#validateProvider(provider.id, generation); + } + + if (pending > 0) { + this.#startSpinner(); + this.#updateList(); + this.#requestRenderCallback?.(); + } + } + + async #validateProvider(providerId: string, generation: number): Promise { + if (!this.#validateAuthCallback) return; + let isValid = false; + try { + isValid = await this.#validateAuthCallback(providerId); + } catch { + isValid = false; + } + + if (generation !== this.#validationGeneration) return; + this.#authState.set(providerId, isValid ? "valid" : "invalid"); + if (![...this.#authState.values()].includes("checking")) { + this.#stopSpinner(); + } + this.#updateList(); + this.#requestRenderCallback?.(); + } + + #startSpinner(): void { + if (this.#spinnerInterval) return; + this.#spinnerInterval = setInterval(() => { + const frameCount = theme.spinnerFrames.length; + if (frameCount > 0) { + this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount; + } + this.#updateList(); + this.#requestRenderCallback?.(); + }, 80); + } + + #stopSpinner(): void { + if (this.#spinnerInterval) { + clearInterval(this.#spinnerInterval); + this.#spinnerInterval = undefined; + } + } + + #getStatusIndicator(providerId: string): string { + const state = this.#authState.get(providerId); + if (state === "checking") { + const frameCount = theme.spinnerFrames.length; + const spinner = frameCount > 0 ? theme.spinnerFrames[this.#spinnerFrame % frameCount] : theme.status.pending; + return theme.fg("warning", ` ${spinner} checking`); + } + if (state === "invalid") { + return theme.fg("error", ` ${theme.status.error} invalid`); + } + if (state === "valid") { + return theme.fg("success", ` ${theme.status.success} logged in`); + } + return this.#authStorage.hasAuth(providerId) ? theme.fg("success", ` ${theme.status.success} logged in`) : ""; + } #updateList(): void { this.#listContainer.clear(); - for (let i = 0; i < this.#allProviders.length; i++) { const provider = this.#allProviders[i]; if (!provider) continue; - const isSelected = i === this.#selectedIndex; const isAvailable = provider.available; - - // Check if user is logged in for this provider - const isLoggedIn = this.#authStorage.hasOAuth(provider.id); - const statusIndicator = isLoggedIn ? theme.fg("success", ` ${theme.status.success} logged in`) : ""; + const statusIndicator = this.#getStatusIndicator(provider.id); let line = ""; if (isSelected) { @@ -82,7 +158,6 @@ export class OAuthSelectorComponent extends Container { const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`); line = text + statusIndicator; } - this.#listContainer.addChild(new TruncatedText(line, 0, 0)); } @@ -92,13 +167,11 @@ export class OAuthSelectorComponent extends Container { this.#mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first."; this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0)); } - if (this.#statusMessage) { this.#listContainer.addChild(new Spacer(1)); this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0)); } } - handleInput(keyData: string): void { // Up arrow if (matchesKey(keyData, "up")) { @@ -121,6 +194,7 @@ export class OAuthSelectorComponent extends Container { const selectedProvider = this.#allProviders[this.#selectedIndex]; if (selectedProvider?.available) { this.#statusMessage = undefined; + this.stopValidation(); this.#onSelectCallback(selectedProvider.id); } else if (selectedProvider) { this.#statusMessage = "Provider unavailable in this environment."; @@ -129,6 +203,7 @@ export class OAuthSelectorComponent extends Container { } // Escape or Ctrl+C else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) { + this.stopValidation(); this.#onCancelCallback(); } } diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 52a011741..7da5bed99 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -1,5 +1,5 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; -import type { OAuthProvider } from "@oh-my-pi/pi-ai"; +import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui"; import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; @@ -32,6 +32,16 @@ import { setPreferredImageProvider, setPreferredSearchProvider } from "../../too export class SelectorController { constructor(private ctx: InteractiveModeContext) {} + async #refreshOAuthProviderAuthState(): Promise { + const oauthProviders = getOAuthProviders(); + await Promise.all( + oauthProviders.map(provider => + this.ctx.session.modelRegistry + .getApiKeyForProvider(provider.id, this.ctx.session.sessionId) + .catch(() => undefined), + ), + ); + } /** * Shows a selector component in place of the editor. * @param create Factory that receives a `done` callback and returns the component and focus target @@ -536,8 +546,11 @@ export class SelectorController { async showOAuthSelector(mode: "login" | "logout"): Promise { if (mode === "logout") { - const providers = this.ctx.session.modelRegistry.authStorage.list(); - const loggedInProviders = providers.filter(p => this.ctx.session.modelRegistry.authStorage.hasOAuth(p)); + await this.#refreshOAuthProviderAuthState(); + const oauthProviders = getOAuthProviders(); + const loggedInProviders = oauthProviders.filter(provider => + this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id), + ); if (loggedInProviders.length === 0) { this.ctx.showStatus("No OAuth providers logged in. Use /login first."); return; @@ -545,15 +558,15 @@ export class SelectorController { } this.showSelector(done => { - const selector = new OAuthSelectorComponent( + let selector: OAuthSelectorComponent; + selector = new OAuthSelectorComponent( mode, this.ctx.session.modelRegistry.authStorage, async (providerId: string) => { + selector.stopValidation(); done(); - if (mode === "login") { this.ctx.showStatus(`Logging in to ${providerId}…`); - try { await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, { onAuth: (info: { url: string; instructions?: string }) => { @@ -567,7 +580,6 @@ export class SelectorController { this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0)); } this.ctx.ui.requestRender(); - this.ctx.openInBrowser(info.url); }, onPrompt: async (prompt: { message: string; placeholder?: string }) => { @@ -577,7 +589,6 @@ export class SelectorController { this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0)); } this.ctx.ui.requestRender(); - return new Promise(resolve => { const codeInput = new Input(); codeInput.onSubmit = () => { @@ -638,9 +649,22 @@ export class SelectorController { } }, () => { + selector.stopValidation(); done(); this.ctx.ui.requestRender(); }, + { + validateAuth: async (providerId: string) => { + const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider( + providerId, + this.ctx.session.sessionId, + ); + return !!apiKey; + }, + requestRender: () => { + this.ctx.ui.requestRender(); + }, + }, ); return { component: selector, focus: selector }; }); diff --git a/packages/stats/package.json b/packages/stats/package.json index fda291150..00791c3cc 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -48,22 +48,27 @@ "directory": "packages/stats" }, "homepage": "https://github.com/can1357/oh-my-pi", - "bugs": { "url": "https://github.com/can1357/oh-my-pi/issues" }, + "bugs": { + "url": "https://github.com/can1357/oh-my-pi/issues" + }, "dependencies": { "@oh-my-pi/pi-ai": "workspace:*", + "@oh-my-pi/pi-utils": "workspace:*", + "chart.js": "4.5.1", "date-fns": "^4.1.0", "lucide-react": "^0.563.0", "react": "^19.2.4", - "react-dom": "^19.2.4", - "recharts": "^3.7.0", - "@oh-my-pi/pi-utils": "workspace:*" + "react-chartjs-2": "5.3.1", + "react-dom": "^19.2.4" }, "devDependencies": { "@types/bun": "^1.3.9", "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.3" + "@types/react-dom": "^19.2.3", + "postcss": "8.5.6", + "tailwindcss": "3" }, "engines": { "bun": ">=1.3.7" } -} \ No newline at end of file +} diff --git a/packages/stats/src/client/App.tsx b/packages/stats/src/client/App.tsx index 52470563f..569c1775c 100644 --- a/packages/stats/src/client/App.tsx +++ b/packages/stats/src/client/App.tsx @@ -1,381 +1,14 @@ -import { format } from "date-fns"; -import { Activity, AlertCircle, BarChart2, ChevronDown, ChevronUp, Database, RefreshCw, Server } from "lucide-react"; -import type { ReactNode } from "react"; import { useCallback, useEffect, useState } from "react"; -import { - Area, - AreaChart, - CartesianGrid, - Legend, - Line, - LineChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; import { getRecentErrors, getRecentRequests, getStats, sync } from "./api"; +import { ChartsContainer } from "./components/ChartsContainer"; +import { Header } from "./components/Header"; +import { ModelsTable } from "./components/ModelsTable"; import { RequestDetail } from "./components/RequestDetail"; import { RequestList } from "./components/RequestList"; -import { StatCard } from "./components/StatCard"; -import type { DashboardStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint } from "./types"; +import { StatsGrid } from "./components/StatsGrid"; +import type { DashboardStats, MessageStats } from "./types"; -const MODEL_COLORS = ["#60a5fa", "#34d399", "#fbbf24", "#f87171", "#a78bfa", "#38bdf8", "#f472b6"]; - -function ChartCard({ title, subtitle, children }: { title: string; subtitle?: string; children: ReactNode }) { - return ( -
-
-
{title}
- {subtitle ?
{subtitle}
: null} -
-
{children}
-
- ); -} - -function formatDateTick(timestamp: number): string { - return format(new Date(timestamp), "MMM d"); -} - -function buildModelPreferenceSeries( - points: ModelTimeSeriesPoint[], - topN = 5, -): { - data: Array>; - series: string[]; -} { - if (points.length === 0) return { data: [], series: [] }; - - const totals = new Map(); - for (const point of points) { - const key = `${point.model}::${point.provider}`; - const label = `${point.model} (${point.provider})`; - const existing = totals.get(key); - if (existing) { - existing.total += point.requests; - } else { - totals.set(key, { label, total: point.requests }); - } - } - - const sorted = [...totals.values()].sort((a, b) => b.total - a.total); - const topLabels = sorted.slice(0, topN).map(entry => entry.label); - const dataMap = new Map>(); - - for (const point of points) { - const label = `${point.model} (${point.provider})`; - const bucket = dataMap.get(point.timestamp) ?? { timestamp: point.timestamp, total: 0 }; - bucket.total += point.requests; - const key = topLabels.includes(label) ? label : "Other"; - bucket[key] = (bucket[key] ?? 0) + point.requests; - dataMap.set(point.timestamp, bucket); - } - - const series = [...topLabels]; - if ([...dataMap.values()].some(row => (row.Other ?? 0) > 0)) { - series.push("Other"); - } - - const data = [...dataMap.values()] - .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)) - .map(row => { - const total = row.total ?? 0; - for (const key of series) { - row[key] = total > 0 ? ((row[key] ?? 0) / total) * 100 : 0; - } - return row; - }); - - return { data, series }; -} - -type ModelPerformanceSeries = { - label: string; - data: Array<{ - timestamp: number; - avgTtftSeconds: number | null; - avgTokensPerSecond: number | null; - requests: number; - }>; -}; - -function buildModelPerformanceLookup( - points: ModelPerformancePoint[], - days = 14, -): { buckets: number[]; seriesByKey: Map } { - const dayMs = 24 * 60 * 60 * 1000; - const maxTimestamp = points.reduce((max, point) => Math.max(max, point.timestamp), 0); - const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() / dayMs) * dayMs; - const start = anchor - (days - 1) * dayMs; - const buckets = Array.from({ length: days }, (_, index) => start + index * dayMs); - const bucketIndex = new Map(buckets.map((timestamp, index) => [timestamp, index])); - const seriesByKey = new Map(); - - for (const point of points) { - const key = `${point.model}::${point.provider}`; - let series = seriesByKey.get(key); - if (!series) { - series = { - label: `${point.model} (${point.provider})`, - data: buckets.map(timestamp => ({ - timestamp, - avgTtftSeconds: null, - avgTokensPerSecond: null, - requests: 0, - })), - }; - seriesByKey.set(key, series); - } - - const index = bucketIndex.get(point.timestamp); - if (index === undefined) continue; - - series.data[index] = { - timestamp: point.timestamp, - avgTtftSeconds: point.avgTtft !== null ? point.avgTtft / 1000 : null, - avgTokensPerSecond: point.avgTokensPerSecond, - requests: point.requests, - }; - } - - return { buckets, seriesByKey }; -} - -function ModelStatsTable({ - models, - performanceSeriesByKey, -}: { - models: ModelStats[]; - performanceSeriesByKey: Map; -}) { - const [expandedKey, setExpandedKey] = useState(null); - const sortedModels = [...models].sort( - (a, b) => b.totalInputTokens + b.totalOutputTokens - (a.totalInputTokens + a.totalOutputTokens), - ); - - return ( -
-
-

Model Statistics

-
-
-
-
Model
-
Requests
-
Cost
-
Tokens
-
Tokens/s
-
TTFT
-
14d Trend
-
-
-
- {sortedModels.map((model, index) => { - const key = `${model.model}::${model.provider}`; - const performance = performanceSeriesByKey.get(key); - const trendData = performance?.data ?? []; - const trendColor = MODEL_COLORS[index % MODEL_COLORS.length]; - const isExpanded = expandedKey === key; - - return ( -
- - {isExpanded && ( -
-
-
-
-
Quality
-
Errors: {(model.errorRate * 100).toFixed(1)}%
-
Cache rate: {(model.cacheRate * 100).toFixed(1)}%
-
-
-
Latency
-
- Avg duration:{" "} - {model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"} -
-
- Avg TTFT: {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"} -
-
-
-
- {trendData.length === 0 ? ( -
- No data yet -
- ) : ( - - - - - `${value}s`} - /> - - - typeof label === "number" ? formatDateTick(label) : "" - } - formatter={( - value: number | string | undefined, - name: string | undefined, - ) => { - const numericValue = value ?? 0; - if (name === "avgTtftSeconds") - return [`${Number(numericValue).toFixed(2)}s`, "TTFT"]; - return [`${Number(numericValue).toFixed(1)}`, "Tokens/s"]; - }} - /> - (value === "avgTtftSeconds" ? "TTFT" : "Tokens/s")} - /> - - - - - )} -
-
-
- )} -
- ); - })} -
-
-
- ); -} +type Tab = "overview" | "requests" | "errors" | "models"; export default function App() { const [stats, setStats] = useState(null); @@ -383,7 +16,7 @@ export default function App() { const [recentErrors, setRecentErrors] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [syncing, setSyncing] = useState(false); - const [activeTab, setActiveTab] = useState<"overview" | "requests" | "errors" | "models">("overview"); + const [activeTab, setActiveTab] = useState("overview"); const loadData = useCallback(async () => { try { @@ -412,202 +45,72 @@ export default function App() { return () => clearInterval(interval); }, [loadData]); - if (!stats) return
Loading stats...
; - - const { seriesByKey: performanceSeriesByKey } = buildModelPerformanceLookup(stats.modelPerformanceSeries); - const modelPreference = buildModelPreferenceSeries(stats.modelSeries); + if (!stats) { + return ( +
+
+
+ Loading analytics... +
+
+ ); + } return ( -
-
-

- - AI Usage Statistics -

-
-
- {(["overview", "requests", "errors", "models"] as const).map(tab => ( - - ))} -
- -
-
+
+
+
- {activeTab === "overview" && ( - <> -
- } - /> - 0 - ? `$${(stats.overall.totalCost / stats.overall.totalRequests).toFixed(4)} avg/req` - : "-" - } - icon={} - /> - } - /> - } - color="var(--error)" - /> - } - /> - } - /> -
+ {activeTab === "overview" && ( +
+ -
+
+ r.id && setSelectedRequest(r.id)} + /> + r.id && setSelectedRequest(r.id)} + /> +
+
+ )} + + {activeTab === "requests" && ( +
r.id && setSelectedRequest(r.id)} /> +
+ )} + + {activeTab === "errors" && ( +
r.id && setSelectedRequest(r.id)} />
- - )} + )} - {activeTab === "requests" && ( -
- r.id && setSelectedRequest(r.id)} - /> -
- )} + {activeTab === "models" && ( +
+ + +
+ )} - {activeTab === "errors" && ( -
- r.id && setSelectedRequest(r.id)} - /> -
- )} - - {activeTab === "models" && ( -
- - {modelPreference.data.length === 0 ? ( -
- No data yet -
- ) : ( - - - - - `${value}%`} /> - - typeof label === "number" ? formatDateTick(label) : "" - } - formatter={(value: number | string | undefined) => [ - `${Number(value ?? 0).toFixed(1)}%`, - "Share", - ]} - /> - - {modelPreference.series.map((seriesName, index) => ( - - ))} - - - )} -
- -
- )} - - {selectedRequest !== null && setSelectedRequest(null)} />} + {selectedRequest !== null && ( + setSelectedRequest(null)} /> + )} +
); } diff --git a/packages/stats/src/client/components/ChartsContainer.tsx b/packages/stats/src/client/components/ChartsContainer.tsx new file mode 100644 index 000000000..e027bcced --- /dev/null +++ b/packages/stats/src/client/components/ChartsContainer.tsx @@ -0,0 +1,185 @@ +import { + CategoryScale, + Chart as ChartJS, + Filler, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from "chart.js"; +import { format } from "date-fns"; +import { useMemo } from "react"; +import { Line } from "react-chartjs-2"; +import type { ModelTimeSeriesPoint } from "../types"; + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler); + +const MODEL_COLORS = [ + "#a78bfa", // violet + "#22d3ee", // cyan + "#ec4899", // pink + "#4ade80", // green + "#fbbf24", // amber + "#f87171", // red + "#60a5fa", // blue +]; + +interface ChartsContainerProps { + modelSeries: ModelTimeSeriesPoint[]; +} + +export function ChartsContainer({ modelSeries }: ChartsContainerProps) { + const chartData = useMemo(() => buildModelPreferenceSeries(modelSeries), [modelSeries]); + + const data = { + labels: chartData.data.map(d => format(new Date(d.timestamp), "MMM d")), + datasets: chartData.series.map((seriesName, index) => ({ + label: seriesName, + data: chartData.data.map(d => d[seriesName] ?? 0), + borderColor: MODEL_COLORS[index % MODEL_COLORS.length], + backgroundColor: `${MODEL_COLORS[index % MODEL_COLORS.length]}20`, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4, + borderWidth: 2, + })), + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: "index" as const, + intersect: false, + }, + plugins: { + legend: { + position: "top" as const, + align: "start" as const, + labels: { + color: "var(--text-secondary)", + usePointStyle: true, + padding: 16, + font: { size: 12 }, + boxWidth: 8, + }, + }, + tooltip: { + backgroundColor: "var(--bg-elevated)", + titleColor: "var(--text-primary)", + bodyColor: "var(--text-secondary)", + borderColor: "var(--border-default)", + borderWidth: 1, + padding: 12, + cornerRadius: 8, + callbacks: { + label: (context: { dataset: { label?: string }; parsed: { y: number | null } }) => { + const label = context.dataset.label ?? ""; + const value = context.parsed.y; + return `${label}: ${(value ?? 0).toFixed(1)}%`; + }, + }, + }, + }, + scales: { + x: { + grid: { + color: "var(--border-subtle)", + drawBorder: false, + }, + ticks: { + color: "var(--text-muted)", + font: { size: 11 }, + }, + }, + y: { + grid: { + color: "var(--border-subtle)", + drawBorder: false, + }, + ticks: { + color: "var(--text-muted)", + font: { size: 11 }, + callback: (value: number | string) => `${value}%`, + }, + min: 0, + max: 100, + }, + }, + }; + + return ( +
+
+

Model Preference

+

Share of requests over the last 14 days

+
+
+ {chartData.data.length === 0 ? ( +
+ No data available +
+ ) : ( +
+ +
+ )} +
+
+ ); +} + +function buildModelPreferenceSeries( + points: ModelTimeSeriesPoint[], + topN = 5, +): { + data: Array>; + series: string[]; +} { + if (points.length === 0) return { data: [], series: [] }; + + const totals = new Map(); + for (const point of points) { + const key = `${point.model}::${point.provider}`; + const label = `${point.model} (${point.provider})`; + const existing = totals.get(key); + if (existing) { + existing.total += point.requests; + } else { + totals.set(key, { label, total: point.requests }); + } + } + + const sorted = [...totals.values()].sort((a, b) => b.total - a.total); + const topLabels = sorted.slice(0, topN).map(entry => entry.label); + const dataMap = new Map>(); + + for (const point of points) { + const label = `${point.model} (${point.provider})`; + const bucket = dataMap.get(point.timestamp) ?? { timestamp: point.timestamp, total: 0 }; + bucket.total += point.requests; + const key = topLabels.includes(label) ? label : "Other"; + bucket[key] = (bucket[key] ?? 0) + point.requests; + dataMap.set(point.timestamp, bucket); + } + + const series = [...topLabels]; + if ([...dataMap.values()].some(row => (row.Other ?? 0) > 0)) { + series.push("Other"); + } + + const data = [...dataMap.values()] + .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)) + .map(row => { + const total = row.total ?? 0; + for (const key of series) { + row[key] = total > 0 ? ((row[key] ?? 0) / total) * 100 : 0; + } + return row; + }); + + return { data, series }; +} diff --git a/packages/stats/src/client/components/Header.tsx b/packages/stats/src/client/components/Header.tsx new file mode 100644 index 000000000..1f974ae1d --- /dev/null +++ b/packages/stats/src/client/components/Header.tsx @@ -0,0 +1,48 @@ +import { Activity, RefreshCw } from "lucide-react"; + +type Tab = "overview" | "requests" | "errors" | "models"; + +interface HeaderProps { + activeTab: Tab; + onTabChange: (tab: Tab) => void; + onSync: () => void; + syncing: boolean; +} + +const tabs: Tab[] = ["overview", "requests", "errors", "models"]; + +export function Header({ activeTab, onTabChange, onSync, syncing }: HeaderProps) { + return ( +
+
+
+ +
+
+

AI Usage

+

Statistics & Analytics

+
+
+ +
+
+ {tabs.map(tab => ( + + ))} +
+ + +
+
+ ); +} diff --git a/packages/stats/src/client/components/ModelsTable.tsx b/packages/stats/src/client/components/ModelsTable.tsx new file mode 100644 index 000000000..d7ee28756 --- /dev/null +++ b/packages/stats/src/client/components/ModelsTable.tsx @@ -0,0 +1,343 @@ +import { + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from "chart.js"; +import { format } from "date-fns"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Line } from "react-chartjs-2"; +import type { ModelPerformancePoint, ModelStats } from "../types"; + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); + +const MODEL_COLORS = [ + "#a78bfa", // violet + "#22d3ee", // cyan + "#ec4899", // pink + "#4ade80", // green + "#fbbf24", // amber + "#f87171", // red + "#60a5fa", // blue +]; + +interface ModelsTableProps { + models: ModelStats[]; + performanceSeries: ModelPerformancePoint[]; +} + +type ModelPerformanceSeries = { + label: string; + data: Array<{ + timestamp: number; + avgTtftSeconds: number | null; + avgTokensPerSecond: number | null; + requests: number; + }>; +}; + +export function ModelsTable({ models, performanceSeries }: ModelsTableProps) { + const [expandedKey, setExpandedKey] = useState(null); + + const performanceSeriesByKey = useMemo(() => buildModelPerformanceLookup(performanceSeries), [performanceSeries]); + const sortedModels = [...models].sort( + (a, b) => b.totalInputTokens + b.totalOutputTokens - (a.totalInputTokens + a.totalOutputTokens), + ); + + return ( +
+
+

Model Statistics

+
+ +
+
+
Model
+
Requests
+
Cost
+
Tokens
+
Tokens/s
+
TTFT
+
14d Trend
+
+
+ +
+ {sortedModels.map((model, index) => { + const key = `${model.model}::${model.provider}`; + const performance = performanceSeriesByKey.get(key); + const trendData = performance?.data ?? []; + const trendColor = MODEL_COLORS[index % MODEL_COLORS.length]; + const isExpanded = expandedKey === key; + const errorRate = model.errorRate * 100; + + return ( +
+ + + {isExpanded && ( +
+
+
+
+
Quality
+
+
+ Error rate + 5 + ? "text-[var(--accent-red)]" + : "text-[var(--accent-green)]" + } + > + {errorRate.toFixed(1)}% + +
+
+ Cache rate + + {(model.cacheRate * 100).toFixed(1)}% + +
+
+
+
+
Latency
+
+
+ Avg duration + + {model.avgDuration ? `${(model.avgDuration / 1000).toFixed(2)}s` : "-"} + +
+
+ Avg TTFT + + {model.avgTtft ? `${(model.avgTtft / 1000).toFixed(2)}s` : "-"} + +
+
+
+
+
+ {trendData.length === 0 ? ( +
+ No data available +
+ ) : ( + + )} +
+
+
+ )} +
+ ); + })} +
+
+
+ ); +} + +function TrendChart({ + data, + color, +}: { + data: Array<{ timestamp: number; avgTokensPerSecond: number | null }>; + color: string; +}) { + const chartData = { + labels: data.map(d => format(new Date(d.timestamp), "MMM d")), + datasets: [ + { + data: data.map(d => d.avgTokensPerSecond ?? 0), + borderColor: color, + backgroundColor: "transparent", + tension: 0.4, + pointRadius: 0, + borderWidth: 2, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false, min: 0 }, + }, + }; + + return ; +} + +function PerformanceChart({ + data, + color, +}: { + data: Array<{ timestamp: number; avgTtftSeconds: number | null; avgTokensPerSecond: number | null }>; + color: string; +}) { + const chartData = { + labels: data.map(d => format(new Date(d.timestamp), "MMM d")), + datasets: [ + { + label: "TTFT", + data: data.map(d => d.avgTtftSeconds ?? null), + borderColor: "#fbbf24", + backgroundColor: "transparent", + tension: 0.4, + pointRadius: 0, + borderWidth: 2, + yAxisID: "y" as const, + }, + { + label: "Tokens/s", + data: data.map(d => d.avgTokensPerSecond ?? null), + borderColor: color, + backgroundColor: "transparent", + tension: 0.4, + pointRadius: 0, + borderWidth: 2, + yAxisID: "y1" as const, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: "top" as const, + labels: { + color: "var(--text-secondary)", + usePointStyle: true, + padding: 12, + font: { size: 10 }, + }, + }, + tooltip: { + backgroundColor: "var(--bg-elevated)", + titleColor: "var(--text-primary)", + bodyColor: "var(--text-secondary)", + borderColor: "var(--border-default)", + borderWidth: 1, + cornerRadius: 8, + }, + }, + scales: { + x: { + grid: { color: "var(--border-subtle)" }, + ticks: { color: "var(--text-muted)", font: { size: 10 } }, + }, + y: { + type: "linear" as const, + display: true, + position: "left" as const, + grid: { color: "var(--border-subtle)" }, + ticks: { color: "var(--text-muted)", font: { size: 10 } }, + }, + y1: { + type: "linear" as const, + display: true, + position: "right" as const, + grid: { drawOnChartArea: false }, + ticks: { color: "var(--text-muted)", font: { size: 10 } }, + }, + }, + }; + + return ; +} + +function buildModelPerformanceLookup(points: ModelPerformancePoint[], days = 14): Map { + const dayMs = 24 * 60 * 60 * 1000; + const maxTimestamp = points.reduce((max, point) => Math.max(max, point.timestamp), 0); + const anchor = maxTimestamp > 0 ? maxTimestamp : Math.floor(Date.now() / dayMs) * dayMs; + const start = anchor - (days - 1) * dayMs; + const buckets = Array.from({ length: days }, (_, index) => start + index * dayMs); + const bucketIndex = new Map(buckets.map((timestamp, index) => [timestamp, index])); + const seriesByKey = new Map(); + + for (const point of points) { + const key = `${point.model}::${point.provider}`; + let series = seriesByKey.get(key); + if (!series) { + series = { + label: `${point.model} (${point.provider})`, + data: buckets.map(timestamp => ({ + timestamp, + avgTtftSeconds: null, + avgTokensPerSecond: null, + requests: 0, + })), + }; + seriesByKey.set(key, series); + } + + const index = bucketIndex.get(point.timestamp); + if (index === undefined) continue; + + series.data[index] = { + timestamp: point.timestamp, + avgTtftSeconds: point.avgTtft !== null ? point.avgTtft / 1000 : null, + avgTokensPerSecond: point.avgTokensPerSecond, + requests: point.requests, + }; + } + + return seriesByKey; +} diff --git a/packages/stats/src/client/components/RequestDetail.tsx b/packages/stats/src/client/components/RequestDetail.tsx index b6d59e973..86d397b1e 100644 --- a/packages/stats/src/client/components/RequestDetail.tsx +++ b/packages/stats/src/client/components/RequestDetail.tsx @@ -1,4 +1,4 @@ -import { X } from "lucide-react"; +import { Clock, Coins, FileJson, Gauge, Hash, X, Zap } from "lucide-react"; import { useEffect, useState } from "react"; import { getRequestDetails } from "../api"; import type { RequestDetails } from "../types"; @@ -21,18 +21,13 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) { if (!details && loading) { return ( -
-
Loading...
+
+
+
+
+ Loading... +
+
); } @@ -43,118 +38,127 @@ export function RequestDetail({ id, onClose }: RequestDetailProps) { // biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop dismissal
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation for modal content */}
e.stopPropagation()} > -
-

Request Details

+ {/* Header */} +
+
+
+ +
+

Request Details

+
-
-
-
Model
-
- {details.model} ({details.provider}) +
+ {/* Model Info */} +
+
+
+
{details.model}
+
{details.provider}
+
+ {details.errorMessage ? ( + Error + ) : ( + Success + )}
-
-
Cost
-
${details.usage.cost.total.toFixed(4)}
-
-
-
Tokens
-
- {details.usage.totalTokens} (In: {details.usage.input}, Out: {details.usage.output}) + + {/* Stats Grid */} +
+
+
+ + Cost +
+
+ ${details.usage.cost.total.toFixed(4)} +
+
+ +
+
+ + Tokens +
+
+ {details.usage.totalTokens.toLocaleString()} +
+
+ {details.usage.input.toLocaleString()} in · {details.usage.output.toLocaleString()} out +
+
+ +
+
+ + Duration +
+
+ {details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"} +
+
+ +
+
+ + TTFT +
+
+ {details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"} +
-
-
Duration
-
{details.duration ? `${(details.duration / 1000).toFixed(2)}s` : "-"}
-
-
-
TTFT
-
{details.ttft ? `${(details.ttft / 1000).toFixed(2)}s` : "-"}
-
-
-
Tokens/sec
-
- {details.duration && details.usage.output - ? ((details.usage.output * 1000) / details.duration).toFixed(1) - : "-"} + + {/* Tokens/Sec */} + {details.duration && details.usage.output > 0 && ( +
+
+
+ + Throughput +
+ + {((details.usage.output * 1000) / details.duration).toFixed(1)} + +
+
tokens/second
+ )} + + {/* Output */} +
+

Output

+
+							{JSON.stringify(details.output, null, 2)}
+						
+
+ + {/* Raw Metadata */} +
+

Raw Metadata

+
+							{JSON.stringify(details, null, 2)}
+						
- -

- Output -

-
-					{JSON.stringify(details.output, null, 2)}
-				
- -

- Raw Metadata -

-
-					{JSON.stringify(details, null, 2)}
-				
); diff --git a/packages/stats/src/client/components/RequestList.tsx b/packages/stats/src/client/components/RequestList.tsx index 8e85d002c..7c0ee6fdf 100644 --- a/packages/stats/src/client/components/RequestList.tsx +++ b/packages/stats/src/client/components/RequestList.tsx @@ -1,4 +1,5 @@ import { formatDistanceToNow } from "date-fns"; +import { CheckCircle2, XCircle } from "lucide-react"; import type { MessageStats } from "../types"; interface RequestListProps { @@ -9,94 +10,20 @@ interface RequestListProps { export function RequestList({ requests, onSelect, title }: RequestListProps) { return ( -
-
-

{title}

+
+
+

{title}

-
- - +
+
+ - - - - - - - + + + + + + @@ -104,42 +31,40 @@ export function RequestList({ requests, onSelect, title }: RequestListProps) { onSelect(req)} - style={{ - cursor: "pointer", - borderBottom: "1px solid var(--border)", - transition: "background 0.1s", - }} - onMouseEnter={e => { - e.currentTarget.style.background = "rgba(255,255,255,0.05)"; - }} - onMouseLeave={e => { - e.currentTarget.style.background = "transparent"; - }} + className="table-row cursor-pointer border-b border-[var(--border-subtle)] last:border-b-0" > - + - - - - + - - ))} + {requests.length === 0 && ( + + + + )}
- Time - - Model - - Tokens - - Cost - - Duration - - TTFT - - Tokens/s - ModelTimeTokensCostDurationStatus
+ +
{req.model}
+
{req.provider}
+
{formatDistanceToNow(req.timestamp, { addSuffix: true })} -
{req.model}
-
{req.provider}
-
+ {req.usage.totalTokens.toLocaleString()} ${req.usage.cost.total.toFixed(4)} + + ${req.usage.cost.total.toFixed(4)} + {req.duration ? `${(req.duration / 1000).toFixed(1)}s` : "-"} - {req.ttft ? `${(req.ttft / 1000).toFixed(2)}s` : "-"} - - {req.duration && req.usage.output - ? ((req.usage.output * 1000) / req.duration).toFixed(1) - : "-"} + + {req.errorMessage ? ( + + ) : ( + + )}
+ No requests found +
diff --git a/packages/stats/src/client/components/StatCard.tsx b/packages/stats/src/client/components/StatCard.tsx deleted file mode 100644 index 2e45db82d..000000000 --- a/packages/stats/src/client/components/StatCard.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { ReactNode } from "react"; - -interface StatCardProps { - title: string; - value: string | number; - detail?: string; - icon?: ReactNode; - color?: string; -} - -export function StatCard({ title, value, detail, icon, color = "#4ade80" }: StatCardProps) { - return ( -
-
-

{title}

- {icon &&
{icon}
} -
-
{value}
- {detail &&
{detail}
} -
- ); -} diff --git a/packages/stats/src/client/components/StatsGrid.tsx b/packages/stats/src/client/components/StatsGrid.tsx new file mode 100644 index 000000000..b02924f9d --- /dev/null +++ b/packages/stats/src/client/components/StatsGrid.tsx @@ -0,0 +1,88 @@ +import { Activity, AlertCircle, BarChart3, Database, Server, Zap } from "lucide-react"; +import type { AggregatedStats } from "../types"; + +interface StatsGridProps { + stats: AggregatedStats; +} + +const statConfig = [ + { + key: "requests", + title: "Total Requests", + icon: Server, + color: "var(--accent-violet)", + getValue: (s: AggregatedStats) => s.totalRequests.toLocaleString(), + getDetail: (s: AggregatedStats) => + `${s.successfulRequests.toLocaleString()} success · ${s.failedRequests.toLocaleString()} errors`, + }, + { + key: "cost", + title: "Total Cost", + icon: Activity, + color: "var(--accent-pink)", + getValue: (s: AggregatedStats) => `$${s.totalCost.toFixed(2)}`, + getDetail: (s: AggregatedStats) => + s.totalRequests > 0 ? `$${(s.totalCost / s.totalRequests).toFixed(4)} avg/req` : "-", + }, + { + key: "cache", + title: "Cache Rate", + icon: Database, + color: "var(--accent-cyan)", + getValue: (s: AggregatedStats) => `${(s.cacheRate * 100).toFixed(1)}%`, + getDetail: (s: AggregatedStats) => `${(s.totalCacheReadTokens / 1000).toFixed(1)}k cached tokens`, + }, + { + key: "errors", + title: "Error Rate", + icon: AlertCircle, + color: "var(--accent-red)", + getValue: (s: AggregatedStats) => `${(s.errorRate * 100).toFixed(1)}%`, + getDetail: (s: AggregatedStats) => `${s.failedRequests.toLocaleString()} failed requests`, + }, + { + key: "tokens", + title: "Tokens/Sec", + icon: BarChart3, + color: "var(--accent-green)", + getValue: (s: AggregatedStats) => s.avgTokensPerSecond?.toFixed(1) ?? "-", + getDetail: (s: AggregatedStats) => `${(s.totalInputTokens + s.totalOutputTokens).toLocaleString()} total tokens`, + }, + { + key: "ttft", + title: "TTFT", + icon: Zap, + color: "var(--accent-amber)", + getValue: (s: AggregatedStats) => (s.avgTtft ? `${(s.avgTtft / 1000).toFixed(2)}s` : "-"), + getDetail: () => "Time to first token", + }, +]; + +export function StatsGrid({ stats }: StatsGridProps) { + return ( +
+ {statConfig.map(stat => { + const Icon = stat.icon; + return ( +
+
+ {stat.title} +
+ +
+
+
{stat.getValue(stats)}
+
{stat.getDetail(stats)}
+
+ ); + })} +
+ ); +} diff --git a/packages/stats/src/client/index.tsx b/packages/stats/src/client/index.tsx index 10e6fef49..a9bf06917 100644 --- a/packages/stats/src/client/index.tsx +++ b/packages/stats/src/client/index.tsx @@ -1,3 +1,4 @@ +import "./styles.css"; import { createRoot } from "react-dom/client"; import App from "./App"; diff --git a/packages/stats/src/client/styles.css b/packages/stats/src/client/styles.css new file mode 100644 index 000000000..cdb24cce5 --- /dev/null +++ b/packages/stats/src/client/styles.css @@ -0,0 +1,277 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --bg-page: #0a0a0f; + --bg-surface: #111118; + --bg-elevated: #16161e; + --bg-hover: rgba(255, 255, 255, 0.03); + --bg-active: rgba(255, 255, 255, 0.06); + + --border-subtle: rgba(255, 255, 255, 0.06); + --border-default: rgba(255, 255, 255, 0.1); + + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-muted: #64748b; + + --accent-pink: #ec4899; + --accent-pink-glow: rgba(236, 72, 153, 0.3); + --accent-cyan: #22d3ee; + --accent-cyan-glow: rgba(34, 211, 238, 0.3); + --accent-violet: #a78bfa; + --accent-green: #4ade80; + --accent-amber: #fbbf24; + --accent-red: #f87171; + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; +} + +@layer base { + * { + box-sizing: border-box; + } + + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: var(--bg-page); + color: var(--text-primary); + margin: 0; + min-height: 100vh; + } + + /* Custom scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: var(--border-default); + border-radius: 4px; + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); + } +} + +@layer components { + .surface { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + } + + .surface-elevated { + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + } + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: all 0.15s ease; + } + + .btn-primary { + background: linear-gradient(135deg, var(--accent-pink) 0%, #db2777 100%); + color: white; + box-shadow: 0 0 20px var(--accent-pink-glow); + } + + .btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 24px var(--accent-pink-glow); + } + + .btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + } + + .btn-secondary { + background: var(--bg-elevated); + color: var(--text-primary); + border: 1px solid var(--border-default); + } + + .btn-secondary:hover { + background: var(--bg-hover); + border-color: var(--border-default); + } + + .tab-btn { + padding: 6px 16px; + font-size: 14px; + font-weight: 500; + color: var(--text-muted); + background: transparent; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.15s ease; + } + + .tab-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); + } + + .tab-btn.active { + color: var(--text-primary); + background: var(--bg-elevated); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); + } + + .stat-card { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + padding: 20px; + transition: all 0.2s ease; + } + + .stat-card:hover { + border-color: var(--border-default); + background: var(--bg-elevated); + } + + .badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + font-size: 12px; + font-weight: 500; + border-radius: 100px; + } + + .badge-success { + background: rgba(74, 222, 128, 0.15); + color: var(--accent-green); + } + + .badge-error { + background: rgba(248, 113, 113, 0.15); + color: var(--accent-red); + } + + .badge-info { + background: rgba(34, 211, 238, 0.15); + color: var(--accent-cyan); + } + + .badge-warning { + background: rgba(251, 191, 36, 0.15); + color: var(--accent-amber); + } + + .table-header { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + } + + .table-row { + transition: background 0.15s ease; + } + + .table-row:hover { + background: var(--bg-hover); + } + + .gradient-text { + background: linear-gradient(135deg, var(--accent-pink) 0%, var(--accent-cyan) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .gradient-border { + position: relative; + } + + .gradient-border::before { + content: ''; + position: absolute; + inset: 0; + padding: 1px; + border-radius: inherit; + background: linear-gradient(135deg, var(--accent-pink), var(--accent-cyan)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + } + + .glow-pink { + box-shadow: 0 0 30px var(--accent-pink-glow); + } + + .glow-cyan { + box-shadow: 0 0 30px var(--accent-cyan-glow); + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .animate-fade-in { + animation: fadeIn 0.3s ease-out; + } + + .animate-slide-up { + animation: slideUp 0.3s ease-out; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .spin { + animation: spin 1s linear infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } +} diff --git a/packages/stats/src/server.ts b/packages/stats/src/server.ts index 8f6ffa3bb..5ae125155 100644 --- a/packages/stats/src/server.ts +++ b/packages/stats/src/server.ts @@ -1,5 +1,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; +import postcss from "postcss"; +import tailwindcss from "tailwindcss"; import { getDashboardStats, getRecentErrors, @@ -12,6 +14,17 @@ import { const CLIENT_DIR = path.join(import.meta.dir, "client"); const STATIC_DIR = path.join(import.meta.dir, "..", "dist", "client"); +async function buildTailwindCss(inputPath: string, outputPath: string): Promise { + const sourceCss = await Bun.file(inputPath).text(); + const result = await postcss([ + tailwindcss({ config: path.join(import.meta.dir, "..", "tailwind.config.js") }), + ]).process(sourceCss, { + from: inputPath, + to: outputPath, + }); + await Bun.write(outputPath, result.css); +} + async function getLatestMtime(dir: string): Promise { let latest = 0; const entries = await fs.readdir(dir, { withFileTypes: true }); @@ -31,12 +44,24 @@ async function getLatestMtime(dir: string): Promise { const ensureClientBuild = async () => { const indexPath = path.join(STATIC_DIR, "index.html"); - const sourceMtime = await getLatestMtime(CLIENT_DIR); - let shouldBuild = true; - + const cssPath = path.join(STATIC_DIR, "styles.css"); + const clientSourceMtime = await getLatestMtime(CLIENT_DIR); + const tailwindConfigPath = path.join(import.meta.dir, "..", "tailwind.config.js"); + let tailwindConfigMtime = 0; try { - const indexStats = await fs.stat(indexPath); - if (indexStats.isFile() && indexStats.mtimeMs >= sourceMtime) { + const tailwindConfigStats = await fs.stat(tailwindConfigPath); + tailwindConfigMtime = tailwindConfigStats.mtimeMs; + } catch {} + const sourceMtime = Math.max(clientSourceMtime, tailwindConfigMtime); + let shouldBuild = true; + try { + const [indexStats, cssStats] = await Promise.all([fs.stat(indexPath), fs.stat(cssPath)]); + if ( + indexStats.isFile() && + cssStats.isFile() && + indexStats.mtimeMs >= sourceMtime && + cssStats.mtimeMs >= sourceMtime + ) { shouldBuild = false; } } catch { @@ -47,6 +72,15 @@ const ensureClientBuild = async () => { await fs.rm(STATIC_DIR, { recursive: true, force: true }); + // Build Tailwind CSS with the library API + console.log("Building Tailwind CSS..."); + try { + await buildTailwindCss(path.join(CLIENT_DIR, "styles.css"), path.join(STATIC_DIR, "styles.css")); + } catch (error) { + console.error("Tailwind build failed:", error); + } + + console.log("Building React app..."); const result = await Bun.build({ entrypoints: [path.join(CLIENT_DIR, "index.tsx")], outdir: STATIC_DIR, @@ -65,32 +99,7 @@ const ensureClientBuild = async () => { AI Usage Statistics - +
diff --git a/packages/stats/tailwind.config.js b/packages/stats/tailwind.config.js new file mode 100644 index 000000000..68a99b600 --- /dev/null +++ b/packages/stats/tailwind.config.js @@ -0,0 +1,40 @@ +/** @type {import('tailwindcss').Config} */ +import * as path from "node:path"; +export default { + content: [path.join(import.meta.dir, "src", "client", "**/*.{js,jsx,ts,tsx}")], + darkMode: "class", + theme: { + extend: { + colors: { + page: "var(--bg-page)", + surface: "var(--bg-surface)", + elevated: "var(--bg-elevated)", + "border-subtle": "var(--border-subtle)", + "border-default": "var(--border-default)", + "text-primary": "var(--text-primary)", + "text-secondary": "var(--text-secondary)", + "text-muted": "var(--text-muted)", + pink: "var(--accent-pink)", + cyan: "var(--accent-cyan)", + violet: "var(--accent-violet)", + }, + fontFamily: { + sans: [ + "-apple-system", + "BlinkMacSystemFont", + '"Segoe UI"', + "Roboto", + "Helvetica", + "Arial", + "sans-serif", + ], + }, + borderRadius: { + sm: "var(--radius-sm)", + md: "var(--radius-md)", + lg: "var(--radius-lg)", + }, + }, + }, + plugins: [], +};