From 5b99dae15f97a37009795fdbe4bb2a740ab402ff Mon Sep 17 00:00:00 2001 From: David Zhao Date: Mon, 6 May 2024 10:07:37 -0700 Subject: [PATCH] Allowing settings to be changed in URL hash (#45) Co-authored-by: mattherzog Co-authored-by: Neil Dwyer --- .env.example | 17 +- package-lock.json | 776 +++++++++++++++++- package.json | 10 +- src/components/PlaygroundConnect.tsx | 27 +- src/components/playground/Playground.tsx | 123 +-- .../playground/PlaygroundHeader.tsx | 6 +- .../playground/SettingsDropdown.tsx | 128 +++ src/components/playground/icons.tsx | 39 + src/hooks/useAppConfig.tsx | 54 -- src/hooks/useConfig.tsx | 198 +++++ src/hooks/useTokenGenerator.tsx | 89 ++ src/pages/api/token.ts | 38 +- src/pages/index.tsx | 136 ++- tailwind.config.js | 4 - 14 files changed, 1353 insertions(+), 292 deletions(-) create mode 100644 src/components/playground/SettingsDropdown.tsx create mode 100644 src/components/playground/icons.tsx delete mode 100644 src/hooks/useAppConfig.tsx create mode 100644 src/hooks/useConfig.tsx create mode 100644 src/hooks/useTokenGenerator.tsx diff --git a/.env.example b/.env.example index 850d81a..1282667 100644 --- a/.env.example +++ b/.env.example @@ -10,14 +10,15 @@ NEXT_PUBLIC_APP_CONFIG=" 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.' github_link: 'https://github.com/livekit/agents-playground' -theme_color: 'cyan' video_fit: 'cover' # 'contain' or 'cover' -outputs: - audio: true # Enable or disable audio output - video: true # Enable or disable video output +settings: + theme_color: 'cyan' chat: true # Enable or disable chat feature -inputs: - mic: true # Enable or disable microphone input - camera: true # Enable or disable camera input - sip: true # Enable or disable SIP input + outputs: + audio: true # Enable or disable audio output + video: true # Enable or disable video output + inputs: + mic: true # Enable or disable microphone input + camera: true # Enable or disable camera input + sip: true # Enable or disable SIP input " diff --git a/package-lock.json b/package-lock.json index 15c5f8c..339967b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,14 @@ "name": "agents-playground", "version": "0.1.0", "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", "js-yaml": "^4.1.0", - "livekit-client": "^2.0.2", - "livekit-server-sdk": "^2.0.3", + "livekit-client": "^2.1.0", + "livekit-server-sdk": "^2.1.2", + "lodash": "^4.17.21", "next": "^14.0.4", "qrcode.react": "^3.1.0", "react": "^18", @@ -20,6 +23,7 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.0", "@types/node": "^20.10.4", "@types/react": "^18.2.43", "@types/react-dom": "^18", @@ -56,7 +60,6 @@ "version": "7.23.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -141,11 +144,11 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz", + "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/dom": { @@ -157,10 +160,22 @@ "@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": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", @@ -307,9 +322,9 @@ } }, "node_modules/@livekit/components-react": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.1.3.tgz", - "integrity": "sha512-DHn3c+buYI8O3IVQ6/9E/tWb4q9lXQomtJd7Y+9lfH4+o3ch+PAtO8aqPtpz4hdt8im0Oh1/Gim410DPgF5FSg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.2.0.tgz", + "integrity": "sha512-TDa2YNBphkdf2dz85pEZs1UBl8wD/LHFeYupNoTqjtlLVlTXpr09Buv3/eegQFJhXoDSK6fAYqKZ4U/oYydv/w==", "dependencies": { "@livekit/components-core": "0.10.0", "@react-hook/latest": "1.0.3", @@ -529,6 +544,535 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", @@ -551,6 +1095,11 @@ "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": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -563,6 +1112,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "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": { "version": "20.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", @@ -576,13 +1131,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.55", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz", "integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -593,7 +1148,7 @@ "version": "18.2.19", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -602,7 +1157,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@typescript-eslint/parser": { "version": "6.18.1", @@ -828,6 +1383,17 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "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": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1320,6 +1886,29 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "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": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1350,7 +1939,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1421,6 +2010,11 @@ "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": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2300,6 +2894,14 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -2583,6 +3185,14 @@ "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": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3095,9 +3705,9 @@ "dev": true }, "node_modules/livekit-client": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.1.0.tgz", - "integrity": "sha512-nJwfRKw1Pafd2napk66l30dlBjsv1VZ+na3mzNezcAFAYT2lQ4Gch57TdbMBDYo+QfrZ98s+kuZzsFhBwM5rqw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.1.1.tgz", + "integrity": "sha512-ffnXHQt210GPJ9sR846o7g0lCg/3TJqZxdu55mzQFS1YXGgn9PYKGzcAhKtuOsQ0NEkkn1zKQ0ABHBt7iADiqg==", "dependencies": { "@livekit/protocol": "1.13.0", "events": "^3.3.0", @@ -3137,6 +3747,11 @@ "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": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -3923,6 +4538,73 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3967,8 +4649,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -4124,9 +4805,9 @@ "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==" }, "node_modules/sdp-transform": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", - "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz", + "integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==", "bin": { "sdp-verify": "checker.js" } @@ -4832,6 +5513,47 @@ "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": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.16.0.tgz", diff --git a/package.json b/package.json index 5f72c2d..4beb88f 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "lint": "next lint" }, "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", "js-yaml": "^4.1.0", - "livekit-client": "^2.0.2", - "livekit-server-sdk": "^2.0.3", + "livekit-client": "^2.1.0", + "livekit-server-sdk": "^2.1.2", + "lodash": "^4.17.21", "next": "^14.0.4", "qrcode.react": "^3.1.0", "react": "^18", @@ -21,6 +24,7 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.0", "@types/node": "^20.10.4", "@types/react": "^18.2.43", "@types/react-dom": "^18", diff --git a/src/components/PlaygroundConnect.tsx b/src/components/PlaygroundConnect.tsx index 962c03a..8468e02 100644 --- a/src/components/PlaygroundConnect.tsx +++ b/src/components/PlaygroundConnect.tsx @@ -1,17 +1,19 @@ +import { useConfig } from "@/hooks/useConfig"; import { Button } from "./button/Button"; -import { useRef } from "react"; +import { useState } from "react"; type PlaygroundConnectProps = { accentColor: string; - onConnectClicked: (url: string, roomToken: string) => void; + onConnectClicked: () => void; }; export const PlaygroundConnect = ({ accentColor, onConnectClicked, }: PlaygroundConnectProps) => { - const urlInput = useRef(null); - const tokenInput = useRef(null); + const { setUserSettings, config } = useConfig(); + const [url, setUrl] = useState(config.settings.ws_url) + const [token, setToken] = useState(config.settings.token) return (
@@ -25,12 +27,14 @@ export const PlaygroundConnect = ({
setUrl(e.target.value)} className="text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2" placeholder="wss://url" > @@ -39,12 +43,11 @@ export const PlaygroundConnect = ({ accentColor={accentColor} className="w-full" onClick={() => { - if (urlInput.current && tokenInput.current) { - onConnectClicked( - urlInput.current.value, - tokenInput.current.value - ); - } + const newSettings = {...config.settings}; + newSettings.ws_url = url; + newSettings.token = token; + setUserSettings(newSettings); + onConnectClicked(); }} > Connect diff --git a/src/components/playground/Playground.tsx b/src/components/playground/Playground.tsx index e6b9401..f02b7c2 100644 --- a/src/components/playground/Playground.tsx +++ b/src/components/playground/Playground.tsx @@ -13,6 +13,7 @@ import { PlaygroundTile, } from "@/components/playground/PlaygroundTile"; import { AgentMultibandAudioVisualizer } from "@/components/visualization/AgentMultibandAudioVisualizer"; +import { useConfig } from "@/hooks/useConfig"; import { useMultibandTrackVolume } from "@/hooks/useTrackVolume"; import { AgentState } from "@/lib/types"; import { @@ -21,9 +22,9 @@ import { useConnectionState, useDataChannel, useLocalParticipant, - useParticipantInfo, - useRemoteParticipant, useRemoteParticipants, + useRoomContext, + useRoomInfo, useTracks, } from "@livekit/components-react"; import { @@ -48,38 +49,24 @@ export interface PlaygroundMeta { export interface PlaygroundProps { logo?: ReactNode; - title?: string; - githubLink?: string; - description?: ReactNode; themeColors: string[]; - defaultColor: string; - outputs?: PlaygroundOutputs[]; - showQR?: boolean; onConnect: (connect: boolean, opts?: { token: string; url: string }) => void; - metadata?: PlaygroundMeta[]; - videoFit?: "contain" | "cover"; } const headerHeight = 56; export default function Playground({ logo, - title, - githubLink, - description, - outputs, - showQR, themeColors, - defaultColor, onConnect, - metadata, - videoFit, }: PlaygroundProps) { + const {config, setUserSettings} = useConfig(); + const { name } = useRoomInfo(); const [agentState, setAgentState] = useState("offline"); - const [themeColor, setThemeColor] = useState(defaultColor); const [messages, setMessages] = useState([]); const [transcripts, setTranscripts] = useState([]); const { localParticipant } = useLocalParticipant(); + const [outputs, setOutputs] = useState([]); const participants = useRemoteParticipants({ updateOnlyOn: [RoomEvent.ParticipantMetadataChanged], @@ -87,18 +74,16 @@ export default function Playground({ const agentParticipant = participants.find((p) => p.isAgent); 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 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( (trackRef) => trackRef.publication.kind === Track.Kind.Audio && @@ -203,7 +188,7 @@ export default function Playground({ useDataChannel(onDataReceived); const videoTileContent = useMemo(() => { - const videoFitClassName = `object-${videoFit}`; + const videoFitClassName = `object-${config.video_fit || "cover"}`; return (
{agentVideoTrack ? ( @@ -219,7 +204,7 @@ export default function Playground({ )}
); - }, [agentVideoTrack, videoFit]); + }, [agentVideoTrack, config]); const audioTileContent = useMemo(() => { return ( @@ -230,7 +215,7 @@ export default function Playground({ barWidth={30} minBarHeight={30} maxBarHeight={150} - accentColor={themeColor} + accentColor={config.settings.theme_color} accentShade={500} frequencies={subscribedVolumes} borderRadius={12} @@ -244,37 +229,46 @@ export default function Playground({ )}
); - }, [agentAudioTrack, subscribedVolumes, themeColor, agentState]); + }, [ + agentAudioTrack, + agentState, + config.settings.theme_color, + subscribedVolumes, + ]); const chatTileContent = useMemo(() => { return ( ); - }, [messages, themeColor, sendChat]); + }, [config.settings.theme_color, messages, sendChat]); const settingsTileContent = useMemo(() => { return (
- {description && ( + {config.description && ( - {description} + {config.description} )} -
- {metadata?.map((data, index) => ( + {localParticipant && ( +
- ))} -
+ +
+ )}
@@ -289,7 +283,7 @@ export default function Playground({ } valueColor={ roomState === ConnectionState.Connected - ? `${themeColor}-500` + ? `${config.settings.theme_color}-500` : "gray-500" } /> @@ -304,7 +298,11 @@ export default function Playground({ "false" ) } - valueColor={isAgentConnected ? `${themeColor}-500` : "gray-500"} + valueColor={ + isAgentConnected + ? `${config.settings.theme_color}-500` + : "gray-500" + } />
@@ -349,14 +349,16 @@ export default function Playground({ { - setThemeColor(color); + const userSettings = { ...config.settings }; + userSettings.theme_color = color; + setUserSettings(userSettings); }} />
- {showQR && ( + {config.show_qr && (
@@ -366,17 +368,18 @@ export default function Playground({
); }, [ - agentState, - description, + config.description, + config.settings, + config.show_qr, + // metadata, + roomState, isAgentConnected, + agentState, + localVideoTrack, localMicTrack, localMultibandVolume, - localVideoTrack, - metadata, - roomState, - themeColor, themeColors, - showQR, + setUserSettings, ]); let mobileTabs: PlaygroundTab[] = []; @@ -432,18 +435,18 @@ export default function Playground({ return ( <> onConnect(roomState === ConnectionState.Disconnected) } />
diff --git a/src/components/playground/PlaygroundHeader.tsx b/src/components/playground/PlaygroundHeader.tsx index ed7be07..ff1dd74 100644 --- a/src/components/playground/PlaygroundHeader.tsx +++ b/src/components/playground/PlaygroundHeader.tsx @@ -1,6 +1,7 @@ import { Button } from "@/components/button/Button"; -import { ConnectionState } from "livekit-client"; import { LoadingSVG } from "@/components/button/LoadingSVG"; +import { SettingsDropdown } from "@/components/playground/SettingsDropdown"; +import { ConnectionState } from "livekit-client"; import { ReactNode } from "react"; type PlaygroundHeader = { @@ -37,7 +38,7 @@ export const PlaygroundHeader = ({ {title}
-
+
{githubLink && ( )} + + + + + {settingsDropdown.map((setting) => { + if (setting.type === "separator") { + return ( +
+ ); + } + + return ( + 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" + > +
+ {isEnabled(setting) && } +
+ {setting.title} +
+ ); + })} + + + + ); +}; \ No newline at end of file diff --git a/src/components/playground/icons.tsx b/src/components/playground/icons.tsx new file mode 100644 index 0000000..d898c43 --- /dev/null +++ b/src/components/playground/icons.tsx @@ -0,0 +1,39 @@ +export const CheckIcon = () => ( + + + + + + + + + + +); + +export const ChevronIcon = () => ( + + + +); diff --git a/src/hooks/useAppConfig.tsx b/src/hooks/useAppConfig.tsx deleted file mode 100644 index 831ed0e..0000000 --- a/src/hooks/useAppConfig.tsx +++ /dev/null @@ -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; - } -}; diff --git a/src/hooks/useConfig.tsx b/src/hooks/useConfig.tsx new file mode 100644 index 0000000..83c2363 --- /dev/null +++ b/src/hooks/useConfig.tsx @@ -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(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(getConfig()); + + // Run things client side because we use cookies + useEffect(() => { + _setConfig(getConfig()); + }, [getConfig]); + + return ( + + {children} + + ); +}; + +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"); diff --git a/src/hooks/useTokenGenerator.tsx b/src/hooks/useTokenGenerator.tsx new file mode 100644 index 0000000..10fa0e1 --- /dev/null +++ b/src/hooks/useTokenGenerator.tsx @@ -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; + connect: (mode: Mode) => Promise; +}; + +const TokenGeneratorContext = createContext(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 ( + + {children} + + ); +}; + +export const useTokenGenerator = () => { + const context = React.useContext(TokenGeneratorContext); + if (context === undefined) { + throw new Error("useTokenGenerator must be used within a TokenGeneratorProvider"); + } + return context; +} \ No newline at end of file diff --git a/src/pages/api/token.ts b/src/pages/api/token.ts index ff62ba4..efae11c 100644 --- a/src/pages/api/token.ts +++ b/src/pages/api/token.ts @@ -1,4 +1,5 @@ import { NextApiRequest, NextApiResponse } from "next"; +import { generateRandomAlphanumeric } from "@/lib/util"; import { AccessToken } 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(); }; -const roomPattern = /\w{4}\-\w{4}/; - export default async function handleToken( req: NextApiRequest, res: NextApiResponse ) { 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) { res.statusMessage = "Environment variables aren't set up correctly"; res.status(500).end(); return; } - if (Array.isArray(name)) { - throw Error("provide max one name"); - } - 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 roomName = `room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`; + const identity = `identity-${generateRandomAlphanumeric(4)}` const grant: VideoGrant = { room: roomName, @@ -64,7 +36,7 @@ export default async function handleToken( canSubscribe: true, }; - const token = await createToken({ identity, name, metadata }, grant); + const token = await createToken({ identity }, grant); const result: TokenResult = { identity, accessToken: token, @@ -75,4 +47,4 @@ export default async function handleToken( res.statusMessage = (e as Error).message; res.status(500).end(); } -} +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2846ccf..f23b7c3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,22 +1,20 @@ -import { generateRandomAlphanumeric } from "@/lib/util"; import { LiveKitRoom, RoomAudioRenderer, StartAudio, - useToken, } from "@livekit/components-react"; import { AnimatePresence, motion } from "framer-motion"; import { Inter } from "next/font/google"; import Head from "next/head"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { PlaygroundConnect } from "@/components/PlaygroundConnect"; -import Playground, { - PlaygroundMeta, - PlaygroundOutputs, -} from "@/components/playground/Playground"; +import Playground, { PlaygroundMeta } from "@/components/playground/Playground"; 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 = [ "cyan", @@ -32,77 +30,47 @@ const themeColors = [ const inter = Inter({ subsets: ["latin"] }); export default function Home() { + return ( + + + + + + ); +} + +export function HomeInner() { const [toastMessage, setToastMessage] = useState<{ message: string; type: ToastType; } | null>(null); - const [shouldConnect, setShouldConnect] = useState(false); - const [liveKitUrl, setLiveKitUrl] = useState( - process.env.NEXT_PUBLIC_LIVEKIT_URL - ); - const [customToken, setCustomToken] = useState(); - const [metadata, setMetadata] = useState([]); + const { shouldConnect, wsUrl, token, connect, disconnect } = + useTokenGenerator(); - const [roomName, setRoomName] = useState(createRoomName()); - - 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 {config} = useConfig(); const handleConnect = useCallback( - (connect: boolean, opts?: { url: string; token: string }) => { - if (connect && opts) { - setLiveKitUrl(opts.url); - setCustomToken(opts.token); - } - setShouldConnect(connect); + (c: boolean, mode: Mode) => { + c ? connect(mode) : disconnect(); }, - [] + [connect, disconnect] ); + const showPG = useMemo(() => { + if (process.env.NEXT_PUBLIC_LIVEKIT_URL) { + return true; + } + if(wsUrl) { + return true; + } + return false; + }, [wsUrl]) + return ( <> - {appConfig?.title ?? "LiveKit Agents Playground"} - + {config.title} + )} - {liveKitUrl ? ( + {showPG ? ( { setToastMessage({ message: e.message, type: "error" }); @@ -150,16 +116,13 @@ export default function Home() { }} > { + const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL + ? "env" + : "manual"; + handleConnect(c, mode); + }} /> @@ -167,18 +130,13 @@ export default function Home() { ) : ( { - handleConnect(true, { url, token }); + onConnectClicked={() => { + const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : "manual"; + handleConnect(true, mode); }} /> )} ); -} - -function createRoomName() { - return [generateRandomAlphanumeric(4), generateRandomAlphanumeric(4)].join( - "-" - ); -} +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index cf8495c..f317825 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -31,8 +31,6 @@ for (const [name, color] of Object.entries(customColors)) { shadowNames.push(`hover:shadow-${name}`); } -console.log(customShadows, textShadows); - const safelist = [ 'bg-black', 'bg-white', @@ -50,8 +48,6 @@ const safelist = [ ]), ]; -console.log("Safe list", safelist); - module.exports = { content: [ "./src/**/*.{js,ts,jsx,tsx,mdx}",