Development environment setup (#22160)

* feat: add pretty view for realtime API logs in dashboard

- Create RealtimePrettyView component that renders structured session
  config, conversation turns with transcripts, and token breakdowns
- Update PrettyMessagesView to detect realtime responses (via
  isRealtimeResponse helper) and delegate to the new component
- Session card shows model, voice, modalities, temperature, instructions
  in a collapsible panel
- Conversation turns show status, per-turn token usage, and audio/text
  transcripts with appropriate icons
- Add 24 tests for RealtimePrettyView and 3 tests for PrettyMessagesView
- All 75 LogDetailsDrawer tests pass

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* chore: remove dev_config.yaml from tracked files

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* feat: show turn count in realtime pretty view session header and output header

- Add purple 'N turns' tag to Session card header for at-a-glance turn count
- Add 'Turns: N' to the Output section header next to tokens/cost
- Extend SectionHeader to accept optional turnCount prop
- Add 3 new tests for turn count display (singular, plural, output header)

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix: address Greptile review feedback

- Remove response.audio.done and conversation.item.created from
  isRealtimeResponse() detection since the view doesn't render them;
  prevents misleading fallback for responses with only those events
- Remove dead code: index >= 0 is always true in .map() callback

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
Ishaan Jaff 2026-02-25 23:20:03 -08:00 • committed by Sameer Kankute
parent d82262224b
commit 1ed4023700
6 changed files with 1064 additions and 79 deletions

View file

@ -90,7 +90,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -1772,7 +1771,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@ -1783,7 +1781,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -1793,14 +1790,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -1978,7 +1973,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@ -1992,7 +1986,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@ -2002,7 +1995,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@ -2326,7 +2318,7 @@
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz",
"integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.1"
@ -3431,14 +3423,12 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.2.48",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@ -3480,7 +3470,6 @@
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": {
@ -4341,14 +4330,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@ -4362,7 +4349,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@ -4375,7 +4361,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@ -4747,7 +4732,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -4773,7 +4757,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@ -4889,7 +4872,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@ -5013,7 +4995,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@ -5038,7 +5019,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@ -5114,7 +5094,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@ -5175,7 +5154,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@ -5589,14 +5567,12 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/doctrine": {
@ -6510,7 +6486,6 @@
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@ -6543,7 +6518,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@ -6581,7 +6555,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@ -6742,7 +6715,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -6893,7 +6865,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@ -7391,7 +7362,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@ -7444,7 +7414,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@ -7505,7 +7474,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -7551,7 +7519,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@ -7600,7 +7567,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@ -7877,7 +7843,6 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@ -8163,7 +8128,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@ -8176,7 +8140,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@ -8491,7 +8454,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@ -8943,7 +8905,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@ -8957,7 +8918,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@ -9072,7 +9032,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@ -9284,7 +9243,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -9303,7 +9261,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@ -9648,7 +9605,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@ -9695,7 +9651,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@ -9708,7 +9663,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -9718,7 +9672,6 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@ -9728,7 +9681,7 @@
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.1"
@ -9747,7 +9700,7 @@
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -9770,7 +9723,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -9799,7 +9751,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@ -9817,7 +9768,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -9843,7 +9793,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -9886,7 +9835,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -9912,7 +9860,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@ -9926,7 +9873,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@ -10040,7 +9986,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@ -10829,7 +10774,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@ -10839,7 +10783,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@ -10852,7 +10795,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@ -11117,7 +11059,6 @@
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
@ -11158,7 +11099,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@ -11214,7 +11154,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@ -11855,7 +11794,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@ -11891,7 +11829,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -11927,7 +11864,6 @@
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@ -11965,7 +11901,6 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@ -11982,7 +11917,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@ -12010,7 +11944,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@ -12020,7 +11953,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@ -12062,7 +11994,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@ -12129,7 +12060,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@ -12217,7 +12147,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tsconfig-paths": {
@ -12334,7 +12263,7 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -12536,7 +12465,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
@ -12990,7 +12918,7 @@
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View file

@ -0,0 +1,81 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { PrettyMessagesView } from "./PrettyMessagesView";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("PrettyMessagesView", () => {
it("should render the component for standard chat completions", () => {
const request = {
messages: [{ role: "user", content: "Hello" }],
};
const response = {
choices: [{ message: { role: "assistant", content: "Hi there!" } }],
};
render(<PrettyMessagesView request={request} response={response} />);
expect(screen.getByText("Hello")).toBeInTheDocument();
expect(screen.getByText("Hi there!")).toBeInTheDocument();
});
it("should render the realtime pretty view for realtime API responses", () => {
const request = {};
const response = {
results: [
{
type: "session.created",
session: {
id: "sess_123",
model: "gpt-4o-mini-realtime-preview",
voice: "alloy",
modalities: ["audio", "text"],
},
},
{
type: "response.done",
response: {
id: "resp_1",
status: "completed",
output: [
{
id: "item_1",
role: "assistant",
type: "message",
content: [{ type: "audio", transcript: "Hello from realtime!" }],
},
],
},
},
],
};
render(<PrettyMessagesView request={request} response={response} />);
expect(screen.getByText("Session")).toBeInTheDocument();
expect(screen.getByText("Hello from realtime!")).toBeInTheDocument();
const modelElements = screen.getAllByText("gpt-4o-mini-realtime-preview");
expect(modelElements.length).toBeGreaterThanOrEqual(1);
});
it("should render standard view when response has results but no realtime events", () => {
const request = {
messages: [{ role: "user", content: "Test" }],
};
const response = {
results: [{ type: "some.other.type" }],
choices: [{ message: { role: "assistant", content: "Reply" } }],
};
render(<PrettyMessagesView request={request} response={response} />);
expect(screen.getByText("Test")).toBeInTheDocument();
expect(screen.getByText("Reply")).toBeInTheDocument();
});
});

View file

@ -1,11 +1,13 @@
/**
* PrettyMessagesView - Datadog-style view with Input/Output cards
* Two main cards showing request and response with token counts and costs
* Two main cards showing request and response with token counts and costs.
* Detects realtime API responses and renders a specialized view.
*/
import { parseMessages } from './prettyMessagesUtils';
import { InputCard } from './InputCard';
import { OutputCard } from './OutputCard';
import { isRealtimeResponse, RealtimePrettyView } from './RealtimePrettyView';
interface PrettyMessagesViewProps {
request: any;
@ -19,6 +21,10 @@ interface PrettyMessagesViewProps {
}
export function PrettyMessagesView({ request, response, metrics }: PrettyMessagesViewProps) {
if (isRealtimeResponse(response)) {
return <RealtimePrettyView response={response} metrics={metrics} />;
}
const { requestMessages, responseMessage } = parseMessages(request, response);
return (

View file

@ -0,0 +1,392 @@
import React from "react";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
const sampleRealtimeResponse = {
usage: {
total_tokens: 587,
prompt_tokens: 294,
completion_tokens: 293,
},
results: [
{
type: "session.created",
session: {
id: "sess_DDNQlPKHjLsokSJPAOWY0",
model: "gpt-4o-mini-realtime-preview",
tools: [],
voice: "alloy",
modalities: ["audio", "text"],
temperature: 0.8,
tool_choice: "auto",
instructions: "You are a helpful assistant.",
turn_detection: {
type: "server_vad",
threshold: 0.5,
},
input_audio_format: "pcm16",
output_audio_format: "pcm16",
max_response_output_tokens: "inf",
},
event_id: "event_DDNQlB4VNUlpqTVIjBbm3",
},
{
type: "response.done",
event_id: "event_DDNQnagYJCZyZATdJCn0L",
response: {
id: "resp_DDNQnlXGHZJB46D5JhJ95",
usage: {
input_tokens: 116,
total_tokens: 162,
output_tokens: 46,
input_token_details: {
text_tokens: 116,
audio_tokens: 0,
},
output_token_details: {
text_tokens: 16,
audio_tokens: 30,
},
},
voice: "alloy",
object: "realtime.response",
output: [
{
id: "item_DDNQnz5uN1b8NEvPOPZOM",
role: "assistant",
type: "message",
status: "completed",
content: [
{
type: "audio",
transcript: "Hello! How's your day going?",
},
],
},
],
status: "completed",
conversation_id: "conv_DDNQlpNllPYhCCfXCtT8X",
max_output_tokens: "inf",
},
},
{
type: "response.done",
event_id: "event_DDNR0VmrRTVU69RxGC29U",
response: {
id: "resp_DDNQy6S4PBZxW4qsKq6Ah",
usage: {
input_tokens: 178,
total_tokens: 425,
output_tokens: 247,
},
voice: "alloy",
object: "realtime.response",
output: [
{
id: "item_DDNQywctWVnYmujg4FSTZ",
role: "assistant",
type: "message",
status: "completed",
content: [
{
type: "audio",
transcript:
"I'm here to help with information and general questions.",
},
],
},
],
status: "completed",
conversation_id: "conv_DDNQlpNllPYhCCfXCtT8X",
max_output_tokens: "inf",
},
},
],
};
describe("isRealtimeResponse", () => {
it("should return true for a valid realtime response with session.created", () => {
expect(isRealtimeResponse(sampleRealtimeResponse)).toBe(true);
});
it("should return true for response with only response.done events", () => {
const resp = {
results: [{ type: "response.done", response: { id: "r1" } }],
};
expect(isRealtimeResponse(resp)).toBe(true);
});
it("should return false for a standard chat completion response", () => {
const chatResponse = {
choices: [{ message: { role: "assistant", content: "Hello" } }],
};
expect(isRealtimeResponse(chatResponse)).toBe(false);
});
it("should return false for null/undefined", () => {
expect(isRealtimeResponse(null)).toBe(false);
expect(isRealtimeResponse(undefined)).toBe(false);
});
it("should return false for empty results array", () => {
expect(isRealtimeResponse({ results: [] })).toBe(false);
});
it("should return false for results with unrecognized event types", () => {
const resp = {
results: [{ type: "some.unknown.event" }],
};
expect(isRealtimeResponse(resp)).toBe(false);
});
});
describe("RealtimePrettyView", () => {
const mockWriteText = vi.fn().mockResolvedValue(undefined);
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(navigator, "clipboard", {
value: { writeText: mockWriteText },
writable: true,
configurable: true,
});
});
it("should render the component successfully", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("Session")).toBeInTheDocument();
});
it("should display the session model name", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
const modelElements = screen.getAllByText("gpt-4o-mini-realtime-preview");
expect(modelElements.length).toBeGreaterThanOrEqual(1);
});
it("should display the session voice tag", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
const voiceElements = screen.getAllByText("alloy");
expect(voiceElements.length).toBeGreaterThanOrEqual(1);
});
it("should display modality tags", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("audio")).toBeInTheDocument();
expect(screen.getByText("text")).toBeInTheDocument();
});
it("should display the turn count in session header", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("2 turns")).toBeInTheDocument();
});
it("should display singular 'turn' for a single response event", () => {
const singleTurnResponse = {
results: [
{
type: "session.created",
session: {
id: "sess_1",
model: "gpt-4o-mini-realtime-preview",
voice: "alloy",
modalities: ["audio"],
},
},
{
type: "response.done",
response: {
id: "r1",
status: "completed",
output: [
{
id: "item1",
role: "assistant",
type: "message",
content: [{ type: "audio", transcript: "Hi!" }],
},
],
},
},
],
};
render(<RealtimePrettyView response={singleTurnResponse} />);
expect(screen.getByText("1 turn")).toBeInTheDocument();
});
it("should display the turn count in the output section header", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("Turns: 2")).toBeInTheDocument();
});
it("should display the Output section header", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("Output")).toBeInTheDocument();
});
it("should display transcript text from response turns", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(
screen.getByText("Hello! How's your day going?")
).toBeInTheDocument();
expect(
screen.getByText(
"I'm here to help with information and general questions."
)
).toBeInTheDocument();
});
it("should display completed status tags for response turns", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
const completedTags = screen.getAllByText("completed");
expect(completedTags.length).toBe(2);
});
it("should display token usage per turn", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText("116 in / 46 out tokens")).toBeInTheDocument();
expect(screen.getByText("178 in / 247 out tokens")).toBeInTheDocument();
});
it("should expand session details when session header is clicked", async () => {
const user = userEvent.setup();
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
await user.click(screen.getByText("Session"));
await waitFor(() => {
expect(screen.getByText("Temperature")).toBeInTheDocument();
});
});
it("should display session instructions when expanded", async () => {
const user = userEvent.setup();
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
await user.click(screen.getByText("Session"));
await waitFor(() => {
expect(screen.getByText("Instructions")).toBeInTheDocument();
expect(
screen.getByText("You are a helpful assistant.")
).toBeInTheDocument();
});
});
it("should display session audio format when expanded", async () => {
const user = userEvent.setup();
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
await user.click(screen.getByText("Session"));
await waitFor(() => {
expect(screen.getByText("Input Audio Format")).toBeInTheDocument();
expect(screen.getAllByText("pcm16").length).toBeGreaterThanOrEqual(1);
});
});
it("should display ASSISTANT label for output messages", () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
const assistantLabels = screen.getAllByText("ASSISTANT");
expect(assistantLabels.length).toBe(2);
});
it("should display fallback message when no recognized events exist", () => {
const emptyResponse = {
results: [{ type: "unknown.event" }],
};
render(<RealtimePrettyView response={emptyResponse} />);
expect(
screen.getByText("No recognized realtime events found")
).toBeInTheDocument();
});
it("should handle response with no output items gracefully", () => {
const noOutputResponse = {
results: [
{
type: "response.done",
response: {
id: "r1",
status: "completed",
output: [],
},
},
],
};
render(<RealtimePrettyView response={noOutputResponse} />);
expect(screen.getByText("completed")).toBeInTheDocument();
});
it("should display metrics tokens when provided", () => {
render(
<RealtimePrettyView
response={sampleRealtimeResponse}
metrics={{ completion_tokens: 500, output_cost: 0.005 }}
/>
);
expect(screen.getByText(/Tokens: 500/)).toBeInTheDocument();
expect(screen.getByText(/Cost: \$0\.005000/)).toBeInTheDocument();
});
it("should toggle output section collapse when header is clicked", async () => {
const user = userEvent.setup();
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
const transcript = screen.getByText("Hello! How's your day going?");
expect(transcript).toBeVisible();
const outputHeader = screen.getByText("Output").closest("div");
if (outputHeader) {
await user.click(outputHeader);
await waitFor(() => {
expect(transcript).not.toBeVisible();
});
}
});
it("should display token breakdown tags when input_token_details are present", async () => {
render(<RealtimePrettyView response={sampleRealtimeResponse} />);
expect(screen.getByText(/Text Tokens: 116/)).toBeInTheDocument();
});
it("should handle text content type in addition to audio", () => {
const textResponse = {
results: [
{
type: "response.done",
response: {
id: "r1",
status: "completed",
output: [
{
id: "item1",
role: "assistant",
type: "message",
content: [
{
type: "text",
text: "This is a text response",
},
],
},
],
},
},
],
};
render(<RealtimePrettyView response={textResponse} />);
expect(screen.getByText("This is a text response")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,570 @@
/**
* RealtimePrettyView - Structured pretty view for OpenAI Realtime API logs
* Displays session config, conversation turns, and token usage
* in a readable format instead of raw JSON.
*/
import { useState } from 'react';
import { Typography, Tag, Tooltip } from 'antd';
import {
SoundOutlined,
MessageOutlined,
SettingOutlined,
AudioOutlined,
DownOutlined,
UpOutlined,
} from '@ant-design/icons';
import { SectionHeader } from './SectionHeader';
const { Text } = Typography;
interface RealtimeEvent {
type: string;
event_id?: string;
session?: RealtimeSession;
response?: RealtimeResponse;
}
interface RealtimeSession {
id: string;
model: string;
voice?: string;
modalities?: string[];
temperature?: number;
tools?: any[];
instructions?: string;
turn_detection?: Record<string, any>;
input_audio_format?: string;
output_audio_format?: string;
max_response_output_tokens?: string | number;
[key: string]: any;
}
interface RealtimeResponse {
id: string;
status: string;
usage?: {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
input_token_details?: Record<string, any>;
output_token_details?: Record<string, any>;
};
output?: RealtimeOutputItem[];
modalities?: string[];
voice?: string;
conversation_id?: string;
[key: string]: any;
}
interface RealtimeOutputItem {
id: string;
role: string;
type: string;
status?: string;
content?: Array<{
type: string;
transcript?: string;
text?: string;
}>;
}
interface RealtimePrettyViewProps {
response: any;
metrics?: {
prompt_tokens?: number;
completion_tokens?: number;
input_cost?: number;
output_cost?: number;
};
}
export function isRealtimeResponse(response: any): boolean {
if (!response || !response.results || !Array.isArray(response.results) || response.results.length === 0) {
return false;
}
return response.results.some(
(r: any) =>
r.type === 'session.created' ||
r.type === 'session.updated' ||
r.type === 'response.done'
);
}
export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProps) {
const events: RealtimeEvent[] = response?.results || [];
const usage = response?.usage;
const sessionEvent = events.find(
(e) => e.type === 'session.created' || e.type === 'session.updated'
);
const responseEvents = events.filter((e) => e.type === 'response.done');
return (
<div>
{/* Session Configuration Card */}
{sessionEvent?.session && (
<SessionCard session={sessionEvent.session} turnCount={responseEvents.length} />
)}
{/* Conversation Turns */}
{responseEvents.length > 0 && (
<ConversationCard
responses={responseEvents.map((e) => e.response!).filter(Boolean)}
totalUsage={usage}
metrics={metrics}
/>
)}
{/* Fallback if no recognized events */}
{!sessionEvent && responseEvents.length === 0 && (
<div
style={{
border: '1px solid #f0f0f0',
borderRadius: 6,
padding: '16px',
color: '#8c8c8c',
fontStyle: 'italic',
fontSize: 13,
}}
>
No recognized realtime events found
</div>
)}
</div>
);
}
function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCount: number }) {
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div
style={{
border: '1px solid #f0f0f0',
borderRadius: 6,
marginBottom: 8,
overflow: 'hidden',
}}
>
<div
onClick={() => setIsCollapsed(!isCollapsed)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 16px',
borderBottom: isCollapsed ? 'none' : '1px solid #f0f0f0',
background: '#fafafa',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f5f5f5';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#fafafa';
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{isCollapsed ? (
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
) : (
<UpOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<SettingOutlined style={{ color: '#8c8c8c', fontSize: 14 }} />
<Text style={{ fontWeight: 500, fontSize: 14 }}>Session</Text>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
{session.model}
</Text>
{turnCount > 0 && (
<Tag
color="purple"
style={{ margin: 0, fontWeight: 500 }}
>
{turnCount} {turnCount === 1 ? 'turn' : 'turns'}
</Tag>
)}
{session.voice && (
<Tag color="blue" style={{ margin: 0 }}>
<SoundOutlined /> {session.voice}
</Tag>
)}
{session.modalities && (
<div style={{ display: 'flex', gap: 4 }}>
{session.modalities.map((m) => (
<Tag key={m} style={{ margin: 0 }}>
{m === 'audio' ? <AudioOutlined /> : <MessageOutlined />} {m}
</Tag>
))}
</div>
)}
</div>
</div>
<div
style={{
maxHeight: isCollapsed ? '0px' : '10000px',
overflow: 'hidden',
transition: 'max-height 0.3s ease-out, opacity 0.3s ease-out',
opacity: isCollapsed ? 0 : 1,
}}
>
<div style={{ padding: '12px 16px' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '8px 24px',
fontSize: 13,
}}
>
<ConfigRow label="Model" value={session.model} />
<ConfigRow label="Voice" value={session.voice} />
<ConfigRow label="Temperature" value={session.temperature} />
<ConfigRow
label="Max Output Tokens"
value={session.max_response_output_tokens}
/>
<ConfigRow
label="Input Audio Format"
value={session.input_audio_format}
/>
<ConfigRow
label="Output Audio Format"
value={session.output_audio_format}
/>
{session.turn_detection && (
<ConfigRow
label="Turn Detection"
value={session.turn_detection.type}
/>
)}
{session.tools && session.tools.length > 0 && (
<ConfigRow
label="Tools"
value={`${session.tools.length} tool(s)`}
/>
)}
</div>
{session.instructions && (
<div style={{ marginTop: 12 }}>
<Text
type="secondary"
style={{
fontSize: 10,
letterSpacing: '0.5px',
textTransform: 'uppercase',
display: 'block',
marginBottom: 4,
}}
>
Instructions
</Text>
<div
style={{
fontSize: 12,
lineHeight: 1.6,
color: '#595959',
background: '#fafafa',
padding: '8px 12px',
borderRadius: 4,
border: '1px solid #f0f0f0',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: 120,
overflowY: 'auto',
}}
>
{session.instructions}
</div>
</div>
)}
</div>
</div>
</div>
);
}
function ConversationCard({
responses,
totalUsage,
metrics,
}: {
responses: RealtimeResponse[];
totalUsage?: any;
metrics?: RealtimePrettyViewProps['metrics'];
}) {
const [isCollapsed, setIsCollapsed] = useState(false);
const totalTokens = totalUsage?.total_tokens;
const turnCount = responses.length;
const handleCopy = () => {
const transcripts = responses
.flatMap((r) =>
(r.output || []).flatMap((o) =>
(o.content || []).map(
(c) => `${o.role}: ${c.transcript || c.text || ''}`
)
)
)
.join('\n');
navigator.clipboard.writeText(transcripts);
};
return (
<div
style={{
border: '1px solid #f0f0f0',
borderRadius: 6,
overflow: 'hidden',
}}
>
<SectionHeader
type="output"
tokens={metrics?.completion_tokens ?? totalTokens}
cost={metrics?.output_cost}
onCopy={handleCopy}
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
turnCount={turnCount}
/>
<div
style={{
maxHeight: isCollapsed ? '0px' : '10000px',
overflow: 'hidden',
transition: 'max-height 0.3s ease-out, opacity 0.3s ease-out',
opacity: isCollapsed ? 0 : 1,
}}
>
<div style={{ padding: '12px 16px' }}>
{responses.map((resp, idx) => (
<ResponseTurn key={resp.id || idx} response={resp} index={idx} />
))}
</div>
</div>
</div>
);
}
function ResponseTurn({
response,
index,
}: {
response: RealtimeResponse;
index: number;
}) {
const outputs = response.output || [];
const usage = response.usage;
return (
<div
style={{
marginBottom: 12,
paddingBottom: 12,
borderBottom: '1px solid #f5f5f5',
}}
>
{/* Turn header */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 8,
}}
>
<Tag
color={response.status === 'completed' ? 'green' : 'orange'}
style={{ margin: 0 }}
>
{response.status || 'unknown'}
</Tag>
{usage && (
<Text type="secondary" style={{ fontSize: 11 }}>
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
</Text>
)}
{response.conversation_id && (
<Tooltip title={response.conversation_id}>
<Text
type="secondary"
style={{ fontSize: 11, cursor: 'help' }}
>
conv: {response.conversation_id.slice(0, 12)}...
</Text>
</Tooltip>
)}
</div>
{/* Output messages / transcripts */}
{outputs.map((output, oIdx) => (
<OutputMessage key={output.id || oIdx} output={output} />
))}
{/* Token breakdown if available */}
{usage?.input_token_details && (
<TokenBreakdown
label="Input"
details={usage.input_token_details}
/>
)}
{usage?.output_token_details && (
<TokenBreakdown
label="Output"
details={usage.output_token_details}
/>
)}
</div>
);
}
function OutputMessage({ output }: { output: RealtimeOutputItem }) {
const contents = output.content || [];
const hasTranscripts = contents.some((c) => c.transcript || c.text);
if (!hasTranscripts) return null;
return (
<div style={{ marginBottom: 8 }}>
<Text
type="secondary"
style={{
fontSize: 10,
letterSpacing: '0.5px',
textTransform: 'uppercase',
display: 'block',
marginBottom: 3,
}}
>
{output.role?.toUpperCase() || 'ASSISTANT'}
</Text>
{contents.map((c, cIdx) => {
const text = c.transcript || c.text;
if (!text) return null;
return (
<div
key={cIdx}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 8,
marginBottom: 4,
}}
>
{c.type === 'audio' && (
<AudioOutlined
style={{
color: '#8c8c8c',
fontSize: 12,
marginTop: 3,
flexShrink: 0,
}}
/>
)}
{c.type === 'text' && (
<MessageOutlined
style={{
color: '#8c8c8c',
fontSize: 12,
marginTop: 3,
flexShrink: 0,
}}
/>
)}
<div
style={{
fontSize: 13,
lineHeight: 1.7,
color: '#262626',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{text}
</div>
</div>
);
})}
</div>
);
}
function TokenBreakdown({
label,
details,
}: {
label: string;
details: Record<string, any>;
}) {
const entries = Object.entries(details).filter(
([, v]) =>
typeof v === 'number' ||
(typeof v === 'object' && v !== null)
);
if (entries.length === 0) return null;
return (
<div style={{ marginTop: 4 }}>
<Text
type="secondary"
style={{ fontSize: 10, letterSpacing: '0.5px', textTransform: 'uppercase' }}
>
{label} Token Breakdown
</Text>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
marginTop: 4,
}}
>
{entries.map(([key, value]) => {
if (typeof value === 'number') {
return (
<Tag key={key} style={{ margin: 0 }}>
{formatTokenLabel(key)}: {value.toLocaleString()}
</Tag>
);
}
return null;
})}
</div>
</div>
);
}
function ConfigRow({
label,
value,
}: {
label: string;
value: any;
}) {
if (value === undefined || value === null) return null;
return (
<div>
<Text type="secondary" style={{ fontSize: 11 }}>
{label}
</Text>
<div style={{ fontSize: 13, color: '#262626' }}>
{String(value)}
</div>
</div>
);
}
function formatTokenLabel(key: string): string {
return key
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
}

View file

@ -19,9 +19,10 @@ interface SectionHeaderProps {
onCopy: () => void;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
turnCount?: number;
}
export function SectionHeader({ type, tokens, cost, onCopy, isCollapsed, onToggleCollapse }: SectionHeaderProps) {
export function SectionHeader({ type, tokens, cost, onCopy, isCollapsed, onToggleCollapse, turnCount }: SectionHeaderProps) {
return (
<div
onClick={onToggleCollapse}
@ -81,6 +82,13 @@ export function SectionHeader({ type, tokens, cost, onCopy, isCollapsed, onToggl
Cost: ${cost.toFixed(6)}
</Text>
)}
{/* Turn count */}
{turnCount !== undefined && turnCount > 0 && (
<Text type="secondary" style={{ fontSize: 12 }}>
Turns: {turnCount}
</Text>
)}
</div>
{/* Copy Button */}