mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 06:34:44 -05:00
Add documentation, clean repository templating
This commit is contained in:
@@ -15,9 +15,7 @@
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
AUTH_SECRET=""
|
||||
|
||||
# Next Auth Discord Provider
|
||||
AUTH_DISCORD_ID=""
|
||||
AUTH_DISCORD_SECRET=""
|
||||
|
||||
|
||||
# Drizzle
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/hristudio"
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5433/hristudio"
|
||||
|
||||
160
bun.lock
160
bun.lock
@@ -5,24 +5,35 @@
|
||||
"name": "hristudio",
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^1.7.2",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^15.2.3",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-auth": "^5.0.0-beta.29",
|
||||
"postgres": "^3.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"zod": "^3.24.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
@@ -34,6 +45,7 @@
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
},
|
||||
@@ -124,6 +136,8 @@
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.1.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
@@ -220,10 +234,22 @@
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
|
||||
|
||||
"@shadcn/ui": ["@shadcn/ui@0.0.4", "", { "dependencies": { "chalk": "5.2.0", "commander": "^10.0.0", "execa": "^7.0.0", "fs-extra": "^11.1.0", "node-fetch": "^3.3.0", "ora": "^6.1.2", "prompts": "^2.4.2", "zod": "^3.20.2" }, "bin": { "ui": "dist/index.js" } }, "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@t3-oss/env-core": ["@t3-oss/env-core@0.12.0", "", { "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw=="],
|
||||
@@ -272,7 +298,7 @@
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
@@ -350,6 +376,8 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
@@ -384,10 +412,18 @@
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
||||
|
||||
"bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
@@ -404,8 +440,18 @@
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
@@ -414,9 +460,9 @@
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
|
||||
|
||||
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
|
||||
|
||||
@@ -426,6 +472,8 @@
|
||||
|
||||
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
@@ -436,6 +484,8 @@
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
@@ -512,6 +562,8 @@
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||
@@ -524,6 +576,8 @@
|
||||
|
||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
@@ -536,6 +590,10 @@
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
@@ -548,6 +606,8 @@
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||
@@ -578,12 +638,18 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
@@ -614,6 +680,8 @@
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
@@ -628,12 +696,16 @@
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
@@ -664,10 +736,14 @@
|
||||
|
||||
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||
|
||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||
@@ -700,16 +776,24 @@
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"log-symbols": ["log-symbols@5.1.0", "", { "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" } }, "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
@@ -730,7 +814,13 @@
|
||||
|
||||
"next": ["next@15.4.1", "", { "dependencies": { "@next/env": "15.4.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.1", "@next/swc-darwin-x64": "15.4.1", "@next/swc-linux-arm64-gnu": "15.4.1", "@next/swc-linux-arm64-musl": "15.4.1", "@next/swc-linux-x64-gnu": "15.4.1", "@next/swc-linux-x64-musl": "15.4.1", "@next/swc-win32-arm64-msvc": "15.4.1", "@next/swc-win32-x64-msvc": "15.4.1", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw=="],
|
||||
|
||||
"next-auth": ["next-auth@5.0.0-beta.25", "", { "dependencies": { "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog=="],
|
||||
"next-auth": ["next-auth@5.0.0-beta.29", "", { "dependencies": { "@auth/core": "0.40.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
||||
|
||||
"oauth4webapi": ["oauth4webapi@3.6.0", "", {}, "sha512-OwXPTXjKPOldTpAa19oksrX9TYHA0rt+VcUFTkJ7QKwgmevPpNm9Cn5vFZUtIo96FiU6AfPuUUGzoXqgOzibWg=="],
|
||||
|
||||
@@ -750,8 +840,12 @@
|
||||
|
||||
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
||||
|
||||
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"ora": ["ora@6.3.1", "", { "dependencies": { "chalk": "^5.0.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.6.1", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.1.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "strip-ansi": "^7.0.1", "wcwidth": "^1.0.1" } }, "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
@@ -786,7 +880,7 @@
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
|
||||
|
||||
"pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="],
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
@@ -798,8 +892,12 @@
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.60.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
@@ -810,12 +908,16 @@
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
@@ -848,8 +950,12 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
@@ -858,6 +964,8 @@
|
||||
|
||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||
|
||||
"stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||
@@ -872,8 +980,14 @@
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
@@ -884,6 +998,8 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||
@@ -900,6 +1016,8 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.3.5", "", {}, "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
@@ -918,10 +1036,18 @@
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
@@ -938,7 +1064,7 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
"zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
@@ -946,6 +1072,10 @@
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@shadcn/ui/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="],
|
||||
|
||||
"@shadcn/ui/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
|
||||
@@ -980,11 +1110,17 @@
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"log-symbols/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"next-auth/@auth/core": ["@auth/core@0.37.2", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "@types/cookie": "0.6.0", "cookie": "0.7.1", "jose": "^5.9.3", "oauth4webapi": "^3.0.0", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw=="],
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"ora/chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
@@ -1038,10 +1174,6 @@
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"next-auth/@auth/core/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"next-auth/@auth/core/preact": ["preact@10.11.3", "", {}, "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="],
|
||||
|
||||
"next-auth/@auth/core/preact-render-to-string": ["preact-render-to-string@5.2.3", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA=="],
|
||||
"restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
}
|
||||
}
|
||||
|
||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "~/components",
|
||||
"utils": "~/lib/utils",
|
||||
"ui": "~/components/ui",
|
||||
"lib": "~/lib",
|
||||
"hooks": "~/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
217
docs/README.md
Normal file
217
docs/README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# HRIStudio Documentation
|
||||
|
||||
Welcome to the comprehensive documentation for HRIStudio - a web-based platform for standardizing and improving Wizard of Oz (WoZ) studies in Human-Robot Interaction research.
|
||||
|
||||
## 📚 Documentation Overview
|
||||
|
||||
This documentation suite provides everything needed to understand, build, deploy, and maintain HRIStudio. It's designed for AI agents, developers, and technical teams who will be implementing the platform.
|
||||
|
||||
### Core Documents
|
||||
|
||||
1. **[Project Overview](./project-overview.md)**
|
||||
- Executive summary and project goals
|
||||
- Core features and system architecture
|
||||
- User roles and permissions
|
||||
- Technology stack overview
|
||||
- Key concepts and success metrics
|
||||
|
||||
2. **[Database Schema](./database-schema.md)**
|
||||
- Complete PostgreSQL schema with Drizzle ORM
|
||||
- Table definitions and relationships
|
||||
- Indexes and performance optimizations
|
||||
- Views and stored procedures
|
||||
- Migration guidelines
|
||||
|
||||
3. **[API Routes](./api-routes.md)**
|
||||
- Comprehensive tRPC route documentation
|
||||
- Request/response schemas
|
||||
- Authentication requirements
|
||||
- WebSocket events
|
||||
- Rate limiting and error handling
|
||||
|
||||
4. **[Feature Requirements](./feature-requirements.md)**
|
||||
- Detailed user stories and acceptance criteria
|
||||
- Functional requirements by module
|
||||
- Non-functional requirements
|
||||
- UI/UX specifications
|
||||
- Integration requirements
|
||||
|
||||
5. **[Implementation Guide](./implementation-guide.md)**
|
||||
- Step-by-step technical implementation
|
||||
- Code examples and patterns
|
||||
- Frontend and backend architecture
|
||||
- Real-time features implementation
|
||||
- Testing strategies
|
||||
|
||||
6. **[Deployment & Operations](./deployment-operations.md)**
|
||||
- Infrastructure requirements
|
||||
- Vercel deployment strategies
|
||||
- Monitoring and observability
|
||||
- Backup and recovery procedures
|
||||
- Security operations
|
||||
|
||||
7. **[ROS2 Integration](./ros2-integration.md)**
|
||||
- rosbridge WebSocket architecture
|
||||
- Client-side ROS connection management
|
||||
- Message type definitions
|
||||
- Robot plugin implementation
|
||||
- Security considerations for robot communication
|
||||
|
||||
## 🚀 Quick Start for Developers
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ with Bun package manager
|
||||
- PostgreSQL 15+
|
||||
- Docker and Docker Compose (for local development)
|
||||
- S3-compatible storage (Cloudflare R2 recommended for Vercel)
|
||||
- ROS2 with rosbridge_suite (for robot integration)
|
||||
|
||||
### Initial Setup
|
||||
1. Clone the repository
|
||||
2. Copy `.env.example` to `.env.local`
|
||||
3. Run `docker-compose up -d` for local services
|
||||
4. Run `bun install` to install dependencies
|
||||
5. Run `bun db:migrate` to set up the database
|
||||
6. Run `bun dev` to start the development server
|
||||
|
||||
### For AI Agents Building the Application
|
||||
|
||||
When implementing HRIStudio, follow this sequence:
|
||||
|
||||
1. **Start with Project Setup**
|
||||
- Use the Implementation Guide to set up the project structure
|
||||
- Follow the rules in `rules.txt` for coding standards
|
||||
- Reference the Project Overview for architectural decisions
|
||||
|
||||
2. **Implement Database Layer**
|
||||
- Use the Database Schema document to create all tables
|
||||
- Implement the schema files with Drizzle ORM
|
||||
- Set up relationships and indexes as specified
|
||||
|
||||
3. **Build API Layer**
|
||||
- Follow the API Routes document to implement all tRPC routes
|
||||
- Ensure proper authentication and authorization
|
||||
- Implement error handling and validation
|
||||
|
||||
4. **Create UI Components**
|
||||
- Reference Feature Requirements for UI specifications
|
||||
- Use shadcn/ui components exclusively
|
||||
- Follow the component patterns in Implementation Guide
|
||||
|
||||
5. **Add Real-time Features**
|
||||
- Implement WebSocket server for trial execution
|
||||
- Add real-time updates for wizard interface
|
||||
- Ensure proper state synchronization
|
||||
|
||||
6. **Implement Robot Integration**
|
||||
- Follow ROS2 Integration guide for robot plugins
|
||||
- Set up rosbridge on robot systems
|
||||
- Test WebSocket communication
|
||||
|
||||
7. **Deploy and Monitor**
|
||||
- Follow Deployment & Operations guide for Vercel
|
||||
- Set up monitoring and logging
|
||||
- Implement backup strategies
|
||||
|
||||
## 📋 Key Implementation Notes
|
||||
|
||||
### Architecture Principles
|
||||
- **Modular Design**: Each feature is self-contained
|
||||
- **Type Safety**: Full TypeScript with strict mode
|
||||
- **Server-First**: Leverage React Server Components
|
||||
- **Real-time**: WebSocket for live trial execution
|
||||
- **Secure**: Role-based access control throughout
|
||||
|
||||
### Technology Choices
|
||||
- **Next.js 15**: App Router for modern React patterns
|
||||
- **tRPC**: Type-safe API communication
|
||||
- **Drizzle ORM**: Type-safe database queries
|
||||
- **NextAuth.js v5**: Authentication and authorization
|
||||
- **shadcn/ui**: Consistent UI components
|
||||
- **Cloudflare R2**: S3-compatible object storage
|
||||
- **roslib.js**: WebSocket-based ROS2 communication
|
||||
- **Vercel KV**: Edge-compatible caching (instead of Redis)
|
||||
|
||||
### Critical Features
|
||||
1. **Visual Experiment Designer**: Drag-and-drop interface
|
||||
2. **Wizard Interface**: Real-time control during trials
|
||||
3. **Plugin System**: Extensible robot platform support
|
||||
4. **Data Capture**: Comprehensive recording of all trial data
|
||||
5. **Collaboration**: Multi-user support with role-based access
|
||||
|
||||
## 🔧 Development Workflow
|
||||
|
||||
### Code Organization
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
├── components/ # Reusable UI components
|
||||
├── features/ # Feature-specific modules
|
||||
├── lib/ # Core utilities and setup
|
||||
├── server/ # Server-side code
|
||||
└── types/ # TypeScript type definitions
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
- Unit tests for utilities and hooks
|
||||
- Integration tests for tRPC procedures
|
||||
- E2E tests for critical user flows
|
||||
- Performance testing for real-time features
|
||||
|
||||
### Deployment Pipeline
|
||||
1. Run tests and type checking
|
||||
2. Build Docker image
|
||||
3. Run security scans
|
||||
4. Deploy to staging
|
||||
5. Run smoke tests
|
||||
6. Deploy to production
|
||||
|
||||
## 🤝 Contributing Guidelines
|
||||
|
||||
### For AI Agents
|
||||
- Always reference the documentation before implementing
|
||||
- Follow the patterns established in the Implementation Guide
|
||||
- Ensure all code follows the rules in `rules.txt`
|
||||
- Implement comprehensive error handling
|
||||
- Add proper TypeScript types for all code
|
||||
|
||||
### Code Quality Standards
|
||||
- No `any` types in TypeScript
|
||||
- All components must be accessible (WCAG 2.1 AA)
|
||||
- API routes must have proper validation
|
||||
- Database queries must be optimized
|
||||
- Real-time features must handle disconnections
|
||||
|
||||
## 📞 Support and Resources
|
||||
|
||||
### Documentation Updates
|
||||
This documentation is designed to be comprehensive and self-contained. If you identify gaps or need clarification:
|
||||
1. Check all related documents first
|
||||
2. Look for patterns in the Implementation Guide
|
||||
3. Reference the rules.txt for coding standards
|
||||
|
||||
### Key Integration Points
|
||||
- **Authentication**: NextAuth.js with database sessions
|
||||
- **File Storage**: Cloudflare R2 with presigned URLs
|
||||
- **Real-time**: WebSocket with reconnection logic (Edge Runtime compatible)
|
||||
- **Robot Control**: ROS2 via rosbridge WebSocket protocol
|
||||
- **Caching**: Vercel KV for serverless-compatible caching
|
||||
- **Monitoring**: Vercel Analytics and structured logging
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
The implementation is considered successful when:
|
||||
- All features from Feature Requirements are implemented
|
||||
- All API routes from API Routes document are functional
|
||||
- Database schema matches the specification exactly
|
||||
- Real-time features work reliably
|
||||
- Security requirements are met
|
||||
- Performance targets are achieved
|
||||
|
||||
## 📝 Document Versions
|
||||
|
||||
- **Version**: 1.0.0
|
||||
- **Last Updated**: December 2024
|
||||
- **Target Platform**: HRIStudio v1.0
|
||||
|
||||
Remember: This documentation represents a complete specification for building HRIStudio. Every technical decision and implementation detail has been carefully considered to create a robust, scalable platform for HRI research.
|
||||
1087
docs/api-routes.md
Normal file
1087
docs/api-routes.md
Normal file
File diff suppressed because it is too large
Load Diff
329
docs/architecture-decisions.md
Normal file
329
docs/architecture-decisions.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# HRIStudio Architecture Decisions
|
||||
|
||||
## Overview
|
||||
|
||||
This document captures key architectural decisions made for HRIStudio, including technology choices, deployment strategies, and integration approaches. These decisions are based on modern web development best practices, scalability requirements, and the specific needs of HRI research.
|
||||
|
||||
## Technology Stack Decisions
|
||||
|
||||
### 1. Next.js 15 (not 14)
|
||||
|
||||
**Decision**: Use Next.js 15 with React 19 RC
|
||||
|
||||
**Rationale**:
|
||||
- Latest stable features and performance improvements
|
||||
- Better server component support
|
||||
- Improved caching mechanisms
|
||||
- Enhanced TypeScript support
|
||||
- Future-proof for upcoming React features
|
||||
|
||||
**Implementation**:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "^15.0.0",
|
||||
"react": "rc",
|
||||
"react-dom": "rc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Vercel Deployment (not self-hosted)
|
||||
|
||||
**Decision**: Deploy on Vercel's serverless platform
|
||||
|
||||
**Rationale**:
|
||||
- Automatic scaling without infrastructure management
|
||||
- Built-in CI/CD with GitHub integration
|
||||
- Global CDN for static assets
|
||||
- Edge runtime support for real-time features
|
||||
- Simplified deployment process
|
||||
- Cost-effective for research projects
|
||||
|
||||
**Implications**:
|
||||
- Use Vercel KV instead of Redis
|
||||
- Edge-compatible WebSocket implementation
|
||||
- Serverless function optimization
|
||||
- Environment variable management via Vercel CLI
|
||||
|
||||
### 3. No Redis - Alternative Solutions
|
||||
|
||||
**Decision**: Avoid Redis dependency, use platform-native solutions
|
||||
|
||||
**Alternatives Implemented**:
|
||||
|
||||
#### For Caching:
|
||||
- **Vercel KV**: Serverless Redis-compatible key-value store
|
||||
- **In-memory Maps**: For real-time state during WebSocket sessions
|
||||
- **Edge Config**: For feature flags and configuration
|
||||
|
||||
#### For Real-time State:
|
||||
```typescript
|
||||
// In-memory stores for active sessions
|
||||
export const trialStateStore = new Map<string, TrialState>();
|
||||
export const activeConnections = new Map<string, Set<WebSocket>>();
|
||||
```
|
||||
|
||||
#### For Background Jobs:
|
||||
- Use Vercel Cron Jobs for scheduled tasks
|
||||
- Implement queue with database-backed job table
|
||||
- Use serverless functions with retry logic
|
||||
|
||||
**Rationale**:
|
||||
- Reduces infrastructure complexity
|
||||
- Better integration with Vercel platform
|
||||
- Lower operational overhead
|
||||
- Sufficient for research scale
|
||||
|
||||
### 4. ROS2 Integration via rosbridge
|
||||
|
||||
**Decision**: Use WebSocket-based rosbridge protocol
|
||||
|
||||
**Architecture**:
|
||||
```
|
||||
HRIStudio (Vercel) → WebSocket → rosbridge_server → ROS2 Robot
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- No ROS2 installation required on server
|
||||
- Works with serverless architecture
|
||||
- Language-agnostic communication
|
||||
- Well-established protocol
|
||||
- Supports real-time control
|
||||
|
||||
**Implementation Details**:
|
||||
- Use `roslib.js` for client-side communication
|
||||
- Plugin system abstracts robot-specific details
|
||||
- Support for topics, services, and actions
|
||||
- SSL/TLS for production security
|
||||
|
||||
### 5. Docker for Development Only
|
||||
|
||||
**Decision**: Use Docker Compose for local development, not production
|
||||
|
||||
**Development Stack**:
|
||||
```yaml
|
||||
services:
|
||||
db: # PostgreSQL 15
|
||||
minio: # S3-compatible storage
|
||||
```
|
||||
|
||||
**Production Stack**:
|
||||
- Vercel for application hosting
|
||||
- Managed PostgreSQL (Vercel Postgres, Neon, or Supabase)
|
||||
- Cloudflare R2 for object storage
|
||||
|
||||
**Rationale**:
|
||||
- Simplifies local development setup
|
||||
- Production uses managed services
|
||||
- Better performance with platform-native solutions
|
||||
- Reduced operational complexity
|
||||
|
||||
## Data Architecture Decisions
|
||||
|
||||
### 1. PostgreSQL with Drizzle ORM
|
||||
|
||||
**Decision**: PostgreSQL as primary database with Drizzle ORM
|
||||
|
||||
**Rationale**:
|
||||
- JSONB support for flexible metadata
|
||||
- Strong consistency for research data
|
||||
- Excellent TypeScript integration with Drizzle
|
||||
- Built-in full-text search
|
||||
- Proven reliability
|
||||
|
||||
### 2. Cloudflare R2 for Object Storage
|
||||
|
||||
**Decision**: Use Cloudflare R2 instead of AWS S3
|
||||
|
||||
**Rationale**:
|
||||
- No egress fees
|
||||
- S3-compatible API
|
||||
- Better pricing for research budgets
|
||||
- Global edge network
|
||||
- Integrated with Cloudflare ecosystem
|
||||
|
||||
### 3. Hierarchical Data Model
|
||||
|
||||
**Decision**: Implement Study → Experiment → Step → Action hierarchy
|
||||
|
||||
**Rationale**:
|
||||
- Matches research methodology
|
||||
- Clear ownership and permissions
|
||||
- Efficient querying patterns
|
||||
- Natural audit trail
|
||||
|
||||
## Security Decisions
|
||||
|
||||
### 1. Database-Level Encryption
|
||||
|
||||
**Decision**: Encrypt sensitive participant data at database level
|
||||
|
||||
**Implementation**:
|
||||
- Use PostgreSQL's pgcrypto extension
|
||||
- Encrypt PII fields
|
||||
- Key management via environment variables
|
||||
|
||||
### 2. Role-Based Access Control
|
||||
|
||||
**Decision**: Four-tier role system
|
||||
|
||||
**Roles**:
|
||||
1. **Administrator**: System-wide access
|
||||
2. **Researcher**: Study creation and management
|
||||
3. **Wizard**: Trial execution only
|
||||
4. **Observer**: Read-only access
|
||||
|
||||
### 3. WebSocket Security
|
||||
|
||||
**Decision**: Token-based authentication for WebSocket connections
|
||||
|
||||
**Implementation**:
|
||||
- Session tokens for authentication
|
||||
- SSL/TLS required in production
|
||||
- Connection-specific permissions
|
||||
- Automatic reconnection with auth
|
||||
|
||||
## Performance Decisions
|
||||
|
||||
### 1. Server Components First
|
||||
|
||||
**Decision**: Maximize use of React Server Components
|
||||
|
||||
**Rationale**:
|
||||
- Reduced client bundle size
|
||||
- Better SEO
|
||||
- Faster initial page loads
|
||||
- Simplified data fetching
|
||||
|
||||
### 2. Edge Runtime for Real-time
|
||||
|
||||
**Decision**: Use Vercel Edge Runtime for WebSocket handling
|
||||
|
||||
**Benefits**:
|
||||
- Global distribution
|
||||
- Low latency
|
||||
- Automatic scaling
|
||||
- WebSocket support
|
||||
|
||||
### 3. Optimistic UI Updates
|
||||
|
||||
**Decision**: Implement optimistic updates for better UX
|
||||
|
||||
**Implementation**:
|
||||
- tRPC mutations with optimistic updates
|
||||
- Rollback on errors
|
||||
- Visual feedback during operations
|
||||
|
||||
## Integration Decisions
|
||||
|
||||
### 1. Plugin Architecture for Robots
|
||||
|
||||
**Decision**: Modular plugin system for robot support
|
||||
|
||||
**Structure**:
|
||||
```typescript
|
||||
interface RobotPlugin {
|
||||
id: string;
|
||||
name: string;
|
||||
actions: ActionDefinition[];
|
||||
executeAction(action, params): Promise<Result>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Event-Driven Architecture
|
||||
|
||||
**Decision**: Use events for loose coupling
|
||||
|
||||
**Applications**:
|
||||
- Trial execution events
|
||||
- System notifications
|
||||
- Audit logging
|
||||
- Real-time updates
|
||||
|
||||
### 3. API Design with tRPC
|
||||
|
||||
**Decision**: tRPC for type-safe API communication
|
||||
|
||||
**Benefits**:
|
||||
- End-to-end type safety
|
||||
- No API documentation needed
|
||||
- Automatic client generation
|
||||
- Smaller bundle sizes
|
||||
|
||||
## Operational Decisions
|
||||
|
||||
### 1. Monitoring Strategy
|
||||
|
||||
**Decision**: Platform-native monitoring
|
||||
|
||||
**Stack**:
|
||||
- Vercel Analytics for web vitals
|
||||
- Sentry for error tracking
|
||||
- PostHog for product analytics
|
||||
- Custom metrics API
|
||||
|
||||
### 2. Backup Strategy
|
||||
|
||||
**Decision**: Automated daily backups
|
||||
|
||||
**Implementation**:
|
||||
- Database: Point-in-time recovery
|
||||
- Media: R2 object versioning
|
||||
- Configuration: Git version control
|
||||
|
||||
### 3. Development Workflow
|
||||
|
||||
**Decision**: GitHub-centric workflow
|
||||
|
||||
**Process**:
|
||||
- Feature branches
|
||||
- PR-based reviews
|
||||
- Automated testing
|
||||
- Preview deployments
|
||||
- Protected main branch
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### 1. Multi-Region Support
|
||||
|
||||
**Preparation**:
|
||||
- Database read replicas
|
||||
- Edge caching strategy
|
||||
- Regional storage buckets
|
||||
|
||||
### 2. Mobile Application
|
||||
|
||||
**Considerations**:
|
||||
- React Native for code sharing
|
||||
- Offline-first architecture
|
||||
- WebSocket compatibility
|
||||
|
||||
### 3. AI Integration
|
||||
|
||||
**Potential Features**:
|
||||
- Experiment design suggestions
|
||||
- Automated analysis
|
||||
- Natural language commands
|
||||
- Anomaly detection
|
||||
|
||||
## Decision Log
|
||||
|
||||
| Date | Decision | Rationale | Status |
|
||||
|------|----------|-----------|---------|
|
||||
| 2024-12 | Next.js 15 | Latest features, better performance | Approved |
|
||||
| 2024-12 | Vercel deployment | Simplified ops, automatic scaling | Approved |
|
||||
| 2024-12 | No Redis | Platform-native alternatives | Approved |
|
||||
| 2024-12 | rosbridge for ROS2 | Serverless compatible | Approved |
|
||||
| 2024-12 | Cloudflare R2 | Cost-effective storage | Approved |
|
||||
|
||||
## Conclusion
|
||||
|
||||
These architectural decisions prioritize:
|
||||
1. **Developer Experience**: Type safety, modern tooling
|
||||
2. **Operational Simplicity**: Managed services, serverless
|
||||
3. **Research Needs**: Data integrity, reproducibility
|
||||
4. **Scalability**: Automatic scaling, edge distribution
|
||||
5. **Cost Efficiency**: Pay-per-use pricing, no egress fees
|
||||
|
||||
The architecture is designed to evolve with the project while maintaining stability for research operations.
|
||||
595
docs/database-schema.md
Normal file
595
docs/database-schema.md
Normal file
@@ -0,0 +1,595 @@
|
||||
# HRIStudio Database Schema
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a comprehensive database schema for HRIStudio using PostgreSQL with Drizzle ORM. The schema follows the hierarchical structure of WoZ studies and implements role-based access control, comprehensive data capture, and collaboration features.
|
||||
|
||||
## Core Entities
|
||||
|
||||
### Users and Authentication
|
||||
|
||||
```sql
|
||||
-- Users table for authentication and profile information
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
email_verified TIMESTAMP,
|
||||
name VARCHAR(255),
|
||||
image TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
|
||||
);
|
||||
|
||||
-- NextAuth accounts table
|
||||
CREATE TABLE accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
provider VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
expires_at INTEGER,
|
||||
token_type VARCHAR(255),
|
||||
scope VARCHAR(255),
|
||||
id_token TEXT,
|
||||
session_state VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(provider, provider_account_id)
|
||||
);
|
||||
|
||||
-- NextAuth sessions table
|
||||
CREATE TABLE sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_token VARCHAR(255) UNIQUE NOT NULL,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- NextAuth verification tokens
|
||||
CREATE TABLE verification_tokens (
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
expires TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (identifier, token)
|
||||
);
|
||||
```
|
||||
|
||||
### Roles and Permissions
|
||||
|
||||
```sql
|
||||
-- System roles
|
||||
CREATE TYPE system_role AS ENUM ('administrator', 'researcher', 'wizard', 'observer');
|
||||
|
||||
-- User system roles
|
||||
CREATE TABLE user_system_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role system_role NOT NULL,
|
||||
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
granted_by UUID REFERENCES users(id),
|
||||
UNIQUE(user_id, role)
|
||||
);
|
||||
|
||||
-- Custom permissions for fine-grained access control
|
||||
CREATE TABLE permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
resource VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Role permissions mapping
|
||||
CREATE TABLE role_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role system_role NOT NULL,
|
||||
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
UNIQUE(role, permission_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Study Hierarchy
|
||||
|
||||
```sql
|
||||
-- Studies: Top-level research projects
|
||||
CREATE TABLE studies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
institution VARCHAR(255),
|
||||
irb_protocol VARCHAR(100),
|
||||
status VARCHAR(50) DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'completed', 'archived')),
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
settings JSONB DEFAULT '{}',
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Study team members with roles
|
||||
CREATE TABLE study_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL CHECK (role IN ('owner', 'researcher', 'wizard', 'observer')),
|
||||
permissions JSONB DEFAULT '[]',
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
invited_by UUID REFERENCES users(id),
|
||||
UNIQUE(study_id, user_id)
|
||||
);
|
||||
|
||||
-- Experiments: Protocol templates within studies
|
||||
CREATE TABLE experiments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
version INTEGER DEFAULT 1,
|
||||
robot_id UUID REFERENCES robots(id),
|
||||
status VARCHAR(50) DEFAULT 'draft' CHECK (status IN ('draft', 'testing', 'ready', 'deprecated')),
|
||||
estimated_duration INTEGER, -- in minutes
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
deleted_at TIMESTAMP,
|
||||
UNIQUE(study_id, name, version)
|
||||
);
|
||||
|
||||
-- Trials: Executable instances of experiments
|
||||
CREATE TABLE trials (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
experiment_id UUID NOT NULL REFERENCES experiments(id),
|
||||
participant_id UUID REFERENCES participants(id),
|
||||
wizard_id UUID REFERENCES users(id),
|
||||
session_number INTEGER NOT NULL DEFAULT 1,
|
||||
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in_progress', 'completed', 'aborted', 'failed')),
|
||||
scheduled_at TIMESTAMP,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
duration INTEGER, -- actual duration in seconds
|
||||
notes TEXT,
|
||||
parameters JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Steps: Phases within experiments
|
||||
CREATE TABLE steps (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
experiment_id UUID NOT NULL REFERENCES experiments(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN ('wizard', 'robot', 'parallel', 'conditional')),
|
||||
order_index INTEGER NOT NULL,
|
||||
duration_estimate INTEGER, -- in seconds
|
||||
required BOOLEAN DEFAULT true,
|
||||
conditions JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(experiment_id, order_index)
|
||||
);
|
||||
|
||||
-- Actions: Atomic tasks within steps
|
||||
CREATE TABLE actions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
step_id UUID NOT NULL REFERENCES steps(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
type VARCHAR(100) NOT NULL, -- e.g., 'speak', 'move', 'wait', 'collect_data'
|
||||
order_index INTEGER NOT NULL,
|
||||
parameters JSONB DEFAULT '{}',
|
||||
validation_schema JSONB,
|
||||
timeout INTEGER, -- in seconds
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(step_id, order_index)
|
||||
);
|
||||
```
|
||||
|
||||
### Participants and Data Protection
|
||||
|
||||
```sql
|
||||
-- Participants in studies
|
||||
CREATE TABLE participants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
participant_code VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
name VARCHAR(255),
|
||||
demographics JSONB DEFAULT '{}', -- encrypted
|
||||
consent_given BOOLEAN DEFAULT false,
|
||||
consent_date TIMESTAMP,
|
||||
notes TEXT, -- encrypted
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(study_id, participant_code)
|
||||
);
|
||||
|
||||
-- Consent forms and documents
|
||||
CREATE TABLE consent_forms (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
version INTEGER DEFAULT 1,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
storage_path TEXT, -- path in MinIO
|
||||
UNIQUE(study_id, version)
|
||||
);
|
||||
|
||||
-- Participant consent records
|
||||
CREATE TABLE participant_consents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
participant_id UUID NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
|
||||
consent_form_id UUID NOT NULL REFERENCES consent_forms(id),
|
||||
signed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
signature_data TEXT, -- encrypted
|
||||
ip_address INET,
|
||||
storage_path TEXT, -- path to signed PDF in MinIO
|
||||
UNIQUE(participant_id, consent_form_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Robot Platform Integration
|
||||
|
||||
```sql
|
||||
-- Robot types/models
|
||||
CREATE TABLE robots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
manufacturer VARCHAR(255),
|
||||
model VARCHAR(255),
|
||||
description TEXT,
|
||||
capabilities JSONB DEFAULT '[]',
|
||||
communication_protocol VARCHAR(50) CHECK (communication_protocol IN ('rest', 'ros2', 'custom')),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Plugin definitions
|
||||
CREATE TABLE plugins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
robot_id UUID REFERENCES robots(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
version VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
author VARCHAR(255),
|
||||
repository_url TEXT,
|
||||
trust_level VARCHAR(20) CHECK (trust_level IN ('official', 'verified', 'community')),
|
||||
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'deprecated', 'disabled')),
|
||||
configuration_schema JSONB,
|
||||
action_definitions JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
UNIQUE(name, version)
|
||||
);
|
||||
|
||||
-- Plugin installations per study
|
||||
CREATE TABLE study_plugins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
plugin_id UUID NOT NULL REFERENCES plugins(id),
|
||||
configuration JSONB DEFAULT '{}',
|
||||
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
installed_by UUID NOT NULL REFERENCES users(id),
|
||||
UNIQUE(study_id, plugin_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Experiment Execution and Data Capture
|
||||
|
||||
```sql
|
||||
-- Trial events log
|
||||
CREATE TABLE trial_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trial_id UUID NOT NULL REFERENCES trials(id) ON DELETE CASCADE,
|
||||
event_type VARCHAR(50) NOT NULL, -- 'action_started', 'action_completed', 'error', 'intervention'
|
||||
action_id UUID REFERENCES actions(id),
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
data JSONB DEFAULT '{}',
|
||||
created_by UUID REFERENCES users(id), -- NULL for system events
|
||||
INDEX idx_trial_events_trial_timestamp (trial_id, timestamp)
|
||||
);
|
||||
|
||||
-- Wizard interventions/quick actions
|
||||
CREATE TABLE wizard_interventions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trial_id UUID NOT NULL REFERENCES trials(id) ON DELETE CASCADE,
|
||||
wizard_id UUID NOT NULL REFERENCES users(id),
|
||||
intervention_type VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
parameters JSONB DEFAULT '{}',
|
||||
reason TEXT
|
||||
);
|
||||
|
||||
-- Media captures (video, audio)
|
||||
CREATE TABLE media_captures (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trial_id UUID NOT NULL REFERENCES trials(id) ON DELETE CASCADE,
|
||||
media_type VARCHAR(20) CHECK (media_type IN ('video', 'audio', 'image')),
|
||||
storage_path TEXT NOT NULL, -- MinIO path
|
||||
file_size BIGINT,
|
||||
duration INTEGER, -- in seconds for video/audio
|
||||
format VARCHAR(20),
|
||||
resolution VARCHAR(20), -- for video
|
||||
start_timestamp TIMESTAMP,
|
||||
end_timestamp TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sensor data captures
|
||||
CREATE TABLE sensor_data (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trial_id UUID NOT NULL REFERENCES trials(id) ON DELETE CASCADE,
|
||||
sensor_type VARCHAR(50) NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
robot_state JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_sensor_data_trial_timestamp (trial_id, timestamp)
|
||||
);
|
||||
|
||||
-- Analysis annotations
|
||||
CREATE TABLE annotations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trial_id UUID NOT NULL REFERENCES trials(id) ON DELETE CASCADE,
|
||||
annotator_id UUID NOT NULL REFERENCES users(id),
|
||||
timestamp_start TIMESTAMP NOT NULL,
|
||||
timestamp_end TIMESTAMP,
|
||||
category VARCHAR(100),
|
||||
description TEXT,
|
||||
tags JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Collaboration and Activity Tracking
|
||||
|
||||
```sql
|
||||
-- Study activity log
|
||||
CREATE TABLE activity_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID REFERENCES studies(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50),
|
||||
resource_id UUID,
|
||||
description TEXT,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_activity_logs_study_created (study_id, created_at DESC)
|
||||
);
|
||||
|
||||
-- Comments and discussions
|
||||
CREATE TABLE comments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
parent_id UUID REFERENCES comments(id) ON DELETE CASCADE,
|
||||
resource_type VARCHAR(50) NOT NULL, -- 'experiment', 'trial', 'annotation'
|
||||
resource_id UUID NOT NULL,
|
||||
author_id UUID NOT NULL REFERENCES users(id),
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- File attachments
|
||||
CREATE TABLE attachments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
mime_type VARCHAR(100),
|
||||
file_size BIGINT,
|
||||
storage_path TEXT NOT NULL, -- MinIO path
|
||||
description TEXT,
|
||||
resource_type VARCHAR(50),
|
||||
resource_id UUID,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Data Export and Sharing
|
||||
|
||||
```sql
|
||||
-- Export jobs
|
||||
CREATE TABLE export_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
requested_by UUID NOT NULL REFERENCES users(id),
|
||||
export_type VARCHAR(50) NOT NULL, -- 'full', 'trials', 'analysis', 'media'
|
||||
format VARCHAR(20) NOT NULL, -- 'json', 'csv', 'zip'
|
||||
filters JSONB DEFAULT '{}',
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
||||
storage_path TEXT,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- Shared resources
|
||||
CREATE TABLE shared_resources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
study_id UUID NOT NULL REFERENCES studies(id) ON DELETE CASCADE,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id UUID NOT NULL,
|
||||
shared_by UUID NOT NULL REFERENCES users(id),
|
||||
share_token VARCHAR(255) UNIQUE,
|
||||
permissions JSONB DEFAULT '["read"]',
|
||||
expires_at TIMESTAMP,
|
||||
access_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### System Configuration
|
||||
|
||||
```sql
|
||||
-- System settings
|
||||
CREATE TABLE system_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key VARCHAR(100) UNIQUE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
updated_by UUID REFERENCES users(id),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Audit log for compliance
|
||||
CREATE TABLE audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50),
|
||||
resource_id UUID,
|
||||
changes JSONB DEFAULT '{}',
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_audit_logs_created (created_at DESC)
|
||||
);
|
||||
```
|
||||
|
||||
## Indexes and Performance
|
||||
|
||||
```sql
|
||||
-- Performance indexes
|
||||
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_studies_created_by ON studies(created_by) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_trials_experiment ON trials(experiment_id);
|
||||
CREATE INDEX idx_trials_status ON trials(status) WHERE status IN ('scheduled', 'in_progress');
|
||||
CREATE INDEX idx_trial_events_type ON trial_events(event_type);
|
||||
CREATE INDEX idx_participants_study ON participants(study_id);
|
||||
CREATE INDEX idx_study_members_user ON study_members(user_id);
|
||||
CREATE INDEX idx_media_captures_trial ON media_captures(trial_id);
|
||||
CREATE INDEX idx_annotations_trial ON annotations(trial_id);
|
||||
|
||||
-- Full text search indexes
|
||||
CREATE INDEX idx_studies_search ON studies USING GIN (to_tsvector('english', name || ' ' || COALESCE(description, '')));
|
||||
CREATE INDEX idx_experiments_search ON experiments USING GIN (to_tsvector('english', name || ' ' || COALESCE(description, '')));
|
||||
```
|
||||
|
||||
## Views for Common Queries
|
||||
|
||||
```sql
|
||||
-- Active studies with member count
|
||||
CREATE VIEW active_studies_summary AS
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.status,
|
||||
s.created_at,
|
||||
u.name as creator_name,
|
||||
COUNT(DISTINCT sm.user_id) as member_count,
|
||||
COUNT(DISTINCT e.id) as experiment_count,
|
||||
COUNT(DISTINCT t.id) as trial_count
|
||||
FROM studies s
|
||||
LEFT JOIN users u ON s.created_by = u.id
|
||||
LEFT JOIN study_members sm ON s.id = sm.study_id
|
||||
LEFT JOIN experiments e ON s.id = e.study_id AND e.deleted_at IS NULL
|
||||
LEFT JOIN trials t ON e.id = t.experiment_id
|
||||
WHERE s.deleted_at IS NULL AND s.status = 'active'
|
||||
GROUP BY s.id, s.name, s.status, s.created_at, u.name;
|
||||
|
||||
-- Trial execution summary
|
||||
CREATE VIEW trial_execution_summary AS
|
||||
SELECT
|
||||
t.id,
|
||||
t.experiment_id,
|
||||
t.status,
|
||||
t.scheduled_at,
|
||||
t.started_at,
|
||||
t.completed_at,
|
||||
t.duration,
|
||||
p.participant_code,
|
||||
w.name as wizard_name,
|
||||
COUNT(DISTINCT te.id) as event_count,
|
||||
COUNT(DISTINCT wi.id) as intervention_count
|
||||
FROM trials t
|
||||
LEFT JOIN participants p ON t.participant_id = p.id
|
||||
LEFT JOIN users w ON t.wizard_id = w.id
|
||||
LEFT JOIN trial_events te ON t.id = te.trial_id
|
||||
LEFT JOIN wizard_interventions wi ON t.id = wi.trial_id
|
||||
GROUP BY t.id, t.experiment_id, t.status, t.scheduled_at, t.started_at,
|
||||
t.completed_at, t.duration, p.participant_code, w.name;
|
||||
```
|
||||
|
||||
## Database Functions and Triggers
|
||||
|
||||
```sql
|
||||
-- Update timestamp trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Apply update trigger to relevant tables
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
CREATE TRIGGER update_studies_updated_at BEFORE UPDATE ON studies
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
CREATE TRIGGER update_experiments_updated_at BEFORE UPDATE ON experiments
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
-- Apply to other tables as needed...
|
||||
|
||||
-- Function to check user permissions
|
||||
CREATE OR REPLACE FUNCTION check_user_permission(
|
||||
p_user_id UUID,
|
||||
p_study_id UUID,
|
||||
p_action VARCHAR
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
v_has_permission BOOLEAN;
|
||||
BEGIN
|
||||
-- Check if user has permission through study membership or system role
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM study_members sm
|
||||
WHERE sm.user_id = p_user_id
|
||||
AND sm.study_id = p_study_id
|
||||
AND (
|
||||
sm.role = 'owner' OR
|
||||
p_action = ANY(sm.permissions::text[])
|
||||
)
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM user_system_roles usr
|
||||
WHERE usr.user_id = p_user_id
|
||||
AND usr.role = 'administrator'
|
||||
) INTO v_has_permission;
|
||||
|
||||
RETURN v_has_permission;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
1. Tables should be created in the order listed to respect foreign key constraints
|
||||
2. Sensitive data in `participants`, `participant_consents`, and related tables should use PostgreSQL's pgcrypto extension for encryption
|
||||
3. Consider partitioning large tables like `sensor_data` and `trial_events` by date for better performance
|
||||
4. Implement regular vacuum and analyze schedules for optimal performance
|
||||
5. Set up appropriate backup strategies for both PostgreSQL and MinIO data
|
||||
1058
docs/deployment-operations.md
Normal file
1058
docs/deployment-operations.md
Normal file
File diff suppressed because it is too large
Load Diff
609
docs/feature-requirements.md
Normal file
609
docs/feature-requirements.md
Normal file
@@ -0,0 +1,609 @@
|
||||
# HRIStudio Feature Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides detailed feature requirements for HRIStudio, organized by functional areas. Each feature includes user stories, acceptance criteria, and technical implementation notes.
|
||||
|
||||
## 1. Authentication and User Management
|
||||
|
||||
### 1.1 User Registration
|
||||
|
||||
**User Story**: As a new researcher, I want to create an account so that I can start using HRIStudio for my studies.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Support email/password registration
|
||||
- Support OAuth providers (Google, GitHub, Microsoft)
|
||||
- Email verification required before account activation
|
||||
- Capture user's name and institution during registration
|
||||
- Password strength requirements enforced
|
||||
- Prevent duplicate email registrations
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] User can register with valid email and strong password
|
||||
- [ ] Email verification sent within 1 minute
|
||||
- [ ] OAuth registration creates account with verified email
|
||||
- [ ] Appropriate error messages for validation failures
|
||||
- [ ] Account creation logged in audit trail
|
||||
|
||||
**Technical Notes**:
|
||||
- Use NextAuth.js v5 for authentication
|
||||
- Store hashed passwords using bcrypt
|
||||
- Implement rate limiting on registration endpoint
|
||||
|
||||
### 1.2 User Login
|
||||
|
||||
**User Story**: As a registered user, I want to log in securely so that I can access my studies.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Support email/password login
|
||||
- Support OAuth login
|
||||
- Remember me functionality
|
||||
- Session timeout after inactivity
|
||||
- Multiple device login support
|
||||
- Failed login attempt tracking
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Successful login redirects to dashboard
|
||||
- [ ] Failed login shows appropriate error
|
||||
- [ ] Session persists based on remember me selection
|
||||
- [ ] Account locked after 5 failed attempts
|
||||
- [ ] OAuth login works seamlessly
|
||||
|
||||
### 1.3 Role Management
|
||||
|
||||
**User Story**: As an administrator, I want to assign system roles to users so that they have appropriate permissions.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Four system roles: Administrator, Researcher, Wizard, Observer
|
||||
- Role assignment by administrators only
|
||||
- Role changes take effect immediately
|
||||
- Role history maintained
|
||||
- Bulk role assignment support
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Admin can view all users and their roles
|
||||
- [ ] Admin can change user roles
|
||||
- [ ] Role changes logged in audit trail
|
||||
- [ ] Users see appropriate UI based on role
|
||||
- [ ] Cannot remove last administrator
|
||||
|
||||
## 2. Study Management
|
||||
|
||||
### 2.1 Study Creation
|
||||
|
||||
**User Story**: As a researcher, I want to create a new study so that I can organize my experiments.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Create study with name and description
|
||||
- Optional IRB protocol number
|
||||
- Institution association
|
||||
- Auto-assign creator as study owner
|
||||
- Study status tracking (draft, active, completed, archived)
|
||||
- Rich metadata support
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Study created with unique identifier
|
||||
- [ ] Creator has full permissions on study
|
||||
- [ ] Study appears in user's study list
|
||||
- [ ] Can edit study details after creation
|
||||
- [ ] Study creation logged
|
||||
|
||||
### 2.2 Team Collaboration
|
||||
|
||||
**User Story**: As a study owner, I want to add team members so that we can collaborate on the research.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Add users by email with specific role
|
||||
- Study-specific roles: Owner, Researcher, Wizard, Observer
|
||||
- Email invitations for non-registered users
|
||||
- Permission customization per member
|
||||
- Member removal capability
|
||||
- Transfer ownership functionality
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Can add existing users immediately
|
||||
- [ ] Invitations sent to new users
|
||||
- [ ] Members see study in their dashboard
|
||||
- [ ] Permissions enforced throughout app
|
||||
- [ ] Activity log shows member changes
|
||||
|
||||
### 2.3 Study Dashboard
|
||||
|
||||
**User Story**: As a study member, I want to see study progress at a glance so that I can track our research.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Overview of experiments and trials
|
||||
- Recent activity timeline
|
||||
- Team member list with online status
|
||||
- Upcoming scheduled trials
|
||||
- Quick statistics (participants, completion rate)
|
||||
- Document repository access
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Dashboard loads within 2 seconds
|
||||
- [ ] Real-time updates for trial status
|
||||
- [ ] Click-through to detailed views
|
||||
- [ ] Responsive design for mobile
|
||||
- [ ] Export study summary report
|
||||
|
||||
## 3. Experiment Design
|
||||
|
||||
### 3.1 Visual Experiment Designer
|
||||
|
||||
**User Story**: As a researcher, I want to design experiments visually so that I don't need programming skills.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Drag-and-drop interface for steps
|
||||
- Step types: Wizard, Robot, Parallel, Conditional
|
||||
- Action library based on robot capabilities
|
||||
- Parameter configuration panels
|
||||
- Visual flow representation
|
||||
- Undo/redo functionality
|
||||
- Auto-save while designing
|
||||
- Version control for designs
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Can create experiment without code
|
||||
- [ ] Visual representation matches execution flow
|
||||
- [ ] Validation prevents invalid configurations
|
||||
- [ ] Can preview experiment flow
|
||||
- [ ] Changes saved automatically
|
||||
- [ ] Can revert to previous versions
|
||||
|
||||
### 3.2 Step Configuration
|
||||
|
||||
**User Story**: As a researcher, I want to configure each step in detail so that the experiment runs correctly.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Name and description for each step
|
||||
- Duration estimates
|
||||
- Required vs optional steps
|
||||
- Conditional logic support
|
||||
- Parameter validation
|
||||
- Help text and examples
|
||||
- Copy/paste steps between experiments
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] All step properties editable
|
||||
- [ ] Validation prevents invalid values
|
||||
- [ ] Conditions use intuitive UI
|
||||
- [ ] Can test conditions with sample data
|
||||
- [ ] Duration estimates aggregate correctly
|
||||
|
||||
### 3.3 Action Management
|
||||
|
||||
**User Story**: As a researcher, I want to add specific actions to steps so that the robot and wizard know what to do.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Action types based on robot plugin
|
||||
- Wizard instruction actions
|
||||
- Robot command actions
|
||||
- Data collection actions
|
||||
- Wait/delay actions
|
||||
- Parameter configuration per action
|
||||
- Action validation
|
||||
- Quick action templates
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Actions appropriate to step type
|
||||
- [ ] Parameters validated in real-time
|
||||
- [ ] Can reorder actions within step
|
||||
- [ ] Action execution time estimates
|
||||
- [ ] Templates speed up common tasks
|
||||
|
||||
### 3.4 Experiment Validation
|
||||
|
||||
**User Story**: As a researcher, I want to validate my experiment before running trials so that I can catch errors early.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Automatic validation on save
|
||||
- Manual validation trigger
|
||||
- Check robot compatibility
|
||||
- Verify parameter completeness
|
||||
- Estimate total duration
|
||||
- Identify potential issues
|
||||
- Suggest improvements
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Validation completes within 5 seconds
|
||||
- [ ] Clear error messages with fixes
|
||||
- [ ] Warnings for non-critical issues
|
||||
- [ ] Can run validation without saving
|
||||
- [ ] Validation status clearly shown
|
||||
|
||||
## 4. Robot Integration
|
||||
|
||||
### 4.1 Plugin Management
|
||||
|
||||
**User Story**: As a researcher, I want to install robot plugins so that I can use different robots in my studies.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Browse available plugins
|
||||
- Filter by robot type and trust level
|
||||
- View plugin details and documentation
|
||||
- One-click installation
|
||||
- Configuration interface
|
||||
- Version management
|
||||
- Plugin updates notifications
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Plugin store loads quickly
|
||||
- [ ] Can search and filter plugins
|
||||
- [ ] Installation completes without errors
|
||||
- [ ] Configuration validated
|
||||
- [ ] Can uninstall plugins cleanly
|
||||
|
||||
### 4.2 Robot Communication
|
||||
|
||||
**User Story**: As a system, I need to communicate with robots reliably so that experiments run smoothly.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Support REST, ROS2, and custom protocols
|
||||
- Connection health monitoring
|
||||
- Automatic reconnection
|
||||
- Command queuing
|
||||
- Response timeout handling
|
||||
- Error recovery
|
||||
- Latency tracking
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Commands sent within 100ms
|
||||
- [ ] Connection status visible
|
||||
- [ ] Graceful handling of disconnections
|
||||
- [ ] Commands never lost
|
||||
- [ ] Errors reported clearly
|
||||
|
||||
### 4.3 Action Translation
|
||||
|
||||
**User Story**: As a system, I need to translate abstract actions to robot commands so that experiments work across platforms.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Map abstract actions to robot-specific commands
|
||||
- Parameter transformation
|
||||
- Capability checking
|
||||
- Fallback behaviors
|
||||
- Success/failure detection
|
||||
- State synchronization
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Translations happen transparently
|
||||
- [ ] Incompatible actions prevented
|
||||
- [ ] Clear error messages
|
||||
- [ ] Robot state tracked accurately
|
||||
- [ ] Performance overhead < 50ms
|
||||
|
||||
## 5. Trial Execution
|
||||
|
||||
### 5.1 Trial Scheduling
|
||||
|
||||
**User Story**: As a researcher, I want to schedule trials in advance so that participants and wizards can plan.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Calendar interface for scheduling
|
||||
- Participant assignment
|
||||
- Wizard assignment
|
||||
- Email notifications
|
||||
- Schedule conflict detection
|
||||
- Recurring trial support
|
||||
- Time zone handling
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Can schedule weeks in advance
|
||||
- [ ] Notifications sent automatically
|
||||
- [ ] No double-booking possible
|
||||
- [ ] Can reschedule easily
|
||||
- [ ] Calendar syncs with external tools
|
||||
|
||||
### 5.2 Wizard Interface
|
||||
|
||||
**User Story**: As a wizard, I want an intuitive interface during trials so that I can focus on the participant.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Step-by-step guidance
|
||||
- Current instruction display
|
||||
- Live video feed
|
||||
- Quick action buttons
|
||||
- Emergency stop
|
||||
- Note-taking ability
|
||||
- Progress indicator
|
||||
- Intervention logging
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Interface loads in < 3 seconds
|
||||
- [ ] Video feed has < 500ms latency
|
||||
- [ ] All controls easily accessible
|
||||
- [ ] Can operate with keyboard only
|
||||
- [ ] Notes saved automatically
|
||||
|
||||
### 5.3 Real-time Execution
|
||||
|
||||
**User Story**: As a wizard, I need real-time control so that I can respond to participant behavior.
|
||||
|
||||
**Functional Requirements**:
|
||||
- WebSocket connection for updates
|
||||
- < 100ms command latency
|
||||
- Synchronized state management
|
||||
- Offline capability with sync
|
||||
- Concurrent observer support
|
||||
- Event stream recording
|
||||
- Bandwidth optimization
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Commands execute immediately
|
||||
- [ ] State synchronized across clients
|
||||
- [ ] Observers see same view
|
||||
- [ ] Works on 4G connection
|
||||
- [ ] No data loss on disconnect
|
||||
|
||||
### 5.4 Data Capture
|
||||
|
||||
**User Story**: As a researcher, I want all trial data captured automatically so that nothing is lost.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Video recording (configurable quality)
|
||||
- Audio recording
|
||||
- Event timeline capture
|
||||
- Robot sensor data
|
||||
- Wizard actions/interventions
|
||||
- Participant responses
|
||||
- Automatic uploads
|
||||
- Encryption for sensitive data
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] All data streams captured
|
||||
- [ ] < 5% frame drop rate
|
||||
- [ ] Uploads complete within 5 min
|
||||
- [ ] Data encrypted at rest
|
||||
- [ ] Can verify data integrity
|
||||
|
||||
## 6. Participant Management
|
||||
|
||||
### 6.1 Participant Registration
|
||||
|
||||
**User Story**: As a researcher, I want to register participants so that I can track their involvement.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Anonymous participant codes
|
||||
- Optional demographic data
|
||||
- Consent form integration
|
||||
- Contact information (encrypted)
|
||||
- Study assignment
|
||||
- Participation history
|
||||
- GDPR compliance tools
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Unique codes generated
|
||||
- [ ] PII encrypted in database
|
||||
- [ ] Can export participant data
|
||||
- [ ] Consent status tracked
|
||||
- [ ] Right to deletion supported
|
||||
|
||||
### 6.2 Consent Management
|
||||
|
||||
**User Story**: As a researcher, I want to manage consent forms so that ethical requirements are met.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Create consent form templates
|
||||
- Version control for forms
|
||||
- Digital signature capture
|
||||
- PDF generation
|
||||
- Multi-language support
|
||||
- Audit trail
|
||||
- Withdrawal handling
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Forms legally compliant
|
||||
- [ ] Signatures timestamped
|
||||
- [ ] PDFs generated automatically
|
||||
- [ ] Can track consent status
|
||||
- [ ] Withdrawal process clear
|
||||
|
||||
## 7. Data Analysis
|
||||
|
||||
### 7.1 Playback Interface
|
||||
|
||||
**User Story**: As a researcher, I want to review trial recordings so that I can analyze participant behavior.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Synchronized playback of all streams
|
||||
- Variable playback speed
|
||||
- Frame-by-frame navigation
|
||||
- Event timeline overlay
|
||||
- Annotation tools
|
||||
- Bookmark important moments
|
||||
- Export clips
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Smooth playback at 1080p
|
||||
- [ ] All streams synchronized
|
||||
- [ ] Can jump to any event
|
||||
- [ ] Annotations saved in real-time
|
||||
- [ ] Clips export in standard formats
|
||||
|
||||
### 7.2 Annotation System
|
||||
|
||||
**User Story**: As a researcher, I want to annotate trials so that I can code behaviors and events.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Time-based annotations
|
||||
- Categorization system
|
||||
- Tag support
|
||||
- Multi-coder support
|
||||
- Inter-rater reliability
|
||||
- Annotation templates
|
||||
- Bulk annotation tools
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Can annotate while playing
|
||||
- [ ] Categories customizable
|
||||
- [ ] Can compare coder annotations
|
||||
- [ ] Export annotations as CSV
|
||||
- [ ] Search annotations easily
|
||||
|
||||
### 7.3 Data Export
|
||||
|
||||
**User Story**: As a researcher, I want to export data so that I can analyze it in external tools.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Multiple export formats (CSV, JSON, SPSS)
|
||||
- Selective data export
|
||||
- Anonymization options
|
||||
- Batch export
|
||||
- Scheduled exports
|
||||
- API access
|
||||
- R/Python integration examples
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Exports complete within minutes
|
||||
- [ ] Data properly formatted
|
||||
- [ ] Anonymization verified
|
||||
- [ ] Can automate exports
|
||||
- [ ] Documentation provided
|
||||
|
||||
## 8. System Administration
|
||||
|
||||
### 8.1 User Management
|
||||
|
||||
**User Story**: As an administrator, I want to manage all users so that the system remains secure.
|
||||
|
||||
**Functional Requirements**:
|
||||
- User search and filtering
|
||||
- Bulk operations
|
||||
- Activity monitoring
|
||||
- Access logs
|
||||
- Password resets
|
||||
- Account suspension
|
||||
- Usage statistics
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Can find users quickly
|
||||
- [ ] Bulk operations reversible
|
||||
- [ ] Activity logs comprehensive
|
||||
- [ ] Can force password reset
|
||||
- [ ] Usage reports exportable
|
||||
|
||||
### 8.2 System Configuration
|
||||
|
||||
**User Story**: As an administrator, I want to configure system settings so that it meets our needs.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Storage configuration
|
||||
- Email settings
|
||||
- Security policies
|
||||
- Backup schedules
|
||||
- Plugin management
|
||||
- Performance tuning
|
||||
- Feature flags
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Changes take effect immediately
|
||||
- [ ] Configuration backed up
|
||||
- [ ] Can test settings safely
|
||||
- [ ] Rollback capability
|
||||
- [ ] Changes logged
|
||||
|
||||
### 8.3 Monitoring and Maintenance
|
||||
|
||||
**User Story**: As an administrator, I want to monitor system health so that I can prevent issues.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Real-time metrics dashboard
|
||||
- Alert configuration
|
||||
- Log aggregation
|
||||
- Performance metrics
|
||||
- Storage usage tracking
|
||||
- Backup verification
|
||||
- Update management
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Metrics update in real-time
|
||||
- [ ] Alerts sent within 1 minute
|
||||
- [ ] Logs searchable
|
||||
- [ ] Can identify bottlenecks
|
||||
- [ ] Updates tested before deploy
|
||||
|
||||
## 9. Mobile Support
|
||||
|
||||
### 9.1 Responsive Web Design
|
||||
|
||||
**User Story**: As a user, I want to access HRIStudio on my tablet so that I can work anywhere.
|
||||
|
||||
**Functional Requirements**:
|
||||
- Responsive layouts for all pages
|
||||
- Touch-optimized controls
|
||||
- Offline capability for critical features
|
||||
- Reduced data usage mode
|
||||
- Native app features via PWA
|
||||
- Biometric authentication
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Works on tablets and large phones
|
||||
- [ ] Touch targets appropriately sized
|
||||
- [ ] Can work offline for 1 hour
|
||||
- [ ] PWA installable
|
||||
- [ ] Performance acceptable on 4G
|
||||
|
||||
## 10. Integration and APIs
|
||||
|
||||
### 10.1 External Tool Integration
|
||||
|
||||
**User Story**: As a researcher, I want to integrate with analysis tools so that I can use my preferred software.
|
||||
|
||||
**Functional Requirements**:
|
||||
- RESTful API with authentication
|
||||
- GraphQL endpoint for complex queries
|
||||
- Webhook support for events
|
||||
- OAuth provider capability
|
||||
- SDK for common languages
|
||||
- OpenAPI documentation
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] API response time < 200ms
|
||||
- [ ] Rate limiting implemented
|
||||
- [ ] Webhooks reliable
|
||||
- [ ] SDKs well documented
|
||||
- [ ] Breaking changes versioned
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
- Page load time < 2 seconds
|
||||
- API response time < 200ms (p95)
|
||||
- Support 100 concurrent users
|
||||
- Video streaming at 1080p30fps
|
||||
- Database queries < 100ms
|
||||
|
||||
### Security
|
||||
- OWASP Top 10 compliance
|
||||
- Data encryption at rest and in transit
|
||||
- Regular security audits
|
||||
- Penetration testing
|
||||
- GDPR and HIPAA compliance options
|
||||
|
||||
### Scalability
|
||||
- Horizontal scaling capability
|
||||
- Database sharding ready
|
||||
- CDN for media delivery
|
||||
- Microservices architecture ready
|
||||
- Multi-region deployment support
|
||||
|
||||
### Reliability
|
||||
- 99.9% uptime SLA
|
||||
- Automated backups every 4 hours
|
||||
- Disaster recovery plan
|
||||
- Data replication
|
||||
- Graceful degradation
|
||||
|
||||
### Usability
|
||||
- WCAG 2.1 AA compliance
|
||||
- Multi-language support
|
||||
- Comprehensive help documentation
|
||||
- In-app tutorials
|
||||
- Context-sensitive help
|
||||
|
||||
### Maintainability
|
||||
- Comprehensive test coverage (>80%)
|
||||
- Automated deployment pipeline
|
||||
- Monitoring and alerting
|
||||
- Clear error messages
|
||||
- Modular architecture
|
||||
1075
docs/implementation-guide.md
Normal file
1075
docs/implementation-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
228
docs/project-overview.md
Normal file
228
docs/project-overview.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# HRIStudio Project Overview
|
||||
|
||||
## Executive Summary
|
||||
|
||||
HRIStudio is a web-based platform designed to standardize and improve the reproducibility of Wizard of Oz (WoZ) studies in Human-Robot Interaction (HRI) research. The platform addresses critical challenges in HRI research by providing a comprehensive experimental workflow management system with standardized terminology, visual experiment design tools, real-time wizard control interfaces, and comprehensive data capture capabilities.
|
||||
|
||||
## Project Goals
|
||||
|
||||
### Primary Objectives
|
||||
1. **Enhance Scientific Rigor**: Standardize WoZ study methodologies to improve reproducibility
|
||||
2. **Lower Barriers to Entry**: Make HRI research accessible to researchers without deep robot programming expertise
|
||||
3. **Enable Collaboration**: Support multi-user workflows with role-based access control
|
||||
4. **Ensure Data Integrity**: Comprehensive capture and secure storage of all experimental data
|
||||
5. **Support Multiple Robot Platforms**: Provide a plugin-based architecture for robot integration
|
||||
|
||||
### Key Problems Addressed
|
||||
- Lack of standardized terminology in WoZ studies
|
||||
- Poor documentation practices leading to unreproducible experiments
|
||||
- Technical barriers preventing non-programmers from conducting HRI research
|
||||
- Inconsistent wizard behavior across trials
|
||||
- Limited data capture and analysis capabilities in existing tools
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Hierarchical Experiment Structure
|
||||
- **Study**: Top-level container for research projects
|
||||
- **Experiment**: Parameterized protocol templates within a study
|
||||
- **Trial**: Executable instances of experiments with specific participants
|
||||
- **Step**: Distinct phases in the execution sequence
|
||||
- **Action**: Atomic tasks for wizards or robots
|
||||
|
||||
### 2. Visual Experiment Designer (EDE)
|
||||
- Drag-and-drop interface for creating experiment workflows
|
||||
- No-code solution for experiment design
|
||||
- Context-sensitive help and best practice guidance
|
||||
- Automatic generation of robot-specific action components
|
||||
- Parameter configuration with validation
|
||||
|
||||
### 3. Adaptive Wizard Interface
|
||||
- Real-time experiment execution dashboard
|
||||
- Step-by-step guidance for consistent execution
|
||||
- Quick actions for unscripted interventions
|
||||
- Live video feed integration
|
||||
- Timestamped event logging
|
||||
- Customizable per-experiment controls
|
||||
|
||||
### 4. Robot Platform Integration
|
||||
- Plugin-based architecture for robot support
|
||||
- Abstract action definitions with platform-specific translations
|
||||
- Support for RESTful APIs, ROS2, and custom protocols
|
||||
- Plugin Store with trust levels (Official, Verified, Community)
|
||||
- Version tracking for reproducibility
|
||||
|
||||
### 5. Comprehensive Data Management
|
||||
- Automatic capture of all experimental data
|
||||
- Synchronized multi-modal data streams (video, audio, logs, sensor data)
|
||||
- Encrypted storage for sensitive participant data
|
||||
- Role-based access control for data security
|
||||
- Export capabilities for analysis tools
|
||||
|
||||
### 6. Collaboration Features
|
||||
- Multi-user support with defined roles
|
||||
- Project dashboards with status tracking
|
||||
- Shared experiment templates and resources
|
||||
- Activity logs and audit trails
|
||||
- Support for double-blind study designs
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Three-Layer Architecture
|
||||
|
||||
#### 1. User Interface Layer
|
||||
- **Experiment Designer**: Visual programming interface for creating experiments
|
||||
- **Wizard Interface**: Real-time control and monitoring during trials
|
||||
- **Playback & Analysis**: Data exploration and visualization tools
|
||||
- **Administration Panel**: System configuration and user management
|
||||
- **Plugin Store**: Browse and install robot platform integrations
|
||||
|
||||
#### 2. Data Management Layer
|
||||
- **Database**: PostgreSQL for structured data and metadata
|
||||
- **Object Storage**: MinIO (S3-compatible) for media files
|
||||
- **Access Control**: Role-based permissions system
|
||||
- **API Layer**: tRPC for type-safe client-server communication
|
||||
- **Data Models**: Drizzle ORM for database operations
|
||||
|
||||
#### 3. Robot Integration Layer
|
||||
- **Plugin System**: Modular robot platform support
|
||||
- **Action Translation**: Abstract to platform-specific command mapping
|
||||
- **Communication Protocols**: Support for REST, ROS2, and custom protocols
|
||||
- **State Management**: Robot status tracking and synchronization
|
||||
|
||||
## User Roles and Permissions
|
||||
|
||||
### Administrator
|
||||
- Full system access
|
||||
- User management capabilities
|
||||
- System configuration
|
||||
- Plugin installation and management
|
||||
- Database maintenance
|
||||
|
||||
### Researcher
|
||||
- Create and manage studies
|
||||
- Design experiments
|
||||
- Manage team members
|
||||
- View all trial data
|
||||
- Export data for analysis
|
||||
|
||||
### Wizard
|
||||
- Execute assigned experiments
|
||||
- Control robot during trials
|
||||
- Make real-time decisions
|
||||
- View experiment instructions
|
||||
- Access quick actions
|
||||
|
||||
### Observer
|
||||
- Read-only access to experiments
|
||||
- Monitor live trial execution
|
||||
- Add notes and annotations
|
||||
- View historical data
|
||||
- No control capabilities
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Frontend
|
||||
- **Framework**: Next.js 14+ (App Router)
|
||||
- **UI Components**: shadcn/ui (built on Radix UI)
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State Management**: nuqs (URL state), React Server Components
|
||||
- **Forms**: React Hook Form with Zod validation
|
||||
- **Real-time**: WebSockets for live updates
|
||||
|
||||
### Backend
|
||||
- **Runtime**: Node.js with Bun package manager
|
||||
- **API**: tRPC for type-safe endpoints
|
||||
- **Database**: PostgreSQL with Drizzle ORM
|
||||
- **Authentication**: NextAuth.js v5 (Auth.js)
|
||||
- **File Storage**: MinIO (S3-compatible object storage)
|
||||
- **Background Jobs**: Bull queue with Redis
|
||||
|
||||
### Infrastructure
|
||||
- **Containerization**: Docker and Docker Compose
|
||||
- **Development**: Hot reloading, TypeScript strict mode
|
||||
- **Testing**: Vitest for unit tests, Playwright for E2E
|
||||
- **CI/CD**: GitHub Actions
|
||||
- **Monitoring**: OpenTelemetry integration
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Experiment Lifecycle
|
||||
1. **Design Phase**: Researchers create experiment templates using visual designer
|
||||
2. **Configuration Phase**: Set parameters and assign team members
|
||||
3. **Execution Phase**: Wizards run trials with participants
|
||||
4. **Analysis Phase**: Review captured data and generate insights
|
||||
5. **Sharing Phase**: Export or share experiment materials
|
||||
|
||||
### Data Flow
|
||||
1. **Input**: Experiment designs, wizard actions, robot responses, sensor data
|
||||
2. **Processing**: Action translation, state management, data synchronization
|
||||
3. **Storage**: Structured data in PostgreSQL, media files in MinIO
|
||||
4. **Output**: Real-time updates, analysis reports, exported datasets
|
||||
|
||||
### Plugin Architecture
|
||||
- **Action Definitions**: Abstract representations of robot capabilities
|
||||
- **Parameter Schemas**: Type-safe configuration with validation
|
||||
- **Communication Adapters**: Platform-specific protocol implementations
|
||||
- **Version Management**: Semantic versioning for compatibility
|
||||
|
||||
## Development Principles
|
||||
|
||||
### Code Quality
|
||||
- TypeScript throughout with strict type checking
|
||||
- Functional programming patterns (avoid classes)
|
||||
- Comprehensive error handling
|
||||
- Extensive logging for debugging
|
||||
- Clean architecture with separation of concerns
|
||||
|
||||
### User Experience
|
||||
- Mobile-first responsive design
|
||||
- Progressive enhancement
|
||||
- Optimistic UI updates
|
||||
- Comprehensive loading states
|
||||
- Intuitive error messages
|
||||
|
||||
### Performance
|
||||
- Server-side rendering where possible
|
||||
- Lazy loading for non-critical components
|
||||
- Image optimization (WebP, proper sizing)
|
||||
- Database query optimization
|
||||
- Caching strategies
|
||||
|
||||
### Security
|
||||
- Role-based access control at all levels
|
||||
- Data encryption at rest and in transit
|
||||
- Input validation and sanitization
|
||||
- Rate limiting on API endpoints
|
||||
- Audit logging for compliance
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical Metrics
|
||||
- Page load time < 2 seconds
|
||||
- API response time < 200ms (p95)
|
||||
- 99.9% uptime for critical services
|
||||
- Zero data loss incidents
|
||||
- Support for 100+ concurrent users
|
||||
|
||||
### User Success Metrics
|
||||
- Time to create first experiment < 30 minutes
|
||||
- Trial execution consistency > 95%
|
||||
- Data capture completeness 100%
|
||||
- User satisfaction score > 4.5/5
|
||||
- Active monthly users growth
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Planned Enhancements
|
||||
- AI-powered experiment design suggestions
|
||||
- Advanced analytics and visualization tools
|
||||
- Mobile app for wizard control
|
||||
- Cloud-hosted SaaS offering
|
||||
- Integration with popular analysis tools (R, Python)
|
||||
|
||||
### Extensibility Points
|
||||
- Custom plugin development SDK
|
||||
- Webhook system for external integrations
|
||||
- Custom report generation
|
||||
- API for third-party tools
|
||||
- Theming and white-labeling support
|
||||
285
docs/root.tex
Normal file
285
docs/root.tex
Normal file
@@ -0,0 +1,285 @@
|
||||
% Standard Paper
|
||||
\documentclass[letterpaper, 10 pt, conference]{subfiles/ieeeconf}
|
||||
|
||||
% A4 Paper
|
||||
%\documentclass[a4paper, 10pt, conference]{ieeeconf}
|
||||
|
||||
% Only needed for \thanks command
|
||||
\IEEEoverridecommandlockouts
|
||||
|
||||
% Needed to meet printer requirements.
|
||||
\overrideIEEEmargins
|
||||
|
||||
%In case you encounter the following error:
|
||||
%Error 1010 The PDF file may be corrupt (unable to open PDF file) OR
|
||||
%Error 1000 An error occurred while parsing a contents stream. Unable to analyze the PDF file.
|
||||
%This is a known problem with pdfLaTeX conversion filter. The file cannot be opened with acrobat reader
|
||||
%Please use one of the alternatives below to circumvent this error by uncommenting one or the other
|
||||
%\pdfobjcompresslevel=0
|
||||
%\pdfminorversion=4
|
||||
|
||||
% See the \addtolength command later in the file to balance the column lengths
|
||||
% on the last page of the document
|
||||
|
||||
% The following packages can be found on http:\\www.ctan.org
|
||||
\usepackage{graphicx} % for pdf, bitmapped graphics files
|
||||
%\usepackage{epsfig} % for postscript graphics files
|
||||
%\usepackage{mathptmx} % assumes new font selection scheme installed
|
||||
%\usepackage{times} % assumes new font selection scheme installed
|
||||
%\usepackage{amsmath} % assumes amsmath package installed
|
||||
%\usepackage{amssymb} % assumes amsmath package installed
|
||||
\usepackage{url}
|
||||
\usepackage{float}
|
||||
|
||||
\hyphenation{analysis}
|
||||
|
||||
\title{\LARGE \bf A Web-Based Wizard-of-Oz Platform for Collaborative and Reproducible Human-Robot Interaction Research}
|
||||
|
||||
\author{Sean O'Connor and L. Felipe Perrone$^{*}$
|
||||
\thanks{$^{*}$Both authors are with the Department of Computer Science at
|
||||
Bucknell University in Lewisburg, PA, USA. They can be reached at {\tt\small sso005@bucknell.edu} and {\tt\small perrone@bucknell.edu}}%
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
\thispagestyle{empty}
|
||||
\pagestyle{empty}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{abstract}
|
||||
|
||||
Human-robot interaction (HRI) research plays a pivotal role in shaping how robots communicate and collaborate with humans. However, conducting HRI studies can be challenging, particularly those employing the Wizard-of-Oz (WoZ) technique. WoZ user studies can have technical and methodological complexities that may render the results irreproducible. We propose to address these challenges with HRIStudio, a modular web-based platform designed to streamline the design, the execution, and the analysis of WoZ experiments. HRIStudio offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility. By lowering technical barriers, promoting collaboration, and offering methodological guidelines, HRIStudio aims to make human-centered robotics research easier and empower researchers to develop scientifically rigorous user studies.
|
||||
|
||||
\end{abstract}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
Human-robot interaction (HRI) is an essential field of study for understanding how robots should communicate, collaborate, and coexist with people. The development of autonomous behaviors in social robot applications, however, offers a number of challenges. The Wizard-of-Oz (WoZ) technique has emerged as a valuable experimental paradigm to address these difficulties, as it allows experimenters to simulate a robot's autonomous behaviors. With WoZ, a human operator (the \emph{``wizard''}) can operate the robot remotely, essentially simulating its autonomous behavior during user studies. This enables the rapid prototyping and continuous refinement of human-robot interactions postponing to later the full development of complex robot behaviors.
|
||||
|
||||
While WoZ is a powerful paradigm, it does not eliminate all experimental challenges. The paradigm is centered on the wizard who must carry out scripted sequences of actions. Ideally, the wizard should execute their script identically across runs of the experiment with different participants. Deviations from the script in one run or another may change experimental conditions significantly decreasing the methodological rigor of the larger study. This kind of problem can be minimized by instrumenting the wizard with a system that prevents deviations from the prescribed interactions with the participant. In addition to the variability that can be introduced by wizard behaviors, WoZ studies can be undermined by technical barriers related to the use of specialized equipment and tools. Different robots may be controlled or programmed through different systems requiring expertise with a range of technologies such as programming languages, development environments, and operating systems.
|
||||
|
||||
The elaboration and the execution of rigorous and reproducible WoZ experiments can be challenging for HRI researchers. Although there do exist solutions to support this kind of endeavor, they often rely on low-level robot operating systems, limited proprietary platforms, or require extensive custom coding, which can restrict their use to domain experts with extensive technical backgrounds. The development of our work was motivated by the desire to offer a platform that would lower the barriers to entry in HRI research with the WoZ paradigm.
|
||||
|
||||
Through the literature review described in the next section, we identified six categories of desirables to be included in a modern system that streamlines the WoZ experimental process: an environment that integrates all the functionalities of the system; mechanisms for the description of WoZ experiments which require minimal to no coding expertise; fine grained, real-time control of scripted experimental runs with a variety of robotic platforms; comprehensive data collection and logging; a platform-agnostic approach to support a wide range of robot hardware; and collaborative features that allow research teams to work together effectively.
|
||||
|
||||
The design and development of HRIStudio were driven by the desirables enumerated above and described in \cite{OConnor2024}, our preliminary report. In this work, our main contribution is to demonstrate how a system such the one we are developing has significant potential to make WoZ experiments easier to carry out, more rigorous, and ultimately reproducible. The remainder of this paper is structured as follows. In Section~\ref{sota}, we establish the context for our contribution through a review of recent literature. In Section~\ref{repchallenges}, we discuss the aspects of the WoZ paradigm that can lead to reproducibility challenges and in Section~\ref{arch} we propose solutions to address these challenges. Subsequently, in Section~\ref{workflow}, we describe our solution to create a structure for the experimental workflow. Finally, in Section~\ref{conclusion}, we conclude the paper with a summary of our contributions, a reflection on the current state of our project, and directions for the future.
|
||||
|
||||
\section{Assessment of the State-of-the-Art}
|
||||
\label{sota}
|
||||
|
||||
Over the last two decades, multiple frameworks to support and automate the WoZ paradigm have been reported in the literature. These frameworks can be categorized according to how they focus on four primary areas of interest, which we discuss below as we expose some of the most important contributions to the field.
|
||||
|
||||
\subsection{Technical Infrastructure and Architectures}
|
||||
|
||||
The foundation of any WoZ framework lies in its technical infrastructure and architectural design. These elements determine not only the system's capabilities but also its longevity and adaptability to different research needs. Several frameworks have focused on providing robust technical infrastructures for WoZ experiments.
|
||||
|
||||
\emph{Polonius}~\cite{Lu2011} utilizes the modular \emph{Robot Operating System} (ROS) platform as its foundation, offering a graphical user interface for wizards to define finite-state machine scripts that drive robot behaviors. A notable feature is its integrated logging system that eliminates the need for post-experiment video coding, allowing researchers to record human-robot interactions in real-time as they occur. Polonius was specifically designed to be accessible to non-programming collaborators, addressing an important accessibility gap in HRI research tools.
|
||||
|
||||
\emph{OpenWoZ}~\cite{Hoffman2016} takes a different approach with its runtime-configurable framework and multi-client architecture, enabling evaluators to modify robot behaviors during experiments without interrupting the flow. This flexibility allows for dynamic adaptation to unexpected participant responses, though it requires programming expertise to create customized robot behaviors. The system's architecture supports distributed operation, where multiple operators can collaborate during an experiment.
|
||||
|
||||
%% Runtime configuration- how much does this ruin reproducibility?
|
||||
|
||||
\subsection{Interface Design and User Experience}
|
||||
|
||||
The design of an interface for the wizard to control the execution of an experiment is important. The qualities of the interface can significantly impact both the quality of data collected and the longevity of the tool itself. \emph{NottReal}~\cite{Porcheron2020} exemplifies careful attention to interface design in its development for voice user interface studies. The system makes it easier for the wizard to play their role featuring tabbed lists of pre-scripted messages, slots for customization, message queuing capabilities, and comprehensive logging. Its visual feedback mechanisms mimic commercial voice assistants, providing participants with familiar interaction cues such as dynamic ``orbs'' that indicate the system is listening and processing states.
|
||||
|
||||
\emph{WoZ4U}~\cite{Rietz2021} prioritizes usability with a GUI specifically designed to make HRI studies accessible to non-programmers. While its tight integration with Aldebaran's Pepper robot constrains generalizability, it demonstrates how specialized interfaces can lower barriers to entry for conducting WoZ studies with specific platforms.
|
||||
|
||||
\subsection{Domain Specialization vs. Generalizability}
|
||||
|
||||
A key tension in WoZ framework development exists between domain specialization and generalizability. Some systems are designed for specific types of interactions or robot platforms, offering deep functionality within a narrow domain. Others aim for broader applicability across various robots and interaction scenarios, potentially sacrificing depth of functionalities for breadth.
|
||||
|
||||
Pettersson and Wik's~\cite{Pettersson2015} systematic review identified this tension as central to the longevity of WoZ tools, that is, their ability to remain operational despite changes in underlying technologies. Their analysis of 24 WoZ systems revealed that most general-purpose tools have a lifespan of only 2-3 years. Their own tool, Ozlab, achieved exceptional longevity (15+ years) through three factors: (1) a truly general-purpose approach from inception, (2) integration into HCI curricula ensuring institutional support, and (3) a flexible wizard interface design that adapts to specific experimental needs rather than forcing standardization.
|
||||
|
||||
\subsection{Standardization Efforts and Methodological Approaches}
|
||||
|
||||
The tension between specialization and generalizability has led to increased interest in developing standardized approaches to WoZ experimentation. Recent efforts have focused on developing standards for HRI research methodology and interaction specification. Porfirio et al.~\cite{Porfirio2023} proposed guidelines for an \emph{interaction specification language} (ISL), emphasizing the need for standardized ways to define and communicate robot behaviors across different platforms. Their work introduces the concept of \emph{Application Development Environments} (ADEs) for HRI and details how hierarchical modularity and formal representations can enhance the reproducibility of robot behaviors. These ADEs would provide structured environments for creating robot behaviors with varying levels of expressiveness while maintaining platform independence.
|
||||
|
||||
This standardization effort addresses a critical gap identified in Riek's~\cite{Riek2012} systematic analysis of published WoZ experiments. Riek's work revealed concerning methodological deficiencies: 24.1\% of papers clearly described their WoZ simulation as part of an iterative design process, 5.4\% described wizard training procedures, and 11\% constrained what the wizard could recognize. This lack of methodological transparency hinders reproducibility and, therefore, scientific progress in the field.
|
||||
|
||||
Methodological considerations extend beyond wizard protocols to the fundamental approaches in HRI evaluation. Steinfeld et al.~\cite{Steinfeld2009} introduced a complementary framework to the traditional WoZ method, which they termed ``the Oz of Wizard.'' While WoZ uses human experimenters to simulate robot capabilities, the Oz of Wizard approach employs simplified human models to evaluate robot behaviors and technologies. Their framework systematically describes various permutations of real versus simulated components in HRI experiments, establishing that both approaches serve valid research objectives. They contend that technological advances in HRI constitute legitimate research even when using simplified human models rather than actual participants, provided certain conditions are met. This framework establishes an important lesson for the development of new WoZ platforms like HRIStudio which must balance standardization with flexibility in experimental design.
|
||||
|
||||
The interdisciplinary nature of HRI creates methodological inconsistencies that Belhassein et al.~\cite{Belhassein2019} examine in depth. Their analysis identifies recurring challenges in HRI user studies: limited participant pools, insufficient reporting of wizard protocols, and barriers to experiment replication. They note that self-assessment measures like questionnaires, though commonly employed, often lack proper validation for HRI contexts and may not accurately capture the participants' experiences. Our platform's design goals align closely with their recommendations to combine multiple evaluation approaches, thoroughly document procedures, and develop validated HRI-specific assessment tools.
|
||||
|
||||
Complementing these theoretical frameworks, Fraune et al.~\cite{Fraune2022} provide practical methodological guidance from an HRI workshop focused on study design. Their work organizes expert insights into themes covering study design improvement, participant interaction strategies, management of technical limitations, and cross-field collaboration. Key recommendations include pre-testing with pilot participants and ensuring robot behaviors are perceived as intended. Their discussion of participant expectations and the ``novelty effect" in first-time robot interactions is particularly relevant for WoZ studies, as these factors can significantly influence experimental outcomes.
|
||||
|
||||
\subsection{Challenges and Research Gaps}
|
||||
|
||||
Despite these advances, significant challenges remain in developing accessible and rigorous WoZ frameworks that can remain usable over non-trivial periods of time. Many existing frameworks require significant programming expertise, constraining their usability by interdisciplinary teams. While technical capabilities have advanced, methodological standardization lags behind, resulting in inconsistent experimental practices. Few platforms provide comprehensive data collection and sharing capabilities that enable robust meta-analyses across multiple studies. We are challenged to create tools that provide sufficient structure for reproducibility while allowing the flexibility needed for the pursuit of answers to diverse research questions.
|
||||
|
||||
HRIStudio aims to address these challenges with a platform that is robot-agnostic, methodologically rigorous, and eminently usable by those with less honed technological skills. By incorporating lessons from previous frameworks and addressing the gaps identified in this section, we designed a system that supports the full lifecycle of WoZ experiments, from design through execution to analysis, with an emphasis on usability, reproducibility, and collaboration.
|
||||
|
||||
\section{Reproducibility Challenges in WoZ Studies}
|
||||
\label{repchallenges}
|
||||
|
||||
Reproducibility is a cornerstone of scientific research, yet it remains a significant challenge in human-robot interaction studies, particularly those centered on the Wizard-of-Oz methodology. Before detailing our platform design, we first examine the critical reproducibility issues that have informed our approach.
|
||||
|
||||
The reproducibility challenges affecting many scientific fields are particularly acute in HRI research employing WoZ techniques. Human wizards may respond differently to similar situations across experimental trials, introducing inconsistency that undermines reproducibility and the integrity of collected data. Published studies often provide insufficient details about wizard protocols, decision-making criteria, and response timing, making replication by other researchers nearly impossible. Without standardized tools, research teams create custom setups that are difficult to recreate, and ad-hoc changes during experiments frequently go unrecorded. Different data collection methodologies and metrics further complicate cross-study comparisons.
|
||||
|
||||
As previously discussed, Riek's~\cite{Riek2012} systematic analysis of WoZ research exposed significant methodological transparency issues in the literature. These documented deficiencies in reporting experimental procedures make replication challenging, undermining the scientific validity of findings and slowing progress in the field as researchers cannot effectively build upon previous work.
|
||||
|
||||
We have identified five key requirements for enhancing reproducibility in WoZ studies. First, standardized terminology and structure provide a common vocabulary for describing experimental components, reducing ambiguity in research communications. Second, wizard behavior formalization establishes clear guidelines for wizard actions that balance consistency with flexibility, enabling reproducible interactions while accommodating the natural variations in human-robot exchanges. Third, comprehensive data capture through time-synchronized recording of all experimental events with precise timestamps allows researchers to accurately analyze interaction patterns. Fourth, experiment specification sharing capabilities enable researchers to package and distribute complete experimental designs, facilitating replication by other teams. Finally, procedural documentation through automatic logging of experimental parameters and methodological details preserves critical information that might otherwise be omitted in publications. These requirements directly informed HRIStudio's architecture and design principles, ensuring that reproducibility is built into the platform rather than treated as an afterthought.
|
||||
|
||||
\section{The Design and Architecture of HRIStudio}
|
||||
\label{arch}
|
||||
|
||||
Informed by our analysis of both existing WoZ frameworks and the reproducibility challenges identified in the previous section, we have developed several guiding design principles for HRIStudio. Our primary goal is to create a platform that enhances the scientific rigor of WoZ studies while remaining accessible to researchers with varying levels of technical expertise. We have been drive by the goal of prioritizing ``accessibility'' in the sense that the platform should be usable by researchers without deep robot programming expertise so as to lower the barrier to entry for HRI studies. Through abstraction, users can focus on experimental design without getting bogged down by the technical details of specific robot platforms. Comprehensive data management enables the system to capture and store all generated data, including logs, audio, video, and study materials. To facilitate teamwork, the platform provides collaboration support through multiple user accounts, role-based access control, and data sharing capabilities that enable effective knowledge transfer while restricting access to sensitive data. Finally, methodological guidance is embedded throughout the platform, directing users toward scientifically sound practices through its design and documentation. These principles directly address the reproducibility requirements identified earlier, particularly the need for standardized terminology, wizard behavior formalization, and comprehensive data capture.
|
||||
|
||||
We have implemented HRIStudio as a modular web application with explicit separation of concerns in accordance with these design principles. The structure of the application into client and server components creates a clear separation of responsibilities and functionalities. While the client exposes interactive elements to users, the server handles data processing, storage, and access control. This architecture provides a foundation for implementing data security through role-based interfaces in which different members of a team have tailored views of the same experimental session.
|
||||
|
||||
As shown in Figure~\ref{fig:system-architecture}, the architecture consists of three main functional layers that work in concert to provide a comprehensive experimental platform. The \emph{User Interface Layer} provides intuitive, browser-based interfaces for three components: an \emph{Experiment Designer} with visual programming capabilities for one to specify experimental details, a \emph{Wizard Interface} that grants real-time control over the execution of a trial, and a \emph{Playback \& Analysis} module that supports data exploration and visualization.
|
||||
|
||||
The \emph{Data Management Layer} provides database functionality to organize, store, and retrieve experiment definitions, metadata, and media assets generated throughout an experiment. Since HRIStudio is a web-based application, users can access this database remotely through an access control system that defines roles such as \emph{researcher}, \emph{wizard}, and \emph{observer} each with appropriate capabilities and constraints. This fine-grained access control protects sensitive participant data while enabling appropriate sharing within research teams, with flexible deployment options either on-premise or in the cloud depending on one's needs. The layer enables collaboration among the parties involved in conducting a user study while keeping information compartmentalized and secure according to each party's requirements.
|
||||
|
||||
The third major component is the \emph{Robot Integration Layer}, which is responsible for translating our standardized abstractions for robot control to the specific commands accepted by different robot platforms. HRIStudio relies on the assumption that at least one of three different mechanisms is available for communication with a robot: a {RESTful API}, standard communication structures provided by ROS, or a plugin that is custom-made for that platform. The \emph{Robot Integration Layer} serves as an intermediary between the \emph{Data Management Layer} with \emph{External Systems} such as robot hardware, external sensors, and analysis tools. This layer allows the main components of the system to remain ``robot-agnostic'' pending the identification or the creation of the correct communication method and changes to a configuration file.
|
||||
|
||||
% System architecture figure
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/diagrams/system-architecture.pdf}
|
||||
\caption{HRIStudio's three-layer architecture.}
|
||||
\label{fig:system-architecture}
|
||||
\end{figure}
|
||||
|
||||
In order to facilitate the deployment of our application, we leverage containerization with Docker to ensure that every component of HRIStudio will be supported by their system dependencies on different environments. This is an important step toward extending the longevity of the tool and toward guaranteeing that experimental environments remain consistent across different platforms. Furthermore, it allows researchers to share not only experimental designs, but also their entire execution environment should a third party wish to reproduce an experimental study.
|
||||
|
||||
\section{Experimental Workflow Support}
|
||||
\label{workflow}
|
||||
|
||||
The experimental workflow in HRIStudio directly addresses the reproducibility challenges identified in Section~\ref{repchallenges} by providing standardized structures, explicit wizard guidance, and comprehensive data capture. This section details how the platform's workflow components implement solutions for each key reproducibility requirement.
|
||||
|
||||
\subsection{Embracing a Hierarchical Structure for WoZ Studies}
|
||||
|
||||
HRIStudio defines its own standard terminology with a hierarchical organization of the elements in WoZ studies as follows.
|
||||
\begin{itemize}
|
||||
\item At the top level, an experiment designer defines a \emph{study} element, which comprises one or more \emph{experiment} elements.
|
||||
|
||||
\item Each \emph{experiment} specifies the experimental protocol for a discrete subcomponent of the overall study and comprises one or more \emph{step} elements, each representing a distinct phase in the execution sequence. The \emph{experiment} functions as a parameterized template.
|
||||
|
||||
\item Defining all the parameters in an \emph{experiment}, one creates a \emph{trial}, which is an executable instance involving a specific participant and conducted under predefined conditions. The data generated by each \emph{trial} is recorded by the system so that later one can examine how the experimental protocol was applied to each participant. The distinction between experiment and trial enables a clear separation between the abstract protocol specification and its concrete instantiation and execution.
|
||||
|
||||
\item Each \emph{step} encapsulates instructions that are meant either for the wizard or for the robot thereby creating the concept of ``type'' for this element. The \emph{step} is a container for a sequence of one or more \emph{action} elements.
|
||||
|
||||
\item Each \emph{action} represents a specific, atomic task for either the wizard or the robot, according to the nature of the \emph{step} element that contains it. An \emph{action} for the robot may represent commands for input gathering, speech, waiting, movement, etc., and may be configured by parameters specific for the \emph{trial}.
|
||||
\end{itemize}
|
||||
|
||||
Figure~\ref{fig:experiment-architecture} illustrates this hierarchical structure through a fictional study. In the diagram, we see a ``Social Robot Greeting Study'' containing an experiment with a specific robot platform, steps containing actions, and a trial with a participant. Note that each trial event is a traceable record of the sequence of actions defined in the experiment. HRIStudio enables researchers to collect the same data across multiple trials while adhering to consistent experimental protocols and recording any reactions the wizard may inject into the process.
|
||||
|
||||
% Experiment architecture figure showing hierarchical organization
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/diagrams/experiment-architecture.pdf}
|
||||
\caption{Hierarchical organization of a sample user study in HRIStudio.}
|
||||
\label{fig:experiment-architecture}
|
||||
\end{figure}
|
||||
|
||||
This standardized hierarchical structure creates a common vocabulary for experimental elements, eliminating ambiguity in descriptions and enabling clearer communication among researchers. Our approach aligns with the guidelines proposed by Porfirio et al.~\cite{Porfirio2023} for an HRI specification language, particularly in regards to standardized formal representations and hierarchical modularity. Our system uses the formal study definitions to create comprehensive procedural documentation requiring no additional effort by the researcher. Beyond this documentation, a study definition can be shared with other researchers for the faithful reproduction of experiments.
|
||||
|
||||
Figure~\ref{fig:study-details} shows how the system displays the data of an experimental study in progress. In this view, researchers can inspect summary data about the execution of a study and its trials, find a list of human subjects (``participants'') and go on to see data and documents associated with them such as consent forms, find the list of teammates collaborating in this study (``members''), read descriptive information on the study (``metadata''), and inspect an audit log that records work that has been done toward the completion of the study (``activity'').
|
||||
|
||||
% Study details figure showing hierarchical organization
|
||||
\begin{figure}[t]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/mockups/study-details.png}
|
||||
\caption{Summary view of system data on an example study.}
|
||||
\label{fig:study-details}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Collaboration and Knowledge Sharing}
|
||||
|
||||
Experiments are reproducible when they are thoroughly documented and when that documentation is easily disseminated. To support this, HRIStudio includes features that enable collaborative experiment design and streamlined sharing of assets generated during experimental studies. The platform provides a dashboard that offers an overview of project status, details about collaborators, a timeline of completed and upcoming trials, and a list of pending tasks.
|
||||
|
||||
As previously noted, the \emph{Data Management Layer} incorporates a role-based access control system that defines distinct user roles aligned with specific responsibilities within a study. This role structure enforces a clear separation of duties and enables fine-grained, need-to-know access to study-related information. This design supports various research scenarios, including double-blind studies where certain team members have restricted access to information. The pre-defined roles are as follows:
|
||||
\begin{itemize}
|
||||
\item \emph{Administrator}, a ``super user'' who can manage the installation and the configuration of the system,
|
||||
\item \emph{Researcher}, a user who can create and configure studies and experiments,
|
||||
\item \emph{Observer}, a user role with read-only access, allowing inspection of experiment assets and real-time monitoring of experiment execution, and
|
||||
\item \emph{Wizard}, a user role that allows one to execute an experiment.
|
||||
\end{itemize}
|
||||
For maximum flexibility, the system allows additional roles with different sets of permissions to be created by the administrator as needed.
|
||||
|
||||
The collaboration system allows multiple researchers to work together on experiment designs, review each other's work, and build shared knowledge about effective methodologies. This approach also enables the packaging and dissemination of complete study materials, including experimental designs, configuration parameters, collected data, and analysis results. By making all aspects of the research process shareable, HRIStudio facilitates replication studies and meta-analyses, enhancing the cumulative nature of scientific knowledge in HRI.
|
||||
|
||||
\subsection{Visual Experiment Design}
|
||||
|
||||
HRIStudio implements an \emph{Experiment Development Environment} (EDE) that builds on Porfirio et al.'s~\cite{Porfirio2023} concept of Application Development Environment. Figure~\ref{fig:experiment-designer} shows how this EDE is implemented as a visual programming, drag-and-drop canvas for sequencing steps and actions. In this example, we see a progression of steps (``Welcome'' and ``Robot Approach'') where each step is customized with specific actions. Robot actions issue abstract commands, which are then translated into platform-specific concrete commands by components known as \emph{plugins}, which are tailored to each type of robot and discussed later in this section.
|
||||
|
||||
% Experiment designer figure
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/mockups/experiment-designer.png}
|
||||
\caption{View of experiment designer.}
|
||||
\label{fig:experiment-designer}
|
||||
\end{figure}
|
||||
|
||||
Our EDE was inspired by Choregraphe~\cite{Pot2009} which enables researchers without coding expertise to build the steps and actions of an experiment visually as flow diagrams. The robot control components shown in the interface are automatically added to the inventory of options according to the experiment configuration, which specifies the robot to be used. We expect that this will make experiment design more accessible to those with reduced programming skills while maintaining the expressivity required for sophisticated studies. Conversely, to support those without knowledge of best practices for WoZ studies, the EDE offers contextual help and documentation as guidance for one to stay on the right track.
|
||||
|
||||
\subsection{The Wizard Interface and Experiment Execution}
|
||||
|
||||
We built into HRIStudio an interface for the wizard to execute experiments and to interact with them in real time. In the development of this component, we drew on lessons from Pettersson and Wik's~\cite{Pettersson2015} work on WoZ tool longevity. From them we have learned that a significant factor that determines the short lifespan of WoZ tools is the trap of a fixed, one-size-fits-all wizard interface. Following the principle incorporated into their Ozlab, we have incorporated into our framework functionality that allows the wizard interface to be adapted to the specific needs of each experiment. One can configure wizard controls and visualizations for their specific study, while keeping other elements of the framework unchanged.
|
||||
|
||||
Figure~\ref{fig:experiment-runner} shows the wizard interface for the fictional experiment ``Initial Greeting Protocol.'' This view shows the current step with an instruction for the wizard that corresponds to an action they will carry out. These instructions are presented one at a time so as not to overwhelm the wizard, but one can also use the ``View More'' button when it becomes desirable to see the complete experimental script. The view also includes a window for the captured video feed showing the robot and the participant, a timestamped log of recent events, and various interaction controls for unscripted actions that can be applied in real time (``quick actions''). By following the instructions which are provided incrementally, the wizard is guided to execute the experimental procedure consistently across its different trials with different participants. To provide live monitoring functionalities to users in the role of \emph{observer}, a similar view is presented to them without the controls that might interfere with the execution of an experiment.
|
||||
|
||||
When a wizard initiates an action during a trial, the system executes a three-step process to implement the command. First, it translates the high-level action into specific API calls as defined by the relevant plugin, converting abstract experimental actions into concrete robot instructions. Next, the system routes these calls to the robot's control system through the appropriate communication channels. Finally, it processes any feedback received from the robot, logs this information in the experimental record, and updates the experiment state accordingly to reflect the current situation. This process ensures reliable communication between the wizard interface and the physical robot while maintaining comprehensive records of all interactions.
|
||||
|
||||
% Experiment runner figure
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/mockups/experiment-runner.png}
|
||||
\caption{View of HRIStudio's wizard interface during experiment execution.}
|
||||
\label{fig:experiment-runner}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Robot Platform Integration}
|
||||
\label{plugin-store}
|
||||
|
||||
The three-step process described above relies on a modular, two-tier system for communication between HRIStudio and each specific robot platform. The EDE offers an experiment designer a number of pre-defined action components representing common tasks and behaviors such as robot movements, speech synthesis, and sensor controls. Although these components can accept parameters for the configuration of each action, they exist at a higher level of abstraction. When actions are executed, the system translates these abstractions so that they match the commands accepted by the robot selected for the experiment. This translation is achieved by a \emph{plugin} for the specific robot, which serves as the communication channel between HRIStudio and the physical robots.
|
||||
|
||||
Each robot plugin contains detailed action definitions with multiple components: action identifiers and metadata such as title, description, and a graphical icon to be presented in the EDE. Additionally, the plugin is programmed with parameter schemas including data types, validation rules, and default values to ensure proper configuration. For robots running ROS2, we support mappings that connect HRIStudio to the robot middleware. This integration approach ensures that HRIStudio can be used with any robot for which a plugin has been built.
|
||||
|
||||
As shown in Figure~\ref{fig:plugins-store}, we have developed a \emph{Plugin Store} to aggregate plugins available for an HRIStudio installation. Currently, it includes a plugin specifically for the TurtleBot3 Burger (illustrated in the figure) as well as a template to support the creation of additional plugins for other robots. Over time, we anticipate that the Plugin Store will expand to include a broader range of plugins, supporting robots of diverse types. In order to let users of the platform know what to expect of the plugins in the store, we have defined three different trust levels:
|
||||
|
||||
\begin{itemize}
|
||||
\item \emph{Official} plugins will have been created and tested by HRIStudio developers.
|
||||
\item \emph{Verified} plugins will have different provenance, but will have undergone a validation process.
|
||||
\item \emph{Community} plugins will have been developed by third-parties but will not yet have been validated.
|
||||
\end{itemize}
|
||||
|
||||
The Plugin Store provides access to the source version control \emph{repositories} which are used in the development of plugins allowing for the precise tracking of which plugin versions are used in each experiment. This system enables community contributions while maintaining reproducibility by documenting exactly which plugin versions were used for any given experiment.
|
||||
|
||||
% Plugin store figure
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\includegraphics[width=1\columnwidth]{assets/mockups/plugins-store.png}
|
||||
\caption{The Plugin Store for plugin selection.}
|
||||
\label{fig:plugins-store}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Comprehensive Data Capture and Analysis}
|
||||
|
||||
We have designed HRIStudio to create detailed logs of experiment executions and to capture and place in persistent storage all the data generated during each trial. The system keeps timestamped records of all executed actions and experimental events so that it is able to create an accurate timeline of the study. It collects robot sensor data including position, orientation, and various sensor readings that provide context about the robot's state throughout the experiment.
|
||||
|
||||
The platform records audio and video of interactions between a robot and participant, enabling post-hoc analysis of verbal and non-verbal behaviors. The system also records wizard decisions and interventions, including any unplanned actions that deviate from the experimental protocol. Finally, it saves with the experiment the observer notes and annotations, capturing qualitative insights from researchers monitoring the study. Together, these synchronized data streams provide a complete record of experimental sessions.
|
||||
|
||||
Experimental data is stored in structured formats to support long-term preservation and seamless integration with analysis tools. Sensitive participant data is encrypted at the database level to safeguard participant privacy while retaining comprehensive records for research use. To facilitate analysis, the platform allows trials to be studied with ``playback'' functionalities that allow one to review the steps in a trial and to annotate any significant events identified.
|
||||
|
||||
\section{Conclusion and Future Directions}
|
||||
\label{conclusion}
|
||||
|
||||
Although Wizard-of-Oz (WoZ) experiments are a powerful method for developing human-robot interaction applications, they demand careful attention to procedural details. Trials involving different participants require wizards to consistently execute the same sequence of events, accurately log any deviations from the prescribed script, and systematically manage all assets associated with each participant. The reproducibility of WoZ experiments depends on the thoroughness of their documentation and the ease with which their experimental setup can be disseminated.
|
||||
|
||||
To support these efforts, we drew on both existing literature and our own experience to develop HRIStudio, a modular platform designed to ease the burden on wizards while enhancing the reproducibility of experiments. \mbox{HRIStudio} maintains detailed records of experimental designs and results, facilitating dissemination and helping third parties interested in replication. The platform offers a hierarchical framework for experiment design and a visual programming interface for specifying sequences of events. By minimizing the need for programming expertise, it lowers the barrier to entry and broadens access to WoZ experimentation.
|
||||
|
||||
HRIStudio is built using a variety of web application and database technologies, which introduce certain dependencies for host systems. To simplify deployment, we are containerizing the platform and developing comprehensive, interface-integrated documentation to guide users through installation and operation. Our next development phase focuses on enhancing execution and analysis capabilities, including advanced wizard guidance, dynamic adaptation, and improved real-time feedback. We are also implementing playback functionality for reviewing synchronized data streams and expanding integration with hardware commonly used HRI research.
|
||||
|
||||
Ongoing engagement with the research community has played a key role in shaping HRIStudio. Feedback from the reviewers of our RO-MAN 2024 late breaking report and conference participants directly influenced our design choices, particularly around integration with existing research infrastructures and workflows. We look forward to creating more systematic opportunities to engage researchers to guide and refine our development as we prepare for an open beta release.
|
||||
|
||||
\bibliography{subfiles/refs}
|
||||
\bibliographystyle{plain}
|
||||
|
||||
\end{document}
|
||||
969
docs/ros2-integration.md
Normal file
969
docs/ros2-integration.md
Normal file
@@ -0,0 +1,969 @@
|
||||
# ROS2 Integration Guide for HRIStudio
|
||||
|
||||
## Overview
|
||||
|
||||
HRIStudio integrates with ROS2-based robots through the rosbridge protocol, enabling web-based control and monitoring of robots without requiring ROS2 installation on the server. This approach provides flexibility and simplifies deployment, especially on platforms like Vercel.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Communication Flow
|
||||
|
||||
```
|
||||
HRIStudio Web App → WebSocket → rosbridge_server → ROS2 Robot
|
||||
↓
|
||||
ROS2 Topics/Services
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **rosbridge_suite**: Provides WebSocket interface to ROS2
|
||||
2. **roslib.js**: JavaScript library for ROS communication
|
||||
3. **HRIStudio Plugin System**: Abstracts robot-specific implementations
|
||||
4. **Message Type Definitions**: TypeScript interfaces for ROS2 messages
|
||||
|
||||
## ROS2 Bridge Setup
|
||||
|
||||
### Robot-Side Configuration
|
||||
|
||||
The robot or a companion computer must run rosbridge:
|
||||
|
||||
```bash
|
||||
# Install rosbridge suite
|
||||
sudo apt update
|
||||
sudo apt install ros-humble-rosbridge-suite
|
||||
|
||||
# Launch rosbridge with WebSocket server
|
||||
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
|
||||
```
|
||||
|
||||
### Custom Launch File
|
||||
|
||||
Create `hristudio_bridge.launch.xml`:
|
||||
|
||||
```xml
|
||||
<launch>
|
||||
<arg name="port" default="9090"/>
|
||||
<arg name="address" default="0.0.0.0"/>
|
||||
<arg name="ssl" default="false"/>
|
||||
<arg name="certfile" default=""/>
|
||||
<arg name="keyfile" default=""/>
|
||||
|
||||
<node pkg="rosbridge_server" exec="rosbridge_websocket" name="rosbridge_websocket">
|
||||
<param name="port" value="$(var port)"/>
|
||||
<param name="address" value="$(var address)"/>
|
||||
<param name="ssl" value="$(var ssl)"/>
|
||||
<param name="certfile" value="$(var certfile)"/>
|
||||
<param name="keyfile" value="$(var keyfile)"/>
|
||||
|
||||
<!-- Limit message sizes for security -->
|
||||
<param name="max_message_size" value="10000000"/>
|
||||
<param name="unregister_timeout" value="10.0"/>
|
||||
|
||||
<!-- Enable specific services only -->
|
||||
<param name="services_glob" value="['/hristudio/*']"/>
|
||||
<param name="topics_glob" value="['/hristudio/*', '/tf', '/tf_static']"/>
|
||||
</node>
|
||||
</launch>
|
||||
```
|
||||
|
||||
## Client-Side Implementation
|
||||
|
||||
### ROS Connection Manager
|
||||
|
||||
`src/lib/ros/connection.ts`:
|
||||
|
||||
```typescript
|
||||
import ROSLIB from 'roslib';
|
||||
|
||||
export class RosConnection {
|
||||
private ros: ROSLIB.Ros | null = null;
|
||||
private url: string;
|
||||
private reconnectInterval: number = 5000;
|
||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||
private listeners: Map<string, Set<(data: any) => void>> = new Map();
|
||||
|
||||
constructor(url: string = process.env.NEXT_PUBLIC_ROSBRIDGE_URL || 'ws://localhost:9090') {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ros?.isConnected) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ros = new ROSLIB.Ros({
|
||||
url: this.url,
|
||||
options: {
|
||||
// Enable compression for better performance
|
||||
compression: 'png',
|
||||
// Throttle rate for topic subscriptions
|
||||
throttle_rate: 100,
|
||||
}
|
||||
});
|
||||
|
||||
this.ros.on('connection', () => {
|
||||
console.log('Connected to ROS bridge');
|
||||
this.clearReconnectTimer();
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ros.on('error', (error) => {
|
||||
console.error('ROS connection error:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.ros.on('close', () => {
|
||||
console.log('ROS connection closed');
|
||||
this.scheduleReconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleReconnect() {
|
||||
if (this.reconnectTimer) return;
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('Attempting to reconnect to ROS...');
|
||||
this.connect().catch(console.error);
|
||||
}, this.reconnectInterval);
|
||||
}
|
||||
|
||||
private clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.clearReconnectTimer();
|
||||
if (this.ros) {
|
||||
this.ros.close();
|
||||
this.ros = null;
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.ros?.isConnected || false;
|
||||
}
|
||||
|
||||
getRos(): ROSLIB.Ros | null {
|
||||
return this.ros;
|
||||
}
|
||||
|
||||
// Topic subscription helper
|
||||
subscribe<T>(topicName: string, messageType: string, callback: (message: T) => void): () => void {
|
||||
if (!this.ros) throw new Error('Not connected to ROS');
|
||||
|
||||
const topic = new ROSLIB.Topic({
|
||||
ros: this.ros,
|
||||
name: topicName,
|
||||
messageType: messageType,
|
||||
compression: 'png',
|
||||
throttle_rate: 100
|
||||
});
|
||||
|
||||
topic.subscribe(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
topic.unsubscribe(callback);
|
||||
};
|
||||
}
|
||||
|
||||
// Service call helper
|
||||
async callService<TRequest, TResponse>(
|
||||
serviceName: string,
|
||||
serviceType: string,
|
||||
request: TRequest
|
||||
): Promise<TResponse> {
|
||||
if (!this.ros) throw new Error('Not connected to ROS');
|
||||
|
||||
const service = new ROSLIB.Service({
|
||||
ros: this.ros,
|
||||
name: serviceName,
|
||||
serviceType: serviceType
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
service.callService(
|
||||
new ROSLIB.ServiceRequest(request),
|
||||
(response: TResponse) => resolve(response),
|
||||
(error: string) => reject(new Error(error))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Action client helper
|
||||
createActionClient(actionName: string, actionType: string): ROSLIB.ActionClient {
|
||||
if (!this.ros) throw new Error('Not connected to ROS');
|
||||
|
||||
return new ROSLIB.ActionClient({
|
||||
ros: this.ros,
|
||||
serverName: actionName,
|
||||
actionName: actionType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const rosConnection = new RosConnection();
|
||||
```
|
||||
|
||||
### ROS2 Message Types
|
||||
|
||||
`src/lib/ros/types.ts`:
|
||||
|
||||
```typescript
|
||||
// Common ROS2 message types
|
||||
export interface Header {
|
||||
stamp: {
|
||||
sec: number;
|
||||
nanosec: number;
|
||||
};
|
||||
frame_id: string;
|
||||
}
|
||||
|
||||
export interface Twist {
|
||||
linear: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
angular: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Pose {
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
orientation: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
w: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface JointState {
|
||||
header: Header;
|
||||
name: string[];
|
||||
position: number[];
|
||||
velocity: number[];
|
||||
effort: number[];
|
||||
}
|
||||
|
||||
export interface BatteryState {
|
||||
header: Header;
|
||||
voltage: number;
|
||||
temperature: number;
|
||||
current: number;
|
||||
charge: number;
|
||||
capacity: number;
|
||||
percentage: number;
|
||||
power_supply_status: number;
|
||||
power_supply_health: number;
|
||||
power_supply_technology: number;
|
||||
present: boolean;
|
||||
}
|
||||
|
||||
// HRIStudio specific messages
|
||||
export interface HRICommand {
|
||||
action_id: string;
|
||||
action_type: string;
|
||||
parameters: Record<string, any>;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface HRIResponse {
|
||||
action_id: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: Record<string, any>;
|
||||
duration_ms: number;
|
||||
}
|
||||
|
||||
export interface HRIState {
|
||||
robot_id: string;
|
||||
connected: boolean;
|
||||
battery: BatteryState;
|
||||
pose: Pose;
|
||||
joint_states: JointState;
|
||||
custom_data: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
## ROS2 Robot Plugin Implementation
|
||||
|
||||
### Base ROS2 Plugin
|
||||
|
||||
`src/lib/plugins/ros2/base-plugin.ts`:
|
||||
|
||||
```typescript
|
||||
import { RobotPlugin, ActionDefinition, ActionResult, RobotState } from '@/lib/plugins/types';
|
||||
import { rosConnection } from '@/lib/ros/connection';
|
||||
import { HRICommand, HRIResponse, HRIState, Twist } from '@/lib/ros/types';
|
||||
import ROSLIB from 'roslib';
|
||||
import { z } from 'zod';
|
||||
|
||||
export abstract class BaseROS2Plugin implements RobotPlugin {
|
||||
abstract id: string;
|
||||
abstract name: string;
|
||||
abstract version: string;
|
||||
abstract robotId: string;
|
||||
|
||||
protected namespace: string = '/hristudio';
|
||||
protected commandTopic: ROSLIB.Topic | null = null;
|
||||
protected stateTopic: ROSLIB.Topic | null = null;
|
||||
protected currentState: HRIState | null = null;
|
||||
protected pendingCommands: Map<string, (response: HRIResponse) => void> = new Map();
|
||||
|
||||
abstract configSchema: z.ZodSchema;
|
||||
abstract defaultConfig: Record<string, any>;
|
||||
abstract actions: ActionDefinition[];
|
||||
|
||||
async initialize(config: any): Promise<void> {
|
||||
// Validate config
|
||||
this.configSchema.parse(config);
|
||||
|
||||
// Set namespace if provided
|
||||
if (config.namespace) {
|
||||
this.namespace = config.namespace;
|
||||
}
|
||||
}
|
||||
|
||||
async connect(): Promise<boolean> {
|
||||
try {
|
||||
await rosConnection.connect();
|
||||
|
||||
const ros = rosConnection.getRos();
|
||||
if (!ros) return false;
|
||||
|
||||
// Subscribe to robot state
|
||||
this.stateTopic = new ROSLIB.Topic({
|
||||
ros,
|
||||
name: `${this.namespace}/robot_state`,
|
||||
messageType: 'hristudio_msgs/HRIState'
|
||||
});
|
||||
|
||||
this.stateTopic.subscribe((state: HRIState) => {
|
||||
this.currentState = state;
|
||||
});
|
||||
|
||||
// Create command publisher
|
||||
this.commandTopic = new ROSLIB.Topic({
|
||||
ros,
|
||||
name: `${this.namespace}/commands`,
|
||||
messageType: 'hristudio_msgs/HRICommand'
|
||||
});
|
||||
|
||||
// Subscribe to responses
|
||||
const responseTopic = new ROSLIB.Topic({
|
||||
ros,
|
||||
name: `${this.namespace}/responses`,
|
||||
messageType: 'hristudio_msgs/HRIResponse'
|
||||
});
|
||||
|
||||
responseTopic.subscribe((response: HRIResponse) => {
|
||||
const handler = this.pendingCommands.get(response.action_id);
|
||||
if (handler) {
|
||||
handler(response);
|
||||
this.pendingCommands.delete(response.action_id);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for initial state
|
||||
await this.waitForState(5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to ROS2:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.stateTopic) {
|
||||
this.stateTopic.unsubscribe();
|
||||
this.stateTopic = null;
|
||||
}
|
||||
|
||||
if (this.commandTopic) {
|
||||
this.commandTopic = null;
|
||||
}
|
||||
|
||||
this.currentState = null;
|
||||
this.pendingCommands.clear();
|
||||
}
|
||||
|
||||
async executeAction(action: ActionDefinition, params: any): Promise<ActionResult> {
|
||||
if (!this.commandTopic) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not connected to robot',
|
||||
duration: 0
|
||||
};
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const actionId = `${action.id}_${Date.now()}`;
|
||||
|
||||
try {
|
||||
// Validate parameters
|
||||
const validatedParams = action.parameterSchema.parse(params);
|
||||
|
||||
// Create command
|
||||
const command: HRICommand = {
|
||||
action_id: actionId,
|
||||
action_type: action.id,
|
||||
parameters: validatedParams,
|
||||
timeout: action.timeout || 30000
|
||||
};
|
||||
|
||||
// Send command and wait for response
|
||||
const response = await this.sendCommand(command);
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
data: response.data,
|
||||
error: response.success ? undefined : response.message,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getState(): Promise<RobotState> {
|
||||
if (!this.currentState) {
|
||||
return {
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
connected: this.currentState.connected,
|
||||
battery: this.currentState.battery.percentage,
|
||||
position: {
|
||||
x: this.currentState.pose.position.x,
|
||||
y: this.currentState.pose.position.y,
|
||||
z: this.currentState.pose.position.z
|
||||
},
|
||||
sensors: {
|
||||
jointStates: this.currentState.joint_states,
|
||||
...this.currentState.custom_data
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected sendCommand(command: HRICommand): Promise<HRIResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.commandTopic) {
|
||||
reject(new Error('Command topic not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingCommands.delete(command.action_id);
|
||||
reject(new Error('Command timeout'));
|
||||
}, command.timeout);
|
||||
|
||||
// Store handler
|
||||
this.pendingCommands.set(command.action_id, (response) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
// Publish command
|
||||
this.commandTopic.publish(command);
|
||||
});
|
||||
}
|
||||
|
||||
protected async waitForState(timeoutMs: number): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
while (!this.currentState && Date.now() - startTime < timeoutMs) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
if (!this.currentState) {
|
||||
throw new Error('Timeout waiting for robot state');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for common robot controls
|
||||
protected async moveBase(linear: number, angular: number): Promise<ActionResult> {
|
||||
const ros = rosConnection.getRos();
|
||||
if (!ros) {
|
||||
return { success: false, error: 'Not connected', duration: 0 };
|
||||
}
|
||||
|
||||
const cmdVelTopic = new ROSLIB.Topic({
|
||||
ros,
|
||||
name: '/cmd_vel',
|
||||
messageType: 'geometry_msgs/Twist'
|
||||
});
|
||||
|
||||
const twist: Twist = {
|
||||
linear: { x: linear, y: 0, z: 0 },
|
||||
angular: { x: 0, y: 0, z: angular }
|
||||
};
|
||||
|
||||
cmdVelTopic.publish(twist);
|
||||
|
||||
return { success: true, duration: 0 };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TurtleBot3 Plugin Example
|
||||
|
||||
`src/lib/plugins/robots/turtlebot3.ts`:
|
||||
|
||||
```typescript
|
||||
import { BaseROS2Plugin } from '../ros2/base-plugin';
|
||||
import { ActionDefinition } from '@/lib/plugins/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
export class TurtleBot3Plugin extends BaseROS2Plugin {
|
||||
id = 'turtlebot3-burger';
|
||||
name = 'TurtleBot3 Burger';
|
||||
version = '1.0.0';
|
||||
robotId = 'turtlebot3';
|
||||
|
||||
configSchema = z.object({
|
||||
namespace: z.string().default('/tb3'),
|
||||
maxLinearVelocity: z.number().default(0.22),
|
||||
maxAngularVelocity: z.number().default(2.84),
|
||||
rosbridge_url: z.string().url().optional(),
|
||||
});
|
||||
|
||||
defaultConfig = {
|
||||
namespace: '/tb3',
|
||||
maxLinearVelocity: 0.22,
|
||||
maxAngularVelocity: 2.84,
|
||||
};
|
||||
|
||||
actions: ActionDefinition[] = [
|
||||
{
|
||||
id: 'move_forward',
|
||||
name: 'Move Forward',
|
||||
description: 'Move the robot forward',
|
||||
category: 'movement',
|
||||
icon: 'arrow-up',
|
||||
parameterSchema: z.object({
|
||||
distance: z.number().min(0).max(5).describe('Distance in meters'),
|
||||
speed: z.number().min(0).max(0.22).default(0.1).describe('Speed in m/s'),
|
||||
}),
|
||||
timeout: 30000,
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
id: 'turn',
|
||||
name: 'Turn',
|
||||
description: 'Turn the robot',
|
||||
category: 'movement',
|
||||
icon: 'rotate-cw',
|
||||
parameterSchema: z.object({
|
||||
angle: z.number().min(-180).max(180).describe('Angle in degrees'),
|
||||
speed: z.number().min(0).max(2.84).default(0.5).describe('Angular speed in rad/s'),
|
||||
}),
|
||||
timeout: 20000,
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
id: 'speak',
|
||||
name: 'Speak',
|
||||
description: 'Make the robot speak using TTS',
|
||||
category: 'interaction',
|
||||
icon: 'volume-2',
|
||||
parameterSchema: z.object({
|
||||
text: z.string().max(500).describe('Text to speak'),
|
||||
voice: z.enum(['male', 'female']).default('female'),
|
||||
speed: z.number().min(0.5).max(2).default(1),
|
||||
}),
|
||||
timeout: 60000,
|
||||
retryable: false,
|
||||
},
|
||||
{
|
||||
id: 'led_color',
|
||||
name: 'Set LED Color',
|
||||
description: 'Change the robot LED color',
|
||||
category: 'feedback',
|
||||
icon: 'lightbulb',
|
||||
parameterSchema: z.object({
|
||||
color: z.enum(['red', 'green', 'blue', 'yellow', 'white', 'off']),
|
||||
duration: z.number().min(0).max(60).default(0).describe('Duration in seconds (0 = permanent)'),
|
||||
}),
|
||||
timeout: 5000,
|
||||
retryable: true,
|
||||
},
|
||||
];
|
||||
|
||||
async initialize(config: any): Promise<void> {
|
||||
await super.initialize(config);
|
||||
|
||||
// TurtleBot3 specific initialization
|
||||
if (config.rosbridge_url) {
|
||||
// Override default rosbridge URL if provided
|
||||
process.env.NEXT_PUBLIC_ROSBRIDGE_URL = config.rosbridge_url;
|
||||
}
|
||||
}
|
||||
|
||||
// Override executeAction for robot-specific implementations
|
||||
async executeAction(action: ActionDefinition, params: any): Promise<ActionResult> {
|
||||
// For movement actions, we can use direct topic publishing
|
||||
// for better real-time control
|
||||
if (action.id === 'move_forward') {
|
||||
return this.moveForward(params.distance, params.speed);
|
||||
} else if (action.id === 'turn') {
|
||||
return this.turn(params.angle, params.speed);
|
||||
}
|
||||
|
||||
// For other actions, use the base implementation
|
||||
return super.executeAction(action, params);
|
||||
}
|
||||
|
||||
private async moveForward(distance: number, speed: number): Promise<ActionResult> {
|
||||
const startTime = Date.now();
|
||||
const duration = (distance / speed) * 1000; // Convert to milliseconds
|
||||
|
||||
// Start moving
|
||||
await this.moveBase(speed, 0);
|
||||
|
||||
// Wait for movement to complete
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
|
||||
// Stop
|
||||
await this.moveBase(0, 0);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { distance, speed },
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
private async turn(angleDegrees: number, speed: number): Promise<ActionResult> {
|
||||
const startTime = Date.now();
|
||||
const angleRad = (angleDegrees * Math.PI) / 180;
|
||||
const duration = Math.abs(angleRad / speed) * 1000;
|
||||
|
||||
// Start turning (negative for clockwise)
|
||||
const angularVel = angleDegrees > 0 ? speed : -speed;
|
||||
await this.moveBase(0, angularVel);
|
||||
|
||||
// Wait for turn to complete
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
|
||||
// Stop
|
||||
await this.moveBase(0, 0);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { angle: angleDegrees, speed },
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ROS2 Node for HRIStudio
|
||||
|
||||
For robots to work with HRIStudio, they need a ROS2 node that implements the HRIStudio protocol:
|
||||
|
||||
`hristudio_robot_node.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from std_msgs.msg import String
|
||||
from geometry_msgs.msg import Twist
|
||||
from sensor_msgs.msg import JointState, BatteryState
|
||||
from hristudio_msgs.msg import HRICommand, HRIResponse, HRIState
|
||||
import json
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
class HRIStudioRobotNode(Node):
|
||||
def __init__(self):
|
||||
super().__init__('hristudio_robot_node')
|
||||
|
||||
# Publishers
|
||||
self.state_pub = self.create_publisher(HRIState, '/hristudio/robot_state', 10)
|
||||
self.response_pub = self.create_publisher(HRIResponse, '/hristudio/responses', 10)
|
||||
|
||||
# Subscribers
|
||||
self.command_sub = self.create_subscription(
|
||||
HRICommand,
|
||||
'/hristudio/commands',
|
||||
self.command_callback,
|
||||
10
|
||||
)
|
||||
|
||||
# Robot control publishers
|
||||
self.cmd_vel_pub = self.create_publisher(Twist, '/cmd_vel', 10)
|
||||
|
||||
# State update timer
|
||||
self.state_timer = self.create_timer(0.1, self.publish_state)
|
||||
|
||||
# Action handlers
|
||||
self.action_handlers = {
|
||||
'move_forward': self.handle_move_forward,
|
||||
'turn': self.handle_turn,
|
||||
'speak': self.handle_speak,
|
||||
'led_color': self.handle_led_color,
|
||||
}
|
||||
|
||||
self.get_logger().info('HRIStudio Robot Node started')
|
||||
|
||||
def command_callback(self, msg):
|
||||
"""Handle incoming commands from HRIStudio"""
|
||||
self.get_logger().info(f'Received command: {msg.action_type}')
|
||||
|
||||
# Execute action in separate thread to avoid blocking
|
||||
thread = Thread(target=self.execute_command, args=(msg,))
|
||||
thread.start()
|
||||
|
||||
def execute_command(self, command):
|
||||
"""Execute a command and send response"""
|
||||
start_time = time.time()
|
||||
response = HRIResponse()
|
||||
response.action_id = command.action_id
|
||||
|
||||
try:
|
||||
# Parse parameters
|
||||
params = json.loads(command.parameters) if isinstance(command.parameters, str) else command.parameters
|
||||
|
||||
# Execute action
|
||||
if command.action_type in self.action_handlers:
|
||||
result = self.action_handlers[command.action_type](params)
|
||||
response.success = result['success']
|
||||
response.message = result.get('message', '')
|
||||
response.data = json.dumps(result.get('data', {}))
|
||||
else:
|
||||
response.success = False
|
||||
response.message = f'Unknown action type: {command.action_type}'
|
||||
|
||||
except Exception as e:
|
||||
response.success = False
|
||||
response.message = str(e)
|
||||
self.get_logger().error(f'Error executing command: {e}')
|
||||
|
||||
response.duration_ms = int((time.time() - start_time) * 1000)
|
||||
self.response_pub.publish(response)
|
||||
|
||||
def publish_state(self):
|
||||
"""Publish current robot state"""
|
||||
state = HRIState()
|
||||
state.robot_id = 'turtlebot3'
|
||||
state.connected = True
|
||||
|
||||
# Add current sensor data
|
||||
# This would come from actual robot sensors
|
||||
state.battery.percentage = 85.0
|
||||
state.pose.position.x = 0.0
|
||||
state.pose.position.y = 0.0
|
||||
state.pose.orientation.w = 1.0
|
||||
|
||||
self.state_pub.publish(state)
|
||||
|
||||
def handle_move_forward(self, params):
|
||||
"""Move robot forward"""
|
||||
distance = params.get('distance', 0)
|
||||
speed = params.get('speed', 0.1)
|
||||
|
||||
# Calculate duration
|
||||
duration = distance / speed
|
||||
|
||||
# Publish velocity command
|
||||
twist = Twist()
|
||||
twist.linear.x = speed
|
||||
self.cmd_vel_pub.publish(twist)
|
||||
|
||||
# Wait for movement to complete
|
||||
time.sleep(duration)
|
||||
|
||||
# Stop
|
||||
twist.linear.x = 0.0
|
||||
self.cmd_vel_pub.publish(twist)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': {'distance': distance, 'speed': speed}
|
||||
}
|
||||
|
||||
def handle_turn(self, params):
|
||||
"""Turn robot"""
|
||||
angle = params.get('angle', 0)
|
||||
speed = params.get('speed', 0.5)
|
||||
|
||||
# Convert to radians
|
||||
angle_rad = angle * 3.14159 / 180
|
||||
duration = abs(angle_rad) / speed
|
||||
|
||||
# Publish velocity command
|
||||
twist = Twist()
|
||||
twist.angular.z = speed if angle > 0 else -speed
|
||||
self.cmd_vel_pub.publish(twist)
|
||||
|
||||
# Wait for turn to complete
|
||||
time.sleep(duration)
|
||||
|
||||
# Stop
|
||||
twist.angular.z = 0.0
|
||||
self.cmd_vel_pub.publish(twist)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': {'angle': angle, 'speed': speed}
|
||||
}
|
||||
|
||||
def handle_speak(self, params):
|
||||
"""Text to speech"""
|
||||
text = params.get('text', '')
|
||||
# Implement TTS here
|
||||
self.get_logger().info(f'Speaking: {text}')
|
||||
return {'success': True, 'data': {'text': text}}
|
||||
|
||||
def handle_led_color(self, params):
|
||||
"""Set LED color"""
|
||||
color = params.get('color', 'off')
|
||||
# Implement LED control here
|
||||
self.get_logger().info(f'Setting LED to: {color}')
|
||||
return {'success': True, 'data': {'color': color}}
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
node = HRIStudioRobotNode()
|
||||
rclpy.spin(node)
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. Authentication
|
||||
- Use SSL/TLS for rosbridge connections in production
|
||||
- Implement token-based authentication for rosbridge
|
||||
- Restrict topic/service access patterns
|
||||
|
||||
### 2. Network Security
|
||||
- Use VPN or SSH tunnels for remote robot connections
|
||||
- Implement firewall rules to restrict rosbridge access
|
||||
- Use separate network segments for robot communication
|
||||
|
||||
### 3. Message Validation
|
||||
- Validate all incoming messages on the robot side
|
||||
- Implement rate limiting to prevent DoS attacks
|
||||
- Sanitize string inputs to prevent injection attacks
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
### Local Development
|
||||
- Robot and development machine on same network
|
||||
- Direct WebSocket connection to rosbridge
|
||||
- No SSL required
|
||||
|
||||
### Production (Vercel)
|
||||
- Robot behind NAT/firewall
|
||||
- Use reverse proxy or tunnel (e.g., ngrok, Cloudflare Tunnel)
|
||||
- SSL/TLS required for secure communication
|
||||
- Consider latency for real-time control
|
||||
|
||||
### Hybrid Approach
|
||||
- Local "robot companion" server near robot
|
||||
- Companion server connects to both robot and Vercel app
|
||||
- Reduces latency for critical operations
|
||||
- Maintains security boundaries
|
||||
|
||||
## Testing ROS2 Integration
|
||||
|
||||
### Unit Tests
|
||||
|
||||
`src/lib/ros/__tests__/connection.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { RosConnection } from '../connection';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('roslib', () => ({
|
||||
default: {
|
||||
Ros: vi.fn().mockImplementation(() => ({
|
||||
on: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
close: vi.fn(),
|
||||
isConnected: true,
|
||||
})),
|
||||
Topic: vi.fn(),
|
||||
Service: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('RosConnection', () => {
|
||||
let connection: RosConnection;
|
||||
|
||||
beforeEach(() => {
|
||||
connection = new RosConnection('ws://test:9090');
|
||||
});
|
||||
|
||||
it('should connect successfully', async () => {
|
||||
await expect(connection.connect()).resolves.toBeUndefined();
|
||||
expect(connection.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
// Add more tests...
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
- Use rosbridge_server in test mode
|
||||
- Mock robot responses
|
||||
- Test error scenarios
|
||||
- Verify message formats
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
1. **Message Throttling**: Limit frequency of state updates
|
||||
2. **Compression**: Enable PNG compression for image topics
|
||||
3. **Selective Subscriptions**: Only subscribe to needed topics
|
||||
4. **Connection Pooling**: Reuse WebSocket connections
|
||||
5. **Client-Side Caching**: Cache robot capabilities
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Refused**
|
||||
- Check rosbridge is running
|
||||
- Verify firewall rules
|
||||
- Check WebSocket URL
|
||||
|
||||
2. **Message Type Errors**
|
||||
- Ensure message types match between client and robot
|
||||
- Verify ROS2 workspace is sourced
|
||||
|
||||
3. **High Latency**
|
||||
- Check network conditions
|
||||
- Consider local rosbridge proxy
|
||||
- Optimize message sizes
|
||||
|
||||
4. **Authentication Failures**
|
||||
- Verify SSL certificates
|
||||
- Check authentication tokens
|
||||
- Review rosbridge configuration
|
||||
52
drizzle/0000_young_obadiah_stane.sql
Normal file
52
drizzle/0000_young_obadiah_stane.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
CREATE TABLE "hristudio_account" (
|
||||
"userId" varchar(255) NOT NULL,
|
||||
"type" varchar(255) NOT NULL,
|
||||
"provider" varchar(255) NOT NULL,
|
||||
"providerAccountId" varchar(255) NOT NULL,
|
||||
"refresh_token" text,
|
||||
"access_token" text,
|
||||
"expires_at" integer,
|
||||
"token_type" varchar(255),
|
||||
"scope" varchar(255),
|
||||
"id_token" text,
|
||||
"session_state" varchar(255),
|
||||
CONSTRAINT "hristudio_account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "hristudio_post" (
|
||||
"id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "hristudio_post_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||
"name" varchar(256),
|
||||
"createdById" varchar(255) NOT NULL,
|
||||
"createdAt" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "hristudio_session" (
|
||||
"sessionToken" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"userId" varchar(255) NOT NULL,
|
||||
"expires" timestamp with time zone NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "hristudio_user" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"name" varchar(255),
|
||||
"email" varchar(255) NOT NULL,
|
||||
"emailVerified" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
||||
"image" varchar(255),
|
||||
"password" varchar(255)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "hristudio_verification_token" (
|
||||
"identifier" varchar(255) NOT NULL,
|
||||
"token" varchar(255) NOT NULL,
|
||||
"expires" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "hristudio_verification_token_identifier_token_pk" PRIMARY KEY("identifier","token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "hristudio_account" ADD CONSTRAINT "hristudio_account_userId_hristudio_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."hristudio_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "hristudio_post" ADD CONSTRAINT "hristudio_post_createdById_hristudio_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."hristudio_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "hristudio_session" ADD CONSTRAINT "hristudio_session_userId_hristudio_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."hristudio_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "account_user_id_idx" ON "hristudio_account" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE INDEX "created_by_idx" ON "hristudio_post" USING btree ("createdById");--> statement-breakpoint
|
||||
CREATE INDEX "name_idx" ON "hristudio_post" USING btree ("name");--> statement-breakpoint
|
||||
CREATE INDEX "t_user_id_idx" ON "hristudio_session" USING btree ("userId");
|
||||
386
drizzle/meta/0000_snapshot.json
Normal file
386
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"id": "bfb29ef1-7ec4-44aa-8d5f-255b1e59456f",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.hristudio_account": {
|
||||
"name": "hristudio_account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"providerAccountId": {
|
||||
"name": "providerAccountId",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_user_id_idx": {
|
||||
"name": "account_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "userId",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"hristudio_account_userId_hristudio_user_id_fk": {
|
||||
"name": "hristudio_account_userId_hristudio_user_id_fk",
|
||||
"tableFrom": "hristudio_account",
|
||||
"tableTo": "hristudio_user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"hristudio_account_provider_providerAccountId_pk": {
|
||||
"name": "hristudio_account_provider_providerAccountId_pk",
|
||||
"columns": [
|
||||
"provider",
|
||||
"providerAccountId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.hristudio_post": {
|
||||
"name": "hristudio_post",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "byDefault",
|
||||
"name": "hristudio_post_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"createdById": {
|
||||
"name": "createdById",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"created_by_idx": {
|
||||
"name": "created_by_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "createdById",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"name_idx": {
|
||||
"name": "name_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "name",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"hristudio_post_createdById_hristudio_user_id_fk": {
|
||||
"name": "hristudio_post_createdById_hristudio_user_id_fk",
|
||||
"tableFrom": "hristudio_post",
|
||||
"tableTo": "hristudio_user",
|
||||
"columnsFrom": [
|
||||
"createdById"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.hristudio_session": {
|
||||
"name": "hristudio_session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"sessionToken": {
|
||||
"name": "sessionToken",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"t_user_id_idx": {
|
||||
"name": "t_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "userId",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"hristudio_session_userId_hristudio_user_id_fk": {
|
||||
"name": "hristudio_session_userId_hristudio_user_id_fk",
|
||||
"tableFrom": "hristudio_session",
|
||||
"tableTo": "hristudio_user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.hristudio_user": {
|
||||
"name": "hristudio_user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.hristudio_verification_token": {
|
||||
"name": "hristudio_verification_token",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"hristudio_verification_token_identifier_token_pk": {
|
||||
"name": "hristudio_verification_token_identifier_token_pk",
|
||||
"columns": [
|
||||
"identifier",
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1752816515816,
|
||||
"tag": "0000_young_obadiah_stane",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
16
package.json
16
package.json
@@ -21,24 +21,35 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^1.7.2",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^15.2.3",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-auth": "^5.0.0-beta.29",
|
||||
"postgres": "^3.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"zod": "^3.24.2"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
@@ -50,6 +61,7 @@
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.27.0"
|
||||
},
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export function LatestPost() {
|
||||
const [latestPost] = api.post.getLatest.useSuspenseQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const [name, setName] = useState("");
|
||||
const createPost = api.post.create.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.post.invalidate();
|
||||
setName("");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xs">
|
||||
{latestPost ? (
|
||||
<p className="truncate">Your most recent post: {latestPost.name}</p>
|
||||
) : (
|
||||
<p>You have no posts yet.</p>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createPost.mutate({ name });
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-full bg-white/10 px-4 py-2 text-white"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
|
||||
disabled={createPost.isPending}
|
||||
>
|
||||
{createPost.isPending ? "Submitting..." : "Submit"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
src/app/auth/signin/page.tsx
Normal file
131
src/app/auth/signin/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
|
||||
export default function SignInPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError("Invalid email or password");
|
||||
} else {
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
setError("An error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<Link href="/" className="inline-block">
|
||||
<h1 className="text-3xl font-bold text-slate-900">HRIStudio</h1>
|
||||
</Link>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Sign in to your research account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sign In Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your credentials to access your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Signing in..." : "Sign In"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-600">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href="/auth/signup"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Sign up here
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center text-xs text-slate-500">
|
||||
<p>
|
||||
© 2024 HRIStudio. A platform for Human-Robot Interaction research.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
170
src/app/auth/signup/page.tsx
Normal file
170
src/app/auth/signup/page.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const createUser = api.auth.register.useMutation({
|
||||
onSuccess: () => {
|
||||
router.push("/auth/signin?message=Account created successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
// Basic validation
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setError("Password must be at least 6 characters long");
|
||||
return;
|
||||
}
|
||||
|
||||
createUser.mutate({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<Link href="/" className="inline-block">
|
||||
<h1 className="text-3xl font-bold text-slate-900">HRIStudio</h1>
|
||||
</Link>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Create your research account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sign Up Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Get started</CardTitle>
|
||||
<CardDescription>
|
||||
Create your account to begin your HRI research
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your full name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={createUser.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={createUser.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={createUser.isPending}
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={createUser.isPending}
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={createUser.isPending}
|
||||
>
|
||||
{createUser.isPending ? "Creating account..." : "Create Account"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-600">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/auth/signin"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Sign in here
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center text-xs text-slate-500">
|
||||
<p>
|
||||
© 2024 HRIStudio. A platform for Human-Robot Interaction research.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import { type Metadata } from "next";
|
||||
import { Geist } from "next/font/google";
|
||||
|
||||
import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create T3 App",
|
||||
description: "Generated by create-t3-app",
|
||||
title: "HRIStudio",
|
||||
description:
|
||||
"Web-based platform for standardizing Wizard of Oz studies in Human-Robot Interaction research",
|
||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||
};
|
||||
|
||||
@@ -22,7 +24,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable}`}>
|
||||
<body>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
<SessionProvider>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
240
src/app/page.tsx
240
src/app/page.tsx
@@ -1,69 +1,197 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { LatestPost } from "~/app/_components/post";
|
||||
import { auth } from "~/server/auth";
|
||||
import { api, HydrateClient } from "~/trpc/server";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
|
||||
export default async function Home() {
|
||||
const hello = await api.post.hello({ text: "from tRPC" });
|
||||
const session = await auth();
|
||||
|
||||
if (session?.user) {
|
||||
void api.post.getLatest.prefetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<HydrateClient>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
|
||||
</h1>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
|
||||
<Link
|
||||
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||
href="https://create.t3.gg/en/usage/first-steps"
|
||||
target="_blank"
|
||||
>
|
||||
<h3 className="text-2xl font-bold">First Steps →</h3>
|
||||
<div className="text-lg">
|
||||
Just the basics - Everything you need to know to set up your
|
||||
database and authentication.
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||
href="https://create.t3.gg/en/introduction"
|
||||
target="_blank"
|
||||
>
|
||||
<h3 className="text-2xl font-bold">Documentation →</h3>
|
||||
<div className="text-lg">
|
||||
Learn more about Create T3 App, the libraries it uses, and how
|
||||
to deploy it.
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-2xl text-white">
|
||||
{hello ? hello.greeting : "Loading tRPC query..."}
|
||||
<main className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
{/* Header */}
|
||||
<div className="mb-16 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="mb-2 text-4xl font-bold text-slate-900">
|
||||
HRIStudio
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600">
|
||||
Web-based platform for Human-Robot Interaction research
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-center text-2xl text-white">
|
||||
{session && <span>Logged in as {session.user?.name}</span>}
|
||||
</p>
|
||||
<Link
|
||||
href={session ? "/api/auth/signout" : "/api/auth/signin"}
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
||||
>
|
||||
{session ? "Sign out" : "Sign in"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session?.user && <LatestPost />}
|
||||
<div className="flex items-center gap-4">
|
||||
{session?.user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-slate-600">
|
||||
Welcome, {session.user.name || session.user.email}
|
||||
</span>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/api/auth/signout">Sign Out</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/auth/signin">Sign In</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/auth/signup">Get Started</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</HydrateClient>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{session?.user ? (
|
||||
// Authenticated user dashboard
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Experiments</CardTitle>
|
||||
<CardDescription>
|
||||
Design and manage your HRI experiments
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className="w-full" asChild>
|
||||
<Link href="/experiments">View Experiments</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wizard Interface</CardTitle>
|
||||
<CardDescription>
|
||||
Control robots during live trials
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className="w-full" asChild>
|
||||
<Link href="/wizard">Open Wizard</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data & Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
Analyze trial results and performance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className="w-full" asChild>
|
||||
<Link href="/analytics">View Data</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
// Public landing page
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-12 max-w-3xl">
|
||||
<h2 className="mb-6 text-3xl font-bold text-slate-900">
|
||||
Standardize Your Wizard of Oz Studies
|
||||
</h2>
|
||||
<p className="mb-8 text-xl text-slate-600">
|
||||
HRIStudio provides a comprehensive platform for designing,
|
||||
executing, and analyzing Human-Robot Interaction experiments
|
||||
with standardized Wizard of Oz methodologies.
|
||||
</p>
|
||||
|
||||
<div className="mb-12 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
||||
Visual Experiment Designer
|
||||
</h3>
|
||||
<p className="text-slate-600">
|
||||
Drag-and-drop interface for creating complex interaction
|
||||
scenarios
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-green-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
||||
Real-time Control
|
||||
</h3>
|
||||
<p className="text-slate-600">
|
||||
Live robot control with responsive wizard interface
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-purple-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
||||
Advanced Analytics
|
||||
</h3>
|
||||
<p className="text-slate-600">
|
||||
Comprehensive data capture and analysis tools
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="lg" asChild>
|
||||
<Link href="/auth/signup">Start Your Research</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
59
src/components/ui/button.tsx
Normal file
59
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
167
src/components/ui/form.tsx
Normal file
167
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Label } from "~/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -11,8 +11,7 @@ export const env = createEnv({
|
||||
process.env.NODE_ENV === "production"
|
||||
? z.string()
|
||||
: z.string().optional(),
|
||||
AUTH_DISCORD_ID: z.string(),
|
||||
AUTH_DISCORD_SECRET: z.string(),
|
||||
|
||||
DATABASE_URL: z.string().url(),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
@@ -34,8 +33,7 @@ export const env = createEnv({
|
||||
*/
|
||||
runtimeEnv: {
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
||||
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
||||
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { postRouter } from "~/server/api/routers/post";
|
||||
import { authRouter } from "~/server/api/routers/auth";
|
||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
|
||||
/**
|
||||
@@ -8,6 +9,7 @@ import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
post: postRouter,
|
||||
auth: authRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
59
src/server/api/routers/auth.ts
Normal file
59
src/server/api/routers/auth.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { z } from "zod";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { users } from "~/server/db/schema";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
register: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { name, email, password } = input;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await ctx.db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "User with this email already exists",
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
try {
|
||||
// Create user
|
||||
const [newUser] = await ctx.db
|
||||
.insert(users)
|
||||
.values({
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
})
|
||||
.returning({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email,
|
||||
});
|
||||
|
||||
return newUser;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create user",
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import { type DefaultSession, type NextAuthConfig } from "next-auth";
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import { db } from "~/server/db";
|
||||
import {
|
||||
@@ -38,29 +40,57 @@ declare module "next-auth" {
|
||||
*/
|
||||
export const authConfig = {
|
||||
providers: [
|
||||
DiscordProvider,
|
||||
/**
|
||||
* ...add more providers here.
|
||||
*
|
||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
||||
*
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
Credentials({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.email, credentials.email as string),
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(
|
||||
credentials.password as string,
|
||||
user.password,
|
||||
);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
adapter: DrizzleAdapter(db, {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
verificationTokensTable: verificationTokens,
|
||||
}),
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
jwt: ({ token, user }) => {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
session: ({ session, token }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
id: token.id as string,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,125 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user