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
+
+
+
+
+
+ |
+ Time |
+ Model |
+ Duration |
+ Input tokens |
+ Output tokens |
+ Status |
+
+
+
+ {loggedCalls.data?.map((loggedCall) => {
+ return (
+ {
+ if (loggedCall.id === expandedRow) {
+ setExpandedRow(null);
+ } else {
+ setExpandedRow(loggedCall.id);
+ }
+ }}
+ />
+ );
+ })}
+
+
+
+ );
+}
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;