Allowing settings to be changed in URL hash (#45)

Co-authored-by: mattherzog <Herzog.Matt@gmail.com>
Co-authored-by: Neil Dwyer <neildwyer1991@gmail.com>
This commit is contained in:
David Zhao
2024-05-06 10:07:37 -07:00
committed by GitHub
parent 0d82ab4b26
commit 5b99dae15f
14 changed files with 1353 additions and 292 deletions

View File

@@ -10,14 +10,15 @@ NEXT_PUBLIC_APP_CONFIG="
title: 'LiveKit Agent Playground' title: 'LiveKit Agent Playground'
description: 'LiveKit Agent Playground allows you to test your LiveKit Agent integration by connecting to your LiveKit Cloud or self-hosted instance.' description: 'LiveKit Agent Playground allows you to test your LiveKit Agent integration by connecting to your LiveKit Cloud or self-hosted instance.'
github_link: 'https://github.com/livekit/agents-playground' github_link: 'https://github.com/livekit/agents-playground'
theme_color: 'cyan'
video_fit: 'cover' # 'contain' or 'cover' video_fit: 'cover' # 'contain' or 'cover'
outputs: settings:
audio: true # Enable or disable audio output theme_color: 'cyan'
video: true # Enable or disable video output
chat: true # Enable or disable chat feature chat: true # Enable or disable chat feature
inputs: outputs:
mic: true # Enable or disable microphone input audio: true # Enable or disable audio output
camera: true # Enable or disable camera input video: true # Enable or disable video output
sip: true # Enable or disable SIP input inputs:
mic: true # Enable or disable microphone input
camera: true # Enable or disable camera input
sip: true # Enable or disable SIP input
" "

776
package-lock.json generated
View File

@@ -8,11 +8,14 @@
"name": "agents-playground", "name": "agents-playground",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.0.0", "@livekit/components-react": "^2.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"cookies-next": "^4.1.1",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"livekit-client": "^2.0.2", "livekit-client": "^2.1.0",
"livekit-server-sdk": "^2.0.3", "livekit-server-sdk": "^2.1.2",
"lodash": "^4.17.21",
"next": "^14.0.4", "next": "^14.0.4",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18", "react": "^18",
@@ -20,6 +23,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.0",
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18", "@types/react-dom": "^18",
@@ -56,7 +60,6 @@
"version": "7.23.8", "version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
"integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -141,11 +144,11 @@
} }
}, },
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.0", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz",
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==",
"dependencies": { "dependencies": {
"@floating-ui/utils": "^0.2.1" "@floating-ui/utils": "^0.2.0"
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
@@ -157,10 +160,22 @@
"@floating-ui/utils": "^0.2.0" "@floating-ui/utils": "^0.2.0"
} }
}, },
"node_modules/@floating-ui/react-dom": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz",
"integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": { "node_modules/@floating-ui/utils": {
"version": "0.2.1", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.13", "version": "0.11.13",
@@ -307,9 +322,9 @@
} }
}, },
"node_modules/@livekit/components-react": { "node_modules/@livekit/components-react": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.2.0.tgz",
"integrity": "sha512-DHn3c+buYI8O3IVQ6/9E/tWb4q9lXQomtJd7Y+9lfH4+o3ch+PAtO8aqPtpz4hdt8im0Oh1/Gim410DPgF5FSg==", "integrity": "sha512-TDa2YNBphkdf2dz85pEZs1UBl8wD/LHFeYupNoTqjtlLVlTXpr09Buv3/eegQFJhXoDSK6fAYqKZ4U/oYydv/w==",
"dependencies": { "dependencies": {
"@livekit/components-core": "0.10.0", "@livekit/components-core": "0.10.0",
"@react-hook/latest": "1.0.3", "@react-hook/latest": "1.0.3",
@@ -529,6 +544,535 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
"integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
"integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-menu": "2.0.6",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz",
"integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/rect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz",
"integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz",
"integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@react-hook/latest": { "node_modules/@react-hook/latest": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz",
@@ -551,6 +1095,11 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"node_modules/@types/js-yaml": { "node_modules/@types/js-yaml": {
"version": "4.0.9", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
@@ -563,6 +1112,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.19", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
@@ -576,13 +1131,13 @@
"version": "15.7.11", "version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.55", "version": "18.2.55",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz",
"integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==", "integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@@ -593,7 +1148,7 @@
"version": "18.2.19", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
} }
@@ -602,7 +1157,7 @@
"version": "0.16.8", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true "devOptional": true
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "6.18.1", "version": "6.18.1",
@@ -828,6 +1383,17 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"node_modules/aria-hidden": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": { "node_modules/aria-query": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -1320,6 +1886,29 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "dev": true
}, },
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookies-next": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.1.1.tgz",
"integrity": "sha512-20QaN0iQSz87Os0BhNg9M71eM++gylT3N5szTlhq2rK6QvXn1FYGPB4eAgU4qFTunbQKhD35zfQ95ZWgzUy3Cg==",
"dependencies": {
"@types/cookie": "^0.6.0",
"@types/node": "^16.10.2",
"cookie": "^0.6.0"
}
},
"node_modules/cookies-next/node_modules/@types/node": {
"version": "16.18.96",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.96.tgz",
"integrity": "sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ=="
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1350,7 +1939,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true "devOptional": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@@ -1421,6 +2010,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2300,6 +2894,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/get-symbol-description": { "node_modules/get-symbol-description": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
@@ -2583,6 +3185,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -3095,9 +3705,9 @@
"dev": true "dev": true
}, },
"node_modules/livekit-client": { "node_modules/livekit-client": {
"version": "2.1.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.1.0.tgz", "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.1.1.tgz",
"integrity": "sha512-nJwfRKw1Pafd2napk66l30dlBjsv1VZ+na3mzNezcAFAYT2lQ4Gch57TdbMBDYo+QfrZ98s+kuZzsFhBwM5rqw==", "integrity": "sha512-ffnXHQt210GPJ9sR846o7g0lCg/3TJqZxdu55mzQFS1YXGgn9PYKGzcAhKtuOsQ0NEkkn1zKQ0ABHBt7iADiqg==",
"dependencies": { "dependencies": {
"@livekit/protocol": "1.13.0", "@livekit/protocol": "1.13.0",
"events": "^3.3.0", "events": "^3.3.0",
@@ -3137,6 +3747,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -3923,6 +4538,73 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true "dev": true
}, },
"node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
"dependencies": {
"react-remove-scroll-bar": "^2.3.3",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
"dependencies": {
"react-style-singleton": "^2.2.1",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"dependencies": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -3967,8 +4649,7 @@
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
"dev": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.1", "version": "1.5.1",
@@ -4124,9 +4805,9 @@
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==" "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
}, },
"node_modules/sdp-transform": { "node_modules/sdp-transform": {
"version": "2.14.1", "version": "2.14.2",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
"integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", "integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
"bin": { "bin": {
"sdp-verify": "checker.js" "sdp-verify": "checker.js"
} }
@@ -4832,6 +5513,47 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-callback-ref": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/usehooks-ts": { "node_modules/usehooks-ts": {
"version": "2.16.0", "version": "2.16.0",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.16.0.tgz", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.16.0.tgz",

View File

@@ -9,11 +9,14 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.0.0", "@livekit/components-react": "^2.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"cookies-next": "^4.1.1",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"livekit-client": "^2.0.2", "livekit-client": "^2.1.0",
"livekit-server-sdk": "^2.0.3", "livekit-server-sdk": "^2.1.2",
"lodash": "^4.17.21",
"next": "^14.0.4", "next": "^14.0.4",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18", "react": "^18",
@@ -21,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.0",
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18", "@types/react-dom": "^18",

View File

@@ -1,17 +1,19 @@
import { useConfig } from "@/hooks/useConfig";
import { Button } from "./button/Button"; import { Button } from "./button/Button";
import { useRef } from "react"; import { useState } from "react";
type PlaygroundConnectProps = { type PlaygroundConnectProps = {
accentColor: string; accentColor: string;
onConnectClicked: (url: string, roomToken: string) => void; onConnectClicked: () => void;
}; };
export const PlaygroundConnect = ({ export const PlaygroundConnect = ({
accentColor, accentColor,
onConnectClicked, onConnectClicked,
}: PlaygroundConnectProps) => { }: PlaygroundConnectProps) => {
const urlInput = useRef<HTMLInputElement>(null); const { setUserSettings, config } = useConfig();
const tokenInput = useRef<HTMLTextAreaElement>(null); const [url, setUrl] = useState(config.settings.ws_url)
const [token, setToken] = useState(config.settings.token)
return ( return (
<div className="flex left-0 top-0 w-full h-full bg-black/80 items-center justify-center text-center"> <div className="flex left-0 top-0 w-full h-full bg-black/80 items-center justify-center text-center">
@@ -25,12 +27,14 @@ export const PlaygroundConnect = ({
</div> </div>
<div className="flex flex-col gap-2 my-4"> <div className="flex flex-col gap-2 my-4">
<input <input
ref={urlInput} value={url}
onChange={(e) => setUrl(e.target.value)}
className="text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2" className="text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2"
placeholder="wss://url" placeholder="wss://url"
></input> ></input>
<textarea <textarea
ref={tokenInput} value={token}
onChange={(e) => setToken(e.target.value)}
className="text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2" className="text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2"
placeholder="room token..." placeholder="room token..."
></textarea> ></textarea>
@@ -39,12 +43,11 @@ export const PlaygroundConnect = ({
accentColor={accentColor} accentColor={accentColor}
className="w-full" className="w-full"
onClick={() => { onClick={() => {
if (urlInput.current && tokenInput.current) { const newSettings = {...config.settings};
onConnectClicked( newSettings.ws_url = url;
urlInput.current.value, newSettings.token = token;
tokenInput.current.value setUserSettings(newSettings);
); onConnectClicked();
}
}} }}
> >
Connect Connect

View File

@@ -13,6 +13,7 @@ import {
PlaygroundTile, PlaygroundTile,
} from "@/components/playground/PlaygroundTile"; } from "@/components/playground/PlaygroundTile";
import { AgentMultibandAudioVisualizer } from "@/components/visualization/AgentMultibandAudioVisualizer"; import { AgentMultibandAudioVisualizer } from "@/components/visualization/AgentMultibandAudioVisualizer";
import { useConfig } from "@/hooks/useConfig";
import { useMultibandTrackVolume } from "@/hooks/useTrackVolume"; import { useMultibandTrackVolume } from "@/hooks/useTrackVolume";
import { AgentState } from "@/lib/types"; import { AgentState } from "@/lib/types";
import { import {
@@ -21,9 +22,9 @@ import {
useConnectionState, useConnectionState,
useDataChannel, useDataChannel,
useLocalParticipant, useLocalParticipant,
useParticipantInfo,
useRemoteParticipant,
useRemoteParticipants, useRemoteParticipants,
useRoomContext,
useRoomInfo,
useTracks, useTracks,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { import {
@@ -48,38 +49,24 @@ export interface PlaygroundMeta {
export interface PlaygroundProps { export interface PlaygroundProps {
logo?: ReactNode; logo?: ReactNode;
title?: string;
githubLink?: string;
description?: ReactNode;
themeColors: string[]; themeColors: string[];
defaultColor: string;
outputs?: PlaygroundOutputs[];
showQR?: boolean;
onConnect: (connect: boolean, opts?: { token: string; url: string }) => void; onConnect: (connect: boolean, opts?: { token: string; url: string }) => void;
metadata?: PlaygroundMeta[];
videoFit?: "contain" | "cover";
} }
const headerHeight = 56; const headerHeight = 56;
export default function Playground({ export default function Playground({
logo, logo,
title,
githubLink,
description,
outputs,
showQR,
themeColors, themeColors,
defaultColor,
onConnect, onConnect,
metadata,
videoFit,
}: PlaygroundProps) { }: PlaygroundProps) {
const {config, setUserSettings} = useConfig();
const { name } = useRoomInfo();
const [agentState, setAgentState] = useState<AgentState>("offline"); const [agentState, setAgentState] = useState<AgentState>("offline");
const [themeColor, setThemeColor] = useState(defaultColor);
const [messages, setMessages] = useState<ChatMessageType[]>([]); const [messages, setMessages] = useState<ChatMessageType[]>([]);
const [transcripts, setTranscripts] = useState<ChatMessageType[]>([]); const [transcripts, setTranscripts] = useState<ChatMessageType[]>([]);
const { localParticipant } = useLocalParticipant(); const { localParticipant } = useLocalParticipant();
const [outputs, setOutputs] = useState<PlaygroundOutputs[]>([]);
const participants = useRemoteParticipants({ const participants = useRemoteParticipants({
updateOnlyOn: [RoomEvent.ParticipantMetadataChanged], updateOnlyOn: [RoomEvent.ParticipantMetadataChanged],
@@ -87,18 +74,16 @@ export default function Playground({
const agentParticipant = participants.find((p) => p.isAgent); const agentParticipant = participants.find((p) => p.isAgent);
const { send: sendChat, chatMessages } = useChat(); const { send: sendChat, chatMessages } = useChat();
const visualizerState = useMemo(() => {
if (agentState === "thinking") {
return "thinking";
} else if (agentState === "speaking") {
return "talking";
}
return "idle";
}, [agentState]);
const roomState = useConnectionState(); const roomState = useConnectionState();
const tracks = useTracks(); const tracks = useTracks();
useEffect(() => {
if (roomState === ConnectionState.Connected) {
localParticipant.setCameraEnabled(config.settings.inputs.camera);
localParticipant.setMicrophoneEnabled(config.settings.inputs.mic);
}
}, [config, localParticipant, roomState]);
const agentAudioTrack = tracks.find( const agentAudioTrack = tracks.find(
(trackRef) => (trackRef) =>
trackRef.publication.kind === Track.Kind.Audio && trackRef.publication.kind === Track.Kind.Audio &&
@@ -203,7 +188,7 @@ export default function Playground({
useDataChannel(onDataReceived); useDataChannel(onDataReceived);
const videoTileContent = useMemo(() => { const videoTileContent = useMemo(() => {
const videoFitClassName = `object-${videoFit}`; const videoFitClassName = `object-${config.video_fit || "cover"}`;
return ( return (
<div className="flex flex-col w-full grow text-gray-950 bg-black rounded-sm border border-gray-800 relative"> <div className="flex flex-col w-full grow text-gray-950 bg-black rounded-sm border border-gray-800 relative">
{agentVideoTrack ? ( {agentVideoTrack ? (
@@ -219,7 +204,7 @@ export default function Playground({
)} )}
</div> </div>
); );
}, [agentVideoTrack, videoFit]); }, [agentVideoTrack, config]);
const audioTileContent = useMemo(() => { const audioTileContent = useMemo(() => {
return ( return (
@@ -230,7 +215,7 @@ export default function Playground({
barWidth={30} barWidth={30}
minBarHeight={30} minBarHeight={30}
maxBarHeight={150} maxBarHeight={150}
accentColor={themeColor} accentColor={config.settings.theme_color}
accentShade={500} accentShade={500}
frequencies={subscribedVolumes} frequencies={subscribedVolumes}
borderRadius={12} borderRadius={12}
@@ -244,37 +229,46 @@ export default function Playground({
)} )}
</div> </div>
); );
}, [agentAudioTrack, subscribedVolumes, themeColor, agentState]); }, [
agentAudioTrack,
agentState,
config.settings.theme_color,
subscribedVolumes,
]);
const chatTileContent = useMemo(() => { const chatTileContent = useMemo(() => {
return ( return (
<ChatTile <ChatTile
messages={messages} messages={messages}
accentColor={themeColor} accentColor={config.settings.theme_color}
onSend={sendChat} onSend={sendChat}
/> />
); );
}, [messages, themeColor, sendChat]); }, [config.settings.theme_color, messages, sendChat]);
const settingsTileContent = useMemo(() => { const settingsTileContent = useMemo(() => {
return ( return (
<div className="flex flex-col gap-4 h-full w-full items-start overflow-y-auto"> <div className="flex flex-col gap-4 h-full w-full items-start overflow-y-auto">
{description && ( {config.description && (
<ConfigurationPanelItem title="Description"> <ConfigurationPanelItem title="Description">
{description} {config.description}
</ConfigurationPanelItem> </ConfigurationPanelItem>
)} )}
<ConfigurationPanelItem title="Settings"> <ConfigurationPanelItem title="Settings">
<div className="flex flex-col gap-2"> {localParticipant && (
{metadata?.map((data, index) => ( <div className="flex flex-col gap-2">
<NameValueRow <NameValueRow
key={data.name + index} name="Room"
name={data.name} value={name}
value={data.value} valueColor={`${config.settings.theme_color}-500`}
/> />
))} <NameValueRow
</div> name="Participant"
value={localParticipant.identity}
/>
</div>
)}
</ConfigurationPanelItem> </ConfigurationPanelItem>
<ConfigurationPanelItem title="Status"> <ConfigurationPanelItem title="Status">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -289,7 +283,7 @@ export default function Playground({
} }
valueColor={ valueColor={
roomState === ConnectionState.Connected roomState === ConnectionState.Connected
? `${themeColor}-500` ? `${config.settings.theme_color}-500`
: "gray-500" : "gray-500"
} }
/> />
@@ -304,7 +298,11 @@ export default function Playground({
"false" "false"
) )
} }
valueColor={isAgentConnected ? `${themeColor}-500` : "gray-500"} valueColor={
isAgentConnected
? `${config.settings.theme_color}-500`
: "gray-500"
}
/> />
<NameValueRow <NameValueRow
name="Agent status" name="Agent status"
@@ -319,7 +317,9 @@ export default function Playground({
) )
} }
valueColor={ valueColor={
agentState === "speaking" ? `${themeColor}-500` : "gray-500" agentState === "speaking"
? `${config.settings.theme_color}-500`
: "gray-500"
} }
/> />
</div> </div>
@@ -349,14 +349,16 @@ export default function Playground({
<ConfigurationPanelItem title="Color"> <ConfigurationPanelItem title="Color">
<ColorPicker <ColorPicker
colors={themeColors} colors={themeColors}
selectedColor={themeColor} selectedColor={config.settings.theme_color}
onSelect={(color) => { onSelect={(color) => {
setThemeColor(color); const userSettings = { ...config.settings };
userSettings.theme_color = color;
setUserSettings(userSettings);
}} }}
/> />
</ConfigurationPanelItem> </ConfigurationPanelItem>
</div> </div>
{showQR && ( {config.show_qr && (
<div className="w-full"> <div className="w-full">
<ConfigurationPanelItem title="QR Code"> <ConfigurationPanelItem title="QR Code">
<QRCodeSVG value={window.location.href} width="128" /> <QRCodeSVG value={window.location.href} width="128" />
@@ -366,17 +368,18 @@ export default function Playground({
</div> </div>
); );
}, [ }, [
agentState, config.description,
description, config.settings,
config.show_qr,
// metadata,
roomState,
isAgentConnected, isAgentConnected,
agentState,
localVideoTrack,
localMicTrack, localMicTrack,
localMultibandVolume, localMultibandVolume,
localVideoTrack,
metadata,
roomState,
themeColor,
themeColors, themeColors,
showQR, setUserSettings,
]); ]);
let mobileTabs: PlaygroundTab[] = []; let mobileTabs: PlaygroundTab[] = [];
@@ -432,18 +435,18 @@ export default function Playground({
return ( return (
<> <>
<PlaygroundHeader <PlaygroundHeader
title={title} title={config.title}
logo={logo} logo={logo}
githubLink={githubLink} githubLink={config.github_link}
height={headerHeight} height={headerHeight}
accentColor={themeColor} accentColor={config.settings.theme_color}
connectionState={roomState} connectionState={roomState}
onConnectClicked={() => onConnectClicked={() =>
onConnect(roomState === ConnectionState.Disconnected) onConnect(roomState === ConnectionState.Disconnected)
} }
/> />
<div <div
className={`flex gap-4 py-4 grow w-full selection:bg-${themeColor}-900`} className={`flex gap-4 py-4 grow w-full selection:bg-${config.settings.theme_color}-900`}
style={{ height: `calc(100% - ${headerHeight}px)` }} style={{ height: `calc(100% - ${headerHeight}px)` }}
> >
<div className="flex flex-col grow basis-1/2 gap-4 h-full lg:hidden"> <div className="flex flex-col grow basis-1/2 gap-4 h-full lg:hidden">

View File

@@ -1,6 +1,7 @@
import { Button } from "@/components/button/Button"; import { Button } from "@/components/button/Button";
import { ConnectionState } from "livekit-client";
import { LoadingSVG } from "@/components/button/LoadingSVG"; import { LoadingSVG } from "@/components/button/LoadingSVG";
import { SettingsDropdown } from "@/components/playground/SettingsDropdown";
import { ConnectionState } from "livekit-client";
import { ReactNode } from "react"; import { ReactNode } from "react";
type PlaygroundHeader = { type PlaygroundHeader = {
@@ -37,7 +38,7 @@ export const PlaygroundHeader = ({
{title} {title}
</div> </div>
</div> </div>
<div className="flex basis-1/3 justify-end items-center gap-4"> <div className="flex basis-1/3 justify-end items-center gap-2">
{githubLink && ( {githubLink && (
<a <a
href={githubLink} href={githubLink}
@@ -47,6 +48,7 @@ export const PlaygroundHeader = ({
<GithubSVG /> <GithubSVG />
</a> </a>
)} )}
<SettingsDropdown />
<Button <Button
accentColor={ accentColor={
connectionState === ConnectionState.Connected ? "red" : accentColor connectionState === ConnectionState.Connected ? "red" : accentColor

View File

@@ -0,0 +1,128 @@
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronIcon } from "./icons";
import { useConfig } from "@/hooks/useConfig";
type SettingType = "inputs" | "outputs" | "chat" | "theme_color"
type SettingValue = {
title: string;
type: SettingType | "separator";
key: string;
};
const settingsDropdown: SettingValue[] = [
{
title: "Show chat",
type: "chat",
key: "N/A",
},
{
title: "---",
type: "separator",
key: "separator_1",
},
{
title: "Show video",
type: "outputs",
key: "video",
},
{
title: "Show audio",
type: "outputs",
key: "audio",
},
{
title: "---",
type: "separator",
key: "separator_2",
},
{
title: "Enable camera",
type: "inputs",
key: "camera",
},
{
title: "Enable mic",
type: "inputs",
key: "mic",
},
];
export const SettingsDropdown = () => {
const {config, setUserSettings} = useConfig();
const isEnabled = (setting: SettingValue) => {
if (setting.type === "separator" || setting.type === "theme_color") return false;
if (setting.type === "chat") {
return config.settings[setting.type];
}
if(setting.type === "inputs") {
const key = setting.key as "camera" | "mic";
return config.settings.inputs[key];
} else if(setting.type === "outputs") {
const key = setting.key as "video" | "audio";
return config.settings.outputs[key];
}
return false;
};
const toggleSetting = (setting: SettingValue) => {
if (setting.type === "separator" || setting.type === "theme_color") return;
const newValue = !isEnabled(setting);
const newSettings = {...config.settings}
if(setting.type === "chat") {
newSettings.chat = newValue;
} else if(setting.type === "inputs") {
newSettings.inputs[setting.key as "camera" | "mic"] = newValue;
} else if(setting.type === "outputs") {
newSettings.outputs[setting.key as "video" | "audio"] = newValue;
}
setUserSettings(newSettings);
};
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger className="group inline-flex max-h-12 items-center gap-1 rounded-md hover:bg-gray-800 bg-gray-900 border-gray-800 p-1 pr-2 text-gray-100">
<button className="my-auto text-sm flex gap-1 pl-2 py-1 h-full items-center">
Settings
<ChevronIcon />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="z-50 flex w-60 flex-col gap-0 overflow-hidden rounded text-gray-100 border border-gray-800 bg-gray-900 py-2 text-sm"
sideOffset={5}
collisionPadding={16}
>
{settingsDropdown.map((setting) => {
if (setting.type === "separator") {
return (
<div
key={setting.key}
className="border-t border-gray-800 my-2"
/>
);
}
return (
<DropdownMenu.Label
key={setting.key}
onClick={() => toggleSetting(setting)}
className="flex max-w-full flex-row items-end gap-2 px-3 py-2 text-xs hover:bg-gray-800 cursor-pointer"
>
<div className="w-4 h-4 flex items-center">
{isEnabled(setting) && <CheckIcon />}
</div>
<span>{setting.title}</span>
</DropdownMenu.Label>
);
})}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};

View File

@@ -0,0 +1,39 @@
export const CheckIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<g clip-path="url(#clip0_718_9977)">
<path
d="M1.5 7.5L4.64706 10L10.5 2"
stroke="white"
stroke-width="1.5"
stroke-linecap="square"
/>
</g>
<defs>
<clipPath id="clip0_718_9977">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
export const ChevronIcon = () => (
<svg
width="16"
height="16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="fill-gray-200 transition-all group-hover:fill-white group-data-[state=open]:rotate-180"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="m8 10.7.4-.3 4-4 .3-.4-.7-.7-.4.3L8 9.3 4.4 5.6 4 5.3l-.7.7.3.4 4 4 .4.3Z"
/>
</svg>
);

View File

@@ -1,54 +0,0 @@
import jsYaml from "js-yaml";
const APP_CONFIG = process.env.NEXT_PUBLIC_APP_CONFIG;
export type AppConfig = {
title: string;
description: string;
github_link?: string;
theme_color?: string;
video_fit?: "cover" | "contain";
outputs: {
audio: boolean;
video: boolean;
chat: boolean;
};
inputs: {
mic: boolean;
camera: boolean;
};
show_qr?: boolean;
};
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
const defaultConfig: AppConfig = {
title: "Agents Playground",
description: "A playground for testing LiveKit Agents",
theme_color: "cyan",
video_fit: "cover",
outputs: {
audio: true,
video: true,
chat: true,
},
inputs: {
mic: true,
camera: true,
},
show_qr: false,
};
export const useAppConfig = (): AppConfig => {
if (APP_CONFIG) {
try {
const parsedConfig = jsYaml.load(APP_CONFIG);
console.log("parsedConfig:", parsedConfig);
return parsedConfig as AppConfig;
} catch (e) {
console.error("Error parsing app config:", e);
return defaultConfig;
}
} else {
return defaultConfig;
}
};

198
src/hooks/useConfig.tsx Normal file
View File

@@ -0,0 +1,198 @@
"use client"
import { getCookie, setCookie } from "cookies-next";
import jsYaml from "js-yaml";
import { useRouter } from "next/navigation";
import React, { createContext, useCallback, useMemo, useState } from "react";
import { useEffect } from "react";
export type AppConfig = {
title: string;
description: string;
github_link?: string;
video_fit?: "cover" | "contain";
settings: UserSettings;
show_qr?: boolean;
};
export type UserSettings = {
theme_color: string;
chat: boolean;
inputs: {
camera: boolean;
mic: boolean;
};
outputs: {
audio: boolean;
video: boolean;
};
ws_url: string;
token: string;
};
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
const defaultConfig: AppConfig = {
title: "LiveKit Agents Playground",
description: "A playground for testing LiveKit Agents",
video_fit: "cover",
settings: {
theme_color: "cyan",
chat: true,
inputs: {
camera: true,
mic: true,
},
outputs: {
audio: true,
video: true,
},
ws_url: "",
token: ""
},
show_qr: false,
};
const useAppConfig = (): AppConfig => {
return useMemo(() => {
if (process.env.NEXT_PUBLIC_APP_CONFIG) {
try {
const parsedConfig = jsYaml.load(
process.env.NEXT_PUBLIC_APP_CONFIG
) as AppConfig;
return parsedConfig;
} catch (e) {
console.error("Error parsing app config:", e);
}
}
return defaultConfig;
}, []);
};
type ConfigData = {
config: AppConfig;
setUserSettings: (settings: UserSettings) => void;
};
const ConfigContext = createContext<ConfigData | undefined>(undefined);
export const ConfigProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const appConfig = useAppConfig();
const router = useRouter();
const getSettingsFromUrl = useCallback(() => {
if(typeof window === 'undefined') {
return null;
}
if (!window.location.hash) {
return null;
}
const params = new URLSearchParams(window.location.hash.replace("#", ""));
return {
chat: params.get("chat") === "1",
theme_color: params.get("theme_color"),
inputs: {
camera: params.get("cam") === "1",
mic: params.get("mic") === "1",
},
outputs: {
audio: params.get("audio") === "1",
video: params.get("video") === "1",
chat: params.get("chat") === "1",
},
ws_url: "",
token: ""
} as UserSettings;
}, [])
const getSettingsFromCookies = useCallback(() => {
const jsonSettings = getCookie("lk_settings");
if (!jsonSettings) {
return null;
}
return JSON.parse(jsonSettings) as UserSettings;
}, [])
const setUrlSettings = useCallback((us: UserSettings) => {
const obj = new URLSearchParams({
cam: boolToString(us.inputs.camera),
mic: boolToString(us.inputs.mic),
video: boolToString(us.outputs.video),
audio: boolToString(us.outputs.audio),
chat: boolToString(us.chat),
theme_color: us.theme_color || "cyan",
});
// Note: We don't set ws_url and token to the URL on purpose
router.replace("/#" + obj.toString());
}, [router])
const setCookieSettings = useCallback((us: UserSettings) => {
const json = JSON.stringify(us);
setCookie("lk_settings", json);
}, [])
const getConfig = useCallback(() => {
const appConfigFromSettings = appConfig;
const cookieSettigs = getSettingsFromCookies();
const urlSettings = getSettingsFromUrl();
if(!cookieSettigs) {
if(urlSettings) {
setCookieSettings(urlSettings);
}
}
if(!urlSettings) {
if(cookieSettigs) {
setUrlSettings(cookieSettigs);
}
}
const newCookieSettings = getSettingsFromCookies();
if(!newCookieSettings) {
return appConfigFromSettings;
}
appConfigFromSettings.settings = newCookieSettings;
return {...appConfigFromSettings};
}, [
appConfig,
getSettingsFromCookies,
getSettingsFromUrl,
setCookieSettings,
setUrlSettings,
]);
const setUserSettings = useCallback((settings: UserSettings) => {
setUrlSettings(settings);
setCookieSettings(settings);
_setConfig((prev) => {
return {
...prev,
settings: settings,
};
})
}, [setCookieSettings, setUrlSettings]);
const [config, _setConfig] = useState<AppConfig>(getConfig());
// Run things client side because we use cookies
useEffect(() => {
_setConfig(getConfig());
}, [getConfig]);
return (
<ConfigContext.Provider value={{ config, setUserSettings }}>
{children}
</ConfigContext.Provider>
);
};
export const useConfig = () => {
const context = React.useContext(ConfigContext);
if (context === undefined) {
throw new Error("useConfig must be used within a ConfigProvider");
}
return context;
}
const boolToString = (b: boolean) => (b ? "1" : "0");

View File

@@ -0,0 +1,89 @@
"use client"
import React, { createContext, useState } from "react";
import { useConfig } from "./useConfig";
import { useCallback } from "react";
// Note: cloud mode is only used in our private, hosted version
export type Mode = "cloud" | "env" | "manual";
type TokenGeneratorData = {
shouldConnect: boolean;
wsUrl: string;
token: string;
disconnect: () => Promise<void>;
connect: (mode: Mode) => Promise<void>;
};
const TokenGeneratorContext = createContext<TokenGeneratorData | undefined>(undefined);
export const TokenGeneratorProvider = ({
children,
generateConnectionDetails,
}: {
children: React.ReactNode;
// generateConnectionDetails is only required in cloud mode
generateConnectionDetails?: () => Promise<{ wsUrl: string; token: string }>;
}) => {
const { config } = useConfig();
const [token, setToken] = useState("");
const [wsUrl, setWsUrl] = useState("");
const [shouldConnect, setShouldConnect] = useState(false);
const connect = useCallback(
async (mode: Mode) => {
if (mode === "cloud") {
if (!generateConnectionDetails) {
throw new Error(
"generateConnectionDetails must be provided in cloud mode"
);
}
const { wsUrl, token } = await generateConnectionDetails();
setWsUrl(wsUrl);
setToken(token);
} else if (mode === "env") {
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
if (!url) {
throw new Error("NEXT_PUBLIC_LIVEKIT_URL must be set in env mode");
}
const res = await fetch("/api/token");
const { accessToken } = await res.json();
setWsUrl(url);
setToken(accessToken);
} else if (mode === "manual") {
setWsUrl(config.settings.ws_url);
setToken(config.settings.token);
}
setShouldConnect(true);
},
[
config.settings.token,
config.settings.ws_url,
generateConnectionDetails,
]
);
const disconnect = useCallback(async () => {
setShouldConnect(false);
}, []);
return (
<TokenGeneratorContext.Provider
value={{
wsUrl,
token,
shouldConnect,
connect,
disconnect,
}}
>
{children}
</TokenGeneratorContext.Provider>
);
};
export const useTokenGenerator = () => {
const context = React.useContext(TokenGeneratorContext);
if (context === undefined) {
throw new Error("useTokenGenerator must be used within a TokenGeneratorProvider");
}
return context;
}

View File

@@ -1,4 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { generateRandomAlphanumeric } from "@/lib/util";
import { AccessToken } from "livekit-server-sdk"; import { AccessToken } from "livekit-server-sdk";
import type { AccessTokenOptions, VideoGrant } from "livekit-server-sdk"; import type { AccessTokenOptions, VideoGrant } from "livekit-server-sdk";
@@ -13,48 +14,19 @@ const createToken = (userInfo: AccessTokenOptions, grant: VideoGrant) => {
return at.toJwt(); return at.toJwt();
}; };
const roomPattern = /\w{4}\-\w{4}/;
export default async function handleToken( export default async function handleToken(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
try { try {
const { roomName, identity, name, metadata } = req.query;
if (typeof identity !== "string" || typeof roomName !== "string") {
res.statusMessage =
"identity and roomName have to be specified in the request";
res.status(403).end();
return;
}
if (!apiKey || !apiSecret) { if (!apiKey || !apiSecret) {
res.statusMessage = "Environment variables aren't set up correctly"; res.statusMessage = "Environment variables aren't set up correctly";
res.status(500).end(); res.status(500).end();
return; return;
} }
if (Array.isArray(name)) { const roomName = `room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`;
throw Error("provide max one name"); const identity = `identity-${generateRandomAlphanumeric(4)}`
}
if (Array.isArray(metadata)) {
throw Error("provide max one metadata string");
}
// enforce room name to be xxxx-xxxx
// this is simple & naive way to prevent user from guessing room names
// please use your own authentication mechanisms in your own app
if (!roomName.match(roomPattern)) {
res.statusMessage = "Invalid roomName";
res.status(400).end();
return;
}
// if (!userSession.isAuthenticated) {
// res.status(403).end();
// return;
// }
const grant: VideoGrant = { const grant: VideoGrant = {
room: roomName, room: roomName,
@@ -64,7 +36,7 @@ export default async function handleToken(
canSubscribe: true, canSubscribe: true,
}; };
const token = await createToken({ identity, name, metadata }, grant); const token = await createToken({ identity }, grant);
const result: TokenResult = { const result: TokenResult = {
identity, identity,
accessToken: token, accessToken: token,
@@ -75,4 +47,4 @@ export default async function handleToken(
res.statusMessage = (e as Error).message; res.statusMessage = (e as Error).message;
res.status(500).end(); res.status(500).end();
} }
} }

View File

@@ -1,22 +1,20 @@
import { generateRandomAlphanumeric } from "@/lib/util";
import { import {
LiveKitRoom, LiveKitRoom,
RoomAudioRenderer, RoomAudioRenderer,
StartAudio, StartAudio,
useToken,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import Head from "next/head"; import Head from "next/head";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useState } from "react";
import { PlaygroundConnect } from "@/components/PlaygroundConnect"; import { PlaygroundConnect } from "@/components/PlaygroundConnect";
import Playground, { import Playground, { PlaygroundMeta } from "@/components/playground/Playground";
PlaygroundMeta,
PlaygroundOutputs,
} from "@/components/playground/Playground";
import { PlaygroundToast, ToastType } from "@/components/toast/PlaygroundToast"; import { PlaygroundToast, ToastType } from "@/components/toast/PlaygroundToast";
import { useAppConfig } from "@/hooks/useAppConfig"; import { ConfigProvider, useConfig } from "@/hooks/useConfig";
import { Mode, TokenGeneratorProvider, useTokenGenerator } from "@/hooks/useTokenGenerator";
import { useRef } from "react";
import { useMemo } from "react";
const themeColors = [ const themeColors = [
"cyan", "cyan",
@@ -32,77 +30,47 @@ const themeColors = [
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export default function Home() { export default function Home() {
return (
<ConfigProvider>
<TokenGeneratorProvider>
<HomeInner />
</TokenGeneratorProvider>
</ConfigProvider>
);
}
export function HomeInner() {
const [toastMessage, setToastMessage] = useState<{ const [toastMessage, setToastMessage] = useState<{
message: string; message: string;
type: ToastType; type: ToastType;
} | null>(null); } | null>(null);
const [shouldConnect, setShouldConnect] = useState(false); const { shouldConnect, wsUrl, token, connect, disconnect } =
const [liveKitUrl, setLiveKitUrl] = useState( useTokenGenerator();
process.env.NEXT_PUBLIC_LIVEKIT_URL
);
const [customToken, setCustomToken] = useState<string>();
const [metadata, setMetadata] = useState<PlaygroundMeta[]>([]);
const [roomName, setRoomName] = useState(createRoomName()); const {config} = useConfig();
const tokenOptions = useMemo(() => {
return {
userInfo: { identity: generateRandomAlphanumeric(16) },
};
}, []);
// set a new room name each time the user disconnects so that a new token gets fetched behind the scenes for a different room
useEffect(() => {
if (shouldConnect === false) {
setRoomName(createRoomName());
}
}, [shouldConnect]);
useEffect(() => {
const md: PlaygroundMeta[] = [];
if (liveKitUrl && liveKitUrl !== process.env.NEXT_PUBLIC_LIVEKIT_URL) {
md.push({ name: "LiveKit URL", value: liveKitUrl });
}
if (!customToken && tokenOptions.userInfo?.identity) {
md.push({ name: "Room Name", value: roomName });
md.push({
name: "Participant Identity",
value: tokenOptions.userInfo.identity,
});
}
setMetadata(md);
}, [liveKitUrl, roomName, tokenOptions, customToken]);
const token = useToken("/api/token", roomName, tokenOptions);
const appConfig = useAppConfig();
const outputs = [
appConfig?.outputs.audio && PlaygroundOutputs.Audio,
appConfig?.outputs.video && PlaygroundOutputs.Video,
appConfig?.outputs.chat && PlaygroundOutputs.Chat,
].filter((item) => typeof item !== "boolean") as PlaygroundOutputs[];
const handleConnect = useCallback( const handleConnect = useCallback(
(connect: boolean, opts?: { url: string; token: string }) => { (c: boolean, mode: Mode) => {
if (connect && opts) { c ? connect(mode) : disconnect();
setLiveKitUrl(opts.url);
setCustomToken(opts.token);
}
setShouldConnect(connect);
}, },
[] [connect, disconnect]
); );
const showPG = useMemo(() => {
if (process.env.NEXT_PUBLIC_LIVEKIT_URL) {
return true;
}
if(wsUrl) {
return true;
}
return false;
}, [wsUrl])
return ( return (
<> <>
<Head> <Head>
<title>{appConfig?.title ?? "LiveKit Agents Playground"}</title> <title>{config.title}</title>
<meta <meta name="description" content={config.description} />
name="description"
content={
appConfig?.description ??
"Quickly prototype and test your multimodal agents"
}
/>
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
@@ -136,13 +104,11 @@ export default function Home() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{liveKitUrl ? ( {showPG ? (
<LiveKitRoom <LiveKitRoom
className="flex flex-col h-full w-full" className="flex flex-col h-full w-full"
serverUrl={liveKitUrl} serverUrl={wsUrl}
token={customToken ?? token} token={token}
audio={appConfig?.inputs.mic}
video={appConfig?.inputs.camera}
connect={shouldConnect} connect={shouldConnect}
onError={(e) => { onError={(e) => {
setToastMessage({ message: e.message, type: "error" }); setToastMessage({ message: e.message, type: "error" });
@@ -150,16 +116,13 @@ export default function Home() {
}} }}
> >
<Playground <Playground
title={appConfig?.title}
githubLink={appConfig?.github_link}
outputs={outputs}
showQR={appConfig?.show_qr}
description={appConfig?.description}
themeColors={themeColors} themeColors={themeColors}
defaultColor={appConfig?.theme_color ?? "cyan"} onConnect={(c) => {
onConnect={handleConnect} const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL
metadata={metadata} ? "env"
videoFit={appConfig?.video_fit ?? "cover"} : "manual";
handleConnect(c, mode);
}}
/> />
<RoomAudioRenderer /> <RoomAudioRenderer />
<StartAudio label="Click to enable audio playback" /> <StartAudio label="Click to enable audio playback" />
@@ -167,18 +130,13 @@ export default function Home() {
) : ( ) : (
<PlaygroundConnect <PlaygroundConnect
accentColor={themeColors[0]} accentColor={themeColors[0]}
onConnectClicked={(url, token) => { onConnectClicked={() => {
handleConnect(true, { url, token }); const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : "manual";
handleConnect(true, mode);
}} }}
/> />
)} )}
</main> </main>
</> </>
); );
} }
function createRoomName() {
return [generateRandomAlphanumeric(4), generateRandomAlphanumeric(4)].join(
"-"
);
}

View File

@@ -31,8 +31,6 @@ for (const [name, color] of Object.entries(customColors)) {
shadowNames.push(`hover:shadow-${name}`); shadowNames.push(`hover:shadow-${name}`);
} }
console.log(customShadows, textShadows);
const safelist = [ const safelist = [
'bg-black', 'bg-black',
'bg-white', 'bg-white',
@@ -50,8 +48,6 @@ const safelist = [
]), ]),
]; ];
console.log("Safe list", safelist);
module.exports = { module.exports = {
content: [ content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}", "./src/**/*.{js,ts,jsx,tsx,mdx}",