diff --git a/app/package.json b/app/package.json index 6bfba73..802dae8 100644 --- a/app/package.json +++ b/app/package.json @@ -62,13 +62,16 @@ "json-schema-to-typescript": "^13.0.2", "json-stringify-pretty-compact": "^4.0.0", "jsonschema": "^1.4.1", + "kysely": "^0.26.1", "lodash-es": "^4.17.21", + "lucide-react": "^0.265.0", "next": "^13.4.2", "next-auth": "^4.22.1", "next-query-params": "^4.2.3", "nextjs-cors": "^2.1.2", "nextjs-routes": "^2.0.1", "openai": "4.0.0-beta.7", + "pg": "^8.11.2", "pluralize": "^8.0.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", @@ -84,6 +87,7 @@ "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.0", "recast": "^0.23.3", + "recharts": "^2.7.2", "replicate": "^0.12.3", "socket.io": "^4.7.1", "socket.io-client": "^4.7.1", @@ -108,6 +112,7 @@ "@types/json-schema": "^7.0.12", "@types/lodash-es": "^4.17.8", "@types/node": "^18.16.0", + "@types/pg": "^8.10.2", "@types/pluralize": "^0.0.30", "@types/prismjs": "^1.26.0", "@types/react": "^18.2.6", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index fa06d85..ba5868d 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -125,9 +125,15 @@ dependencies: jsonschema: specifier: ^1.4.1 version: 1.4.1 + kysely: + specifier: ^0.26.1 + version: 0.26.1 lodash-es: specifier: ^4.17.21 version: 4.17.21 + lucide-react: + specifier: ^0.265.0 + version: 0.265.0(react@18.2.0) next: specifier: ^13.4.2 version: 13.4.2(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) @@ -146,6 +152,9 @@ dependencies: openai: specifier: 4.0.0-beta.7 version: 4.0.0-beta.7 + pg: + specifier: ^8.11.2 + version: 8.11.2 pluralize: specifier: ^8.0.0 version: 8.0.0 @@ -191,6 +200,9 @@ dependencies: recast: specifier: ^0.23.3 version: 0.23.3 + recharts: + specifier: ^2.7.2 + version: 2.7.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) replicate: specifier: ^0.12.3 version: 0.12.3 @@ -259,6 +271,9 @@ devDependencies: '@types/node': specifier: ^18.16.0 version: 18.16.0 + '@types/pg': + specifier: ^8.10.2 + version: 8.10.2 '@types/pluralize': specifier: ^0.0.30 version: 0.0.30 @@ -3077,7 +3092,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: true /@types/cookie@0.4.1: @@ -3089,6 +3104,48 @@ packages: dependencies: '@types/node': 18.16.0 + /@types/d3-array@3.0.5: + resolution: {integrity: sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==} + dev: false + + /@types/d3-color@3.1.0: + resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==} + dev: false + + /@types/d3-ease@3.0.0: + resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==} + dev: false + + /@types/d3-interpolate@3.0.1: + resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==} + dependencies: + '@types/d3-color': 3.1.0 + dev: false + + /@types/d3-path@3.0.0: + resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==} + dev: false + + /@types/d3-scale@4.0.3: + resolution: {integrity: sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==} + dependencies: + '@types/d3-time': 3.0.0 + dev: false + + /@types/d3-shape@3.1.1: + resolution: {integrity: sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==} + dependencies: + '@types/d3-path': 3.0.0 + dev: false + + /@types/d3-time@3.0.0: + resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + dev: false + + /@types/d3-timer@3.0.0: + resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==} + dev: false + /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: @@ -3139,7 +3196,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: false /@types/hast@2.3.5: @@ -3199,7 +3256,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 form-data: 3.0.1 dev: false @@ -3220,10 +3277,9 @@ packages: /@types/pg@8.10.2: resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 pg-protocol: 1.6.0 pg-types: 4.0.1 - dev: false /@types/pluralize@0.0.30: resolution: {integrity: sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==} @@ -3284,7 +3340,7 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: true /@types/serve-static@1.15.2: @@ -4364,6 +4420,10 @@ packages: postcss-value-parser: 4.2.0 dev: false + /css-unit-converter@1.1.2: + resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==} + dev: false + /csstype@2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} dev: false @@ -4375,6 +4435,77 @@ packages: resolution: {integrity: sha512-JiQosUWiOFgp4hQn0an+SBoV9IKdqzhROM0iiN4LB7UpfJBlsSJlWl9nq4zGgxgMAzHJ6V4t29VAVD+3+2NJAg==} dev: true + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -4430,6 +4561,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: false @@ -4556,6 +4691,12 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.22.6 + dev: false + /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -4635,7 +4776,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.13 - '@types/node': 18.16.0 + '@types/node': 18.17.3 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -5182,6 +5323,10 @@ packages: engines: {node: '>=6'} dev: false + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5264,6 +5409,11 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -5849,6 +5999,11 @@ packages: side-channel: 1.0.4 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -6213,6 +6368,11 @@ packages: object.values: 1.1.6 dev: true + /kysely@0.26.1: + resolution: {integrity: sha512-FVRomkdZofBu3O8SiwAOXrwbhPZZr8mBN5ZeUWyprH29jzvy6Inzqbd0IMmGxpd4rcOCL9HyyBNWBa8FBqDAdg==} + engines: {node: '>=14.0.0'} + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -6343,6 +6503,14 @@ packages: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} dev: false + /lucide-react@0.265.0(react@18.2.0): + resolution: {integrity: sha512-znyvziBEUQ7CKR31GiU4viomQbJrpDLG5ac+FajwiZIavC3YbPFLkzQx3dCXT4JWJx/pB34EwmtiZ0ElGZX0PA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -6782,7 +6950,6 @@ packages: /obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - dev: false /oidc-token-hash@5.0.3: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} @@ -7009,12 +7176,10 @@ packages: /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - dev: false /pg-numeric@1.0.2: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - dev: false /pg-pool@3.6.1(pg@8.11.2): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} @@ -7026,7 +7191,6 @@ packages: /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false /pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -7050,7 +7214,6 @@ packages: postgres-date: 2.0.1 postgres-interval: 3.0.0 postgres-range: 1.1.3 - dev: false /pg@8.11.2: resolution: {integrity: sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ==} @@ -7098,6 +7261,10 @@ packages: engines: {node: '>=4'} dev: false + /postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + dev: false + /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} dev: false @@ -7128,7 +7295,6 @@ packages: /postgres-array@3.0.2: resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} engines: {node: '>=12'} - dev: false /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} @@ -7140,7 +7306,6 @@ packages: engines: {node: '>= 6'} dependencies: obuf: 1.1.2 - dev: false /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} @@ -7150,7 +7315,6 @@ packages: /postgres-date@2.0.1: resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} engines: {node: '>=12'} - dev: false /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} @@ -7162,11 +7326,9 @@ packages: /postgres-interval@3.0.0: resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} engines: {node: '>=12'} - dev: false /postgres-range@1.1.3: resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} - dev: false /posthog-js@1.75.3: resolution: {integrity: sha512-q5xP4R/Tx8E6H0goZQjY+URMLATFiYXc2raHA+31aNvpBs118fPTmExa4RK6MgRZDFhBiMUBZNT6aj7dM3SyUQ==} @@ -7453,6 +7615,10 @@ packages: react-base16-styling: 0.9.1 dev: false + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + /react-remove-scroll-bar@2.3.4(@types/react@18.2.6)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -7488,6 +7654,17 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) dev: false + /react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-select@5.7.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==} peerDependencies: @@ -7509,6 +7686,20 @@ packages: - '@types/react' dev: false + /react-smooth@2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yl4y3XiMorss7ayF5QnBiSprig0+qFHui8uh7Hgg46QX5O+aRMRKlfGGNGLHno35JkQSvSYY8eCWkBfHfrSHfg==} + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-ssr-prepass@1.5.0(react@18.2.0): resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} peerDependencies: @@ -7561,6 +7752,20 @@ packages: - '@types/react' dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -7621,6 +7826,41 @@ packages: tslib: 2.6.1 dev: false + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.7.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HMKRBkGoOXHW+7JcRa6+MukPSifNtJlqbc+JreGVNA407VLE/vOP+8n3YYjprDVVIF9E2ZgwWnL3D7K/LUFzBg==} + engines: {node: '>=12'} + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + classnames: 2.3.2 + eventemitter3: 4.0.7 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-resize-detector: 8.1.0(react-dom@18.2.0)(react@18.2.0) + react-smooth: 2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + reduce-css-calc: 2.1.8 + victory-vendor: 36.6.11 + dev: false + + /reduce-css-calc@2.1.8: + resolution: {integrity: sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==} + dependencies: + css-unit-converter: 1.1.2 + postcss-value-parser: 3.3.1 + dev: false + /refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} dependencies: @@ -8628,6 +8868,25 @@ packages: engines: {node: '>= 0.8'} dev: false + /victory-vendor@36.6.11: + resolution: {integrity: sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==} + dependencies: + '@types/d3-array': 3.0.5 + '@types/d3-ease': 3.0.0 + '@types/d3-interpolate': 3.0.1 + '@types/d3-scale': 4.0.3 + '@types/d3-shape': 3.1.1 + '@types/d3-time': 3.0.0 + '@types/d3-timer': 3.0.0 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /vite-node@0.33.0(@types/node@18.16.0): resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} engines: {node: '>=v14.18.0'} diff --git a/app/src/components/dashboard/LoggedCallTable.tsx b/app/src/components/dashboard/LoggedCallTable.tsx new file mode 100644 index 0000000..70aa7a2 --- /dev/null +++ b/app/src/components/dashboard/LoggedCallTable.tsx @@ -0,0 +1,201 @@ +import { + Box, + Card, + CardHeader, + Heading, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + Tooltip, + Collapse, + HStack, + VStack, + IconButton, + useToast, + Icon, + Button, + ButtonGroup, +} from "@chakra-ui/react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { type RouterOutputs, api } from "~/utils/api"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; +import stringify from "json-stringify-pretty-compact"; +import Link from "next/link"; + +dayjs.extend(relativeTime); + +type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0]; + +const FormattedJson = ({ json }: { json: any }) => { + const jsonString = stringify(json, { maxLength: 40 }); + const toast = useToast(); + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + status: "success", + duration: 2000, + }); + } catch (err) { + toast({ + title: "Failed to copy to clipboard", + status: "error", + duration: 2000, + }); + } + }; + + return ( + + + {jsonString} + + } + position="absolute" + top={1} + right={1} + size="xs" + variant="ghost" + onClick={() => void copyToClipboard(jsonString)} + /> + + ); +}; + +function TableRow({ + loggedCall, + isExpanded, + onToggle, +}: { + loggedCall: LoggedCall; + isExpanded: boolean; + onToggle: () => void; +}) { + const isError = loggedCall.modelResponse?.respStatus !== 200; + const timeAgo = dayjs(loggedCall.startTime).fromNow(); + const fullTime = dayjs(loggedCall.startTime).toString(); + + const model = useMemo( + () => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value, + [loggedCall.tags], + ); + + return ( + <> + td": { borderBottom: "none" }, + }} + > + + + + + + + {timeAgo} + + + + {model} + {((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2)}s + {loggedCall.modelResponse?.inputTokens} + {loggedCall.modelResponse?.outputTokens} + + {loggedCall.modelResponse?.respStatus ?? "No response"} + + + + + + + + + Input + + + + Output + + + + + + + + + + + + ); +} + +export default function LoggedCallTable() { + const [expandedRow, setExpandedRow] = useState(null); + const loggedCalls = api.dashboard.loggedCalls.useQuery({}); + + return ( + + + + Logged Calls + + + + + + + + + + + + + + + {loggedCalls.data?.map((loggedCall) => { + return ( + { + if (loggedCall.id === expandedRow) { + setExpandedRow(null); + } else { + setExpandedRow(loggedCall.id); + } + }} + /> + ); + })} + +
+ TimeModelDurationInput tokensOutput tokensStatus
+
+ ); +} diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 3347590..35e2606 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -1,13 +1,62 @@ -import { Breadcrumb, BreadcrumbItem, Divider, Text, VStack } from "@chakra-ui/react"; +import { + Heading, + Text, + Stat, + StatLabel, + StatNumber, + VStack, + HStack, + Card, + CardBody, + CardHeader, + Icon, + Table, + Tbody, + Tr, + Td, + Divider, + Breadcrumb, + BreadcrumbItem, +} from "@chakra-ui/react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import { Ban, DollarSign, Hash } from "lucide-react"; +import { useMemo } from "react"; import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import { useSelectedOrg } from "~/utils/hooks"; +import dayjs from "~/utils/dayjs"; +import { api } from "~/utils/api"; +import LoggedCallTable from "~/components/dashboard/LoggedCallTable"; export default function HomePage() { const { data: selectedOrg } = useSelectedOrg(); + const stats = api.dashboard.stats.useQuery( + { organizationId: selectedOrg?.id ?? "" }, + { enabled: !!selectedOrg }, + ); + + const data = useMemo(() => { + return ( + stats.data?.periods.map(({ period, numQueries, totalCost }) => ({ + period, + Requests: numQueries, + "Total Spent (USD)": parseFloat(totalCost.toString()), + })) || [] + ); + }, [stats.data]); + return ( @@ -25,6 +74,107 @@ export default function HomePage() { {selectedOrg?.name} + + + + + + Usage Statistics + + + + + + dayjs(str).format("MMM D")} + /> + + + + + + + + + + + + + + + + + Total Spent + + + + ${parseFloat(stats.data?.totals?.totalCost?.toString() ?? "0").toFixed(2)} + + + + + + + + + Total Requests + + + + {stats.data?.totals?.numQueries + ? parseInt(stats.data?.totals?.numQueries.toString())?.toLocaleString() + : undefined} + + + + + + + + + Errors + + + + + + {stats.data?.errors?.map((error) => ( + + + + + ))} + +
+ {error.name} ({error.code}) + + {parseInt(error.count.toString()).toLocaleString()} +
+
+
+
+
+ +
); diff --git a/app/src/server/api/root.router.ts b/app/src/server/api/root.router.ts index d50d907..702f1ea 100644 --- a/app/src/server/api/root.router.ts +++ b/app/src/server/api/root.router.ts @@ -10,6 +10,7 @@ import { datasetsRouter } from "./routers/datasets.router"; import { datasetEntries } from "./routers/datasetEntries.router"; import { externalApiRouter } from "./routers/externalApi.router"; import { organizationsRouter } from "./routers/organizations.router"; +import { dashboardRouter } from "./routers/dashboard.router"; /** * This is the primary router for your server. @@ -27,6 +28,7 @@ export const appRouter = createTRPCRouter({ datasets: datasetsRouter, datasetEntries: datasetEntries, organizations: organizationsRouter, + dashboard: dashboardRouter, externalApi: externalApiRouter, }); diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts new file mode 100644 index 0000000..f4de2a9 --- /dev/null +++ b/app/src/server/api/routers/dashboard.router.ts @@ -0,0 +1,95 @@ +import { sql } from "kysely"; +import { z } from "zod"; +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { kysely, prisma } from "~/server/db"; + +export const dashboardRouter = createTRPCRouter({ + stats: publicProcedure + .input( + z.object({ + startDate: z.string().optional(), + organizationId: z.string(), + }), + ) + .query(async ({ input }) => { + console.log("made it 1"); + // Return the stats group by hour + const periods = await kysely + .selectFrom("LoggedCall") + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .where("organizationId", "=", input.organizationId) + .select(({ fn }) => [ + sql`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"), + sql`count("LoggedCall"."id")::int`.as("numQueries"), + fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), + ]) + .groupBy("period") + .orderBy("period") + .execute(); + + console.log("made it 2"); + + const totals = await kysely + .selectFrom("LoggedCall") + .where("organizationId", "=", input.organizationId) + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .select(({ fn }) => [ + fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), + fn.count("id").as("numQueries"), + ]) + .executeTakeFirst(); + + console.log("made it 3"); + + const errors = await kysely + .selectFrom("LoggedCall") + .where("organizationId", "=", input.organizationId) + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .select(({ fn }) => [fn.count("id").as("count"), "respStatus as code"]) + .where("respStatus", ">", 200) + .groupBy("code") + .orderBy("count", "desc") + .execute(); + + console.log("made it 4"); + + const namedErrors = errors.map((e) => { + if (e.code === 429) { + return { ...e, name: "Rate limited" }; + } else if (e.code === 500) { + return { ...e, name: "Internal server error" }; + } else { + return { ...e, name: "Other" }; + } + }); + + console.log("data is", { periods, totals, errors: namedErrors }); + + return { periods, totals, errors: namedErrors }; + // const resp = await kysely.selectFrom("LoggedCall").selectAll().execute(); + }), + + // TODO useInfiniteQuery + // https://discord.com/channels/966627436387266600/1122258443886153758/1122258443886153758 + loggedCalls: publicProcedure.input(z.object({})).query(async ({ input }) => { + const loggedCalls = await prisma.loggedCall.findMany({ + orderBy: { startTime: "desc" }, + include: { tags: true, modelResponse: true }, + take: 20, + }); + + return loggedCalls; + }), +}); diff --git a/app/src/server/db.ts b/app/src/server/db.ts index 1cbc4cf..8ed322b 100644 --- a/app/src/server/db.ts +++ b/app/src/server/db.ts @@ -1,6 +1,56 @@ -import { PrismaClient } from "@prisma/client"; +import { + type Experiment, + type PromptVariant, + type TestScenario, + type TemplateVariable, + type ScenarioVariantCell, + type ModelResponse, + type Evaluation, + type OutputEvaluation, + type Dataset, + type DatasetEntry, + type Organization, + type OrganizationUser, + type WorldChampEntrant, + type LoggedCall, + type LoggedCallModelResponse, + type LoggedCallTag, + type ApiKey, + type Account, + type Session, + type User, + type VerificationToken, + PrismaClient, +} from "@prisma/client"; +import { Kysely, PostgresDialect } from "kysely"; +import { Pool } from "pg"; + import { env } from "~/env.mjs"; +interface DB { + Experiment: Experiment; + PromptVariant: PromptVariant; + TestScenario: TestScenario; + TemplateVariable: TemplateVariable; + ScenarioVariantCell: ScenarioVariantCell; + ModelResponse: ModelResponse; + Evaluation: Evaluation; + OutputEvaluation: OutputEvaluation; + Dataset: Dataset; + DatasetEntry: DatasetEntry; + Organization: Organization; + OrganizationUser: OrganizationUser; + WorldChampEntrant: WorldChampEntrant; + LoggedCall: LoggedCall; + LoggedCallModelResponse: LoggedCallModelResponse; + LoggedCallTag: LoggedCallTag; + ApiKey: ApiKey; + Account: Account; + Session: Session; + User: User; + VerificationToken: VerificationToken; +} + const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; @@ -14,4 +64,12 @@ export const prisma = : ["error"], }); +export const kysely = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: env.DATABASE_URL, + }), + }), +}); + if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;