mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-06-24 00:11:44 -04:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b147f81322 | |||
| 880980dccd | |||
| 356084a3f1 | |||
| 14182bf078 | |||
| 943c7bd963 | |||
| 6b54724171 | |||
| 86c1f35537 | |||
| 5b5490cb90 | |||
| 6b98cad53e | |||
| 3e2aa894a0 | |||
| 27f633fb4b | |||
| 6243b62d3b | |||
| f16dd4aa22 | |||
| 7483e4a72b | |||
| 426b5e761b | |||
| cf21a27995 | |||
| 74b5507769 | |||
| 5c67fc106e | |||
| 4b04f2c415 | |||
| c959e61f95 | |||
| de1b125b13 | |||
| 143cf2ce50 | |||
| 61c7cc1e94 | |||
| 8f330cf5f0 | |||
| 254805008e | |||
| c923c63099 | |||
| c05384d1a0 | |||
| c0e5a4ffb8 | |||
| 51aaaa5208 | |||
| e402c51483 | |||
| 7c360dc860 | |||
| 1c7f0297a6 | |||
| 3959cf23f7 | |||
| 3270e3f8fe | |||
| bfd1924897 | |||
| 0827a791c6 | |||
| ecf0ab9103 | |||
| 49e0df016a | |||
| 8529d0ef89 | |||
| 67ad904f62 | |||
| 519e6a2606 | |||
| b353ef7c9f | |||
| cbd31e9aa4 | |||
| 37feea8df3 | |||
| cf3597881b | |||
| add3380307 | |||
| 79bb298756 | |||
| a5762ec935 | |||
| 20d6d3de1a | |||
| 4bed537943 | |||
| 73f70f6550 | |||
| 3fafd61553 | |||
| 3491bf4463 | |||
| cc58593891 | |||
| bbbe397ba8 | |||
| bbc34921b5 | |||
| 8e647c958e | |||
| 4e86546311 | |||
| e84c794962 | |||
| 70064f487e | |||
| 91d03a789d | |||
| 31d2173703 | |||
| 4a9abf4ff1 | |||
| 487f97c5c2 | |||
| db147f2294 | |||
| a705c720fb | |||
| e460c1b029 | |||
| eb0d86f570 | |||
| e40c37cfd0 | |||
| f8e6fccae3 | |||
| 3f87588fea | |||
| 18e5aab4a5 |
+10
-2
@@ -16,11 +16,19 @@
|
|||||||
AUTH_SECRET=""
|
AUTH_SECRET=""
|
||||||
|
|
||||||
# Drizzle
|
# Drizzle
|
||||||
DATABASE_URL="postgresql://postgres:password@localhost:5433/hristudio"
|
DATABASE_URL="postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@localhost:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-hristudio}"
|
||||||
|
|
||||||
|
# PostgreSQL (used by docker-compose)
|
||||||
|
POSTGRES_USER="postgres"
|
||||||
|
POSTGRES_PASSWORD="postgres"
|
||||||
|
POSTGRES_DB="hristudio"
|
||||||
|
POSTGRES_PORT="5432"
|
||||||
|
|
||||||
# MinIO/S3 Configuration
|
# MinIO/S3 Configuration
|
||||||
MINIO_ENDPOINT="http://localhost:9000"
|
MINIO_ENDPOINT="http://localhost:${MINIO_PORT_API:-9000}"
|
||||||
MINIO_REGION="us-east-1"
|
MINIO_REGION="us-east-1"
|
||||||
MINIO_ACCESS_KEY="minioadmin"
|
MINIO_ACCESS_KEY="minioadmin"
|
||||||
MINIO_SECRET_KEY="minioadmin"
|
MINIO_SECRET_KEY="minioadmin"
|
||||||
MINIO_BUCKET_NAME="hristudio-data"
|
MINIO_BUCKET_NAME="hristudio-data"
|
||||||
|
MINIO_PORT_API="9000"
|
||||||
|
MINIO_PORT_CONSOLE="9001"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "robot-plugins"]
|
||||||
|
path = robot-plugins
|
||||||
|
url = git@github.com:soconnor0919/robot-plugins.git
|
||||||
|
branch = main
|
||||||
@@ -64,16 +64,15 @@ bun dev
|
|||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
- **Framework**: Next.js 15 with App Router and React 19 RC
|
- **Framework**: Next.js 15 (16.x compatible) with App Router and React 19
|
||||||
- **Language**: TypeScript (strict mode) - 100% type safety throughout
|
- **Language**: TypeScript (strict mode) - 100% type safety throughout
|
||||||
- **Database**: PostgreSQL with Drizzle ORM for type-safe operations
|
- **Database**: PostgreSQL with Drizzle ORM for type-safe operations
|
||||||
- **Authentication**: NextAuth.js v5 with database sessions and JWT
|
- **Authentication**: Better Auth with database sessions
|
||||||
- **API**: tRPC for end-to-end type-safe client-server communication
|
- **API**: tRPC for end-to-end type-safe client-server communication
|
||||||
- **UI**: Tailwind CSS + shadcn/ui (built on Radix UI primitives)
|
- **UI**: Tailwind CSS + shadcn/ui (built on Radix UI primitives)
|
||||||
- **Storage**: Cloudflare R2 (S3-compatible) for media files
|
- **Storage**: Cloudflare R2 (S3-compatible) for media files
|
||||||
- **Deployment**: Vercel serverless platform with Edge Runtime
|
- **Real-time**: WebSocket with polling fallback for trial execution
|
||||||
- **Package Manager**: Bun exclusively
|
- **Package Manager**: Bun exclusively
|
||||||
- **Real-time**: WebSocket with Edge Runtime compatibility
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -203,14 +202,12 @@ src/
|
|||||||
|
|
||||||
Comprehensive documentation available in the `docs/` folder:
|
Comprehensive documentation available in the `docs/` folder:
|
||||||
|
|
||||||
- **[Quick Reference](docs/quick-reference.md)**: 5-minute setup guide and essential commands
|
- **[Tutorials](docs/tutorials/README.md)**: Step-by-step guides for new users
|
||||||
- **[Project Overview](docs/project-overview.md)**: Complete feature overview and architecture
|
- **[Quick Reference](docs/quick-reference.md)**: Essential commands and setup
|
||||||
- **[Implementation Details](docs/implementation-details.md)**: Architecture decisions and patterns
|
- **[Implementation Guide](docs/implementation-guide.md)**: Technical implementation details
|
||||||
- **[Database Schema](docs/database-schema.md)**: Complete PostgreSQL schema documentation
|
- **[Project Status](docs/project-status.md)**: Current development state
|
||||||
- **[API Routes](docs/api-routes.md)**: Comprehensive tRPC API reference
|
- **[NAO6 Integration](docs/nao6-quick-reference.md)**: Robot setup and commands
|
||||||
- **[Core Blocks System](docs/core-blocks-system.md)**: Repository-based block architecture
|
- **[Archive](docs/_archive/)**: Historical documentation (outdated)
|
||||||
- **[Plugin System](docs/plugin-system-implementation-guide.md)**: Robot integration guide
|
|
||||||
- **[Project Status](docs/project-status.md)**: Current completion status (98% complete)
|
|
||||||
|
|
||||||
## Research Paper
|
## Research Paper
|
||||||
|
|
||||||
@@ -234,19 +231,39 @@ Full paper available at: [docs/paper.md](docs/paper.md)
|
|||||||
- **4 User Roles**: Complete role-based access control
|
- **4 User Roles**: Complete role-based access control
|
||||||
- **Plugin System**: Extensible robot integration architecture
|
- **Plugin System**: Extensible robot integration architecture
|
||||||
- **Trial System**: Unified design with real-time execution capabilities
|
- **Trial System**: Unified design with real-time execution capabilities
|
||||||
|
- **WebSocket Ready**: Real-time trial updates with polling fallback
|
||||||
|
- **Docker Integration**: NAO6 deployment via docker-compose
|
||||||
|
- **Conditional Branching**: Experiment flow with wizard choices and convergence paths
|
||||||
|
|
||||||
## NAO6 Robot Integration
|
## NAO6 Robot Integration
|
||||||
|
|
||||||
Complete NAO6 robot integration is available in the separate **[nao6-hristudio-integration](../nao6-hristudio-integration/)** repository.
|
Complete NAO6 robot integration is available in the separate **[nao6-hristudio-integration](../nao6-hristudio-integration/)** repository.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Complete ROS2 driver integration for NAO V6.0
|
- Complete ROS2 Humble driver integration for NAO V6.0
|
||||||
|
- Docker-based deployment with three services: nao_driver, ros_bridge, ros_api
|
||||||
- WebSocket communication via rosbridge
|
- WebSocket communication via rosbridge
|
||||||
- 9 robot actions: speech, movement, gestures, sensors, LEDs
|
- 14 robot actions: speech, movement, gestures, sensors, LEDs, animations
|
||||||
- Real-time control from wizard interface
|
- Real-time control from wizard interface
|
||||||
- Production-ready with NAOqi 2.8.7.4
|
- Production-ready with NAOqi 2.8.7.4, ROS2 Humble
|
||||||
|
|
||||||
### Quick Start
|
### Docker Deployment
|
||||||
|
```bash
|
||||||
|
# Start NAO integration
|
||||||
|
cd nao6-hristudio-integration
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Start HRIStudio
|
||||||
|
cd ~/Documents/Projects/hristudio
|
||||||
|
bun dev
|
||||||
|
|
||||||
|
# Access
|
||||||
|
# - HRIStudio: http://localhost:3000
|
||||||
|
# - Test page: http://localhost:3000/nao-test
|
||||||
|
# - rosbridge: ws://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start Commands
|
||||||
```bash
|
```bash
|
||||||
# Start NAO integration
|
# Start NAO integration
|
||||||
cd ~/naoqi_ros2_ws
|
cd ~/naoqi_ros2_ws
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hristudio",
|
"name": "hristudio",
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
"@auth/drizzle-adapter": "^1.11.1",
|
"@auth/drizzle-adapter": "^1.11.1",
|
||||||
"@aws-sdk/client-s3": "^3.989.0",
|
"@aws-sdk/client-s3": "^3.989.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.989.0",
|
"@aws-sdk/s3-request-presigner": "^3.989.0",
|
||||||
|
"@better-auth/drizzle-adapter": "^1.5.5",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -48,24 +50,25 @@
|
|||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"better-auth": "^1.5.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"driver.js": "^1.4.0",
|
"driver.js": "^1.4.0",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"html2pdf.js": "^0.14.0",
|
"html2pdf.js": "^0.14.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.536.0",
|
"lucide-react": "^0.536.0",
|
||||||
"minio": "^8.0.6",
|
"minio": "^8.0.6",
|
||||||
"next": "^16.1.6",
|
"next": "16.2.6",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
@@ -87,12 +90,12 @@
|
|||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^20.19.33",
|
"@types/node": "^20.19.33",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-next": "^15.5.12",
|
"eslint-config-next": "16.2.1",
|
||||||
"eslint-plugin-drizzle": "^0.2.3",
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
@@ -112,6 +115,10 @@
|
|||||||
"sharp",
|
"sharp",
|
||||||
"unrs-resolver",
|
"unrs-resolver",
|
||||||
],
|
],
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
@@ -205,8 +212,58 @@
|
|||||||
|
|
||||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
|
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="],
|
||||||
|
|
||||||
|
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="],
|
||||||
|
|
||||||
|
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g=="],
|
||||||
|
|
||||||
|
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0" } }, "sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g=="],
|
||||||
|
|
||||||
|
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg=="],
|
||||||
|
|
||||||
|
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||||
|
|
||||||
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
||||||
@@ -377,27 +434,33 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
|
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
|
"@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.12", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
|
||||||
|
|
||||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
@@ -585,8 +648,6 @@
|
|||||||
|
|
||||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="],
|
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="],
|
||||||
@@ -855,6 +916,10 @@
|
|||||||
|
|
||||||
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
||||||
|
|
||||||
|
"@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
|
||||||
|
|
||||||
|
"@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.55.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.55.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ=="],
|
||||||
@@ -987,6 +1052,10 @@
|
|||||||
|
|
||||||
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="],
|
||||||
|
|
||||||
"bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="],
|
"bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="],
|
||||||
|
|
||||||
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
||||||
@@ -999,6 +1068,10 @@
|
|||||||
|
|
||||||
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
|
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="],
|
||||||
|
|
||||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
||||||
@@ -1015,7 +1088,7 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
|
||||||
|
|
||||||
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
|
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
|
||||||
|
|
||||||
@@ -1045,6 +1118,8 @@
|
|||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||||
|
|
||||||
"core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="],
|
"core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="],
|
||||||
@@ -1085,6 +1160,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
@@ -1097,10 +1174,12 @@
|
|||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
|
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
|
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||||
@@ -1131,11 +1210,13 @@
|
|||||||
|
|
||||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||||
|
|
||||||
"eslint-config-next": ["eslint-config-next@15.5.12", "", { "dependencies": { "@next/eslint-plugin-next": "15.5.12", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-ktW3XLfd+ztEltY5scJNjxjHwtKWk6vU2iwzZqSN09UsbBmMeE/cVlJ1yESg6Yx5LW7p/Z8WzUAgYXGLEmGIpg=="],
|
"eslint-config-next": ["eslint-config-next@16.2.1", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.1", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
||||||
|
|
||||||
@@ -1151,7 +1232,7 @@
|
|||||||
|
|
||||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
@@ -1225,6 +1306,8 @@
|
|||||||
|
|
||||||
"gel": ["gel@2.1.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="],
|
"gel": ["gel@2.1.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
@@ -1261,6 +1344,10 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||||
|
|
||||||
|
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||||
|
|
||||||
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
||||||
|
|
||||||
"html2pdf.js": ["html2pdf.js@0.14.0", "", { "dependencies": { "dompurify": "^3.3.1", "html2canvas": "^1.0.0", "jspdf": "^4.0.0" } }, "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ=="],
|
"html2pdf.js": ["html2pdf.js@0.14.0", "", { "dependencies": { "dompurify": "^3.3.1", "html2canvas": "^1.0.0", "jspdf": "^4.0.0" } }, "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ=="],
|
||||||
@@ -1353,7 +1440,7 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
"jose": ["jose@6.0.12", "", {}, "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ=="],
|
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||||
|
|
||||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||||
|
|
||||||
@@ -1361,6 +1448,8 @@
|
|||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
@@ -1379,6 +1468,8 @@
|
|||||||
|
|
||||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.14", "", {}, "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA=="],
|
||||||
|
|
||||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
"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=="],
|
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||||
@@ -1423,6 +1514,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.536.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A=="],
|
"lucide-react": ["lucide-react@0.536.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
@@ -1435,6 +1528,8 @@
|
|||||||
|
|
||||||
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
||||||
|
|
||||||
|
"memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
|
||||||
|
|
||||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
@@ -1453,15 +1548,21 @@
|
|||||||
|
|
||||||
"minio": ["minio@8.0.6", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^4.4.1", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "web-encoding": "^1.1.5", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ=="],
|
"minio": ["minio@8.0.6", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^4.4.1", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "web-encoding": "^1.1.5", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ=="],
|
||||||
|
|
||||||
|
"mongodb": ["mongodb@7.1.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg=="],
|
||||||
|
|
||||||
|
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="],
|
||||||
|
|
||||||
"napi-postinstall": ["napi-postinstall@0.3.2", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw=="],
|
"napi-postinstall": ["napi-postinstall@0.3.2", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw=="],
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "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-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
|
"next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "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-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
|
||||||
|
|
||||||
"next-auth": ["next-auth@5.0.0-beta.30", "", { "dependencies": { "@auth/core": "0.41.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", "nodemailer": "^7.0.7", "react": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg=="],
|
"next-auth": ["next-auth@5.0.0-beta.30", "", { "dependencies": { "@auth/core": "0.41.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", "nodemailer": "^7.0.7", "react": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg=="],
|
||||||
|
|
||||||
@@ -1471,6 +1572,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||||
|
|
||||||
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
||||||
|
|
||||||
"oauth4webapi": ["oauth4webapi@3.6.1", "", {}, "sha512-b39+drVyA4aNUptFOhkkmGWnG/BE7dT29SW/8PVYElqp7j/DBqzm5SS1G+MUD07XlTcBOAG+6Cb/35Cx2kHIuQ=="],
|
"oauth4webapi": ["oauth4webapi@3.6.1", "", {}, "sha512-b39+drVyA4aNUptFOhkkmGWnG/BE7dT29SW/8PVYElqp7j/DBqzm5SS1G+MUD07XlTcBOAG+6Cb/35Cx2kHIuQ=="],
|
||||||
@@ -1641,6 +1744,8 @@
|
|||||||
|
|
||||||
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
|
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"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-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=="],
|
||||||
@@ -1659,6 +1764,8 @@
|
|||||||
|
|
||||||
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
@@ -1697,6 +1804,8 @@
|
|||||||
|
|
||||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="],
|
||||||
|
|
||||||
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
|
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
|
||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
@@ -1773,6 +1882,8 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
|
||||||
|
|
||||||
"trim-canvas": ["trim-canvas@0.1.2", "", {}, "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ=="],
|
"trim-canvas": ["trim-canvas@0.1.2", "", {}, "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||||
@@ -1809,6 +1920,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
@@ -1837,6 +1950,10 @@
|
|||||||
|
|
||||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||||
|
|
||||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1857,12 +1974,18 @@
|
|||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||||
|
|
||||||
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
||||||
|
|
||||||
|
"@auth/core/jose": ["jose@6.0.12", "", {}, "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.840.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA=="],
|
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.840.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA=="],
|
||||||
|
|
||||||
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.840.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA=="],
|
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.840.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA=="],
|
||||||
@@ -1883,6 +2006,16 @@
|
|||||||
|
|
||||||
"@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="],
|
"@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="],
|
||||||
|
|
||||||
|
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
@@ -1969,6 +2102,8 @@
|
|||||||
|
|
||||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"eslint-config-next/globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
"eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
@@ -2097,6 +2232,8 @@
|
|||||||
|
|
||||||
"eslint-import-resolver-typescript/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
"eslint-import-resolver-typescript/tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||||
|
|
||||||
|
"next-auth/@auth/core/jose": ["jose@6.0.12", "", {}, "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ=="],
|
||||||
|
|
||||||
"prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
"prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||||
|
|
||||||
"prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
"prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||||
|
|||||||
+11
-11
@@ -2,13 +2,13 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
POSTGRES_DB: hristudio
|
POSTGRES_DB: ${POSTGRES_DB:-hristudio}
|
||||||
PGSSLMODE: disable
|
PGSSLMODE: disable
|
||||||
command: -c ssl=off
|
command: -c ssl=off
|
||||||
ports:
|
ports:
|
||||||
- "5140:5432"
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -20,11 +20,11 @@ services:
|
|||||||
minio:
|
minio:
|
||||||
image: minio/minio
|
image: minio/minio
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000" # API
|
- "${MINIO_PORT_API:-9000}:9000" # API
|
||||||
- "9001:9001" # Console
|
- "${MINIO_PORT_CONSOLE:-9001}:9001" # Console
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: minioadmin
|
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||||
MINIO_ROOT_PASSWORD: minioadmin
|
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
command: server --console-address ":9001" /data
|
command: server --console-address ":9001" /data
|
||||||
@@ -35,9 +35,9 @@ services:
|
|||||||
- minio
|
- minio
|
||||||
entrypoint: >
|
entrypoint: >
|
||||||
/bin/sh -c "
|
/bin/sh -c "
|
||||||
/usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
|
/usr/bin/mc alias set myminio http://minio:9000 ${MINIO_ACCESS_KEY:-minioadmin} ${MINIO_SECRET_KEY:-minioadmin};
|
||||||
/usr/bin/mc mb myminio/hristudio;
|
/usr/bin/mc mb myminio/${MINIO_BUCKET_NAME:-hristudio-data};
|
||||||
/usr/bin/mc anonymous set public myminio/hristudio;
|
/usr/bin/mc anonymous set public myminio/${MINIO_BUCKET_NAME:-hristudio-data};
|
||||||
exit 0;
|
exit 0;
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
+180
-283
@@ -1,307 +1,204 @@
|
|||||||
# HRIStudio Documentation
|
# 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.
|
HRIStudio is a web-based Wizard-of-Oz platform for Human-Robot Interaction research.
|
||||||
|
|
||||||
## 📚 Documentation Overview
|
## Quick Links
|
||||||
|
|
||||||
This documentation suite provides everything needed to understand, build, deploy, and maintain HRIStudio. It's designed for AI agents, developers, and technical teams implementing the platform.
|
| Document | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **[Tutorials](tutorials/README.md)** | Step-by-step guides for using HRIStudio |
|
||||||
|
| **[Quick Reference](quick-reference.md)** | Essential commands, setup, troubleshooting |
|
||||||
|
| **[Project Status](project-status.md)** | Current development state (March 2026) |
|
||||||
|
| **[Implementation Guide](implementation-guide.md)** | Full technical implementation |
|
||||||
|
| **[NAO6 Integration](nao6-quick-reference.md)** | Robot setup and commands |
|
||||||
|
|
||||||
### **🚀 Quick Start**
|
## Tutorials
|
||||||
|
|
||||||
**New to HRIStudio?** Start here:
|
New to HRIStudio? Start with our comprehensive tutorials:
|
||||||
1. **[Quick Reference](./quick-reference.md)** - 5-minute setup and key concepts
|
|
||||||
2. **[Project Overview](./project-overview.md)** - Complete feature overview and goals
|
|
||||||
3. **[Implementation Guide](./implementation-guide.md)** - Step-by-step technical implementation
|
|
||||||
|
|
||||||
### **📋 Core Documentation** (8 Files)
|
| Tutorial | Description | Time |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| [Getting Started](tutorials/01-getting-started.md) | Installation and first login | 10 min |
|
||||||
|
| [Your First Study](tutorials/02-your-first-study.md) | Creating a research study | 15 min |
|
||||||
|
| [Designing Experiments](tutorials/03-designing-experiments.md) | Building experiment protocols | 25 min |
|
||||||
|
| [Running Trials](tutorials/04-running-trials.md) | Executing experiments | 20 min |
|
||||||
|
| [Wizard Interface](tutorials/05-wizard-interface.md) | Real-time trial control | 15 min |
|
||||||
|
| [Robot Integration](tutorials/06-robot-integration.md) | Connecting NAO6 robot | 20 min |
|
||||||
|
| [Forms & Surveys](tutorials/07-forms-and-surveys.md) | Managing consent and data | 15 min |
|
||||||
|
| [Data & Analysis](tutorials/08-data-and-analysis.md) | Collecting and exporting data | 15 min |
|
||||||
|
| [Simulation Mode](tutorials/09-simulation-mode.md) | Testing without a robot | 10 min |
|
||||||
|
|
||||||
#### **Project Specifications**
|
## Getting Started
|
||||||
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. **[Feature Requirements](./feature-requirements.md)**
|
|
||||||
- Detailed user stories and acceptance criteria
|
|
||||||
- Functional requirements by module
|
|
||||||
- Non-functional requirements
|
|
||||||
- UI/UX specifications
|
|
||||||
- Integration requirements
|
|
||||||
|
|
||||||
#### **Technical Implementation**
|
|
||||||
3. **[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
|
|
||||||
|
|
||||||
4. **[API Routes](./api-routes.md)**
|
|
||||||
- Comprehensive tRPC route documentation
|
|
||||||
- Request/response schemas
|
|
||||||
- Authentication requirements
|
|
||||||
- WebSocket events
|
|
||||||
- Rate limiting and error handling
|
|
||||||
|
|
||||||
5. **[Core Blocks System](./core-blocks-system.md)**
|
|
||||||
- Repository-based plugin architecture
|
|
||||||
- 26 essential blocks across 4 categories
|
|
||||||
- Event triggers, wizard actions, control flow, observation
|
|
||||||
- Block loading and validation system
|
|
||||||
- Integration with experiment designer
|
|
||||||
|
|
||||||
6. **[Plugin System Implementation](./plugin-system-implementation-guide.md)**
|
|
||||||
- Robot plugin architecture and development
|
|
||||||
- Repository management and trust levels
|
|
||||||
- Plugin installation and configuration
|
|
||||||
- Action definitions and parameter schemas
|
|
||||||
- ROS2 integration patterns
|
|
||||||
|
|
||||||
7. **[Implementation Guide](./implementation-guide.md)**
|
|
||||||
- Step-by-step technical implementation
|
|
||||||
- Code examples and patterns
|
|
||||||
- Frontend and backend architecture
|
|
||||||
- Real-time features implementation
|
|
||||||
- Testing strategies
|
|
||||||
|
|
||||||
8. **[Implementation Details](./implementation-details.md)**
|
|
||||||
- Architecture decisions and rationale
|
|
||||||
- Unified editor experiences (significant code reduction)
|
|
||||||
- DataTable migration achievements
|
|
||||||
- Development database and seed system
|
|
||||||
- Performance optimization strategies
|
|
||||||
|
|
||||||
#### **Operations & Deployment**
|
|
||||||
9. **[Deployment & Operations](./deployment-operations.md)**
|
|
||||||
- Infrastructure requirements
|
|
||||||
- Vercel deployment strategies
|
|
||||||
- Monitoring and observability
|
|
||||||
- Backup and recovery procedures
|
|
||||||
- Security operations
|
|
||||||
|
|
||||||
10. **[ROS2 Integration](./ros2-integration.md)**
|
|
||||||
- rosbridge WebSocket architecture
|
|
||||||
- Client-side ROS connection management
|
|
||||||
- Message type definitions
|
|
||||||
- Robot plugin implementation
|
|
||||||
- Security considerations for robot communication
|
|
||||||
|
|
||||||
### **📊 Project Status**
|
|
||||||
|
|
||||||
11. **[Project Status](./project-status.md)**
|
|
||||||
- Overall completion status (complete)
|
|
||||||
- Implementation progress by feature
|
|
||||||
- Sprint planning and development velocity
|
|
||||||
- Production readiness assessment
|
|
||||||
- Core blocks system completion
|
|
||||||
|
|
||||||
12. **[Quick Reference](./quick-reference.md)**
|
|
||||||
- 5-minute setup guide
|
|
||||||
- Essential commands and patterns
|
|
||||||
- API reference and common workflows
|
|
||||||
- Core blocks system overview
|
|
||||||
- Key concepts and architecture overview
|
|
||||||
|
|
||||||
13. **[Work in Progress](./work_in_progress.md)**
|
|
||||||
- Recent changes and improvements
|
|
||||||
- Core blocks system implementation
|
|
||||||
- Plugin architecture enhancements
|
|
||||||
- Panel-based wizard interface (matching experiment designer)
|
|
||||||
- Technical debt resolution
|
|
||||||
- UI/UX enhancements
|
|
||||||
|
|
||||||
### **🤖 Robot Integration Guides**
|
|
||||||
|
|
||||||
14. **[NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)** - Comprehensive NAO6 setup, troubleshooting, and production deployment
|
|
||||||
15. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential commands and troubleshooting for NAO6 integration
|
|
||||||
16. **[NAO6 ROS2 Setup](./nao6-ros2-setup.md)** - Basic NAO6 ROS2 driver installation guide
|
|
||||||
|
|
||||||
### **📖 Academic References**
|
|
||||||
|
|
||||||
17. **[Research Paper](./root.tex)** - Academic LaTeX document
|
|
||||||
18. **[Bibliography](./refs.bib)** - Research references
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Documentation Structure Benefits**
|
|
||||||
|
|
||||||
### **Streamlined Organization**
|
|
||||||
- **Consolidated documentation** - Easier navigation and maintenance
|
|
||||||
- **Logical progression** - From overview → implementation → deployment
|
|
||||||
- **Consolidated achievements** - All progress tracking in unified documents
|
|
||||||
- **Clear entry points** - Quick reference for immediate needs
|
|
||||||
|
|
||||||
### **Comprehensive Coverage**
|
|
||||||
- **Complete technical specs** - Database, API, and implementation details
|
|
||||||
- **Step-by-step guidance** - From project setup to production deployment
|
|
||||||
- **Real-world examples** - Code patterns and configuration samples
|
|
||||||
- **Performance insights** - Optimization strategies and benchmark results
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 **Getting Started Paths**
|
|
||||||
|
|
||||||
### **For Developers**
|
|
||||||
1. **[Quick Reference](./quick-reference.md)** - Immediate setup and key commands
|
|
||||||
2. **[Implementation Guide](./implementation-guide.md)** - Technical implementation steps
|
|
||||||
3. **[Database Schema](./database-schema.md)** - Data model understanding
|
|
||||||
4. **[API Routes](./api-routes.md)** - Backend integration
|
|
||||||
|
|
||||||
### **For Project Managers**
|
|
||||||
1. **[Project Overview](./project-overview.md)** - Complete feature understanding
|
|
||||||
2. **[Project Status](./project-status.md)** - Current progress and roadmap
|
|
||||||
3. **[Feature Requirements](./feature-requirements.md)** - Detailed specifications
|
|
||||||
4. **[Deployment & Operations](./deployment-operations.md)** - Infrastructure planning
|
|
||||||
|
|
||||||
### **For Researchers**
|
|
||||||
1. **[Project Overview](./project-overview.md)** - Research platform capabilities
|
|
||||||
2. **[Feature Requirements](./feature-requirements.md)** - User workflows and features
|
|
||||||
3. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential NAO6 robot control commands
|
|
||||||
4. **[ROS2 Integration](./ros2-integration.md)** - Robot platform integration
|
|
||||||
5. **[Research Paper](./root.tex)** - Academic context and methodology
|
|
||||||
|
|
||||||
### **For Robot Integration**
|
|
||||||
1. **[NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)** - Full NAO6 setup and troubleshooting
|
|
||||||
2. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential commands and quick fixes
|
|
||||||
3. **[ROS2 Integration](./ros2-integration.md)** - General robot integration patterns
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ **Prerequisites**
|
|
||||||
|
|
||||||
### **Development Environment**
|
|
||||||
- **[Bun](https://bun.sh)** - Package manager and runtime
|
|
||||||
- **[PostgreSQL](https://postgresql.org)** 15+ - Primary database
|
|
||||||
- **[Docker](https://docker.com)** - Containerized development (optional)
|
|
||||||
|
|
||||||
### **Production Deployment**
|
|
||||||
- **[Vercel](https://vercel.com)** account - Serverless deployment platform
|
|
||||||
- **PostgreSQL** database - Vercel Postgres or external provider
|
|
||||||
- **[Cloudflare R2](https://cloudflare.com/products/r2/)** - S3-compatible storage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ **Quick Setup (5 Minutes)**
|
|
||||||
|
|
||||||
|
### 1. Clone & Install
|
||||||
```bash
|
```bash
|
||||||
# Clone and install
|
git clone https://github.com/soconnor0919/hristudio.git
|
||||||
git clone <repo-url> hristudio
|
|
||||||
cd hristudio
|
cd hristudio
|
||||||
|
git submodule update --init --recursive
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
# Start database
|
|
||||||
bun run docker:up
|
|
||||||
|
|
||||||
# Setup database and seed data
|
|
||||||
bun db:push
|
|
||||||
bun db:seed
|
|
||||||
|
|
||||||
# Start development
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Default Login**: `sean@soconnor.dev` / `password123`
|
### 2. Start Database
|
||||||
|
```bash
|
||||||
|
bun run docker:up
|
||||||
|
bun db:push
|
||||||
|
bun db:seed
|
||||||
|
```
|
||||||
|
|
||||||
---
|
### 3. Start Application
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
# Visit http://localhost:3000
|
||||||
|
# Login: sean@soconnor.dev / password123
|
||||||
|
```
|
||||||
|
|
||||||
## 📋 **Key Features Overview**
|
### 4. Start NAO6 Robot (optional)
|
||||||
|
```bash
|
||||||
|
cd ~/Documents/Projects/nao6-hristudio-integration
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ HRIStudio Platform │
|
||||||
|
├──────────────────────────────────────────────────────────┤
|
||||||
|
│ UI Layer (Next.js + React + shadcn/ui) │
|
||||||
|
│ ├── Experiment Designer (drag-and-drop) │
|
||||||
|
│ ├── Wizard Interface (real-time trial execution) │
|
||||||
|
│ └── Observer/Participant Views │
|
||||||
|
├──────────────────────────────────────────────────────────┤
|
||||||
|
│ Logic Layer (tRPC + Better Auth) │
|
||||||
|
│ ├── 12 tRPC routers (studies, experiments, trials...) │
|
||||||
|
│ ├── Role-based authentication (4 roles) │
|
||||||
|
│ └── WebSocket for real-time updates │
|
||||||
|
├──────────────────────────────────────────────────────────┤
|
||||||
|
│ Data Layer (PostgreSQL + Drizzle ORM) │
|
||||||
|
│ ├── 31 tables with complete relationships │
|
||||||
|
│ ├── Plugin system with identifier-based lookup │
|
||||||
|
│ └── Comprehensive event logging │
|
||||||
|
├──────────────────────────────────────────────────────────┤
|
||||||
|
│ Robot Integration (ROS2 via WebSocket) │
|
||||||
|
│ Docker: nao_driver, ros_bridge, ros_api │
|
||||||
|
│ Plugin identifier: "nao6-ros2" │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
### **Research Workflow Support**
|
|
||||||
- **Hierarchical Structure**: Study → Experiment → Trial → Step → Action
|
- **Hierarchical Structure**: Study → Experiment → Trial → Step → Action
|
||||||
- **Visual Experiment Designer**: Repository-based plugin architecture with 26 core blocks
|
- **Visual Designer**: 26+ core blocks (events, wizard actions, control flow, observation)
|
||||||
- **Core Block Categories**: Events, wizard actions, control flow, observation blocks
|
- **Conditional Branching**: Wizard choices with convergence paths
|
||||||
- **Real-time Trial Execution**: Live wizard control with data capture
|
- **WebSocket Real-time**: Trial updates with auto-reconnect
|
||||||
- **Multi-role Collaboration**: Administrator, Researcher, Wizard, Observer
|
- **Plugin System**: Robot-agnostic via identifier lookup
|
||||||
- **Comprehensive Data Management**: Synchronized multi-modal capture
|
- **Docker NAO6**: Three-service ROS2 integration
|
||||||
|
- **Forms System**: Consent forms, surveys, questionnaires with templates
|
||||||
|
- **Role-based Access**: Owner, Researcher, Wizard, Observer permissions
|
||||||
|
|
||||||
### **Technical Excellence**
|
## System Components
|
||||||
- **Full Type Safety**: End-to-end TypeScript with strict mode
|
|
||||||
- **Production Ready**: Vercel deployment with Edge Runtime
|
|
||||||
- **Performance Optimized**: Database indexes and query optimization
|
|
||||||
- **Security First**: Role-based access control throughout
|
|
||||||
- **Modern Stack**: Next.js 15, tRPC, Drizzle ORM, shadcn/ui
|
|
||||||
- **Consistent Architecture**: Panel-based interfaces across visual programming tools
|
|
||||||
|
|
||||||
### **Development Experience**
|
### Backend (src/server/)
|
||||||
- **Unified Components**: Significant reduction in code duplication
|
- `api/routers/` - 13 tRPC routers (studies, experiments, trials, participants, forms, etc.)
|
||||||
- **Panel Architecture**: 90% code sharing between experiment designer and wizard interface
|
- `db/schema.ts` - Drizzle schema (33 tables)
|
||||||
- **Consolidated Wizard**: 3-panel design with trial controls, horizontal timeline, and unified robot controls
|
- `services/trial-execution.ts` - Trial execution engine
|
||||||
- **Enterprise DataTables**: Advanced filtering, export, pagination
|
- `services/websocket-manager.ts` - Real-time connections
|
||||||
- **Comprehensive Testing**: Realistic seed data with complete scenarios
|
|
||||||
- **Developer Friendly**: Clear patterns and extensive documentation
|
|
||||||
|
|
||||||
### **Robot Integration**
|
### Frontend (src/)
|
||||||
- **NAO6 Full Support**: Complete ROS2 integration with movement, speech, and sensor control
|
- `app/` - Next.js App Router pages
|
||||||
- **Real-time Control**: WebSocket-based robot control through web interface
|
- `components/trials/wizard/` - Wizard interface
|
||||||
- **Safety Features**: Emergency stops, movement limits, and comprehensive monitoring
|
- `components/trials/forms/` - Form builder and viewer
|
||||||
- **Production Ready**: Tested with NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble
|
- `hooks/useWebSocket.ts` - Real-time trial updates
|
||||||
- **Troubleshooting Guides**: Complete documentation for setup and problem resolution
|
- `lib/ros/wizard-ros-service.ts` - Robot control
|
||||||
|
|
||||||
|
## Plugin Identifier System
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plugins table has:
|
||||||
|
// - identifier: "nao6-ros2" (unique, machine-readable)
|
||||||
|
// - name: "NAO6 Robot (ROS2 Integration)" (display)
|
||||||
|
|
||||||
|
// Lookup order in trial execution:
|
||||||
|
1. Look up by identifier (e.g., "nao6-ros2")
|
||||||
|
2. Fall back to name (e.g., "NAO6 Robot")
|
||||||
|
3. Return null if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branching Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 3 (Comprehension Check)
|
||||||
|
└── wizard_wait_for_response
|
||||||
|
├── "Correct" → nextStepId = step4a.id
|
||||||
|
└── "Incorrect" → nextStepId = step4b.id
|
||||||
|
|
||||||
|
Step 4a/4b (Branch A/B)
|
||||||
|
└── conditions.nextStepId: step5.id → converge
|
||||||
|
|
||||||
|
Step 5 (Story Continues)
|
||||||
|
└── Linear progression to conclusion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make changes
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
bun typecheck
|
||||||
|
bun lint
|
||||||
|
|
||||||
|
# Push schema (if changed)
|
||||||
|
bun db:push
|
||||||
|
|
||||||
|
# Reseed (if data changed)
|
||||||
|
bun db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Build errors | `rm -rf .next && bun build` |
|
||||||
|
| DB issues | `bun db:push --force && bun db:seed` |
|
||||||
|
| Type errors | Check `bun typecheck` output |
|
||||||
|
| WebSocket fails | Verify port 3001 available |
|
||||||
|
|
||||||
|
## External Resources
|
||||||
|
|
||||||
|
- [Thesis (honors-thesis)](https://github.com/soconnor0919/honors-thesis)
|
||||||
|
- [NAO6 Integration](https://github.com/soconnor0919/nao6-hristudio-integration)
|
||||||
|
- [Robot Plugins](https://github.com/soconnor0919/robot-plugins)
|
||||||
|
|
||||||
|
## File Index
|
||||||
|
|
||||||
|
### Primary Documentation
|
||||||
|
- `README.md` - Project overview
|
||||||
|
- `docs/README.md` - This file
|
||||||
|
- `docs/quick-reference.md` - Commands & setup
|
||||||
|
- `docs/nao6-quick-reference.md` - NAO6 commands
|
||||||
|
|
||||||
|
### Tutorials
|
||||||
|
- `docs/tutorials/README.md` - Tutorial overview
|
||||||
|
- `docs/tutorials/01-getting-started.md` - Installation & setup
|
||||||
|
- `docs/tutorials/02-your-first-study.md` - Creating studies
|
||||||
|
- `docs/tutorials/03-designing-experiments.md` - Building protocols
|
||||||
|
- `docs/tutorials/04-running-trials.md` - Executing trials
|
||||||
|
- `docs/tutorials/05-wizard-interface.md` - Trial control
|
||||||
|
- `docs/tutorials/06-robot-integration.md` - Robot setup
|
||||||
|
- `docs/tutorials/07-forms-and-surveys.md` - Forms management
|
||||||
|
- `docs/tutorials/08-data-and-analysis.md` - Data collection
|
||||||
|
- `docs/tutorials/09-simulation-mode.md` - Testing without robot
|
||||||
|
|
||||||
|
### Technical Documentation
|
||||||
|
- `docs/implementation-guide.md` - Full technical implementation
|
||||||
|
- `docs/project-status.md` - Development status
|
||||||
|
- `docs/mock-robot-simulation.md` - Robot simulation
|
||||||
|
|
||||||
|
### Archive (Historical)
|
||||||
|
- `docs/_archive/` - Old documentation (outdated but preserved)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎊 **Project Status: Production Ready**
|
**Last Updated**: March 22, 2026
|
||||||
|
|
||||||
**Current Completion**: Complete ✅
|
|
||||||
**Status**: Ready for immediate deployment
|
|
||||||
**Active Work**: Experiment designer enhancement
|
|
||||||
|
|
||||||
### **Completed Achievements**
|
|
||||||
- ✅ **Complete Backend** - Full API coverage with 11 tRPC routers
|
|
||||||
- ✅ **Professional UI** - Unified experiences with shadcn/ui components
|
|
||||||
- ✅ **Type Safety** - Zero TypeScript errors in production code
|
|
||||||
- ✅ **Database Schema** - 31 tables with comprehensive relationships
|
|
||||||
- ✅ **Authentication** - Role-based access control system
|
|
||||||
- ✅ **Visual Designer** - Repository-based plugin architecture
|
|
||||||
- ✅ **Consolidated Wizard Interface** - 3-panel design with horizontal timeline and unified robot controls
|
|
||||||
- ✅ **Core Blocks System** - 26 blocks across events, wizard, control, observation
|
|
||||||
- ✅ **Plugin Architecture** - Unified system for core blocks and robot actions
|
|
||||||
- ✅ **Development Environment** - Realistic test data and scenarios
|
|
||||||
- ✅ **NAO6 Robot Integration** - Full ROS2 integration with comprehensive control and monitoring
|
|
||||||
- ✅ **Intelligent Control Flow** - Loops with implicit approval, branching, parallel execution
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 **Support and Resources**
|
|
||||||
|
|
||||||
### **Documentation Quality**
|
|
||||||
This documentation is comprehensive and self-contained. For implementation:
|
|
||||||
1. **Start with Quick Reference** for immediate setup
|
|
||||||
2. **Follow Implementation Guide** for step-by-step development
|
|
||||||
3. **Reference Technical Specs** for detailed implementation
|
|
||||||
4. **Check Project Status** for current progress and roadmap
|
|
||||||
|
|
||||||
### **Key Integration Points**
|
|
||||||
- **Authentication**: NextAuth.js v5 with database sessions
|
|
||||||
- **File Storage**: Cloudflare R2 with presigned URLs
|
|
||||||
- **Real-time**: WebSocket with Edge Runtime compatibility
|
|
||||||
- **Robot Control**: ROS2 via rosbridge WebSocket protocol
|
|
||||||
- **Caching**: Vercel KV for serverless-compatible caching
|
|
||||||
- **Monitoring**: Vercel Analytics and structured logging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏆 **Success Criteria**
|
|
||||||
|
|
||||||
The platform is considered production-ready when:
|
|
||||||
- ✅ All features from requirements are implemented
|
|
||||||
- ✅ All API routes are functional and documented
|
|
||||||
- ✅ Database schema matches specification exactly
|
|
||||||
- ✅ Real-time features work reliably
|
|
||||||
- ✅ Security requirements are met
|
|
||||||
- ✅ Performance targets are achieved
|
|
||||||
- ✅ Type safety is complete throughout
|
|
||||||
|
|
||||||
**All success criteria have been met. HRIStudio is ready for production deployment with full NAO6 robot integration support.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 **Documentation Maintenance**
|
|
||||||
|
|
||||||
- **Version**: 2.0.0 (Streamlined)
|
|
||||||
- **Last Updated**: December 2024
|
|
||||||
- **Target Platform**: HRIStudio v1.0
|
|
||||||
- **Structure**: Consolidated for clarity and maintainability
|
|
||||||
|
|
||||||
This documentation represents a complete, streamlined specification for building and deploying HRIStudio. Every technical decision has been carefully considered to create a robust, scalable platform for HRI research.
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# HRIStudio - March 2026 Development Summary
|
||||||
|
|
||||||
|
## What We Did This Session
|
||||||
|
|
||||||
|
### 1. Docker Integration for NAO6 Robot
|
||||||
|
**Files**: `nao6-hristudio-integration/`
|
||||||
|
|
||||||
|
- Created `Dockerfile` with ROS2 Humble + naoqi packages
|
||||||
|
- Created `docker-compose.yaml` with 3 services: `nao_driver`, `ros_bridge`, `ros_api`
|
||||||
|
- Created `scripts/init_robot.sh` - Bash script to wake up robot via SSH when Docker starts
|
||||||
|
- Fixed autonomous life disable issue (previously used Python `naoqi` package which isn't on PyPI)
|
||||||
|
|
||||||
|
**Key insight**: Robot init via SSH + `qicli` calls instead of Python SDK
|
||||||
|
|
||||||
|
### 2. Plugin System Fixes
|
||||||
|
**Files**: `robot-plugins/plugins/nao6-ros2.json`, `src/lib/ros/wizard-ros-service.ts`
|
||||||
|
|
||||||
|
- **Topic fixes**: Removed `/naoqi_driver/` prefix from topics (driver already provides unprefixed topics)
|
||||||
|
- **say_with_emotion**: Fixed with proper NAOqi markup (`\rspd=120\^start(animations/...)`)
|
||||||
|
- **wave_goodbye**: Added animated speech with waving gesture
|
||||||
|
- **play_animation**: Added for predefined NAO animations
|
||||||
|
- **Sensor topics**: Fixed camera, IMU, bumper, sonar, touch topics (removed prefix)
|
||||||
|
|
||||||
|
### 3. Database Schema - Plugin Identifier
|
||||||
|
**Files**: `src/server/db/schema.ts`, `src/server/services/trial-execution.ts`
|
||||||
|
|
||||||
|
- Added `identifier` column to `plugins` table (unique, machine-readable ID like `nao6-ros2`)
|
||||||
|
- `name` now for display only ("NAO6 Robot (ROS2 Integration)")
|
||||||
|
- Updated trial-execution to look up by `identifier` first, then `name` (backwards compat)
|
||||||
|
- Created migration script: `scripts/migrate-add-identifier.ts`
|
||||||
|
|
||||||
|
### 4. Seed Script Improvements
|
||||||
|
**Files**: `scripts/seed-dev.ts`
|
||||||
|
|
||||||
|
- Fixed to use local plugin file (not remote `repo.hristudio.com`)
|
||||||
|
- Added `identifier` field for all plugins (nao6, hristudio-core, hristudio-woz)
|
||||||
|
- Experiment structure:
|
||||||
|
- Step 1: The Hook
|
||||||
|
- Step 2: The Narrative
|
||||||
|
- Step 3: Comprehension Check (conditional with wizard choices)
|
||||||
|
- Step 4a/4b: Branch A/B (with `nextStepId` conditions to converge)
|
||||||
|
- Step 5: Story Continues (convergence point)
|
||||||
|
- Step 6: Conclusion
|
||||||
|
|
||||||
|
### 5. Robot Action Timing Fix
|
||||||
|
**Files**: `src/lib/ros/wizard-ros-service.ts`
|
||||||
|
|
||||||
|
- Speech actions now estimate duration: `1500ms emotion overhead + word_count * 300ms`
|
||||||
|
- Added `say_with_emotion` and `wave_goodbye` as explicit built-in actions
|
||||||
|
- Fixed 100ms timeout that was completing actions before robot finished
|
||||||
|
|
||||||
|
### 6. Branching Logic Fixes (Critical!)
|
||||||
|
**Files**: `src/components/trials/wizard/`
|
||||||
|
|
||||||
|
**Bug 1**: `onClick={onNextStep}` passed event object instead of calling function
|
||||||
|
- Fixed: `onClick={() => onNextStep()}`
|
||||||
|
|
||||||
|
**Bug 2**: `onCompleted()` called after branch choice incremented action count
|
||||||
|
- Fixed: Removed `onCompleted()` call after branch selection
|
||||||
|
|
||||||
|
**Bug 3**: Branch A/B had no `nextStepId` condition, fell through to linear progression
|
||||||
|
- Fixed: Added `conditions.nextStepId: step5.id` to Branch A and B
|
||||||
|
|
||||||
|
**Bug 4**: Robot actions from previous step executed on new step (branching jumped but actions from prior step still triggered)
|
||||||
|
- Root cause: `completedActionsCount` not being reset properly
|
||||||
|
- Fixed: `handleNextStep()` now resets `completedActionsCount(0)` on explicit jump
|
||||||
|
|
||||||
|
### 7. Auth.js to Better Auth Migration (Attempted, Reverted)
|
||||||
|
**Status**: Incomplete - 41+ type errors remain
|
||||||
|
|
||||||
|
The migration requires significant changes to how `session.user.roles` is accessed since Better Auth doesn't include roles in session by default. Would need to fetch roles from database on each request.
|
||||||
|
|
||||||
|
**Recommendation**: Defer until more development time available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
### Plugin Identifier System
|
||||||
|
```
|
||||||
|
plugins table:
|
||||||
|
- id: UUID (primary key)
|
||||||
|
- identifier: varchar (unique, e.g. "nao6-ros2")
|
||||||
|
- name: varchar (display, e.g. "NAO6 Robot (ROS2 Integration)")
|
||||||
|
- robotId: UUID (optional FK to robots)
|
||||||
|
- actionDefinitions: JSONB
|
||||||
|
|
||||||
|
actions table:
|
||||||
|
- type: "plugin.action" (e.g., "nao6-ros2.say_with_emotion")
|
||||||
|
- pluginId: varchar (references plugins.identifier)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branching Flow
|
||||||
|
```
|
||||||
|
Step 3 (Comprehension Check)
|
||||||
|
└── wizard_wait_for_response action
|
||||||
|
├── Click "Correct" → setLastResponse("Correct") → nextStepId=step4a.id
|
||||||
|
└── Click "Incorrect" → setLastResponse("Incorrect") → nextStepId=step4b.id
|
||||||
|
|
||||||
|
Step 4a/4b (Branches)
|
||||||
|
└── conditions.nextStepId: step5.id → jump to Story Continues
|
||||||
|
|
||||||
|
Step 5 (Story Continues)
|
||||||
|
└── Linear progression to Step 6
|
||||||
|
|
||||||
|
Step 6 (Conclusion)
|
||||||
|
└── Trial complete
|
||||||
|
```
|
||||||
|
|
||||||
|
### ROS Topics (NAO6)
|
||||||
|
```
|
||||||
|
/speech - Text-to-speech
|
||||||
|
/cmd_vel - Velocity commands
|
||||||
|
/joint_angles - Joint position commands
|
||||||
|
/camera/front/image_raw
|
||||||
|
/camera/bottom/image_raw
|
||||||
|
/imu/torso
|
||||||
|
/bumper
|
||||||
|
/{hand,head}_touch
|
||||||
|
/sonar/{left,right}
|
||||||
|
/info
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues / Remaining Work
|
||||||
|
|
||||||
|
1. **Auth.js to Better Auth Migration** - Deferred, requires significant refactoring
|
||||||
|
2. **robots.executeSystemAction** - Procedure not found error (fallback works but should investigate)
|
||||||
|
3. **say_with_emotion via WebSocket** - May need proper plugin config to avoid fallback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd nao6-hristudio-integration
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Robot init runs automatically on startup (via `init_robot.sh`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [x] Docker builds and starts
|
||||||
|
- [x] Robot wakes up (autonomous life disabled)
|
||||||
|
- [x] Seed script runs successfully
|
||||||
|
- [x] Trial executes with proper branching
|
||||||
|
- [x] Branch A → Story Continues (not Branch B)
|
||||||
|
- [x] Robot speaks with emotion (say_with_emotion)
|
||||||
|
- [x] Wave gesture works
|
||||||
|
- [ ] Robot movement (walk, turn) tested
|
||||||
|
- [ ] All NAO6 actions verified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: March 21, 2026*
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This guide provides step-by-step technical instructions for implementing HRIStudio using the T3 stack with Next.js, tRPC, Drizzle ORM, NextAuth.js v5, and supporting infrastructure.
|
This guide provides step-by-step technical instructions for implementing HRIStudio using the T3 stack with Next.js, tRPC, Drizzle ORM, Better Auth, and supporting infrastructure.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@@ -25,7 +25,14 @@ This guide provides step-by-step technical instructions for implementing HRIStud
|
|||||||
### 1. Initialize Project
|
### 1. Initialize Project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create new Next.js project with T3 stack
|
# Clone repository (includes robot-plugins as submodule)
|
||||||
|
git clone https://github.com/soconnor0919/hristudio.git
|
||||||
|
cd hristudio
|
||||||
|
|
||||||
|
# Initialize submodules
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Or create from scratch with T3 stack:
|
||||||
bunx create-t3-app@latest hristudio \
|
bunx create-t3-app@latest hristudio \
|
||||||
--nextjs \
|
--nextjs \
|
||||||
--tailwind \
|
--tailwind \
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# HRIStudio Mock Robot Simulation
|
||||||
|
|
||||||
|
This directory contains a mock robot server for simulating NAO6 robot connections without a physical robot.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Standalone Mock Server (Recommended for testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts/mock-robot
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the mock robot WebSocket server on `ws://localhost:9090`.
|
||||||
|
|
||||||
|
### Option 2: Docker Compose Mock Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd nao6-hristudio-integration
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.mock.yml --profile mock up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Client-Side Simulation (No server needed)
|
||||||
|
|
||||||
|
Enable simulation mode in the wizard interface:
|
||||||
|
- Set `NEXT_PUBLIC_SIMULATION_MODE=true` in your `.env` file
|
||||||
|
- Or use the simulation toggle in the UI
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HRIStudio Platform │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Wizard Interface (Browser) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ wizard-ros-service.ts │ │
|
||||||
|
│ │ ├── simulationMode: true → Simulates locally │ │
|
||||||
|
│ │ └── simulationMode: false → Connects to server │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────┴────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Real Mode Simulation Mode │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ Mock Robot │ │ Local JS │ │
|
||||||
|
│ │ WebSocket │ │ Simulation │ │
|
||||||
|
│ │ Server │ │ (No server) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mock Robot Server Protocol
|
||||||
|
|
||||||
|
The mock server implements the rosbridge WebSocket protocol:
|
||||||
|
|
||||||
|
### Supported Operations
|
||||||
|
|
||||||
|
| Operation | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `subscribe` | Subscribe to a topic |
|
||||||
|
| `unsubscribe` | Unsubscribe from a topic |
|
||||||
|
| `publish` | Publish a message to a topic |
|
||||||
|
| `call_service` | Call a ROS service |
|
||||||
|
| `advertise` | Advertise a topic |
|
||||||
|
| `unadvertise` | Stop advertising a topic |
|
||||||
|
|
||||||
|
### Simulated Topics
|
||||||
|
|
||||||
|
| Topic | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `/joint_states` | `sensor_msgs/JointState` | Joint positions (26 joints) |
|
||||||
|
| `/naoqi_driver/battery` | `naoqi_bridge_msgs/Battery` | Battery status (85%) |
|
||||||
|
| `/bumper` | `naoqi_bridge_msgs/Bumper` | Bumper contact sensors |
|
||||||
|
| `/hand_touch` | `naoqi_bridge_msgs/HandTouch` | Hand touch sensors |
|
||||||
|
| `/head_touch` | `naoqi_bridge_msgs/HeadTouch` | Head touch sensors |
|
||||||
|
| `/sonar/left` | `sensor_msgs/Range` | Left sonar distance |
|
||||||
|
| `/sonar/right` | `sensor_msgs/Range` | Right sonar distance |
|
||||||
|
|
||||||
|
### Simulated Services
|
||||||
|
|
||||||
|
| Service | Response |
|
||||||
|
|---------|----------|
|
||||||
|
| `/naoqi_driver/get_robot_info` | `{ robotName: "MOCK-NAO6", robotVersion: "6.0" }` |
|
||||||
|
| `/naoqi_driver/get_joint_names` | List of 26 joint names |
|
||||||
|
| `/naoqi_driver/get_position` | Current position `{ x, y, theta }` |
|
||||||
|
| `/naoqi_driver/is_waking_up` | `{ is_waking_up: false }` |
|
||||||
|
|
||||||
|
### Supported Actions
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `say_text` | `text` | Speak text |
|
||||||
|
| `walk_forward` | `speed` | Walk forward |
|
||||||
|
| `walk_backward` | `speed` | Walk backward |
|
||||||
|
| `turn_left` | `speed` | Turn left |
|
||||||
|
| `turn_right` | `speed` | Turn right |
|
||||||
|
| `stop` | - | Stop all movement |
|
||||||
|
| `move_head` | `yaw`, `pitch`, `speed` | Move head |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mock Robot Server (scripts/mock-robot)
|
||||||
|
MOCK_ROBOT_PORT=9090 # WebSocket port
|
||||||
|
MOCK_PUBLISH_INTERVAL=100 # Sensor update interval (ms)
|
||||||
|
|
||||||
|
# HRIStudio Client
|
||||||
|
NEXT_PUBLIC_SIMULATION_MODE=true # Enable client-side simulation
|
||||||
|
NEXT_PUBLIC_ROS_BRIDGE_URL=ws://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### 1. Start Mock Server
|
||||||
|
```bash
|
||||||
|
cd scripts/mock-robot
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start HRIStudio
|
||||||
|
```bash
|
||||||
|
cd hristudio
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Connection
|
||||||
|
Visit `http://localhost:3000/nao-test` and click "Connect". You should see:
|
||||||
|
- Connection status: `connected`
|
||||||
|
- Battery: ~85%
|
||||||
|
- Joint states updating
|
||||||
|
- Log messages showing subscriptions
|
||||||
|
|
||||||
|
### 4. Test Actions
|
||||||
|
Use the wizard interface to test:
|
||||||
|
- Speech actions
|
||||||
|
- Movement actions
|
||||||
|
- Head control
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Connection timeout" error
|
||||||
|
- Ensure mock server is running: `curl ws://localhost:9090`
|
||||||
|
- Check port is correct (default 9090)
|
||||||
|
|
||||||
|
### "Not connected to ROS bridge" error
|
||||||
|
- Enable simulation mode: `NEXT_PUBLIC_SIMULATION_MODE=true`
|
||||||
|
- Or connect to mock server first
|
||||||
|
|
||||||
|
### Actions not executing
|
||||||
|
- Check connection status in wizard interface
|
||||||
|
- Enable simulation mode if using client-side simulation
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `scripts/mock-robot/src/server.ts` | TypeScript mock server |
|
||||||
|
| `scripts/mock-robot/server.js` | JavaScript mock server (for Docker) |
|
||||||
|
| `src/lib/ros/wizard-ros-service.ts` | Client with simulation mode |
|
||||||
|
| `src/hooks/useWizardRos.ts` | React hook with simulation support |
|
||||||
|
| `docker-compose.mock.yml` | Docker mock service |
|
||||||
|
| `robot-plugins/plugins/nao6-mock.json` | Mock NAO6 plugin |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New Simulated Actions
|
||||||
|
|
||||||
|
1. Edit `scripts/mock-robot/src/server.ts`
|
||||||
|
2. Add handler in `handlePublish()` or `handleServiceCall()`
|
||||||
|
3. Update `nao6-mock.json` plugin with new action definition
|
||||||
|
|
||||||
|
### Adding New Simulated Sensors
|
||||||
|
|
||||||
|
1. Edit `scripts/mock-robot/src/server.ts`
|
||||||
|
2. Add topic publishing in `publishRobotState()`
|
||||||
|
3. Update subscriber topics in `WizardRosService.subscribeToRobotTopics()`
|
||||||
+131
-110
@@ -2,88 +2,112 @@
|
|||||||
|
|
||||||
Essential commands for using NAO6 robots with HRIStudio.
|
Essential commands for using NAO6 robots with HRIStudio.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start (Docker)
|
||||||
|
|
||||||
### 1. Start NAO Integration
|
### 1. Start Docker Integration
|
||||||
```bash
|
```bash
|
||||||
cd ~/naoqi_ros2_ws
|
cd ~/Documents/Projects/nao6-hristudio-integration
|
||||||
source install/setup.bash
|
docker compose up -d
|
||||||
ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Wake Robot
|
The robot will automatically wake up and autonomous life will be disabled on startup.
|
||||||
Press chest button for 3 seconds, or use:
|
|
||||||
```bash
|
|
||||||
# Via SSH (institution-specific password)
|
|
||||||
ssh nao@nao.local
|
|
||||||
# Then run wake-up command (see integration repo docs)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Start HRIStudio
|
### 2. Start HRIStudio
|
||||||
```bash
|
```bash
|
||||||
cd ~/Documents/Projects/hristudio
|
cd ~/Documents/Projects/hristudio
|
||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Test Connection
|
### 3. Verify Connection
|
||||||
- Open: `http://localhost:3000/nao-test`
|
- Open: `http://localhost:3000`
|
||||||
- Click "Connect"
|
- Navigate to trial wizard
|
||||||
- Test robot commands
|
- WebSocket should connect automatically
|
||||||
|
|
||||||
## Essential Commands
|
## Docker Services
|
||||||
|
|
||||||
### Test Connectivity
|
| Service | Port | Description |
|
||||||
```bash
|
|---------|------|-------------|
|
||||||
ping nao.local # Test network
|
| nao_driver | - | NAOqi driver + robot init |
|
||||||
ros2 topic list | grep naoqi # Check ROS topics
|
| ros_bridge | 9090 | WebSocket bridge |
|
||||||
```
|
| ros_api | - | ROS API services |
|
||||||
|
|
||||||
### Manual Control
|
**Auto-initialization**: On Docker startup, `init_robot.sh` runs automatically via SSH to:
|
||||||
```bash
|
- Wake up the robot (`ALMotion.wakeUp`)
|
||||||
# Speech
|
- Disable autonomous life (`ALAutonomousLife.setState disabled`)
|
||||||
ros2 topic pub --once /speech std_msgs/String "data: 'Hello world'"
|
- Ensure robot is ready for commands
|
||||||
|
|
||||||
# Movement (robot must be awake!)
|
|
||||||
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.1}}'
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitor Status
|
|
||||||
```bash
|
|
||||||
ros2 topic echo /naoqi_driver/battery # Battery level
|
|
||||||
ros2 topic echo /naoqi_driver/joint_states # Joint positions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Robot not moving:** Press chest button for 3 seconds to wake up
|
|
||||||
|
|
||||||
**WebSocket fails:** Check rosbridge is running on port 9090
|
|
||||||
```bash
|
|
||||||
ss -an | grep 9090
|
|
||||||
```
|
|
||||||
|
|
||||||
**Connection lost:** Restart rosbridge
|
|
||||||
```bash
|
|
||||||
pkill -f rosbridge
|
|
||||||
ros2 run rosbridge_server rosbridge_websocket
|
|
||||||
```
|
|
||||||
|
|
||||||
## ROS Topics
|
## ROS Topics
|
||||||
|
|
||||||
**Commands (Input):**
|
**Commands (Publish to these):**
|
||||||
- `/speech` - Text-to-speech
|
```
|
||||||
- `/cmd_vel` - Movement
|
/speech - Text-to-speech
|
||||||
- `/joint_angles` - Joint control
|
/cmd_vel - Velocity commands (movement)
|
||||||
|
/joint_angles - Joint position commands
|
||||||
|
```
|
||||||
|
|
||||||
**Sensors (Output):**
|
**Sensors (Subscribe to these):**
|
||||||
- `/naoqi_driver/joint_states` - Joint data
|
```
|
||||||
- `/naoqi_driver/battery` - Battery level
|
/camera/front/image_raw - Front camera
|
||||||
- `/naoqi_driver/bumper` - Foot sensors
|
/camera/bottom/image_raw - Bottom camera
|
||||||
- `/naoqi_driver/sonar/*` - Distance sensors
|
/joint_states - Joint positions
|
||||||
- `/naoqi_driver/camera/*` - Camera feeds
|
/imu/torso - IMU data
|
||||||
|
/bumper - Foot bumpers
|
||||||
|
/{hand,head}_touch - Touch sensors
|
||||||
|
/sonar/{left,right} - Ultrasonic sensors
|
||||||
|
/info - Robot info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Robot Actions (HRIStudio)
|
||||||
|
|
||||||
|
When actions are triggered via the wizard interface, they publish to these topics:
|
||||||
|
|
||||||
|
| Action | Topic | Message Type |
|
||||||
|
|--------|-------|--------------|
|
||||||
|
| say | `/speech` | `std_msgs/String` |
|
||||||
|
| say_with_emotion | `/speech` | `std_msgs/String` (with NAOqi markup) |
|
||||||
|
| wave_goodbye | `/speech` | `std_msgs/String` + gesture |
|
||||||
|
| walk | `/cmd_vel` | `geometry_msgs/Twist` |
|
||||||
|
| turn | `/cmd_vel` | `geometry_msgs/Twist` |
|
||||||
|
| move_to_posture | `/service/robot_pose` | `naoqi_bridge_msgs/SetRobotPose` |
|
||||||
|
| play_animation | `/animation` | `std_msgs/String` |
|
||||||
|
| set_eye_leds | `/leds/eyes` | `std_msgs/ColorRGBA` |
|
||||||
|
|
||||||
|
## Manual Control
|
||||||
|
|
||||||
|
### Test Connectivity
|
||||||
|
```bash
|
||||||
|
# Network
|
||||||
|
ping 10.0.0.42
|
||||||
|
|
||||||
|
# ROS topics (inside Docker)
|
||||||
|
docker exec -it nao6-hristudio-integration-nao_driver-1 ros2 topic list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Commands (inside Docker)
|
||||||
|
```bash
|
||||||
|
# Speech
|
||||||
|
docker exec -it nao6-hristudio-integration-nao_driver-1 \
|
||||||
|
ros2 topic pub --once /speech std_msgs/String "{data: 'Hello'}"
|
||||||
|
|
||||||
|
# Movement (robot must be awake!)
|
||||||
|
docker exec -it nao6-hristudio-integration-nao_driver-1 \
|
||||||
|
ros2 topic pub --once /cmd_vel geometry_msgs/Twist "{linear: {x: 0.1, y: 0.0, z: 0.0}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Robot Control via SSH
|
||||||
|
```bash
|
||||||
|
# SSH to robot
|
||||||
|
sshpass -p "nao" ssh nao@10.0.0.42
|
||||||
|
|
||||||
|
# Wake up
|
||||||
|
qicli call ALMotion.wakeUp
|
||||||
|
|
||||||
|
# Disable autonomous life
|
||||||
|
qicli call ALAutonomousLife.setState disabled
|
||||||
|
|
||||||
|
# Go to stand
|
||||||
|
qicli call ALRobotPosture.goToPosture Stand 0.5
|
||||||
|
```
|
||||||
|
|
||||||
## WebSocket
|
## WebSocket
|
||||||
|
|
||||||
@@ -99,79 +123,76 @@ ros2 run rosbridge_server rosbridge_websocket
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## More Information
|
## Troubleshooting
|
||||||
|
|
||||||
See **[nao6-hristudio-integration](../../nao6-hristudio-integration/)** repository for:
|
**Robot not moving:**
|
||||||
- Complete installation guide
|
- Check robot is awake: `qicli call ALMotion.isWakeUp` → returns `true`
|
||||||
- Detailed usage instructions
|
- If not: `qicli call ALMotion.wakeUp`
|
||||||
- Full troubleshooting guide
|
|
||||||
- Plugin definitions
|
|
||||||
- Launch file configurations
|
|
||||||
|
|
||||||
## Common Use Cases
|
**WebSocket fails:**
|
||||||
|
|
||||||
### Make Robot Speak
|
|
||||||
```bash
|
```bash
|
||||||
ros2 topic pub --once /speech std_msgs/String "data: 'Welcome to the experiment'"
|
# Check rosbridge is running
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs ros_bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
### Walk Forward 3 Steps
|
**Connection issues:**
|
||||||
```bash
|
```bash
|
||||||
ros2 topic pub --times 3 /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'
|
# Restart Docker
|
||||||
|
docker compose down && docker compose up -d
|
||||||
|
|
||||||
|
# Check robot IP in .env
|
||||||
|
cat nao6-hristudio-integration/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Turn Head Left
|
## Environment Variables
|
||||||
```bash
|
|
||||||
ros2 topic pub --once /joint_angles naoqi_bridge_msgs/msg/JointAnglesWithSpeed '{joint_names: ["HeadYaw"], joint_angles: [0.8], speed: 0.2}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Emergency Stop
|
Create `nao6-hristudio-integration/.env`:
|
||||||
```bash
|
```
|
||||||
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'
|
NAO_IP=10.0.0.42
|
||||||
|
NAO_USERNAME=nao
|
||||||
|
NAO_PASSWORD=nao
|
||||||
|
BRIDGE_PORT=9090
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚨 Safety Notes
|
## 🚨 Safety Notes
|
||||||
|
|
||||||
- **Always wake up robot before movement commands**
|
- **Always verify robot is awake before movement commands**
|
||||||
- **Keep emergency stop accessible**
|
- **Keep emergency stop accessible** (`qicli call ALMotion.rest()`)
|
||||||
- **Start with small movements (0.05 m/s)**
|
- **Start with small movements (0.05 m/s)**
|
||||||
- **Monitor battery level during experiments**
|
- **Monitor battery level**
|
||||||
- **Ensure clear space around robot**
|
- **Ensure clear space around robot**
|
||||||
|
|
||||||
## 📝 Credentials
|
## Credentials
|
||||||
|
|
||||||
**Default NAO Login:**
|
**NAO Robot:**
|
||||||
|
- IP: `10.0.0.42` (configurable)
|
||||||
- Username: `nao`
|
- Username: `nao`
|
||||||
- Password: `robolab` (institution-specific)
|
- Password: `nao`
|
||||||
|
|
||||||
**HRIStudio Login:**
|
**HRIStudio:**
|
||||||
- Email: `sean@soconnor.dev`
|
- Email: `sean@soconnor.dev`
|
||||||
- Password: `password123`
|
- Password: `password123`
|
||||||
|
|
||||||
## 🔄 Complete Restart Procedure
|
## Complete Restart
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Kill all processes
|
# 1. Restart Docker integration
|
||||||
sudo fuser -k 9090/tcp
|
cd ~/Documents/Projects/nao6-hristudio-integration
|
||||||
pkill -f "rosbridge\|naoqi\|ros2"
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
# 2. Restart database
|
# 2. Verify robot is awake (check logs)
|
||||||
sudo docker compose down && sudo docker compose up -d
|
docker compose logs nao_driver | grep -i "wake\|autonomous"
|
||||||
|
|
||||||
# 3. Start ROS integration
|
# 3. Start HRIStudio
|
||||||
cd ~/naoqi_ros2_ws && source install/setup.bash
|
cd ~/Documents/Projects/hristudio
|
||||||
ros2 launch install/nao_launch/share/nao_launch/launch/nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab
|
bun dev
|
||||||
|
|
||||||
# 4. Wake up robot (in another terminal)
|
|
||||||
sshpass -p "robolab" ssh nao@nao.local "python2 -c \"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; naoqi.ALProxy('ALMotion', '127.0.0.1', 9559).wakeUp()\""
|
|
||||||
|
|
||||||
# 5. Start HRIStudio (in another terminal)
|
|
||||||
cd /home/robolab/Documents/Projects/hristudio && bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**📖 For detailed setup instructions, see:** [NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)
|
|
||||||
|
|
||||||
**✅ Integration Status:** Production Ready
|
**✅ Integration Status:** Production Ready
|
||||||
**🤖 Tested With:** NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble
|
**🤖 Tested With:** NAO V6 / ROS2 Humble / Docker
|
||||||
|
|||||||
+135
-348
@@ -1,402 +1,189 @@
|
|||||||
# HRIStudio Project Status
|
# HRIStudio Project Status
|
||||||
|
|
||||||
## 🎯 **Current Status: Production Ready**
|
## Current Status: Active Development
|
||||||
|
|
||||||
**Project Version**: 1.0.0
|
**Project Version**: 1.0.0
|
||||||
**Last Updated**: December 2024
|
**Last Updated**: March 2026
|
||||||
**Overall Completion**: Complete ✅
|
**Overall Completion**: 98%
|
||||||
**Status**: Ready for Production Deployment
|
**Status**: Thesis research phase
|
||||||
|
|
||||||
### **🎉 Recent Major Achievement: Wizard Interface Multi-View Implementation Complete**
|
|
||||||
Successfully implemented role-based trial execution interface with Wizard, Observer, and Participant views. Fixed layout issues and eliminated route duplication for clean, production-ready trial execution system.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 **Executive Summary**
|
## Executive Summary
|
||||||
|
|
||||||
HRIStudio has successfully completed all major development milestones and achieved production readiness. The platform provides a comprehensive, type-safe, and user-friendly environment for conducting Wizard of Oz studies in Human-Robot Interaction research.
|
HRIStudio is a complete platform for Wizard-of-Oz HRI research. Key milestones achieved:
|
||||||
|
|
||||||
### **Key Achievements**
|
### Recent Updates (March 2026)
|
||||||
- ✅ **Complete Backend Infrastructure** - Full API with 12 tRPC routers
|
- ✅ WebSocket real-time trial updates implemented
|
||||||
- ✅ **Complete Frontend Implementation** - Professional UI with unified experiences
|
- ✅ Better Auth migration complete (replaced NextAuth.js)
|
||||||
- ✅ **Full Type Safety** - Zero TypeScript errors in production code
|
- ✅ Docker integration for NAO6 (3 services: nao_driver, ros_bridge, ros_api)
|
||||||
- ✅ **Complete Authentication** - Role-based access control system
|
- ✅ Conditional branching with wizard choices and convergence
|
||||||
- ✅ **Visual Experiment Designer** - Repository-based plugin architecture
|
- ✅ 14 NAO6 robot actions (speech, movement, gestures, sensors, LEDs, animations)
|
||||||
- ✅ **Core Blocks System** - 26 blocks across 4 categories (events, wizard, control, observation)
|
- ✅ Plugin identifier system for clean plugin lookup
|
||||||
- ✅ **Production Database** - 31 tables with comprehensive relationships
|
- ✅ Seed script with branching experiment structure
|
||||||
- ✅ **Development Environment** - Realistic seed data and testing scenarios
|
|
||||||
- ✅ **Trial System Overhaul** - Unified EntityView patterns with real-time execution
|
### Key Achievements
|
||||||
- ✅ **WebSocket Integration** - Real-time updates with polling fallback
|
- ✅ Complete backend with 12 tRPC routers
|
||||||
- ✅ **Route Consolidation** - Study-scoped architecture with eliminated duplicate components
|
- ✅ Professional UI with unified experiences
|
||||||
- ✅ **Multi-View Trial Interface** - Role-based Wizard, Observer, and Participant views for thesis research
|
- ✅ Full TypeScript coverage (strict mode)
|
||||||
- ✅ **Dashboard Resolution** - Fixed routing issues and implemented proper layout structure
|
- ✅ Role-based access control (4 roles)
|
||||||
|
- ✅ 31 database tables with relationships
|
||||||
|
- ✅ Experiment designer with 26+ core blocks
|
||||||
|
- ✅ Real-time trial execution wizard interface
|
||||||
|
- ✅ NAO6 robot integration via ROS2 Humble
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗️ **Implementation Status by Feature**
|
## Architecture
|
||||||
|
|
||||||
### **Core Infrastructure** ✅ **Complete**
|
### Three-Layer Architecture
|
||||||
|
|
||||||
#### **Plugin Architecture** ✅ **Complete**
|
```
|
||||||
- **Core Blocks System**: Repository-based architecture with 26 essential blocks
|
┌─────────────────────────────────────────────────────┐
|
||||||
- **Robot Plugin Integration**: Unified plugin loading for robot actions
|
│ User Interface Layer │
|
||||||
- **Repository Management**: Admin tools for plugin repositories and trust levels
|
│ ├── Experiment Designer (visual programming) │
|
||||||
- **Plugin Store**: Study-scoped plugin installation and configuration
|
│ ├── Wizard Interface (trial execution) │
|
||||||
- **Block Categories**: Events, wizard actions, control flow, observation blocks
|
│ ├── Observer View (live monitoring) │
|
||||||
- **Type Safety**: Full TypeScript support for all plugin definitions
|
│ └── Participant View (thesis study) │
|
||||||
- **Documentation**: Complete guides for core blocks and robot plugins
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ Data Management Layer │
|
||||||
|
│ ├── PostgreSQL + Drizzle ORM │
|
||||||
**Database Schema**
|
│ ├── tRPC API (12 routers) │
|
||||||
- ✅ 31 tables covering all research workflows
|
│ └── Better Auth (role-based auth) │
|
||||||
- ✅ Complete relationships with foreign keys and indexes
|
├─────────────────────────────────────────────────────┤
|
||||||
- ✅ Audit logging and soft deletes implemented
|
│ Robot Integration Layer │
|
||||||
- ✅ Performance optimizations with strategic indexing
|
│ ├── Plugin system (robot-agnostic) │
|
||||||
- ✅ JSONB support for flexible metadata storage
|
│ ├── ROS2 via rosbridge WebSocket │
|
||||||
|
│ └── Docker deployment (nao_driver, ros_bridge) │
|
||||||
**API Infrastructure**
|
└─────────────────────────────────────────────────────┘
|
||||||
- ✅ 12 tRPC routers providing comprehensive functionality
|
|
||||||
- ✅ Type-safe with Zod validation throughout
|
|
||||||
- ✅ Role-based authorization on all endpoints
|
|
||||||
- ✅ Comprehensive error handling and validation
|
|
||||||
- ✅ Optimistic updates and real-time subscriptions ready
|
|
||||||
|
|
||||||
**Authentication & Authorization**
|
|
||||||
- ✅ NextAuth.js v5 with database sessions
|
|
||||||
- ✅ 4 system roles: Administrator, Researcher, Wizard, Observer
|
|
||||||
- ✅ Role-based middleware protecting all routes
|
|
||||||
- ✅ User profile management with password changes
|
|
||||||
- ✅ Admin dashboard for user and role management
|
|
||||||
|
|
||||||
### **User Interface** ✅ **Complete**
|
|
||||||
|
|
||||||
**Core UI Framework**
|
|
||||||
- ✅ shadcn/ui integration with custom theme
|
|
||||||
- ✅ Responsive design across all screen sizes
|
|
||||||
- ✅ Accessibility compliance (WCAG 2.1 AA)
|
|
||||||
- ✅ Loading states and comprehensive error boundaries
|
|
||||||
- ✅ Form validation with react-hook-form + Zod
|
|
||||||
|
|
||||||
**Major Interface Components**
|
|
||||||
- ✅ Dashboard with role-based navigation
|
|
||||||
- ✅ Authentication pages (signin/signup/profile)
|
|
||||||
- ✅ Study management with team collaboration
|
|
||||||
- ✅ Visual experiment designer with drag-and-drop
|
|
||||||
- ✅ Participant management and consent tracking
|
|
||||||
- ✅ Trial execution and monitoring interfaces
|
|
||||||
- ✅ Data tables with advanced filtering and export
|
|
||||||
|
|
||||||
### **Key Feature Implementations** ✅ **Complete**
|
|
||||||
|
|
||||||
**Visual Experiment Designer**
|
|
||||||
- ✅ Professional drag-and-drop interface
|
|
||||||
- ✅ 4 step types: Wizard Action, Robot Action, Parallel Steps, Conditional Branch
|
|
||||||
- ✅ Real-time saving with conflict resolution
|
|
||||||
- ✅ Parameter configuration framework
|
|
||||||
- ✅ Professional UI with loading states and error handling
|
|
||||||
|
|
||||||
**Unified Editor Experiences**
|
|
||||||
- ✅ Significant reduction in form-related code duplication
|
|
||||||
- ✅ Consistent EntityForm component across all entities
|
|
||||||
- ✅ Standardized validation and error handling
|
|
||||||
- ✅ Context-aware creation for nested workflows
|
|
||||||
- ✅ Progressive workflow guidance with next steps
|
|
||||||
|
|
||||||
**DataTable System**
|
|
||||||
- ✅ Unified DataTable component with enterprise features
|
|
||||||
- ✅ Server-side filtering, sorting, and pagination
|
|
||||||
- ✅ Column visibility controls and export functionality
|
|
||||||
- ✅ Responsive design with proper overflow handling
|
|
||||||
- ✅ Consistent experience across all entity lists
|
|
||||||
|
|
||||||
**Robot Integration Framework**
|
|
||||||
- ✅ Plugin system for extensible robot support
|
|
||||||
- ✅ RESTful API and ROS2 integration via WebSocket
|
|
||||||
- ✅ Type-safe action definitions and parameter schemas
|
|
||||||
- ✅ Connection testing and health monitoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎊 **Major Development Achievements**
|
|
||||||
|
|
||||||
### **Code Quality Excellence**
|
|
||||||
- **Type Safety**: Complete TypeScript coverage with strict mode
|
|
||||||
- **Code Reduction**: Significant decrease in form-related duplication
|
|
||||||
- **Performance**: Optimized database queries and client bundles
|
|
||||||
- **Security**: Comprehensive role-based access control
|
|
||||||
- **Testing**: Unit, integration, and E2E testing frameworks ready
|
|
||||||
|
|
||||||
### **User Experience Innovation**
|
|
||||||
- **Consistent Interface**: Unified patterns across all features
|
|
||||||
- **Professional Design**: Enterprise-grade UI components
|
|
||||||
- **Accessibility**: WCAG 2.1 AA compliance throughout
|
|
||||||
- **Responsive**: Mobile-friendly across all screen sizes
|
|
||||||
- **Intuitive Workflows**: Clear progression from study to trial execution
|
|
||||||
|
|
||||||
### **Development Infrastructure**
|
|
||||||
- **Comprehensive Seed Data**: 3 studies, 8 participants, 5 experiments, 7 trials
|
|
||||||
- **Realistic Test Scenarios**: Elementary education, elderly care, navigation trust
|
|
||||||
- **Development Database**: Instant setup with `bun db:seed`
|
|
||||||
- **Documentation**: Complete technical and user documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ **Trial System Overhaul - COMPLETE**
|
|
||||||
|
|
||||||
### **Visual Design Standardization**
|
|
||||||
- **EntityView Integration**: All trial pages now use unified EntityView patterns
|
|
||||||
- **Consistent Headers**: Standard EntityViewHeader with icons, status badges, and actions
|
|
||||||
- **Sidebar Layout**: Professional EntityViewSidebar with organized information panels
|
|
||||||
- **Breadcrumb Integration**: Proper navigation context throughout trial workflow
|
|
||||||
|
|
||||||
### **Wizard Interface Redesign**
|
|
||||||
- **Panel-Based Architecture**: Adopted PanelsContainer system from experiment designer
|
|
||||||
- **Three-Panel Layout**: Left (controls), Center (execution), Right (monitoring)
|
|
||||||
- **Breadcrumb Navigation**: Proper navigation hierarchy matching platform standards
|
|
||||||
- **Component Reuse**: 90% code sharing with experiment designer patterns
|
|
||||||
- **Real-time Status**: Clean connection indicators without UI flashing
|
|
||||||
- **Resizable Panels**: Drag-to-resize functionality with overflow containment
|
|
||||||
|
|
||||||
### **Component Unification**
|
|
||||||
- **ActionControls**: Updated to match unified component interface patterns
|
|
||||||
- **ParticipantInfo**: Streamlined for sidebar display with essential information
|
|
||||||
- **EventsLogSidebar**: New component for real-time event monitoring
|
|
||||||
- **RobotStatus**: Integrated mock robot simulation for development testing
|
|
||||||
|
|
||||||
### **Technical Improvements**
|
|
||||||
- **WebSocket Stability**: Enhanced connection handling with polling fallback
|
|
||||||
- **Error Management**: Improved development mode error handling without UI flashing
|
|
||||||
- **Type Safety**: Complete TypeScript compatibility across all trial components
|
|
||||||
- **State Management**: Simplified trial state updates and real-time synchronization
|
|
||||||
|
|
||||||
### **Production Capabilities**
|
|
||||||
- **Mock Robot Integration**: Complete simulation for development and testing
|
|
||||||
- **Real-time Execution**: WebSocket-based live updates with automatic fallback
|
|
||||||
- **Data Capture**: Comprehensive event logging and trial progression tracking
|
|
||||||
- **Role-based Access**: Proper wizard, researcher, and observer role enforcement
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ **Experiment Designer Redesign - COMPLETE**
|
|
||||||
|
|
||||||
### **Development Status**
|
|
||||||
**Priority**: High
|
|
||||||
**Target**: Enhanced visual programming capabilities
|
|
||||||
**Status**: ✅ Complete
|
|
||||||
|
|
||||||
**Completed Enhancements**:
|
|
||||||
- ✅ Enhanced visual programming interface with modern iconography
|
|
||||||
- ✅ Advanced step configuration with parameter editing
|
|
||||||
- ✅ Real-time validation with comprehensive error detection
|
|
||||||
- ✅ Deterministic hashing for reproducibility
|
|
||||||
- ✅ Plugin drift detection and signature tracking
|
|
||||||
- ✅ Modern drag-and-drop interface with @dnd-kit
|
|
||||||
- ✅ Type-safe state management with Zustand
|
|
||||||
- ✅ Export/import functionality with integrity verification
|
|
||||||
|
|
||||||
### **Technical Implementation**
|
|
||||||
```typescript
|
|
||||||
// Completed step configuration interface
|
|
||||||
interface StepConfiguration {
|
|
||||||
type: 'wizard_action' | 'robot_action' | 'parallel' | 'conditional' | 'timer' | 'loop';
|
|
||||||
parameters: StepParameters;
|
|
||||||
validation: ValidationRules;
|
|
||||||
dependencies: StepDependency[];
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Key Fixes Applied**
|
### Plugin Identifier System
|
||||||
- ✅ **Step Addition Bug**: Fixed JSX structure and type import issues
|
|
||||||
- ✅ **TypeScript Compilation**: All type errors resolved
|
```
|
||||||
- ✅ **Drag and Drop**: Fully functional with DndContext properly configured
|
plugins table:
|
||||||
- ✅ **State Management**: Zustand store working correctly with all actions
|
- id: UUID (primary key)
|
||||||
- ✅ **UI Layout**: Three-panel layout with Action Library, Step Flow, and Properties
|
- identifier: varchar (unique, e.g. "nao6-ros2")
|
||||||
|
- name: varchar (display, e.g. "NAO6 Robot (ROS2)")
|
||||||
|
- robotId: UUID (optional FK to robots)
|
||||||
|
- actionDefinitions: JSONB
|
||||||
|
|
||||||
|
actions table:
|
||||||
|
- type: "plugin.action" (e.g., "nao6-ros2.say_with_emotion")
|
||||||
|
- pluginId: varchar (references plugins.identifier)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 **Sprint Planning & Progress**
|
## Branching Flow
|
||||||
|
|
||||||
### **Current Sprint (February 2025)**
|
Experiment steps support conditional branching with wizard choices:
|
||||||
**Theme**: Production Deployment Preparation
|
|
||||||
|
|
||||||
**Goals**:
|
```
|
||||||
1. ✅ Complete experiment designer redesign
|
Step 3 (Comprehension Check)
|
||||||
2. ✅ Fix step addition functionality
|
└── wizard_wait_for_response
|
||||||
3. ✅ Resolve TypeScript compilation issues
|
├── Click "Correct" → setLastResponse("Correct") → nextStepId=step4a
|
||||||
4. ⏳ Final code quality improvements
|
└── Click "Incorrect" → setLastResponse("Incorrect") → nextStepId=step4b
|
||||||
|
|
||||||
**Sprint Metrics**:
|
Step 4a/4b (Branches)
|
||||||
- **Story Points**: 34 total
|
└── conditions.nextStepId: step5.id → convergence point
|
||||||
- **Completed**: 30 points
|
|
||||||
- **In Progress**: 4 points
|
|
||||||
- **Planned**: 0 points
|
|
||||||
|
|
||||||
### **Development Velocity**
|
Step 5 (Story Continues)
|
||||||
- **Sprint 1**: 28 story points completed
|
└── Linear progression to Step 6
|
||||||
- **Sprint 2**: 32 story points completed
|
```
|
||||||
- **Sprint 3**: 34 story points completed
|
|
||||||
- **Sprint 4**: 30 story points completed (current)
|
|
||||||
- **Average**: 31.0 story points per sprint
|
|
||||||
|
|
||||||
### **Quality Metrics**
|
|
||||||
- **Critical Bugs**: Zero (all step addition issues resolved)
|
|
||||||
- **Code Coverage**: High coverage maintained across all components
|
|
||||||
- **Build Time**: Consistently under 3 minutes
|
|
||||||
- **TypeScript Errors**: Zero in production code
|
|
||||||
- **Designer Functionality**: 100% operational
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 **Success Criteria Validation**
|
## NAO6 Robot Actions (14 total)
|
||||||
|
|
||||||
### **Technical Requirements** ✅ **Met**
|
| Category | Actions |
|
||||||
- ✅ End-to-end type safety throughout platform
|
|----------|---------|
|
||||||
- ✅ Role-based access control with 4 distinct roles
|
| Speech | say, say_with_emotion, wave_goodbye |
|
||||||
- ✅ Comprehensive API covering all research workflows
|
| Movement | walk, turn, move_to_posture |
|
||||||
- ✅ Visual experiment designer with drag-and-drop interface
|
| Gestures | play_animation, gesture |
|
||||||
- ✅ Real-time trial execution framework ready
|
| Sensors | get_sensors, bumper_state, touch_state |
|
||||||
- ✅ Scalable architecture built for research teams
|
| LEDs | set_eye_leds, set_breathing_lights |
|
||||||
|
|
||||||
### **User Experience Goals** ✅ **Met**
|
|
||||||
- ✅ Intuitive interface following modern design principles
|
|
||||||
- ✅ Consistent experience across all features
|
|
||||||
- ✅ Responsive design working on all devices
|
|
||||||
- ✅ Accessibility compliance for inclusive research
|
|
||||||
- ✅ Professional appearance suitable for academic use
|
|
||||||
|
|
||||||
### **Research Workflow Support** ✅ **Met**
|
|
||||||
- ✅ Hierarchical study structure (Study → Experiment → Trial → Step → Action)
|
|
||||||
- ✅ Multi-role collaboration with proper permissions
|
|
||||||
- ✅ Comprehensive data capture for all trial activities
|
|
||||||
- ✅ Flexible robot integration supporting multiple platforms
|
|
||||||
- ✅ Data analysis and export capabilities
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 **Production Readiness**
|
## Tech Stack
|
||||||
|
|
||||||
### **Deployment Checklist** ✅ **Complete**
|
| Component | Technology | Version |
|
||||||
- ✅ Environment variables configured for Vercel
|
|-----------|------------|---------|
|
||||||
- ✅ Database migrations ready for production
|
| Framework | Next.js | 15-16.x |
|
||||||
- ✅ Security headers and CSRF protection configured
|
| Language | TypeScript | 5.x (strict) |
|
||||||
- ✅ Error tracking and performance monitoring setup
|
| Database | PostgreSQL | 14+ |
|
||||||
- ✅ Build process optimized for Edge Runtime
|
| ORM | Drizzle | latest |
|
||||||
- ✅ Static assets and CDN configuration ready
|
| Auth | NextAuth.js | v5 |
|
||||||
|
| API | tRPC | latest |
|
||||||
### **Performance Validation** ✅ **Passed**
|
| UI | Tailwind + shadcn/ui | latest |
|
||||||
- ✅ Page load time < 2 seconds (Currently optimal)
|
| Real-time | WebSocket | with polling fallback |
|
||||||
- ✅ API response time < 200ms (Currently optimal)
|
| Robot | ROS2 Humble | via rosbridge |
|
||||||
- ✅ Database query time < 50ms (Currently optimal)
|
| Package Manager | Bun | latest |
|
||||||
- ✅ Build completes in < 3 minutes (Currently optimal)
|
|
||||||
- ✅ Zero TypeScript compilation errors
|
|
||||||
- ✅ All ESLint rules passing
|
|
||||||
|
|
||||||
### **Security Validation** ✅ **Verified**
|
|
||||||
- ✅ Role-based access control at all levels
|
|
||||||
- ✅ Input validation and sanitization comprehensive
|
|
||||||
- ✅ SQL injection protection via Drizzle ORM
|
|
||||||
- ✅ XSS prevention with proper content handling
|
|
||||||
- ✅ Secure session management with NextAuth.js
|
|
||||||
- ✅ Audit logging for all sensitive operations
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📈 **Platform Capabilities**
|
## Development Status
|
||||||
|
|
||||||
### **Research Workflow Support**
|
### Completed Features
|
||||||
- **Study Management**: Complete lifecycle from creation to analysis
|
| Feature | Status | Notes |
|
||||||
- **Team Collaboration**: Multi-user support with role-based permissions
|
|---------|--------|-------|
|
||||||
- **Experiment Design**: Visual programming interface for protocol creation
|
| Database Schema | ✅ | 31 tables |
|
||||||
- **Trial Execution**: Panel-based wizard interface matching experiment designer architecture
|
| Authentication | ✅ | 4 roles |
|
||||||
- **Real-time Updates**: WebSocket integration with intelligent polling fallback
|
| Experiment Designer | ✅ | 26+ blocks |
|
||||||
- **Data Capture**: Synchronized multi-modal data streams with comprehensive event logging
|
| Wizard Interface | ✅ | 3-panel design |
|
||||||
- **Robot Integration**: Plugin-based support for multiple platforms
|
| Real-time Updates | ✅ | WebSocket |
|
||||||
|
| Plugin System | ✅ | Robot-agnostic |
|
||||||
|
| NAO6 Integration | ✅ | Docker deployment |
|
||||||
|
| Conditional Branching | ✅ | Wizard choices |
|
||||||
|
| Mock Robot | ✅ | Development mode |
|
||||||
|
|
||||||
### **Technical Capabilities**
|
### Known Issues
|
||||||
- **Scalability**: Architecture supporting large research institutions
|
| Issue | Status | Notes |
|
||||||
- **Performance**: Optimized for concurrent multi-user environments
|
|-------|--------|-------|
|
||||||
- **Security**: Research-grade data protection and access control
|
| robots.executeSystemAction | Known error | Fallback works |
|
||||||
- **Flexibility**: Customizable workflows for diverse methodologies
|
|
||||||
- **Integration**: Robot platform agnostic with plugin architecture
|
|
||||||
- **Compliance**: Research ethics and data protection compliance
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔮 **Roadmap & Future Work**
|
## SSH Deployment Commands
|
||||||
|
|
||||||
### **Immediate Priorities** (Next 30 days)
|
```bash
|
||||||
- **Wizard Interface Development** - Complete rebuild of trial execution interface
|
# Local development
|
||||||
- **Robot Control Implementation** - NAO6 integration with WebSocket communication
|
bun dev
|
||||||
- **Trial Execution Engine** - Step-by-step protocol execution with real-time data capture
|
|
||||||
- **User Experience Testing** - Validate study-scoped workflows with target users
|
|
||||||
|
|
||||||
### **Short-term Goals** (Next 60 days)
|
# Database
|
||||||
- **IRB Application Preparation** - Complete documentation and study protocols
|
bun db:push # Push schema changes
|
||||||
- **Reference Experiment Implementation** - Well-documented HRI experiment for comparison study
|
bun db:seed # Seed with test data
|
||||||
- **Training Materials Development** - Comprehensive materials for both HRIStudio and Choregraphe
|
bun run docker:up # Start PostgreSQL
|
||||||
- **Platform Validation** - Extensive testing and reliability verification
|
|
||||||
|
|
||||||
### **Long-term Vision** (Next 90+ days)
|
# Quality
|
||||||
- **User Study Execution** - Comparative study with 10-12 non-engineering participants
|
bun typecheck # TypeScript validation
|
||||||
- **Thesis Research Completion** - Data analysis and academic paper preparation
|
bun lint # ESLint
|
||||||
- **Platform Refinement** - Post-study improvements based on real user feedback
|
```
|
||||||
- **Community Release** - Open source release for broader HRI research community
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎊 **Project Success Declaration**
|
## Thesis Timeline
|
||||||
|
|
||||||
**HRIStudio is officially ready for production deployment.**
|
Current phase: **March 2026** - Implementation complete, preparing user study
|
||||||
|
|
||||||
### **Completion Summary**
|
| Phase | Status | Date |
|
||||||
The platform successfully provides researchers with a comprehensive, professional, and scientifically rigorous environment for conducting Wizard of Oz studies in Human-Robot Interaction research. All major development goals have been achieved, including the complete modernization of the experiment designer with advanced visual programming capabilities and the successful consolidation of routes into a logical study-scoped architecture. Quality standards have been exceeded, and the system is prepared for thesis research and eventual community use.
|
|-------|--------|------|
|
||||||
|
| Proposal | ✅ | Sept 2025 |
|
||||||
### **Key Success Metrics**
|
| IRB Application | ✅ | Dec 2025 |
|
||||||
- **Development Velocity**: Consistently meeting sprint goals with 30+ story points
|
| Implementation | ✅ | Feb 2026 |
|
||||||
- **Code Quality**: Zero production TypeScript errors, fully functional designer
|
| User Study | 🔄 In Progress | Mar-Apr 2026 |
|
||||||
- **Architecture Quality**: Clean study-scoped hierarchy with eliminated code duplication
|
| Defense | Scheduled | April 2026 |
|
||||||
- **User Experience**: Intuitive navigation flow from studies to entity management
|
|
||||||
- **Route Health**: All routes functional with proper error handling and helpful redirects
|
|
||||||
- **User Experience**: Professional, accessible, consistent interface with modern UX
|
|
||||||
- **Performance**: All benchmarks exceeded, sub-100ms hash computation
|
|
||||||
- **Security**: Comprehensive protection and compliance
|
|
||||||
- **Documentation**: Complete technical and user guides
|
|
||||||
- **Designer Functionality**: 100% operational with step addition working perfectly
|
|
||||||
|
|
||||||
### **Ready For**
|
|
||||||
- ✅ Immediate Vercel deployment
|
|
||||||
- ✅ Research team onboarding
|
|
||||||
- ✅ Academic pilot studies
|
|
||||||
- ✅ Full production research use
|
|
||||||
- ✅ Institutional deployment
|
|
||||||
|
|
||||||
**The development team has successfully delivered a world-class platform that will advance Human-Robot Interaction research by providing standardized, reproducible, and efficient tools for conducting high-quality scientific studies.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 **Development Notes**
|
## Next Steps
|
||||||
|
|
||||||
### **Technical Debt Status**
|
1. Complete user study (10-12 participants)
|
||||||
- **High Priority**: None identified
|
2. Data analysis and thesis writing
|
||||||
- **Medium Priority**: Minor database query optimizations possible
|
3. Final defense April 2026
|
||||||
- **Low Priority**: Some older components could benefit from modern React patterns
|
4. Open source release
|
||||||
|
|
||||||
### **Development Restrictions**
|
|
||||||
Following Vercel Edge Runtime compatibility:
|
|
||||||
- ❌ No development servers during implementation sessions
|
|
||||||
- ❌ No Drizzle Studio during development work
|
|
||||||
- ✅ Use `bun db:push` for schema changes
|
|
||||||
- ✅ Use `bun typecheck` for validation
|
|
||||||
- ✅ Use `bun build` for production testing
|
|
||||||
|
|
||||||
### **Quality Gates**
|
|
||||||
- ✅ All TypeScript compilation errors resolved
|
|
||||||
- ✅ All ESLint rules passing with autofix enabled
|
|
||||||
- ✅ All Prettier formatting applied consistently
|
|
||||||
- ✅ No security vulnerabilities detected
|
|
||||||
- ✅ Performance benchmarks met
|
|
||||||
- ✅ Accessibility standards validated
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This document consolidates all project status, progress tracking, and achievement documentation. It serves as the single source of truth for HRIStudio's development state and production readiness.*
|
*Last Updated: March 22, 2026*
|
||||||
+7
-5
@@ -107,7 +107,7 @@ This work addresses a significant bottleneck in HRI research. By creating HRIStu
|
|||||||
\hline
|
\hline
|
||||||
September & Finalize and submit this proposal (Due: Sept. 20).
|
September & Finalize and submit this proposal (Due: Sept. 20).
|
||||||
|
|
||||||
Submit IRB application for the user study. \\
|
Submit IRB application for the study. \\
|
||||||
\hline
|
\hline
|
||||||
Oct -- Nov & Complete final implementation of core HRIStudio features.
|
Oct -- Nov & Complete final implementation of core HRIStudio features.
|
||||||
|
|
||||||
@@ -119,13 +119,15 @@ Begin recruiting participants. \\
|
|||||||
\hline
|
\hline
|
||||||
\multicolumn{2}{|l|}{\textbf{Spring 2026: Execution, Analysis, and Writing}} \\
|
\multicolumn{2}{|l|}{\textbf{Spring 2026: Execution, Analysis, and Writing}} \\
|
||||||
\hline
|
\hline
|
||||||
Jan -- Feb & Upon receiving IRB approval, conduct all user study sessions. \\
|
Jan -- Feb & Run pilot tests with platform.
|
||||||
|
|
||||||
|
Refine based on testing feedback. \\
|
||||||
\hline
|
\hline
|
||||||
March & Analyze all data from the user study.
|
March & Execute user study sessions (10-12 participants).
|
||||||
|
|
||||||
Draft Results and Discussion sections.
|
Analyze data from the user study.
|
||||||
|
|
||||||
Submit ``Intent to Defend'' form (Due: March 1). \\
|
Draft Results and Discussion sections. \\
|
||||||
\hline
|
\hline
|
||||||
April & Submit completed thesis draft to the defense committee (Due: April 1).
|
April & Submit completed thesis draft to the defense committee (Due: April 1).
|
||||||
|
|
||||||
|
|||||||
+108
-508
@@ -1,566 +1,166 @@
|
|||||||
# HRIStudio Quick Reference Guide
|
# HRIStudio Quick Reference Guide
|
||||||
|
|
||||||
## 🚀 **Getting Started (5 Minutes)**
|
## Quick Setup
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- [Bun](https://bun.sh) (package manager)
|
|
||||||
- [PostgreSQL](https://postgresql.org) 14+
|
|
||||||
- [Docker](https://docker.com) (optional)
|
|
||||||
|
|
||||||
### Quick Setup
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and install
|
# Clone with submodules
|
||||||
git clone <repo-url> hristudio
|
git clone https://github.com/soconnor0919/hristudio.git
|
||||||
cd hristudio
|
cd hristudio
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Install and setup
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
# Start database
|
|
||||||
bun run docker:up
|
bun run docker:up
|
||||||
|
|
||||||
# Setup database
|
|
||||||
bun db:push
|
bun db:push
|
||||||
bun db:seed
|
bun db:seed
|
||||||
|
|
||||||
# Single command now syncs all repositories:
|
# Start
|
||||||
# - Core blocks from localhost:3000/hristudio-core
|
|
||||||
# - Robot plugins from https://repo.hristudio.com
|
|
||||||
|
|
||||||
# Start development
|
|
||||||
bun dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default Login
|
**Login**: `sean@soconnor.dev` / `password123`
|
||||||
- **Admin**: `sean@soconnor.dev` / `password123`
|
|
||||||
- **Researcher**: `alice.rodriguez@university.edu` / `password123`
|
|
||||||
- **Wizard**: `emily.watson@lab.edu` / `password123`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 **Project Structure**
|
## Key Concepts
|
||||||
|
|
||||||
```
|
### Hierarchy
|
||||||
src/
|
|
||||||
├── app/ # Next.js App Router pages
|
|
||||||
│ ├── (auth)/ # Authentication pages
|
|
||||||
│ ├── (dashboard)/ # Main application
|
|
||||||
│ └── api/ # API routes
|
|
||||||
├── components/ # UI components
|
|
||||||
│ ├── ui/ # shadcn/ui components
|
|
||||||
│ ├── experiments/ # Feature components
|
|
||||||
│ ├── studies/
|
|
||||||
│ ├── participants/
|
|
||||||
│ └── trials/
|
|
||||||
├── server/ # Backend code
|
|
||||||
│ ├── api/routers/ # tRPC routers
|
|
||||||
│ ├── auth/ # NextAuth config
|
|
||||||
│ └── db/ # Database schema
|
|
||||||
└── lib/ # Utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Key Concepts**
|
|
||||||
|
|
||||||
### Hierarchical Structure
|
|
||||||
```
|
```
|
||||||
Study → Experiment → Trial → Step → Action
|
Study → Experiment → Trial → Step → Action
|
||||||
```
|
```
|
||||||
|
|
||||||
### User Roles
|
### User Roles (Study-level)
|
||||||
- **Administrator**: Full system access
|
- **Owner**: Full study control, manage members
|
||||||
- **Researcher**: Create studies, design experiments
|
- **Researcher**: Design experiments, manage participants
|
||||||
- **Wizard**: Execute trials, control robots
|
- **Wizard**: Execute trials, control robot during sessions
|
||||||
- **Observer**: Read-only access
|
- **Observer**: Read-only access to study data
|
||||||
|
|
||||||
### Core Workflows
|
### Plugin Identifier System
|
||||||
1. **Study Creation** → Team setup → Participant recruitment
|
- `identifier`: Machine-readable key (e.g., `nao6-ros2`)
|
||||||
2. **Experiment Design** → Visual designer → Protocol validation
|
- `name`: Display name (e.g., `NAO6 Robot (ROS2 Integration)`)
|
||||||
3. **Trial Execution** → Wizard interface → Data capture
|
- Lookup order: identifier → name → fallback
|
||||||
4. **Data Analysis** → Export → Insights
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠 **Development Commands**
|
## Development Commands
|
||||||
|
|
||||||
| Command | Purpose |
|
| Command | Description |
|
||||||
|---------|---------|
|
|---------|-------------|
|
||||||
| `bun dev` | Start development server |
|
| `bun dev` | Start dev server |
|
||||||
| `bun build` | Build for production |
|
| `bun build` | Production build |
|
||||||
| `bun typecheck` | TypeScript validation |
|
| `bun typecheck` | TypeScript validation |
|
||||||
| `bun lint` | Code quality checks |
|
|
||||||
| `bun db:push` | Push schema changes |
|
| `bun db:push` | Push schema changes |
|
||||||
| `bun db:seed` | Seed data & sync repositories |
|
| `bun db:seed` | Seed data + sync plugins + forms |
|
||||||
| `bun db:studio` | Open database GUI |
|
| `bun run docker:up` | Start PostgreSQL + MinIO |
|
||||||
|
|
||||||
|
## Forms System
|
||||||
|
|
||||||
|
### Form Types
|
||||||
|
- **Consent**: Legal/IRB consent documents with signature fields
|
||||||
|
- **Survey**: Multi-question questionnaires (ratings, multiple choice)
|
||||||
|
- **Questionnaire**: Custom data collection forms
|
||||||
|
|
||||||
|
### Templates (seeded by default)
|
||||||
|
- Informed Consent - Standard consent template
|
||||||
|
- Post-Session Survey - Participant feedback form
|
||||||
|
- Demographics - Basic demographic collection
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
- `/studies/[id]/forms` - List forms
|
||||||
|
- `/studies/[id]/forms/new` - Create form (from template or scratch)
|
||||||
|
- `/studies/[id]/forms/[formId]` - View/edit form, preview, responses
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌐 **API Reference**
|
## NAO6 Robot Docker
|
||||||
|
|
||||||
### Base URL
|
```bash
|
||||||
```
|
cd ~/Documents/Projects/nao6-hristudio-integration
|
||||||
http://localhost:3000/api/trpc/
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Routers
|
**Services**: nao_driver, ros_bridge (:9090), ros_api
|
||||||
- **`auth`**: Login, logout, registration
|
|
||||||
- **`studies`**: CRUD operations, team management
|
|
||||||
- **`experiments`**: Design, configuration, validation
|
|
||||||
- **`participants`**: Registration, consent, demographics
|
|
||||||
- **`trials`**: Execution, monitoring, data capture, real-time control
|
|
||||||
- **`robots`**: Integration, communication, actions, plugins
|
|
||||||
- **`dashboard`**: Overview stats, recent activity, study progress
|
|
||||||
- **`admin`**: Repository management, system settings
|
|
||||||
|
|
||||||
### Example Usage
|
**Topics**:
|
||||||
```typescript
|
- `/speech` - TTS
|
||||||
// Get user's studies
|
- `/cmd_vel` - Movement
|
||||||
const studies = api.studies.getUserStudies.useQuery();
|
- `/leds/eyes` - LEDs
|
||||||
|
|
||||||
// Create new experiment
|
---
|
||||||
const createExperiment = api.experiments.create.useMutation();
|
|
||||||
|
## Architecture Layers
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ UI: Design / Execute / Playback │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ Server: tRPC, Auth, Trial Logic │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ Data: PostgreSQL, File Storage │
|
||||||
|
│ Robot: ROS2 via WebSocket │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗄️ **Database Quick Reference**
|
## WebSocket Architecture
|
||||||
|
|
||||||
|
- **Trial Updates**: `ws://localhost:3001/api/websocket`
|
||||||
|
- **ROS Bridge**: `ws://localhost:9090` (rosbridge)
|
||||||
|
- **Real-time**: Auto-reconnect with exponential backoff
|
||||||
|
- **Message Types**: trial_event, trial_status, connection_established
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
### Core Tables
|
### Core Tables
|
||||||
```sql
|
- `users` - Authentication
|
||||||
users -- Authentication & profiles
|
- `studies` - Research projects
|
||||||
studies -- Research projects
|
- `experiments` - Protocol templates
|
||||||
experiments -- Protocol templates
|
- `trials` - Execution instances
|
||||||
participants -- Study participants
|
- `steps` - Experiment phases
|
||||||
trials -- Experiment instances
|
- `actions` - Atomic tasks
|
||||||
steps -- Experiment phases
|
- `plugins` - Robot integrations (identifier column)
|
||||||
trial_events -- Execution logs
|
- `trial_events` - Execution logs
|
||||||
robots -- Available platforms
|
|
||||||
```
|
---
|
||||||
|
|
||||||
|
## Route Structure
|
||||||
|
|
||||||
### Key Relationships
|
|
||||||
```
|
```
|
||||||
studies → experiments → trials
|
/dashboard - Global overview
|
||||||
studies → participants
|
/studies - Study list
|
||||||
trials → trial_events
|
/studies/[id] - Study details
|
||||||
experiments → steps
|
/studies/[id]/experiments
|
||||||
|
/studies/[id]/trials
|
||||||
|
/studies/[id]/participants
|
||||||
|
/trials/[id]/wizard - Trial execution
|
||||||
|
/experiments/[id]/designer - Visual editor
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 **UI Components**
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Build errors**: `rm -rf .next && bun build`
|
||||||
|
|
||||||
|
**Database reset**: `bun db:push --force && bun db:seed`
|
||||||
|
|
||||||
|
**Check types**: `bun typecheck`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 **Trial System Quick Reference**
|
## Plugin System
|
||||||
|
|
||||||
### Trial Workflow
|
|
||||||
```
|
|
||||||
1. Create Study → 2. Design Experiment → 3. Add Participants → 4. Schedule Trial → 5. Execute with Wizard Interface → 6. Analyze Results
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Trial Pages
|
|
||||||
- **`/studies/[id]/trials`**: List trials for specific study
|
|
||||||
- **`/trials/[id]`**: Individual trial details and management
|
|
||||||
- **`/trials/[id]/wizard`**: Panel-based real-time execution interface
|
|
||||||
- **`/trials/[id]/analysis`**: Post-trial data analysis
|
|
||||||
|
|
||||||
### Trial Status Flow
|
|
||||||
```
|
|
||||||
scheduled → in_progress → completed
|
|
||||||
↘ aborted
|
|
||||||
↘ failed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wizard Interface Architecture (Panel-Based)
|
|
||||||
The wizard interface uses the same proven panel system as the experiment designer:
|
|
||||||
|
|
||||||
#### **Layout Components**
|
|
||||||
- **PageHeader**: Consistent navigation with breadcrumbs
|
|
||||||
- **PanelsContainer**: Three-panel resizable layout
|
|
||||||
- **Proper Navigation**: Dashboard → Studies → [Study] → Trials → [Trial] → Wizard Control
|
|
||||||
|
|
||||||
#### **Panel Organization**
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ PageHeader: Wizard Control │
|
|
||||||
├──────────┬─────────────────────────┬────────────────────┤
|
|
||||||
│ Left │ Center │ Right │
|
|
||||||
│ Panel │ Panel │ Panel │
|
|
||||||
│ │ │ │
|
|
||||||
│ Trial │ Current Step │ Robot Status │
|
|
||||||
│ Controls │ & Wizard Actions │ Participant Info │
|
|
||||||
│ Step │ │ Live Events │
|
|
||||||
│ List │ │ Connection Status │
|
|
||||||
└──────────┴─────────────────────────┴────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Panel Features**
|
|
||||||
- **Left Panel**: Trial controls, status, step navigation
|
|
||||||
- **Center Panel**: Main execution area with current step and wizard actions
|
|
||||||
- **Right Panel**: Real-time monitoring and context information
|
|
||||||
- **Resizable**: Drag separators to adjust panel sizes
|
|
||||||
- **Overflow Contained**: No page-level scrolling, internal panel scrolling
|
|
||||||
|
|
||||||
### Technical Features
|
|
||||||
- **Real-time Control**: Step-by-step protocol execution
|
|
||||||
- **WebSocket Integration**: Live updates with polling fallback
|
|
||||||
- **Component Reuse**: 90% code sharing with experiment designer
|
|
||||||
- **Type Safety**: Complete TypeScript compatibility
|
|
||||||
- **Mock Robot System**: TurtleBot3 simulation ready for development
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layout Components
|
|
||||||
```typescript
|
```typescript
|
||||||
// Page wrapper with navigation
|
// Loading a plugin by identifier
|
||||||
<PageLayout title="Studies" description="Manage research studies">
|
const plugin = await trialExecution.loadPlugin("nao6-ros2");
|
||||||
<StudiesTable />
|
|
||||||
</PageLayout>
|
|
||||||
|
|
||||||
// Entity forms (unified pattern)
|
// Action execution
|
||||||
<EntityForm
|
await robot.execute("nao6-ros2.say_with_emotion", { text: "Hello" });
|
||||||
mode="create"
|
|
||||||
entityName="Study"
|
|
||||||
form={form}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
/>
|
|
||||||
|
|
||||||
// Data tables (consistent across entities)
|
|
||||||
<DataTable
|
|
||||||
columns={studiesColumns}
|
|
||||||
data={studies}
|
|
||||||
searchKey="name"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Patterns
|
|
||||||
```typescript
|
|
||||||
// Standard form setup
|
|
||||||
const form = useForm<StudyFormData>({
|
|
||||||
resolver: zodResolver(studySchema),
|
|
||||||
defaultValues: { /* ... */ }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unified submission
|
|
||||||
const onSubmit = async (data: StudyFormData) => {
|
|
||||||
await createStudy.mutateAsync(data);
|
|
||||||
router.push(`/studies/${result.id}`);
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 **Route Structure**
|
Last updated: March 2026
|
||||||
|
|
||||||
### Study-Scoped Architecture
|
|
||||||
All study-dependent functionality flows through studies for complete organizational consistency:
|
|
||||||
|
|
||||||
```
|
|
||||||
Platform Routes (Global):
|
|
||||||
/dashboard # Global overview with study filtering
|
|
||||||
/studies # Study management hub
|
|
||||||
/profile # User account management
|
|
||||||
/admin # System administration
|
|
||||||
|
|
||||||
Study-Scoped Routes (All Study-Dependent):
|
|
||||||
/studies/[id] # Study details and overview
|
|
||||||
/studies/[id]/participants # Study participants
|
|
||||||
/studies/[id]/trials # Study trials
|
|
||||||
/studies/[id]/experiments # Study experiment protocols
|
|
||||||
/studies/[id]/plugins # Study robot plugins
|
|
||||||
/studies/[id]/analytics # Study analytics
|
|
||||||
|
|
||||||
Individual Entity Routes (Cross-Study):
|
|
||||||
/trials/[id] # Individual trial details
|
|
||||||
/trials/[id]/wizard # Trial execution interface (TO BE BUILT)
|
|
||||||
/experiments/[id] # Individual experiment details
|
|
||||||
/experiments/[id]/designer # Visual experiment designer
|
|
||||||
|
|
||||||
Helpful Redirects (User Guidance):
|
|
||||||
/participants # → Study selection guidance
|
|
||||||
/trials # → Study selection guidance
|
|
||||||
/experiments # → Study selection guidance
|
|
||||||
/plugins # → Study selection guidance
|
|
||||||
/analytics # → Study selection guidance
|
|
||||||
```
|
|
||||||
|
|
||||||
### Architecture Benefits
|
|
||||||
- **Complete Consistency**: All study-dependent functionality properly scoped
|
|
||||||
- **Clear Mental Model**: Platform-level vs study-level separation
|
|
||||||
- **No Duplication**: Single source of truth for each functionality
|
|
||||||
- **User-Friendly**: Helpful guidance for moved functionality
|
|
||||||
|
|
||||||
## 🔐 **Authentication**
|
|
||||||
|
|
||||||
### Protecting Routes
|
|
||||||
```typescript
|
|
||||||
// Middleware protection
|
|
||||||
export default withAuth(
|
|
||||||
function middleware(request) {
|
|
||||||
// Route logic
|
|
||||||
},
|
|
||||||
{
|
|
||||||
callbacks: {
|
|
||||||
authorized: ({ token }) => !!token,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Component protection
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
if (status === "loading") return <Loading />;
|
|
||||||
if (!session) return <SignIn />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Role Checking
|
|
||||||
```typescript
|
|
||||||
// Server-side
|
|
||||||
ctx.session.user.role === "administrator"
|
|
||||||
|
|
||||||
// Client-side
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
const hasRole = (role: string) => session?.user.role === role;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤖 **Robot Integration**
|
|
||||||
|
|
||||||
### Core Block System
|
|
||||||
```typescript
|
|
||||||
// Core blocks loaded from local repository during development
|
|
||||||
// Repository sync: localhost:3000/hristudio-core → database
|
|
||||||
|
|
||||||
// Block categories (27 total blocks in 4 groups):
|
|
||||||
// - Events (4): when_trial_starts, when_participant_speaks, etc.
|
|
||||||
// - Wizard Actions (6): wizard_say, wizard_gesture, etc.
|
|
||||||
// - Control Flow (8): wait, repeat, if_condition, etc.
|
|
||||||
// - Observation (9): observe_behavior, record_audio, etc.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Plugin Repository System
|
|
||||||
```typescript
|
|
||||||
// Repository sync (admin only)
|
|
||||||
await api.admin.repositories.sync.mutate({ id: repoId });
|
|
||||||
|
|
||||||
// Plugin installation
|
|
||||||
await api.robots.plugins.install.mutate({
|
|
||||||
studyId: 'study-id',
|
|
||||||
pluginId: 'plugin-id'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get study plugins
|
|
||||||
const plugins = api.robots.plugins.getStudyPlugins.useQuery({
|
|
||||||
studyId: selectedStudyId
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Plugin Structure
|
|
||||||
```typescript
|
|
||||||
interface Plugin {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
trustLevel: 'official' | 'verified' | 'community';
|
|
||||||
actionDefinitions: RobotAction[];
|
|
||||||
metadata: {
|
|
||||||
platform: string;
|
|
||||||
category: string;
|
|
||||||
repositoryId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Repository Integration
|
|
||||||
- **Robot Plugins**: `https://repo.hristudio.com` (live)
|
|
||||||
- **Core Blocks**: `localhost:3000/hristudio-core` (development)
|
|
||||||
- **Auto-sync**: Integrated into `bun db:seed` command
|
|
||||||
- **Plugin Store**: Browse → Install → Use in experiments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 **Common Patterns**
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
await mutation.mutateAsync(data);
|
|
||||||
toast.success("Success!");
|
|
||||||
router.push("/success-page");
|
|
||||||
} catch (error) {
|
|
||||||
setError(error.message);
|
|
||||||
toast.error("Failed to save");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading States
|
|
||||||
```typescript
|
|
||||||
const { data, isLoading, error } = api.studies.getAll.useQuery();
|
|
||||||
|
|
||||||
if (isLoading) return <Skeleton />;
|
|
||||||
if (error) return <ErrorMessage error={error} />;
|
|
||||||
return <DataTable data={data} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Validation
|
|
||||||
```typescript
|
|
||||||
const schema = z.object({
|
|
||||||
name: z.string().min(1, "Name required"),
|
|
||||||
description: z.string().min(10, "Description too short"),
|
|
||||||
duration: z.number().min(5, "Minimum 5 minutes")
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 **Deployment**
|
|
||||||
|
|
||||||
### Vercel Deployment
|
|
||||||
```bash
|
|
||||||
# Install Vercel CLI
|
|
||||||
bun add -g vercel
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
vercel --prod
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
vercel env add DATABASE_URL
|
|
||||||
vercel env add NEXTAUTH_SECRET
|
|
||||||
vercel env add CLOUDFLARE_R2_*
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
```bash
|
|
||||||
# Required
|
|
||||||
DATABASE_URL=postgresql://...
|
|
||||||
NEXTAUTH_URL=https://your-domain.com
|
|
||||||
NEXTAUTH_SECRET=your-secret
|
|
||||||
|
|
||||||
# Storage
|
|
||||||
CLOUDFLARE_R2_ACCOUNT_ID=...
|
|
||||||
CLOUDFLARE_R2_ACCESS_KEY_ID=...
|
|
||||||
CLOUDFLARE_R2_SECRET_ACCESS_KEY=...
|
|
||||||
CLOUDFLARE_R2_BUCKET_NAME=hristudio-files
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Experiment Designer — Quick Tips
|
|
||||||
|
|
||||||
- Panels layout
|
|
||||||
- Uses Tailwind-first grid via `PanelsContainer` with fraction-based columns (no hardcoded px).
|
|
||||||
- Left/Center/Right panels are minmax(0, …) columns to prevent horizontal overflow.
|
|
||||||
- Status bar lives inside the bordered container; no gap below the panels.
|
|
||||||
|
|
||||||
- Resizing (no persistence)
|
|
||||||
- Drag separators between Left↔Center and Center↔Right to resize panels.
|
|
||||||
- Fractions are clamped (min/max) to keep panels usable and avoid page overflow.
|
|
||||||
- Keyboard on handles: Arrow keys to resize; Shift+Arrow for larger steps.
|
|
||||||
|
|
||||||
- Overflow rules (no page-level X scroll)
|
|
||||||
- Root containers: `overflow-hidden`, `min-h-0`.
|
|
||||||
- Each panel wrapper: `min-w-0 overflow-hidden`.
|
|
||||||
- Each panel content: `overflow-y-auto overflow-x-hidden` (scroll inside the panel).
|
|
||||||
- If X scroll appears, clamp the offending child (truncate, `break-words`, `overflow-x-hidden`).
|
|
||||||
|
|
||||||
- Action Library scroll
|
|
||||||
- Search/categories header and footer are fixed; the list uses internal scroll (`ScrollArea` with `flex-1`).
|
|
||||||
- Long lists never scroll the page — only the panel.
|
|
||||||
|
|
||||||
- Inspector tabs (shadcn/ui)
|
|
||||||
- Single Tabs root controls both header and content.
|
|
||||||
- TabsList uses simple grid or inline-flex; triggers are plain `TabsTrigger`.
|
|
||||||
- Active state is styled globally (via `globals.css`) using Radix `data-state="active"`.
|
|
||||||
|
|
||||||
## 🔧 **Troubleshooting**
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Build Errors**
|
|
||||||
```bash
|
|
||||||
# Clear cache and rebuild
|
|
||||||
rm -rf .next
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Database Issues**
|
|
||||||
```bash
|
|
||||||
# Reset database
|
|
||||||
bun db:push --force
|
|
||||||
bun db:seed
|
|
||||||
```
|
|
||||||
|
|
||||||
**TypeScript Errors**
|
|
||||||
```bash
|
|
||||||
# Check types
|
|
||||||
bun typecheck
|
|
||||||
|
|
||||||
# Common fixes
|
|
||||||
# - Check imports
|
|
||||||
# - Verify API return types
|
|
||||||
# - Update schema types
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Tips
|
|
||||||
- Use React Server Components where possible
|
|
||||||
- Implement proper pagination for large datasets
|
|
||||||
- Add database indexes for frequently queried fields
|
|
||||||
- Use optimistic updates for better UX
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 **Further Reading**
|
|
||||||
|
|
||||||
### Documentation Files
|
|
||||||
- **[Project Overview](./project-overview.md)**: Complete feature overview
|
|
||||||
- **[Implementation Details](./implementation-details.md)**: Architecture decisions and patterns
|
|
||||||
- **[Database Schema](./database-schema.md)**: Complete database documentation
|
|
||||||
- **[API Routes](./api-routes.md)**: Comprehensive API reference
|
|
||||||
- **[Core Blocks System](./core-blocks-system.md)**: Repository-based block architecture
|
|
||||||
- **[Plugin System Guide](./plugin-system-implementation-guide.md)**: Robot integration guide
|
|
||||||
- **[Project Status](./project-status.md)**: Current development status
|
|
||||||
- **[Work in Progress](./work_in_progress.md)**: Recent changes and active development
|
|
||||||
|
|
||||||
### External Resources
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs)
|
|
||||||
- [tRPC Documentation](https://trpc.io/docs)
|
|
||||||
- [Drizzle ORM Guide](https://orm.drizzle.team/docs)
|
|
||||||
- [shadcn/ui Components](https://ui.shadcn.com)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Quick Tips**
|
|
||||||
### Quick Tips
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
1. Always run `bun typecheck` before commits
|
|
||||||
2. Use the unified `EntityForm` for all CRUD operations
|
|
||||||
3. Follow the established component patterns
|
|
||||||
4. Add proper error boundaries for new features
|
|
||||||
5. Test with multiple user roles
|
|
||||||
6. Use single `bun db:seed` for complete setup
|
|
||||||
|
|
||||||
### Code Standards
|
|
||||||
- Use TypeScript strict mode
|
|
||||||
- Prefer Server Components over Client Components
|
|
||||||
- Implement proper error handling
|
|
||||||
- Add loading states for all async operations
|
|
||||||
- Use Zod for input validation
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
- Keep components focused and composable
|
|
||||||
- Use the established file naming conventions
|
|
||||||
- Implement proper RBAC for new features
|
|
||||||
- Add comprehensive logging for debugging
|
|
||||||
- Follow accessibility guidelines (WCAG 2.1 AA)
|
|
||||||
- Use repository-based plugins instead of hardcoded robot actions
|
|
||||||
- Test plugin installation/uninstallation in different studies
|
|
||||||
|
|
||||||
### Route Architecture
|
|
||||||
- **Study-Scoped**: All entity management flows through studies
|
|
||||||
- **Individual Entities**: Trial/experiment details maintain separate routes
|
|
||||||
- **Helpful Redirects**: Old routes guide users to new locations
|
|
||||||
- **Consistent Navigation**: Breadcrumbs reflect the study → entity hierarchy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This quick reference covers the most commonly needed information for HRIStudio development. For detailed implementation guidance, refer to the comprehensive documentation files.*
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Tutorial 1: Getting Started
|
||||||
|
|
||||||
|
Learn how to set up HRIStudio and log in for the first time.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Install HRIStudio dependencies
|
||||||
|
- Start the development environment
|
||||||
|
- Log in and explore the interface
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Bun](https://bun.sh) installed
|
||||||
|
- [Docker](https://docker.com) installed
|
||||||
|
- [Git](https://git-scm.com) installed
|
||||||
|
|
||||||
|
## Step 1: Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/soconnor0919/hristudio.git
|
||||||
|
cd hristudio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Install Dependencies
|
||||||
|
|
||||||
|
HRIStudio uses Bun as its package manager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Start the Database
|
||||||
|
|
||||||
|
HRIStudio requires PostgreSQL. The easiest way is using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start PostgreSQL and MinIO (for file storage)
|
||||||
|
bun run docker:up
|
||||||
|
|
||||||
|
# Push database schema
|
||||||
|
bun db:push
|
||||||
|
|
||||||
|
# Seed with sample data
|
||||||
|
bun db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates the database schema and populates it with:
|
||||||
|
- 4 default user accounts
|
||||||
|
- Sample study and experiments
|
||||||
|
- Test participants and trials
|
||||||
|
|
||||||
|
## Step 4: Start the Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Step 5: Log In
|
||||||
|
|
||||||
|
Use one of the default accounts:
|
||||||
|
|
||||||
|
| Role | Email | Password |
|
||||||
|
|------|-------|----------|
|
||||||
|
| Administrator | `sean@soconnor.dev` | `password123` |
|
||||||
|
| Researcher | `felipe.perrone@bucknell.edu` | `password123` |
|
||||||
|
| Wizard | `emily.watson@lab.edu` | `password123` |
|
||||||
|
| Observer | `maria.santos@tech.edu` | `password123` |
|
||||||
|
|
||||||
|
## Exploring the Interface
|
||||||
|
|
||||||
|
After logging in, you'll see the main dashboard:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HRIStudio [User] [Settings] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ Studies │ │ Trials │ │Plugins │ │ Admin │ │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Recent Activity │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ • Study: Comparative WoZ Study - Ready │ │
|
||||||
|
│ │ • Trial: P101 - Completed (5 min ago) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
- **Studies** - View and manage your research studies
|
||||||
|
- **Trials** - Monitor and manage experiment trials
|
||||||
|
- **Plugins** - Manage robot integrations
|
||||||
|
- **Admin** - System administration (admins only)
|
||||||
|
|
||||||
|
## Using Simulation Mode
|
||||||
|
|
||||||
|
If you don't have a physical robot, enable simulation mode:
|
||||||
|
|
||||||
|
1. Create or edit `hristudio/.env.local`
|
||||||
|
2. Add: `NEXT_PUBLIC_SIMULATION_MODE=true`
|
||||||
|
3. Restart the dev server
|
||||||
|
|
||||||
|
Simulation mode allows you to test experiments without connecting to a real robot.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Database Connection Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Docker is running
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Restart the database
|
||||||
|
bun run docker:down
|
||||||
|
bun run docker:up
|
||||||
|
bun db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
|
||||||
|
If port 3000 is in use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use a different port
|
||||||
|
PORT=3001 bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed Script Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset the database
|
||||||
|
bun run docker:down -v
|
||||||
|
bun run docker:up
|
||||||
|
bun db:push
|
||||||
|
bun db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you're set up:
|
||||||
|
|
||||||
|
1. **[Your First Study](02-your-first-study.md)** - Create a research study
|
||||||
|
2. **[Designing Experiments](03-designing-experiments.md)** - Build your first protocol
|
||||||
|
3. **[Simulation Mode](09-simulation-mode.md)** - Test without a robot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Tutorials Overview](../tutorials/README.md) | **Next**: [Your First Study](02-your-first-study.md)
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Tutorial 2: Your First Study
|
||||||
|
|
||||||
|
Learn how to create a research study and configure team access.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Create a new research study
|
||||||
|
- Configure study settings (IRB, institution)
|
||||||
|
- Add team members with appropriate roles
|
||||||
|
|
||||||
|
## What is a Study?
|
||||||
|
|
||||||
|
In HRIStudio, a **Study** is the top-level container for your research:
|
||||||
|
|
||||||
|
```
|
||||||
|
Study
|
||||||
|
├── Experiments (multiple protocols)
|
||||||
|
├── Participants (study participants)
|
||||||
|
├── Team Members (collaborators)
|
||||||
|
├── Forms & Surveys (consent, questionnaires)
|
||||||
|
└── Trials (individual experiment runs)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Create a New Study
|
||||||
|
|
||||||
|
1. Log in as **Researcher** or **Administrator**
|
||||||
|
2. Click **Studies** in the sidebar
|
||||||
|
3. Click **Create Study**
|
||||||
|
|
||||||
|
### Study Settings
|
||||||
|
|
||||||
|
| Field | Description | Required |
|
||||||
|
|-------|-------------|----------|
|
||||||
|
| Name | Study title | Yes |
|
||||||
|
| Description | Brief overview of research goals | Yes |
|
||||||
|
| Institution | University or organization | No |
|
||||||
|
| IRB Protocol | Protocol number (e.g., 2024-HRI-001) | No |
|
||||||
|
| Status | Draft, Active, Completed, Archived | Yes |
|
||||||
|
|
||||||
|
### Example: Creating "Robot Trust Study"
|
||||||
|
|
||||||
|
```
|
||||||
|
Name: Robot Trust Study
|
||||||
|
Description: Investigating how robot appearance affects human trust in collaborative tasks.
|
||||||
|
Institution: Bucknell University
|
||||||
|
IRB Protocol: 2024-HRI-TRUST
|
||||||
|
Status: Draft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Add Team Members
|
||||||
|
|
||||||
|
Studies can have multiple collaborators with different roles:
|
||||||
|
|
||||||
|
| Role | Permissions |
|
||||||
|
|------|-------------|
|
||||||
|
| Owner | Full access, can delete study |
|
||||||
|
| Researcher | Create/edit experiments, manage participants |
|
||||||
|
| Wizard | Execute trials, control robot |
|
||||||
|
| Observer | View-only access, add annotations |
|
||||||
|
|
||||||
|
### Adding a Wizard
|
||||||
|
|
||||||
|
1. Open your study
|
||||||
|
2. Go to **Team** tab
|
||||||
|
3. Click **Add Member**
|
||||||
|
4. Enter the wizard's email
|
||||||
|
5. Select **Wizard** role
|
||||||
|
6. Click **Invite**
|
||||||
|
|
||||||
|
The wizard will receive access to:
|
||||||
|
- View the study and experiments
|
||||||
|
- Execute trials
|
||||||
|
- Control the robot during trials
|
||||||
|
- Add notes to trials
|
||||||
|
|
||||||
|
## Step 3: Install Robot Plugins
|
||||||
|
|
||||||
|
For studies involving robots, you need to install the appropriate plugin:
|
||||||
|
|
||||||
|
1. Go to **Plugins** in the sidebar
|
||||||
|
2. Select your study from the dropdown
|
||||||
|
3. Click **Browse Plugins**
|
||||||
|
4. Find your robot (e.g., "NAO6 Robot (ROS2 Integration)")
|
||||||
|
5. Click **Install**
|
||||||
|
6. Configure robot settings (IP address, etc.)
|
||||||
|
|
||||||
|
### Plugin Configuration
|
||||||
|
|
||||||
|
For NAO6 robots:
|
||||||
|
|
||||||
|
```
|
||||||
|
Robot IP: 192.168.1.100
|
||||||
|
Connection Type: ROS2 Bridge
|
||||||
|
WebSocket URL: ws://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Create Forms
|
||||||
|
|
||||||
|
Before running trials, you need consent forms:
|
||||||
|
|
||||||
|
1. Go to **Forms** tab in your study
|
||||||
|
2. Click **Create Form**
|
||||||
|
3. Select form type:
|
||||||
|
- **Consent** - Informed consent documents
|
||||||
|
- **Survey** - Post-session questionnaires
|
||||||
|
- **Questionnaire** - Demographic forms
|
||||||
|
|
||||||
|
### Form Templates
|
||||||
|
|
||||||
|
HRIStudio provides templates to get started:
|
||||||
|
|
||||||
|
| Template | Use Case |
|
||||||
|
|----------|----------|
|
||||||
|
| Informed Consent | Required for all participants |
|
||||||
|
| Post-Session Survey | Collect feedback after trials |
|
||||||
|
| Demographics | Collect participant information |
|
||||||
|
|
||||||
|
## Step 5: Add Participants
|
||||||
|
|
||||||
|
1. Go to **Participants** tab
|
||||||
|
2. Click **Add Participant**
|
||||||
|
3. Enter participant code (e.g., "P001")
|
||||||
|
4. Fill in optional details
|
||||||
|
|
||||||
|
### Batch Import
|
||||||
|
|
||||||
|
For large studies, import from CSV:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
participantCode,name,email,notes
|
||||||
|
P001,John Smith,john@email.com,Condition A
|
||||||
|
P002,Jane Doe,jane@email.com,Condition B
|
||||||
|
```
|
||||||
|
|
||||||
|
## Study Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
Draft → Active → Recruiting → In Progress → Completed
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ └── All trials done
|
||||||
|
│ │ │ └── Trials running
|
||||||
|
│ │ └── Recruiting participants
|
||||||
|
│ └── Ready to collect data
|
||||||
|
└── Setting up study
|
||||||
|
```
|
||||||
|
|
||||||
|
## Study Settings Deep Dive
|
||||||
|
|
||||||
|
### IRB Compliance
|
||||||
|
|
||||||
|
Store your IRB information:
|
||||||
|
- Protocol number
|
||||||
|
- Approval date
|
||||||
|
- Expiration date
|
||||||
|
- Consent form versions
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
|
||||||
|
Configure data retention:
|
||||||
|
- Anonymization settings
|
||||||
|
- Export formats (CSV, JSON)
|
||||||
|
- Backup frequency
|
||||||
|
|
||||||
|
### Notification Settings
|
||||||
|
|
||||||
|
Configure alerts for:
|
||||||
|
- Trial completion
|
||||||
|
- Participant issues
|
||||||
|
- Robot disconnection
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Clone a Study
|
||||||
|
|
||||||
|
Create a copy of an existing study:
|
||||||
|
|
||||||
|
1. Open the study
|
||||||
|
2. Click **Settings** (gear icon)
|
||||||
|
3. Select **Duplicate Study**
|
||||||
|
4. Enter new study name
|
||||||
|
|
||||||
|
### Archive a Study
|
||||||
|
|
||||||
|
When a study is complete:
|
||||||
|
|
||||||
|
1. Go to study settings
|
||||||
|
2. Change status to **Archived**
|
||||||
|
3. Data is preserved but study is read-only
|
||||||
|
|
||||||
|
### Transfer Ownership
|
||||||
|
|
||||||
|
Change the study owner:
|
||||||
|
|
||||||
|
1. Go to **Team** tab
|
||||||
|
2. Find the new owner
|
||||||
|
3. Click **Make Owner**
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Can't Add Team Member
|
||||||
|
|
||||||
|
- Check email is correct
|
||||||
|
- User must have an HRIStudio account
|
||||||
|
- You must be an owner or admin
|
||||||
|
|
||||||
|
### Plugin Installation Failed
|
||||||
|
|
||||||
|
- Check robot is on the network
|
||||||
|
- Verify WebSocket URL is correct
|
||||||
|
- Check Docker services are running
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that your study is set up:
|
||||||
|
|
||||||
|
1. **[Designing Experiments](03-designing-experiments.md)** - Create your first experiment protocol
|
||||||
|
2. **[Forms & Surveys](07-forms-and-surveys.md)** - Customize your consent forms
|
||||||
|
3. **[Running Trials](04-running-trials.md)** - Learn about trial management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Getting Started](01-getting-started.md) | **Next**: [Designing Experiments](03-designing-experiments.md)
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
# Tutorial 3: Designing Experiments
|
||||||
|
|
||||||
|
Learn how to create experiment protocols using the visual block designer.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Navigate the experiment designer
|
||||||
|
- Use core blocks (events, wizard actions, control flow)
|
||||||
|
- Build a branching experiment protocol
|
||||||
|
|
||||||
|
## What is an Experiment?
|
||||||
|
|
||||||
|
An **Experiment** defines the protocol for your study:
|
||||||
|
|
||||||
|
```
|
||||||
|
Experiment
|
||||||
|
├── Steps (ordered sequence)
|
||||||
|
│ ├── Actions (robot behaviors)
|
||||||
|
│ ├── Wizard Blocks (human decisions)
|
||||||
|
│ └── Control Flow (loops, branches)
|
||||||
|
├── Robot Actions (from plugins)
|
||||||
|
└── Parameters (configurable values)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Create an Experiment
|
||||||
|
|
||||||
|
1. Open your study
|
||||||
|
2. Go to **Experiments** tab
|
||||||
|
3. Click **New Experiment**
|
||||||
|
|
||||||
|
### Experiment Settings
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Name | Protocol title |
|
||||||
|
| Description | What the experiment measures |
|
||||||
|
| Robot | Which robot to use |
|
||||||
|
| Version | Track protocol versions |
|
||||||
|
|
||||||
|
## Step 2: The Experiment Designer Interface
|
||||||
|
|
||||||
|
The designer has three main areas:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Experiment: Robot Trust Study v1 [Save] │
|
||||||
|
├────────────┬─────────────────────────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ Blocks │ Canvas │
|
||||||
|
│ Library │ │
|
||||||
|
│ │ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ ┌──────┐ │ │ Step 1 │───▶│ Step 2 │ │
|
||||||
|
│ │Events│ │ │ Hook │ │ Story │ │
|
||||||
|
│ ├──────┤ │ └─────────┘ └────┬────┘ │
|
||||||
|
│ │Wizard│ │ │ │
|
||||||
|
│ ├──────┤ │ ┌────▼────┐ │
|
||||||
|
│ │Control│ │ │ Step 3 │ │
|
||||||
|
│ ├──────┤ │ │ Check │ │
|
||||||
|
│ │Robot │ │ └────┬────┘ │
|
||||||
|
│ └──────┘ │ ┌────┴────┐ │
|
||||||
|
│ │ ┌────┴───┐ ┌───┴────┐ │
|
||||||
|
│ │ │Step 4a │ │Step 4b │ │
|
||||||
|
│ │ │Correct │ │ Wrong │ │
|
||||||
|
│ │ └───┬────┘ └───┬────┘ │
|
||||||
|
│ │ └─────┬─────┘ │
|
||||||
|
│ │ ┌────▼────┐ │
|
||||||
|
│ │ │ Step 5 │ │
|
||||||
|
│ │ │Conclude │ │
|
||||||
|
│ │ └─────────┘ │
|
||||||
|
├────────────┴─────────────────────────────────────────────────┤
|
||||||
|
│ Properties Panel │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Step 1: The Hook │ │
|
||||||
|
│ │ Duration: 25 seconds │ │
|
||||||
|
│ │ Actions: 2 blocks │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Understanding Block Categories
|
||||||
|
|
||||||
|
### Events (Triggers)
|
||||||
|
|
||||||
|
Start your experiment with these blocks:
|
||||||
|
|
||||||
|
| Block | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **Trial Start** | Triggers when trial begins |
|
||||||
|
| **Wizard Button** | Waits for wizard to press a button |
|
||||||
|
| **Timer** | Waits for a specified duration |
|
||||||
|
| **Participant Response** | Waits for participant input |
|
||||||
|
|
||||||
|
### Wizard Actions
|
||||||
|
|
||||||
|
Blocks the wizard can control:
|
||||||
|
|
||||||
|
| Block | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **Say Text** | Robot speaks text |
|
||||||
|
| **Play Animation** | Play a predefined animation |
|
||||||
|
| **Show Image** | Display image on robot screen |
|
||||||
|
| **Move Robot** | Move robot to position |
|
||||||
|
|
||||||
|
### Control Flow
|
||||||
|
|
||||||
|
Control experiment progression:
|
||||||
|
|
||||||
|
| Block | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **Branch** | Split into multiple paths |
|
||||||
|
| **Loop** | Repeat a sequence |
|
||||||
|
| **Wait** | Pause for duration |
|
||||||
|
| **Converge** | Merge multiple paths back |
|
||||||
|
|
||||||
|
### Robot Actions
|
||||||
|
|
||||||
|
Actions from your installed robot plugin:
|
||||||
|
|
||||||
|
| Block | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **say_text** | Robot speaks |
|
||||||
|
| **walk_forward** | Robot walks forward |
|
||||||
|
| **turn_left** | Robot turns |
|
||||||
|
| **wave** | Robot waves |
|
||||||
|
|
||||||
|
## Step 4: Building "The Interactive Storyteller"
|
||||||
|
|
||||||
|
Let's build a simple storytelling experiment with branching:
|
||||||
|
|
||||||
|
### Step 1: The Hook (Start)
|
||||||
|
|
||||||
|
1. Click **+ Add Step**
|
||||||
|
2. Name it "The Hook"
|
||||||
|
3. Set type to **Robot**
|
||||||
|
4. Drag **Say Text** block:
|
||||||
|
```
|
||||||
|
text: "Hello! I have a story to tell you. Are you ready?"
|
||||||
|
```
|
||||||
|
5. Drag **Move Arm** block:
|
||||||
|
```
|
||||||
|
arm: right
|
||||||
|
gesture: welcome
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: The Narrative
|
||||||
|
|
||||||
|
1. Add new step "The Narrative"
|
||||||
|
2. Connect from Step 1
|
||||||
|
3. Add **Say Text**:
|
||||||
|
```
|
||||||
|
text: "Once upon a time, a traveler flew to Mars..."
|
||||||
|
```
|
||||||
|
4. Add **Turn Head** for gaze behavior:
|
||||||
|
```
|
||||||
|
yaw: 1.5
|
||||||
|
pitch: 0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Comprehension Check (Branching)
|
||||||
|
|
||||||
|
1. Add new step "Comprehension Check"
|
||||||
|
2. Set type to **Conditional**
|
||||||
|
3. Add **Ask Question**:
|
||||||
|
```
|
||||||
|
question: "What color was the rock?"
|
||||||
|
options:
|
||||||
|
- Correct: "Red"
|
||||||
|
- Incorrect: "Blue"
|
||||||
|
```
|
||||||
|
4. This creates two paths automatically
|
||||||
|
|
||||||
|
### Step 4: Branch Paths
|
||||||
|
|
||||||
|
**Branch A (Correct):**
|
||||||
|
```
|
||||||
|
Say: "Yes! It was a glowing red rock."
|
||||||
|
Emotion: Happy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branch B (Incorrect):**
|
||||||
|
```
|
||||||
|
Say: "Actually, it was red."
|
||||||
|
Emotion: Sad
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Converge
|
||||||
|
|
||||||
|
1. Add new step "Story Continues"
|
||||||
|
2. Set type to **Converge**
|
||||||
|
3. Connect both branches to this step
|
||||||
|
4. Add concluding speech
|
||||||
|
|
||||||
|
### Step 6: Conclusion
|
||||||
|
|
||||||
|
1. Add final step "Conclusion"
|
||||||
|
2. Add **Say Text**: "The End. Thank you for listening!"
|
||||||
|
3. Add **Bow** animation
|
||||||
|
|
||||||
|
## Step 5: Block Properties
|
||||||
|
|
||||||
|
Each block has configurable properties:
|
||||||
|
|
||||||
|
### Say Text Block
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Hello, how are you?",
|
||||||
|
"language": "en-US",
|
||||||
|
"speed": 1.0,
|
||||||
|
"emotion": "neutral"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch Block
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"variable": "last_response",
|
||||||
|
"options": [
|
||||||
|
{ "label": "Yes", "value": "yes", "nextStepId": "step_abc" },
|
||||||
|
{ "label": "No", "value": "no", "nextStepId": "step_xyz" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loop Block
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"iterations": 3,
|
||||||
|
"maxDuration": 60,
|
||||||
|
"children": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Testing Your Experiment
|
||||||
|
|
||||||
|
### Preview Mode
|
||||||
|
|
||||||
|
Test your experiment without running a real trial:
|
||||||
|
|
||||||
|
1. Click **Preview** button
|
||||||
|
2. Step through each block
|
||||||
|
3. See timing and flow
|
||||||
|
4. Test branching decisions
|
||||||
|
|
||||||
|
### Simulation Mode
|
||||||
|
|
||||||
|
Run with a simulated robot:
|
||||||
|
|
||||||
|
1. Enable `NEXT_PUBLIC_SIMULATION_MODE=true`
|
||||||
|
2. Start a trial
|
||||||
|
3. Robot actions are logged but not executed
|
||||||
|
4. Great for protocol testing
|
||||||
|
|
||||||
|
## Advanced: Parallel Execution
|
||||||
|
|
||||||
|
Run multiple actions simultaneously:
|
||||||
|
|
||||||
|
```
|
||||||
|
Step: Greeting
|
||||||
|
├── Parallel Block
|
||||||
|
│ ├── Say: "Hello!"
|
||||||
|
│ ├── Move Arm: Wave
|
||||||
|
│ └── Move Head: Look at participant
|
||||||
|
```
|
||||||
|
|
||||||
|
## Experiment Versioning
|
||||||
|
|
||||||
|
Track protocol changes:
|
||||||
|
|
||||||
|
1. **Draft** - Experiment being designed
|
||||||
|
2. **Testing** - Being tested with participants
|
||||||
|
3. **Ready** - Approved for data collection
|
||||||
|
4. **Deprecated** - Superseded by newer version
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Linear Protocol
|
||||||
|
|
||||||
|
```
|
||||||
|
Start → Step 1 → Step 2 → Step 3 → End
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branching Protocol
|
||||||
|
|
||||||
|
```
|
||||||
|
Start → Step 1
|
||||||
|
├── Condition A → Step 2a
|
||||||
|
└── Condition B → Step 2b
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loop Protocol
|
||||||
|
|
||||||
|
```
|
||||||
|
Start → Step 1 → Loop (3x) → Step 2 → End
|
||||||
|
↑
|
||||||
|
└── (back to Step 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parallel Protocol
|
||||||
|
|
||||||
|
```
|
||||||
|
Start → Parallel
|
||||||
|
├── Action A
|
||||||
|
├── Action B
|
||||||
|
└── Action C
|
||||||
|
→ Continue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Block Not Connecting
|
||||||
|
|
||||||
|
- Check step types are compatible
|
||||||
|
- Ensure no circular dependencies
|
||||||
|
- Verify conditions are complete
|
||||||
|
|
||||||
|
### Robot Action Not Available
|
||||||
|
|
||||||
|
- Install the robot plugin
|
||||||
|
- Check plugin is enabled for study
|
||||||
|
- Verify robot is connected
|
||||||
|
|
||||||
|
### Timing Issues
|
||||||
|
|
||||||
|
- Adjust duration estimates
|
||||||
|
- Use explicit wait blocks
|
||||||
|
- Test with real timing
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you've designed your experiment:
|
||||||
|
|
||||||
|
1. **[Running Trials](04-running-trials.md)** - Execute your experiment
|
||||||
|
2. **[Wizard Interface](05-wizard-interface.md)** - Learn real-time control
|
||||||
|
3. **[Robot Integration](06-robot-integration.md)** - Connect your robot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Your First Study](02-your-first-study.md) | **Next**: [Running Trials](04-running-trials.md)
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# Tutorial 4: Running Trials
|
||||||
|
|
||||||
|
Learn how to execute experiments and manage participant trials.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Schedule and start trials
|
||||||
|
- Monitor trial progress
|
||||||
|
- Handle trial interruptions
|
||||||
|
- Collect trial data
|
||||||
|
|
||||||
|
## What is a Trial?
|
||||||
|
|
||||||
|
A **Trial** is a single execution of an experiment with one participant:
|
||||||
|
|
||||||
|
```
|
||||||
|
Trial
|
||||||
|
├── Participant (who took part)
|
||||||
|
├── Experiment (which protocol)
|
||||||
|
├── Status (scheduled, in_progress, completed)
|
||||||
|
├── Events (timestamped actions)
|
||||||
|
└── Data (collected responses)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trial Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
Scheduled → In Progress → Completed
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Aborted ◄────────┤
|
||||||
|
│ │ │
|
||||||
|
└────────► Failed ◄───────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Scheduled | Trial is planned but not started |
|
||||||
|
| In Progress | Trial is currently running |
|
||||||
|
| Completed | Trial finished successfully |
|
||||||
|
| Aborted | Trial stopped early by wizard |
|
||||||
|
| Failed | Trial failed due to error |
|
||||||
|
|
||||||
|
## Step 1: Schedule a Trial
|
||||||
|
|
||||||
|
### Create Trial for Participant
|
||||||
|
|
||||||
|
1. Go to your **Study**
|
||||||
|
2. Open **Trials** tab
|
||||||
|
3. Click **Schedule Trial**
|
||||||
|
4. Select:
|
||||||
|
- **Participant**: P001
|
||||||
|
- **Experiment**: The Interactive Storyteller
|
||||||
|
- **Scheduled Time**: Today, 2:00 PM
|
||||||
|
|
||||||
|
### Batch Scheduling
|
||||||
|
|
||||||
|
For multiple participants:
|
||||||
|
|
||||||
|
1. Click **Batch Schedule**
|
||||||
|
2. Select participants (P001-P020)
|
||||||
|
3. Select experiment
|
||||||
|
4. Set time slots
|
||||||
|
|
||||||
|
```
|
||||||
|
| Time | Participant |
|
||||||
|
|------------|-------------|
|
||||||
|
| 2:00 PM | P001 |
|
||||||
|
| 2:15 PM | P002 |
|
||||||
|
| 2:30 PM | P003 |
|
||||||
|
| ... | ... |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Prepare for Trial
|
||||||
|
|
||||||
|
Before starting:
|
||||||
|
|
||||||
|
1. **Verify Robot Connection**
|
||||||
|
- Check robot is powered on
|
||||||
|
- Verify network connection
|
||||||
|
- Test WebSocket connection
|
||||||
|
|
||||||
|
2. **Review Experiment**
|
||||||
|
- Ensure experiment is "Ready" status
|
||||||
|
- Check step count and timing
|
||||||
|
- Verify all actions are configured
|
||||||
|
|
||||||
|
3. **Prepare Environment**
|
||||||
|
- Ensure participant consent is obtained
|
||||||
|
- Set up recording equipment (if needed)
|
||||||
|
- Remove distractions
|
||||||
|
|
||||||
|
## Step 3: Start a Trial
|
||||||
|
|
||||||
|
### From Trials List
|
||||||
|
|
||||||
|
1. Find the scheduled trial
|
||||||
|
2. Click **Start Trial**
|
||||||
|
3. Confirm participant is ready
|
||||||
|
4. Click **Begin**
|
||||||
|
|
||||||
|
### From Wizard Interface
|
||||||
|
|
||||||
|
1. Open **Wizard Interface**
|
||||||
|
2. Select trial from queue
|
||||||
|
3. Click **Start**
|
||||||
|
|
||||||
|
## Step 4: During the Trial
|
||||||
|
|
||||||
|
### Wizard Interface Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Trial: P001 - Interactive Storyteller [00:05:23]│
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌────────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ Trial │ │ Timeline │ │ Robot Control │ │
|
||||||
|
│ │ Controls │ │ │ │ │ │
|
||||||
|
│ │ │ │ ●───●───○───○ │ │ ┌─────────────┐ │ │
|
||||||
|
│ │ [▶ Play] │ │ Step 1 2 3 4 │ │ │ Connected ✓ │ │ │
|
||||||
|
│ │ [⏸ Pause] │ │ ↑ │ │ │ Battery: 85%│ │ │
|
||||||
|
│ │ [⏹ Stop] │ │ Current: Step 2 │ │ └─────────────┘ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ [📝 Notes] │ │ Progress: 40% │ │ [Say Text] │ │
|
||||||
|
│ │ [⚠ Alert] │ │ │ │ [Move Robot] │ │
|
||||||
|
│ └──────────────┘ └────────────────────┘ │ [Custom Action]│ │
|
||||||
|
│ └─────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trial Controls
|
||||||
|
|
||||||
|
| Button | Action | Keyboard |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| Play | Resume trial | Space |
|
||||||
|
| Pause | Pause trial | Space |
|
||||||
|
| Stop | End trial early | Escape |
|
||||||
|
| Notes | Add timestamped note | N |
|
||||||
|
| Alert | Send alert notification | A |
|
||||||
|
|
||||||
|
### Monitoring Progress
|
||||||
|
|
||||||
|
**Timeline View:**
|
||||||
|
- Visual step progression
|
||||||
|
- Current step highlighted
|
||||||
|
- Completed steps checked
|
||||||
|
- Estimated time remaining
|
||||||
|
|
||||||
|
**Event Log:**
|
||||||
|
- Timestamped events
|
||||||
|
- Action executions
|
||||||
|
- Wizard interventions
|
||||||
|
- Robot responses
|
||||||
|
|
||||||
|
## Step 5: Wizard Interventions
|
||||||
|
|
||||||
|
During Wizard-of-Oz studies, wizards can intervene:
|
||||||
|
|
||||||
|
### Add Intervention
|
||||||
|
|
||||||
|
1. Click **+ Intervention**
|
||||||
|
2. Select type:
|
||||||
|
- **Pause**: Temporarily stop trial
|
||||||
|
- **Resume**: Continue after pause
|
||||||
|
- **Note**: Add observation
|
||||||
|
- **Alert**: Notify researcher
|
||||||
|
|
||||||
|
### Branch Selection
|
||||||
|
|
||||||
|
When reaching a conditional step:
|
||||||
|
|
||||||
|
1. Observe participant response
|
||||||
|
2. Select appropriate branch:
|
||||||
|
- **Correct**: Proceed to positive path
|
||||||
|
- **Incorrect**: Proceed to correction path
|
||||||
|
3. Select is logged for analysis
|
||||||
|
|
||||||
|
### Manual Actions
|
||||||
|
|
||||||
|
Execute unplanned actions:
|
||||||
|
|
||||||
|
1. Click **+ Action**
|
||||||
|
2. Select from robot actions
|
||||||
|
3. Configure parameters
|
||||||
|
4. Execute immediately
|
||||||
|
|
||||||
|
## Step 6: Trial Completion
|
||||||
|
|
||||||
|
### Automatic Completion
|
||||||
|
|
||||||
|
When all steps complete:
|
||||||
|
1. Final step executes
|
||||||
|
2. Trial status → "Completed"
|
||||||
|
3. Data is saved automatically
|
||||||
|
4. Summary shown
|
||||||
|
|
||||||
|
### Manual Completion
|
||||||
|
|
||||||
|
To end early:
|
||||||
|
|
||||||
|
1. Click **Stop Trial**
|
||||||
|
2. Confirm completion
|
||||||
|
3. Select reason:
|
||||||
|
- Participant fatigue
|
||||||
|
- Technical issue
|
||||||
|
- Protocol complete
|
||||||
|
4. Save partial data
|
||||||
|
|
||||||
|
## Step 7: Post-Trial
|
||||||
|
|
||||||
|
### Automatic Prompts
|
||||||
|
|
||||||
|
After trial completion:
|
||||||
|
|
||||||
|
1. **Participant Debrief**
|
||||||
|
- Thank participant
|
||||||
|
- Answer questions
|
||||||
|
- Collect final feedback
|
||||||
|
|
||||||
|
2. **Survey Distribution**
|
||||||
|
- Send post-session survey
|
||||||
|
- Collect responses
|
||||||
|
|
||||||
|
3. **Data Export**
|
||||||
|
- Download trial data
|
||||||
|
- Export event log
|
||||||
|
|
||||||
|
### Trial Summary
|
||||||
|
|
||||||
|
View trial summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Trial Summary - P001 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Duration: 5:23 │
|
||||||
|
│ Steps Completed: 6/6 (100%) │
|
||||||
|
│ Interventions: 2 │
|
||||||
|
│ │
|
||||||
|
│ Actions: │
|
||||||
|
│ ✓ Say Text: "Hello..." (2.3s) │
|
||||||
|
│ ✓ Turn Head: yaw=1.5 (1.1s) │
|
||||||
|
│ ✓ Say Text: "What color..." (3.2s) │
|
||||||
|
│ ⚠ Intervention: Pause (10s) │
|
||||||
|
│ ✓ Branch: Correct selected │
|
||||||
|
│ ✓ Say Text: "Yes! It was red" (2.8s) │
|
||||||
|
│ │
|
||||||
|
│ Events: 18 logged │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Managing Multiple Trials
|
||||||
|
|
||||||
|
### Trial Queue
|
||||||
|
|
||||||
|
View upcoming trials:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Trial Queue [Refresh] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 2:00 PM │ P001 │ Interactive Storyteller │ Scheduled │
|
||||||
|
│ 2:20 PM │ P002 │ Interactive Storyteller │ Scheduled │
|
||||||
|
│ 2:40 PM │ P003 │ Interactive Storyteller │ Scheduled │
|
||||||
|
│ 3:00 PM │ P004 │ Interactive Storyteller │ Scheduled │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trial History
|
||||||
|
|
||||||
|
View past trials:
|
||||||
|
|
||||||
|
| Participant | Started | Duration | Status | Interventions |
|
||||||
|
|-------------|---------|----------|--------|---------------|
|
||||||
|
| P001 | Today 2:00 PM | 5:23 | Completed | 2 |
|
||||||
|
| P002 | Today 2:20 PM | 4:58 | Completed | 1 |
|
||||||
|
| P003 | Today 2:45 PM | - | In Progress | 0 |
|
||||||
|
|
||||||
|
## Data Collection
|
||||||
|
|
||||||
|
### Automatic Data Capture
|
||||||
|
|
||||||
|
HRIStudio automatically logs:
|
||||||
|
|
||||||
|
- Timestamps for all events
|
||||||
|
- Action executions
|
||||||
|
- Robot responses
|
||||||
|
- Wizard interventions
|
||||||
|
- Participant responses
|
||||||
|
- Timing data
|
||||||
|
|
||||||
|
### Manual Data
|
||||||
|
|
||||||
|
Wizards can add:
|
||||||
|
|
||||||
|
- Timestamped notes
|
||||||
|
- Observation categories
|
||||||
|
- Participant behavior codes
|
||||||
|
- Custom annotations
|
||||||
|
|
||||||
|
### Export Formats
|
||||||
|
|
||||||
|
Download trial data:
|
||||||
|
|
||||||
|
| Format | Contents |
|
||||||
|
|--------|----------|
|
||||||
|
| CSV | Tabular data for spreadsheets |
|
||||||
|
| JSON | Full event log with metadata |
|
||||||
|
| Video | Screen recording (if enabled) |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Trial Won't Start
|
||||||
|
|
||||||
|
1. Check robot connection
|
||||||
|
2. Verify experiment is "Ready"
|
||||||
|
3. Check participant consent
|
||||||
|
4. Review error logs
|
||||||
|
|
||||||
|
### Trial Paused Unexpectedly
|
||||||
|
|
||||||
|
- Robot may have disconnected
|
||||||
|
- Check network connection
|
||||||
|
- Resume when connection restored
|
||||||
|
|
||||||
|
### Data Not Saved
|
||||||
|
|
||||||
|
- Ensure database connection
|
||||||
|
- Check disk space
|
||||||
|
- Export data manually
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Trials
|
||||||
|
|
||||||
|
- [ ] Robot connected and tested
|
||||||
|
- [ ] Experiment verified
|
||||||
|
- [ ] Participant consent obtained
|
||||||
|
- [ ] Recording equipment ready
|
||||||
|
- [ ] Wizard briefed on protocol
|
||||||
|
|
||||||
|
### During Trials
|
||||||
|
|
||||||
|
- [ ] Monitor timeline progress
|
||||||
|
- [ ] Take timestamped notes
|
||||||
|
- [ ] Document interventions
|
||||||
|
- [ ] Watch for issues
|
||||||
|
|
||||||
|
### After Trials
|
||||||
|
|
||||||
|
- [ ] Review trial summary
|
||||||
|
- [ ] Export data promptly
|
||||||
|
- [ ] Send follow-up surveys
|
||||||
|
- [ ] Update participant status
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you can run trials:
|
||||||
|
|
||||||
|
1. **[Wizard Interface](05-wizard-interface.md)** - Master real-time control
|
||||||
|
2. **[Data & Analysis](08-data-and-analysis.md)** - Analyze your results
|
||||||
|
3. **[Forms & Surveys](07-forms-and-surveys.md)** - Collect post-trial data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Designing Experiments](03-designing-experiments.md) | **Next**: [Wizard Interface](05-wizard-interface.md)
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
# Tutorial 5: Wizard Interface
|
||||||
|
|
||||||
|
Learn how to use the real-time wizard control interface for Wizard-of-Oz studies.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Navigate the wizard interface
|
||||||
|
- Control robot actions in real-time
|
||||||
|
- Make branching decisions
|
||||||
|
- Handle trial interruptions
|
||||||
|
|
||||||
|
## What is the Wizard Interface?
|
||||||
|
|
||||||
|
The **Wizard Interface** is your control center during trials. It provides:
|
||||||
|
|
||||||
|
- Real-time trial monitoring
|
||||||
|
- Robot action controls
|
||||||
|
- Decision-making tools
|
||||||
|
- Intervention capabilities
|
||||||
|
- Event logging
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ WIZARD INTERFACE │
|
||||||
|
├────────────────┬─────────────────────┬──────────────────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ Trial │ Timeline │ Robot │
|
||||||
|
│ Controls │ Progress │ Status │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌──────────┐ │ ┌───────────────┐ │ ┌────────────────┐ │
|
||||||
|
│ │ ▶ Play │ │ │ 1 → 2 → 3 → │ │ │ ● Connected │ │
|
||||||
|
│ │ ⏸ Pause │ │ │ ↑ │ │ │ Battery: 85% │ │
|
||||||
|
│ │ ⏹ Stop │ │ │ Step 2 │ │ │ Position: (0,0)│ │
|
||||||
|
│ └──────────┘ │ └───────────────┘ │ └────────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌──────────┐ │ Progress: 40% │ ┌────────────────┐ │
|
||||||
|
│ │ 📝 Notes │ │ Time: 00:05:23 │ │ Action Panel │ │
|
||||||
|
│ │ ⚠ Alert │ │ │ │ │ │
|
||||||
|
│ └──────────┘ │ │ │ [Say Text] │ │
|
||||||
|
│ │ │ │ [Move Robot] │ │
|
||||||
|
│ │ │ │ [Wave] │ │
|
||||||
|
│ │ │ │ [Custom...] │ │
|
||||||
|
│ │ │ └────────────────┘ │
|
||||||
|
└────────────────┴─────────────────────┴──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Accessing the Wizard Interface
|
||||||
|
|
||||||
|
### Method 1: From Trials List
|
||||||
|
|
||||||
|
1. Go to **Trials** in sidebar
|
||||||
|
2. Find your scheduled trial
|
||||||
|
3. Click **Open Wizard**
|
||||||
|
|
||||||
|
### Method 2: Direct URL
|
||||||
|
|
||||||
|
```
|
||||||
|
/trials/{trialId}/wizard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Trial Queue
|
||||||
|
|
||||||
|
1. Go to **Wizard Queue**
|
||||||
|
2. See all pending trials
|
||||||
|
3. Click **Start** on any trial
|
||||||
|
|
||||||
|
## Step 2: Understanding the Layout
|
||||||
|
|
||||||
|
### Left Panel: Trial Controls
|
||||||
|
|
||||||
|
| Control | Function |
|
||||||
|
|---------|----------|
|
||||||
|
| Play/Pause | Start or pause trial |
|
||||||
|
| Stop | End trial early |
|
||||||
|
| Notes | Add timestamped observations |
|
||||||
|
| Alert | Send alert to researchers |
|
||||||
|
|
||||||
|
### Center Panel: Timeline
|
||||||
|
|
||||||
|
- **Visual Progress**: See step progression
|
||||||
|
- **Current Position**: Highlighted current step
|
||||||
|
- **Navigation**: Click to jump to step (if allowed)
|
||||||
|
- **Time Display**: Elapsed and estimated remaining
|
||||||
|
|
||||||
|
### Right Panel: Robot Control
|
||||||
|
|
||||||
|
**Status Section:**
|
||||||
|
- Connection indicator
|
||||||
|
- Battery level
|
||||||
|
- Position tracking
|
||||||
|
- Sensor readings
|
||||||
|
|
||||||
|
**Action Section:**
|
||||||
|
- Quick action buttons
|
||||||
|
- Custom action builder
|
||||||
|
- Action history
|
||||||
|
|
||||||
|
## Step 3: Controlling the Robot
|
||||||
|
|
||||||
|
### Quick Actions
|
||||||
|
|
||||||
|
Pre-configured robot actions:
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Say Text | Make robot speak |
|
||||||
|
| Wave | Wave gesture |
|
||||||
|
| Look at Me | Turn head toward participant |
|
||||||
|
| Look Away | Turn head elsewhere |
|
||||||
|
| Nod | Confirmation nod |
|
||||||
|
| Shake Head | Negation shake |
|
||||||
|
|
||||||
|
### Custom Say Text
|
||||||
|
|
||||||
|
1. Click **Say Text**
|
||||||
|
2. Enter text in popup:
|
||||||
|
```
|
||||||
|
"Hello! Nice to meet you."
|
||||||
|
```
|
||||||
|
3. Select options:
|
||||||
|
- Speed: Normal / Slow / Fast
|
||||||
|
- Emotion: Neutral / Happy / Excited
|
||||||
|
4. Click **Execute**
|
||||||
|
5. Robot speaks the text
|
||||||
|
|
||||||
|
### Move Robot
|
||||||
|
|
||||||
|
1. Click **Move Robot**
|
||||||
|
2. Select movement type:
|
||||||
|
- Walk Forward/Back
|
||||||
|
- Turn Left/Right
|
||||||
|
- Move Head
|
||||||
|
- Move Arm
|
||||||
|
3. Set parameters
|
||||||
|
4. Execute
|
||||||
|
|
||||||
|
### Custom Actions
|
||||||
|
|
||||||
|
For advanced control:
|
||||||
|
|
||||||
|
1. Click **Custom...**
|
||||||
|
2. Select action from plugin
|
||||||
|
3. Configure parameters
|
||||||
|
4. Execute
|
||||||
|
|
||||||
|
## Step 4: Making Decisions
|
||||||
|
|
||||||
|
When the experiment reaches a branching point:
|
||||||
|
|
||||||
|
### Decision Popup
|
||||||
|
|
||||||
|
A popup appears with options:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Branch Decision Required │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Step: Comprehension Check │
|
||||||
|
│ Question: "What color was the rock?" │
|
||||||
|
│ │
|
||||||
|
│ Participant's response: They said "blue" (incorrect) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ○ Correct Response (Red) │ │
|
||||||
|
│ │ → Robot celebrates │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ● Incorrect Response (Other) │ │
|
||||||
|
│ │ → Robot gently corrects │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Confirm Selection] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision Guidelines
|
||||||
|
|
||||||
|
1. **Observe** participant's actual response
|
||||||
|
2. **Consider** protocol criteria
|
||||||
|
3. **Select** appropriate branch
|
||||||
|
4. **Confirm** selection
|
||||||
|
|
||||||
|
### After Selection
|
||||||
|
|
||||||
|
- Decision is logged with timestamp
|
||||||
|
- Trial continues on selected path
|
||||||
|
- Both participant and robot continue
|
||||||
|
|
||||||
|
## Step 5: Handling Interruptions
|
||||||
|
|
||||||
|
### Pause Trial
|
||||||
|
|
||||||
|
When you need to pause:
|
||||||
|
|
||||||
|
1. Click **Pause** button
|
||||||
|
2. Optionally add reason:
|
||||||
|
- Participant needs break
|
||||||
|
- Technical issue
|
||||||
|
- External interruption
|
||||||
|
3. Trial pauses, robot holds position
|
||||||
|
|
||||||
|
### Resume Trial
|
||||||
|
|
||||||
|
1. Click **Play** button
|
||||||
|
2. Trial resumes from pause point
|
||||||
|
3. Pause duration is logged
|
||||||
|
|
||||||
|
### Stop Trial
|
||||||
|
|
||||||
|
For early termination:
|
||||||
|
|
||||||
|
1. Click **Stop** button
|
||||||
|
2. Select reason:
|
||||||
|
- Participant fatigue
|
||||||
|
- Technical failure
|
||||||
|
- Protocol deviation
|
||||||
|
- Participant withdrawal
|
||||||
|
3. Confirm stop
|
||||||
|
4. Partial data is saved
|
||||||
|
|
||||||
|
### Add Notes
|
||||||
|
|
||||||
|
Record observations:
|
||||||
|
|
||||||
|
1. Click **Notes** button
|
||||||
|
2. Enter observation:
|
||||||
|
```
|
||||||
|
Participant laughed at the robot's gesture.
|
||||||
|
```
|
||||||
|
3. Note is timestamped automatically
|
||||||
|
4. Notes appear in event log
|
||||||
|
|
||||||
|
### Send Alert
|
||||||
|
|
||||||
|
Notify researchers:
|
||||||
|
|
||||||
|
1. Click **Alert** button
|
||||||
|
2. Select alert type:
|
||||||
|
- Technical issue
|
||||||
|
- Safety concern
|
||||||
|
- Protocol question
|
||||||
|
- Other
|
||||||
|
3. Add description
|
||||||
|
4. Send alert
|
||||||
|
|
||||||
|
## Step 6: Monitoring Robot Status
|
||||||
|
|
||||||
|
### Connection Status
|
||||||
|
|
||||||
|
| Status | Icon | Meaning |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Connected | ● Green | Robot responding |
|
||||||
|
| Connecting | ● Yellow | Attempting connection |
|
||||||
|
| Disconnected | ● Red | No robot connection |
|
||||||
|
| Error | ⚠ Orange | Connection error |
|
||||||
|
|
||||||
|
### Battery Monitor
|
||||||
|
|
||||||
|
View battery level:
|
||||||
|
- Green: > 50%
|
||||||
|
- Yellow: 20-50%
|
||||||
|
- Red: < 20%
|
||||||
|
|
||||||
|
### Sensor Display
|
||||||
|
|
||||||
|
Real-time sensor readings:
|
||||||
|
- Joint positions
|
||||||
|
- Touch sensors
|
||||||
|
- Sonar distances
|
||||||
|
- Camera feed (if available)
|
||||||
|
|
||||||
|
### Action Queue
|
||||||
|
|
||||||
|
See pending/executing actions:
|
||||||
|
```
|
||||||
|
Executing: Say Text "Hello!"
|
||||||
|
Pending: Move Head (queued)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Keyboard Shortcuts
|
||||||
|
|
||||||
|
Speed up your workflow:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| Space | Play/Pause toggle |
|
||||||
|
| Escape | Stop trial |
|
||||||
|
| N | Add note |
|
||||||
|
| A | Send alert |
|
||||||
|
| 1-9 | Execute quick action |
|
||||||
|
| ← → | Navigate timeline |
|
||||||
|
| ↑ ↓ | Select branch option |
|
||||||
|
|
||||||
|
## Step 8: Event Logging
|
||||||
|
|
||||||
|
All actions are logged automatically:
|
||||||
|
|
||||||
|
```
|
||||||
|
[14:32:05] Trial started
|
||||||
|
[14:32:07] Step 1: The Hook
|
||||||
|
[14:32:08] Action: Say Text "Hello!"
|
||||||
|
[14:32:11] Action: Move Arm Wave
|
||||||
|
[14:32:15] Step 2: The Narrative
|
||||||
|
[14:32:16] Action: Say Text "Once upon a time..."
|
||||||
|
[14:33:05] Step 3: Comprehension Check
|
||||||
|
[14:33:06] Action: Say Text "What color was the rock?"
|
||||||
|
[14:33:28] Wizard Note: "Participant said blue"
|
||||||
|
[14:33:30] Branch: Incorrect selected
|
||||||
|
[14:33:31] Step 4b: Correction
|
||||||
|
[14:33:32] Action: Say Text "Actually, it was red."
|
||||||
|
[14:34:05] Trial completed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trial Modes
|
||||||
|
|
||||||
|
### Observer Mode
|
||||||
|
|
||||||
|
For observers (read-only):
|
||||||
|
- View trial progress
|
||||||
|
- See robot status
|
||||||
|
- Cannot execute actions
|
||||||
|
- Can add notes
|
||||||
|
|
||||||
|
### Active Wizard Mode
|
||||||
|
|
||||||
|
Full control:
|
||||||
|
- Execute actions
|
||||||
|
- Make decisions
|
||||||
|
- Pause/resume
|
||||||
|
- Add notes/alerts
|
||||||
|
|
||||||
|
### Training Mode
|
||||||
|
|
||||||
|
Practice without real data:
|
||||||
|
- Simulated robot
|
||||||
|
- No data saved
|
||||||
|
- Safe to experiment
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Trial
|
||||||
|
|
||||||
|
- [ ] Review experiment protocol
|
||||||
|
- [ ] Test robot connection
|
||||||
|
- [ ] Familiarize with action panel
|
||||||
|
- [ ] Know decision criteria
|
||||||
|
|
||||||
|
### During Trial
|
||||||
|
|
||||||
|
- [ ] Stay focused on participant
|
||||||
|
- [ ] Make decisions based on observation
|
||||||
|
- [ ] Document notable events
|
||||||
|
- [ ] Keep action log clean
|
||||||
|
|
||||||
|
### After Trial
|
||||||
|
|
||||||
|
- [ ] Review event log
|
||||||
|
- [ ] Add final notes
|
||||||
|
- [ ] Confirm data saved
|
||||||
|
- [ ] Prepare for next trial
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Robot Not Responding
|
||||||
|
|
||||||
|
1. Check connection indicator
|
||||||
|
2. Verify network
|
||||||
|
3. Check robot power
|
||||||
|
4. Restart connection
|
||||||
|
|
||||||
|
### Actions Not Executing
|
||||||
|
|
||||||
|
1. Check action queue
|
||||||
|
2. Verify parameters
|
||||||
|
3. Check robot state (not in rest mode)
|
||||||
|
|
||||||
|
### Decision Popup Not Appearing
|
||||||
|
|
||||||
|
1. Check if step has branches
|
||||||
|
2. Verify step type is "conditional"
|
||||||
|
3. Contact researcher
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Mastered the wizard interface?
|
||||||
|
|
||||||
|
1. **[Robot Integration](06-robot-integration.md)** - Deep dive into robot control
|
||||||
|
2. **[Data & Analysis](08-data-and-analysis.md)** - Review trial data
|
||||||
|
3. **[Simulation Mode](09-simulation-mode.md)** - Practice without a robot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Running Trials](04-running-trials.md) | **Next**: [Robot Integration](06-robot-integration.md)
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
# Tutorial 6: Robot Integration
|
||||||
|
|
||||||
|
Learn how to connect and configure robots for your HRI studies.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Connect NAO6 robot to HRIStudio
|
||||||
|
- Configure robot plugins
|
||||||
|
- Test robot connection
|
||||||
|
- Troubleshoot common issues
|
||||||
|
|
||||||
|
## Supported Robots
|
||||||
|
|
||||||
|
HRIStudio supports multiple robot platforms:
|
||||||
|
|
||||||
|
| Robot | Protocol | Actions |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| **NAO6** | ROS2 | Speech, movement, gestures, sensors |
|
||||||
|
| **TurtleBot3** | ROS2 | Navigation, sensors |
|
||||||
|
| **Mock Robot** | WebSocket | All actions (simulation) |
|
||||||
|
|
||||||
|
## Understanding the Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ HRIStudio Platform │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ Wizard │◄────────────►│ Robot Communication │ │
|
||||||
|
│ │ Interface │ WebSocket │ Service │ │
|
||||||
|
│ └──────────────┘ └──────────┬───────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ │ ROS Bridge │
|
||||||
|
│ ┌─────▼─────┐ │
|
||||||
|
│ │ rosbridge │ │
|
||||||
|
│ │ :9090 │ │
|
||||||
|
│ └─────┬─────┘ │
|
||||||
|
└────────────────────────────────────────────┼─────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌─────▼─────┐ ┌─────▼─────┐ │
|
||||||
|
│ NAO │ │ NAO │ │
|
||||||
|
│ Driver │ │ Robot │ │
|
||||||
|
│ (ROS2) │◄───►│ (naoqi) │ │
|
||||||
|
└───────────┘ └───────────┘ │
|
||||||
|
Network Robot │
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Set Up NAO6 Robot
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
1. Connect NAO6 to your network:
|
||||||
|
```
|
||||||
|
# On the robot, say "Connect to Wi-Fi"
|
||||||
|
# Or use the Choregraphe interface
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Note the robot's IP address:
|
||||||
|
```
|
||||||
|
# On the robot, say "What is my IP address?"
|
||||||
|
# Or check robot's network settings
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verify network access:
|
||||||
|
```bash
|
||||||
|
ping nao.local
|
||||||
|
# Or ping the IP directly:
|
||||||
|
ping 192.168.1.100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Robot Credentials
|
||||||
|
|
||||||
|
Default credentials:
|
||||||
|
```
|
||||||
|
Username: nao
|
||||||
|
Password: robolab
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wake Up Robot
|
||||||
|
|
||||||
|
Before connecting, wake up the robot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh nao@192.168.1.100
|
||||||
|
# Enter password when prompted
|
||||||
|
|
||||||
|
# Wake up the robot
|
||||||
|
python -c "from naoqi import ALProxy; proxy = ALProxy('ALMotion', '192.168.1.100', 9559); proxy.wakeUp()"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Start Docker Services
|
||||||
|
|
||||||
|
### Using Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/nao6-hristudio-integration
|
||||||
|
|
||||||
|
# Set robot IP
|
||||||
|
export NAO_IP=192.168.1.100
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services Overview
|
||||||
|
|
||||||
|
| Service | Port | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `nao_driver` | - | ROS2 driver for NAO |
|
||||||
|
| `ros_bridge` | 9090 | WebSocket bridge |
|
||||||
|
| `ros_api` | - | Topic introspection |
|
||||||
|
|
||||||
|
### Verify Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check running containers
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Test WebSocket connection
|
||||||
|
ws://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Configure HRIStudio
|
||||||
|
|
||||||
|
### Install Robot Plugin
|
||||||
|
|
||||||
|
1. Go to **Plugins** in sidebar
|
||||||
|
2. Select your study
|
||||||
|
3. Click **Browse Plugins**
|
||||||
|
4. Find **NAO6 Robot (ROS2 Integration)**
|
||||||
|
5. Click **Install**
|
||||||
|
|
||||||
|
### Configure Plugin
|
||||||
|
|
||||||
|
Set robot connection:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ NAO6 Robot Configuration │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Robot Name: NAO6-Lab │
|
||||||
|
│ Robot IP: 192.168.1.100 │
|
||||||
|
│ WebSocket URL: ws://localhost:9090 │
|
||||||
|
│ │
|
||||||
|
│ Advanced Settings: │
|
||||||
|
│ □ Use Simulation Mode │
|
||||||
|
│ Connection Timeout: 30 seconds │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create `hristudio/.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Robot connection
|
||||||
|
NAO_ROBOT_IP=192.168.1.100
|
||||||
|
NAO_PASSWORD=robolab
|
||||||
|
NAO_USERNAME=nao
|
||||||
|
|
||||||
|
# WebSocket bridge
|
||||||
|
NEXT_PUBLIC_ROS_BRIDGE_URL=ws://localhost:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Test Connection
|
||||||
|
|
||||||
|
### Using the NAO Test Page
|
||||||
|
|
||||||
|
1. Navigate to: `http://localhost:3000/nao-test`
|
||||||
|
2. Click **Connect**
|
||||||
|
3. Verify connection status
|
||||||
|
|
||||||
|
### Connection Status Indicators
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| **Connected** | Robot responding normally |
|
||||||
|
| **Connecting** | Attempting connection |
|
||||||
|
| **Error** | Connection failed |
|
||||||
|
| **Timeout** | Robot not responding |
|
||||||
|
|
||||||
|
### Test Actions
|
||||||
|
|
||||||
|
Test basic robot actions:
|
||||||
|
|
||||||
|
| Action | Expected Behavior |
|
||||||
|
|--------|-------------------|
|
||||||
|
| Say Text | Robot speaks |
|
||||||
|
| Wave | Robot waves arm |
|
||||||
|
| Walk Forward | Robot walks |
|
||||||
|
| Turn Left | Robot turns |
|
||||||
|
|
||||||
|
## Step 5: Robot Actions Reference
|
||||||
|
|
||||||
|
### Speech Actions
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `say_text` | `text` | Speak text |
|
||||||
|
| `say_with_emotion` | `text`, `emotion` | Emotional speech |
|
||||||
|
| `set_volume` | `level` | Set speech volume |
|
||||||
|
| `set_language` | `language` | Set speech language |
|
||||||
|
|
||||||
|
### Movement Actions
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `walk_forward` | `speed`, `duration` | Walk forward |
|
||||||
|
| `walk_backward` | `speed` | Walk backward |
|
||||||
|
| `turn_left` | `speed` | Turn left |
|
||||||
|
| `turn_right` | `speed` | Turn right |
|
||||||
|
| `stop` | - | Stop all movement |
|
||||||
|
|
||||||
|
### Head Actions
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `move_head` | `yaw`, `pitch`, `speed` | Move head to position |
|
||||||
|
| `turn_head` | `yaw`, `pitch` | Turn head (relative) |
|
||||||
|
|
||||||
|
### Arm Actions
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `move_arm` | `arm`, joint angles | Move arm to position |
|
||||||
|
| `wave` | `arm` | Wave gesture |
|
||||||
|
|
||||||
|
### Autonomous Life
|
||||||
|
|
||||||
|
| Action | Parameters | Description |
|
||||||
|
|--------|------------|-------------|
|
||||||
|
| `wake_up` | - | Wake robot from rest |
|
||||||
|
| `rest` | - | Put robot to rest |
|
||||||
|
| `set_autonomous_life` | `enabled` | Toggle autonomous behavior |
|
||||||
|
|
||||||
|
## Step 6: Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Robot Not Found
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Cannot connect to robot at 192.168.1.100
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify IP address: `ping 192.168.1.100`
|
||||||
|
2. Check robot is powered on
|
||||||
|
3. Verify network connectivity
|
||||||
|
4. Try `nao.local` hostname
|
||||||
|
|
||||||
|
#### WebSocket Connection Failed
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: WebSocket connection to ws://localhost:9090 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Check Docker is running
|
||||||
|
2. Verify ros_bridge container: `docker ps`
|
||||||
|
3. Check port 9090 is not blocked
|
||||||
|
4. Restart services: `docker compose restart`
|
||||||
|
|
||||||
|
#### Robot Not Responding
|
||||||
|
|
||||||
|
Robot connected but actions don't execute.
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Wake up robot: `ssh nao@IP python -c "from naoqi import ALProxy; p=ALProxy('ALMotion','IP',9559);p.wakeUp()"`
|
||||||
|
2. Check robot is not in rest mode
|
||||||
|
3. Verify no blocking software on robot
|
||||||
|
|
||||||
|
#### Action Timeout
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Action timed out after 30 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Robot may be busy with previous action
|
||||||
|
2. Check network latency
|
||||||
|
3. Increase timeout in settings
|
||||||
|
|
||||||
|
### Diagnostic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Docker containers
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# View all logs
|
||||||
|
docker compose logs
|
||||||
|
|
||||||
|
# View specific service
|
||||||
|
docker compose logs ros_bridge
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker compose restart
|
||||||
|
|
||||||
|
# Stop and start fresh
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check robot IP
|
||||||
|
ssh nao@IP "ifconfig"
|
||||||
|
|
||||||
|
# Test from robot
|
||||||
|
ssh nao@IP "curl localhost:9090"
|
||||||
|
|
||||||
|
# Check firewall
|
||||||
|
sudo iptables -L
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Robot Maintenance
|
||||||
|
|
||||||
|
### Battery Management
|
||||||
|
|
||||||
|
- Check battery before each session
|
||||||
|
- Aim for >50% battery
|
||||||
|
- Charge during breaks
|
||||||
|
- Replace battery if <20% capacity
|
||||||
|
|
||||||
|
### Calibration
|
||||||
|
|
||||||
|
Periodically calibrate:
|
||||||
|
- Joint positions
|
||||||
|
- Camera alignment
|
||||||
|
- Touch sensors
|
||||||
|
- Sound localization
|
||||||
|
|
||||||
|
### Software Updates
|
||||||
|
|
||||||
|
Keep robot software updated:
|
||||||
|
- NAOqi version
|
||||||
|
- ROS2 packages
|
||||||
|
- HRIStudio plugin
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- Use encrypted network (WPA2/WPA3)
|
||||||
|
- Firewall robot from internet
|
||||||
|
- Use strong passwords
|
||||||
|
|
||||||
|
### SSH Access
|
||||||
|
|
||||||
|
- Change default passwords
|
||||||
|
- Use SSH keys when possible
|
||||||
|
- Limit SSH access
|
||||||
|
|
||||||
|
### Data Security
|
||||||
|
|
||||||
|
- Robot camera data may be sensitive
|
||||||
|
- Store data securely
|
||||||
|
- Follow IRB guidelines
|
||||||
|
|
||||||
|
## Simulation Mode
|
||||||
|
|
||||||
|
For testing without a robot:
|
||||||
|
|
||||||
|
1. Enable simulation mode in settings
|
||||||
|
2. Or set `NEXT_PUBLIC_SIMULATION_MODE=true`
|
||||||
|
3. All actions are simulated locally
|
||||||
|
|
||||||
|
See [Simulation Mode Tutorial](09-simulation-mode.md) for details.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that your robot is connected:
|
||||||
|
|
||||||
|
1. **[Running Trials](04-running-trials.md)** - Execute trials with robot
|
||||||
|
2. **[Wizard Interface](05-wizard-interface.md)** - Control the robot
|
||||||
|
3. **[Data & Analysis](08-data-and-analysis.md)** - Collect interaction data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Wizard Interface](05-wizard-interface.md) | **Next**: [Forms & Surveys](07-forms-and-surveys.md)
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
# Tutorial 7: Forms & Surveys
|
||||||
|
|
||||||
|
Learn how to create and manage consent forms, surveys, and questionnaires.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Create consent forms for IRB compliance
|
||||||
|
- Build post-session surveys
|
||||||
|
- Collect participant responses
|
||||||
|
- Manage form templates
|
||||||
|
|
||||||
|
## Form Types
|
||||||
|
|
||||||
|
HRIStudio supports three form types:
|
||||||
|
|
||||||
|
| Type | Purpose | When |
|
||||||
|
|------|---------|------|
|
||||||
|
| **Consent** | Informed consent for participation | Before trial |
|
||||||
|
| **Survey** | Collect feedback and observations | After trial |
|
||||||
|
| **Questionnaire** | Demographic data collection | Any time |
|
||||||
|
|
||||||
|
## Step 1: Access Forms
|
||||||
|
|
||||||
|
1. Go to your **Study**
|
||||||
|
2. Click **Forms** tab
|
||||||
|
3. View existing forms and templates
|
||||||
|
|
||||||
|
### Form List View
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Forms [+ Create] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Name Type Responses Status │
|
||||||
|
│ ─────────────────────────────────────────────────────────── │
|
||||||
|
│ Informed Consent Consent 12/20 Active │
|
||||||
|
│ Post-Session Survey Survey 8/20 Active │
|
||||||
|
│ Demographics Questionnaire 15/20 Active │
|
||||||
|
│ Template: Standard Consent - Template │
|
||||||
|
│ Template: Feedback Survey - Template │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Create a Form
|
||||||
|
|
||||||
|
### Using a Template
|
||||||
|
|
||||||
|
1. Click **Create Form**
|
||||||
|
2. Select **Use Template**
|
||||||
|
3. Choose template:
|
||||||
|
- Informed Consent
|
||||||
|
- Post-Session Survey
|
||||||
|
- Demographics
|
||||||
|
4. Customize as needed
|
||||||
|
|
||||||
|
### From Scratch
|
||||||
|
|
||||||
|
1. Click **Create Form**
|
||||||
|
2. Select **Blank Form**
|
||||||
|
3. Choose form type
|
||||||
|
4. Build fields manually
|
||||||
|
|
||||||
|
## Step 3: Form Builder
|
||||||
|
|
||||||
|
The form builder lets you create custom fields:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Form Builder: Post-Session Survey │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Form Settings │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Title: Post-Session Survey │ │
|
||||||
|
│ │ Type: Survey │ │
|
||||||
|
│ │ Active: ☑ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Fields │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 1. [Rating] How engaging was the robot? [✕] │ │
|
||||||
|
│ │ 2. [Text] What did you enjoy most? [✕] │ │
|
||||||
|
│ │ 3. [Multiple Choice] Robot personality? [✕] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [+ Add Field] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Preview │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ How engaging was the robot? │ │
|
||||||
|
│ │ ○ 1 ○ 2 ○ 3 ○ 4 ○ 5 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Save] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Field Types
|
||||||
|
|
||||||
|
### Text Field
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Text │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: Participant Age │
|
||||||
|
│ Required: ☑ │
|
||||||
|
│ Placeholder: e.g., 25 │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Participant Age * │ │
|
||||||
|
│ │ [e.g., 25 ] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rating Scale
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Rating │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: How engaging was the robot? │
|
||||||
|
│ Required: ☑ │
|
||||||
|
│ Scale: 1 to [5] │
|
||||||
|
│ Low Label: Not at all engaging │
|
||||||
|
│ High Label: Very engaging │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ How engaging was the robot? * │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 1 2 3 4 5 │ │
|
||||||
|
│ │ ○ ○ ○ ○ ○ │ │
|
||||||
|
│ │ Not at all Very engaging │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Choice
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Multiple Choice │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: Did the robot respond appropriately? │
|
||||||
|
│ Required: ☑ │
|
||||||
|
│ Options: │
|
||||||
|
│ 1. Yes, always │
|
||||||
|
│ 2. Yes, most of the time │
|
||||||
|
│ 3. Sometimes │
|
||||||
|
│ 4. Rarely │
|
||||||
|
│ 5. No │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Did the robot respond appropriately? * │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ○ Yes, always │ │
|
||||||
|
│ │ ○ Yes, most of the time │ │
|
||||||
|
│ │ ○ Sometimes │ │
|
||||||
|
│ │ ○ Rarely │ │
|
||||||
|
│ │ ○ No │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Yes/No
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Yes/No │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: Would you interact with this robot again? │
|
||||||
|
│ Required: ☐ │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Would you interact with this robot again? │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ○ Yes ○ No │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Area
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Text Area │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: What did you enjoy most about the interaction? │
|
||||||
|
│ Required: ☐ │
|
||||||
|
│ Rows: [4] │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ What did you enjoy most about the interaction? │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [ ] │ │
|
||||||
|
│ │ [ ] │ │
|
||||||
|
│ │ [ ] │ │
|
||||||
|
│ │ [ ] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Date │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: Session Date │
|
||||||
|
│ Required: ☑ │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Session Date * │ │
|
||||||
|
│ │ [📅 Select date ] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signature
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Field Type: Signature │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Label: Participant Signature │
|
||||||
|
│ Required: ☑ │
|
||||||
|
│ │
|
||||||
|
│ Preview: │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Participant Signature * │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌───────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ [Sign here] │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ └───────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Consent Forms
|
||||||
|
|
||||||
|
### Required Elements
|
||||||
|
|
||||||
|
For IRB compliance, consent forms must include:
|
||||||
|
|
||||||
|
- [ ] Study title and purpose
|
||||||
|
- [ ] Principal investigator
|
||||||
|
- [ ] Procedures description
|
||||||
|
- [ ] Risks and benefits
|
||||||
|
- [ ] Confidentiality statement
|
||||||
|
- [ ] Voluntary participation note
|
||||||
|
- [ ] Signature and date fields
|
||||||
|
|
||||||
|
### Consent Form Template
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Informed Consent",
|
||||||
|
"type": "consent",
|
||||||
|
"fields": [
|
||||||
|
{ "type": "text", "label": "Study Title", "required": true },
|
||||||
|
{ "type": "text", "label": "Principal Investigator", "required": true },
|
||||||
|
{ "type": "textarea", "label": "Purpose of the Study", "required": true },
|
||||||
|
{ "type": "textarea", "label": "Procedures", "required": true },
|
||||||
|
{ "type": "textarea", "label": "Risks and Benefits", "required": true },
|
||||||
|
{ "type": "textarea", "label": "Confidentiality", "required": true },
|
||||||
|
{ "type": "yes_no", "label": "I consent to participate", "required": true },
|
||||||
|
{ "type": "signature", "label": "Participant Signature", "required": true },
|
||||||
|
{ "type": "date", "label": "Date", "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Surveys
|
||||||
|
|
||||||
|
### Post-Session Survey Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Post-Session Questionnaire",
|
||||||
|
"type": "survey",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "rating",
|
||||||
|
"label": "How engaging was the robot?",
|
||||||
|
"settings": { "scale": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "rating",
|
||||||
|
"label": "How natural did the interaction feel?",
|
||||||
|
"settings": { "scale": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multiple_choice",
|
||||||
|
"label": "Did the robot respond appropriately?",
|
||||||
|
"options": ["Always", "Usually", "Sometimes", "Rarely", "Never"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "textarea",
|
||||||
|
"label": "What did you like most?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "textarea",
|
||||||
|
"label": "What could be improved?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Questionnaire Example (Demographics)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Demographics",
|
||||||
|
"type": "questionnaire",
|
||||||
|
"fields": [
|
||||||
|
{ "type": "text", "label": "Age" },
|
||||||
|
{
|
||||||
|
"type": "multiple_choice",
|
||||||
|
"label": "Gender",
|
||||||
|
"options": ["Male", "Female", "Non-binary", "Prefer not to say"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multiple_choice",
|
||||||
|
"label": "Experience with robots",
|
||||||
|
"options": ["None", "A little", "Moderate", "Extensive"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Form Versions
|
||||||
|
|
||||||
|
Forms support versioning for IRB compliance:
|
||||||
|
|
||||||
|
1. Create new version when modifying:
|
||||||
|
- Question text changes
|
||||||
|
- New fields added
|
||||||
|
- Required fields changed
|
||||||
|
|
||||||
|
2. Version history:
|
||||||
|
```
|
||||||
|
Version 1 (Current) - Active
|
||||||
|
Version 2 - Draft
|
||||||
|
Version 3 - Archived
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Track changes:
|
||||||
|
- Version number
|
||||||
|
- Change date
|
||||||
|
- Change description
|
||||||
|
|
||||||
|
## Step 8: Distributing Forms
|
||||||
|
|
||||||
|
### Automatic Distribution
|
||||||
|
|
||||||
|
Configure automatic form sending:
|
||||||
|
|
||||||
|
1. Open form settings
|
||||||
|
2. Enable **Auto-distribute**
|
||||||
|
3. Set trigger:
|
||||||
|
- Before trial (consent)
|
||||||
|
- After trial (survey)
|
||||||
|
4. Select participants
|
||||||
|
|
||||||
|
### Manual Distribution
|
||||||
|
|
||||||
|
Send forms manually:
|
||||||
|
|
||||||
|
1. Open form
|
||||||
|
2. Click **Distribute**
|
||||||
|
3. Select participants
|
||||||
|
4. Choose delivery method
|
||||||
|
|
||||||
|
### Participant Link
|
||||||
|
|
||||||
|
Generate shareable link:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://hristudio.example.com/forms/{formId}?participant={participantCode}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 9: Collecting Responses
|
||||||
|
|
||||||
|
### View Responses
|
||||||
|
|
||||||
|
1. Open form
|
||||||
|
2. Click **Responses** tab
|
||||||
|
3. View individual submissions
|
||||||
|
|
||||||
|
### Response Dashboard
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Form Responses: Post-Session Survey │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Total Responses: 15/20 (75%) │
|
||||||
|
│ │
|
||||||
|
│ Question: How engaging was the robot? │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 5 ████████████████████████████████████ 8 responses │ │
|
||||||
|
│ │ 4 ██████████████████ 5 responses │ │
|
||||||
|
│ │ 3 ████████ 2 responses │ │
|
||||||
|
│ │ 2 ████ 1 response │ │
|
||||||
|
│ │ 1 ████ 1 response │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Average: 4.2 / 5.0 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Responses
|
||||||
|
|
||||||
|
Download collected data:
|
||||||
|
|
||||||
|
| Format | Contents |
|
||||||
|
|--------|----------|
|
||||||
|
| CSV | Tabular data |
|
||||||
|
| JSON | Full response objects |
|
||||||
|
| PDF | Printed consent forms |
|
||||||
|
|
||||||
|
## Step 10: Form Templates
|
||||||
|
|
||||||
|
### Creating Templates
|
||||||
|
|
||||||
|
1. Create form with desired fields
|
||||||
|
2. Click **Save as Template**
|
||||||
|
3. Enter template name
|
||||||
|
4. Template is available for reuse
|
||||||
|
|
||||||
|
### Template Library
|
||||||
|
|
||||||
|
| Template | Use Case |
|
||||||
|
|----------|----------|
|
||||||
|
| Standard Consent | Generic research consent |
|
||||||
|
| Child Consent | Studies with minors |
|
||||||
|
| Extended Consent | Complex procedures |
|
||||||
|
| Feedback Survey | Post-session feedback |
|
||||||
|
| NASA-TLX | Workload assessment |
|
||||||
|
| SUS | System usability |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Consent Forms
|
||||||
|
|
||||||
|
- [ ] Review with IRB before use
|
||||||
|
- [ ] Keep language simple
|
||||||
|
- [ ] Include all required elements
|
||||||
|
- [ ] Version control for changes
|
||||||
|
- [ ] Store signed forms securely
|
||||||
|
|
||||||
|
### Surveys
|
||||||
|
|
||||||
|
- [ ] Keep questions concise
|
||||||
|
- [ ] Use appropriate scales
|
||||||
|
- [ ] Test with pilot participants
|
||||||
|
- [ ] Randomize order when appropriate
|
||||||
|
- [ ] Include open-ended questions
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
|
||||||
|
- [ ] Export data regularly
|
||||||
|
- [ ] Backup responses
|
||||||
|
- [ ] Anonymize data for analysis
|
||||||
|
- [ ] Follow data retention policy
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Form Not Loading
|
||||||
|
|
||||||
|
- Check form is active
|
||||||
|
- Verify participant access
|
||||||
|
- Check network connection
|
||||||
|
|
||||||
|
### Response Not Saving
|
||||||
|
|
||||||
|
- Check required fields
|
||||||
|
- Verify session active
|
||||||
|
- Try again or refresh
|
||||||
|
|
||||||
|
### Participant Can't Access
|
||||||
|
|
||||||
|
- Verify participant code valid
|
||||||
|
- Check form is distributed
|
||||||
|
- Confirm study is active
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you've created your forms:
|
||||||
|
|
||||||
|
1. **[Running Trials](04-running-trials.md)** - Connect forms to trials
|
||||||
|
2. **[Data & Analysis](08-data-and-analysis.md)** - Analyze collected data
|
||||||
|
3. **[Your First Study](02-your-first-study.md)** - Set up your study
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Robot Integration](06-robot-integration.md) | **Next**: [Data & Analysis](08-data-and-analysis.md)
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
# Tutorial 8: Data & Analysis
|
||||||
|
|
||||||
|
Learn how to collect, export, and analyze trial data from HRIStudio.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Understand data collection in HRIStudio
|
||||||
|
- Export trial data in various formats
|
||||||
|
- Analyze event logs
|
||||||
|
- Generate reports
|
||||||
|
|
||||||
|
## Data Collection Overview
|
||||||
|
|
||||||
|
HRIStudio automatically captures comprehensive data during trials:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Collection │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Trial Metadata │
|
||||||
|
│ ├── Start/End times │
|
||||||
|
│ ├── Duration │
|
||||||
|
│ ├── Participant info │
|
||||||
|
│ └── Experiment version │
|
||||||
|
│ │
|
||||||
|
│ Event Log (Timestamped) │
|
||||||
|
│ ├── Step changes │
|
||||||
|
│ ├── Action executions │
|
||||||
|
│ ├── Robot responses │
|
||||||
|
│ └── Wizard interventions │
|
||||||
|
│ │
|
||||||
|
│ Form Responses │
|
||||||
|
│ ├── Consent forms │
|
||||||
|
│ ├── Surveys │
|
||||||
|
│ └── Questionnaires │
|
||||||
|
│ │
|
||||||
|
│ Sensor Data │
|
||||||
|
│ ├── Joint positions │
|
||||||
|
│ ├── Touch events │
|
||||||
|
│ └── Audio/video (if enabled) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Accessing Trial Data
|
||||||
|
|
||||||
|
### From Trial List
|
||||||
|
|
||||||
|
1. Go to **Trials** tab
|
||||||
|
2. Find completed trial
|
||||||
|
3. Click **View Details**
|
||||||
|
|
||||||
|
### From Study Dashboard
|
||||||
|
|
||||||
|
1. Open your study
|
||||||
|
2. Go to **Data** tab
|
||||||
|
3. Select trial or view aggregate
|
||||||
|
|
||||||
|
## Step 2: Trial Event Log
|
||||||
|
|
||||||
|
Each trial generates a complete event log:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trialId": "trial_abc123",
|
||||||
|
"participantCode": "P001",
|
||||||
|
"experimentName": "Interactive Storyteller",
|
||||||
|
"startedAt": "2024-03-15T14:00:00Z",
|
||||||
|
"completedAt": "2024-03-15T14:05:23Z",
|
||||||
|
"duration": 323,
|
||||||
|
"status": "completed",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:00:00.123Z",
|
||||||
|
"type": "trial_started",
|
||||||
|
"stepId": null,
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:00:02.456Z",
|
||||||
|
"type": "step_changed",
|
||||||
|
"stepId": "step_1",
|
||||||
|
"stepName": "The Hook",
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:00:03.789Z",
|
||||||
|
"type": "action_executed",
|
||||||
|
"actionName": "Say Text",
|
||||||
|
"parameters": { "text": "Hello!" },
|
||||||
|
"duration": 2300,
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:00:08.012Z",
|
||||||
|
"type": "action_executed",
|
||||||
|
"actionName": "Wave",
|
||||||
|
"duration": 1500,
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:02:30.123Z",
|
||||||
|
"type": "intervention",
|
||||||
|
"interventionType": "note",
|
||||||
|
"data": { "note": "Participant laughed" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:03:00.456Z",
|
||||||
|
"type": "wizard_response",
|
||||||
|
"variable": "last_response",
|
||||||
|
"selectedValue": "correct",
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-03-15T14:05:23.789Z",
|
||||||
|
"type": "trial_completed",
|
||||||
|
"data": { "stepsCompleted": 6 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Types
|
||||||
|
|
||||||
|
| Event Type | Description | Data Captured |
|
||||||
|
|------------|-------------|---------------|
|
||||||
|
| `trial_started` | Trial began | Timestamp |
|
||||||
|
| `step_changed` | New step began | Step ID, name |
|
||||||
|
| `action_executed` | Robot action | Action details, duration |
|
||||||
|
| `action_completed` | Action finished | Duration, result |
|
||||||
|
| `action_failed` | Action failed | Error details |
|
||||||
|
| `wizard_response` | Wizard decision | Selected option |
|
||||||
|
| `intervention` | Wizard intervention | Type, note |
|
||||||
|
| `trial_paused` | Trial paused | Reason |
|
||||||
|
| `trial_resumed` | Trial resumed | Pause duration |
|
||||||
|
| `trial_completed` | Trial finished | Summary |
|
||||||
|
|
||||||
|
## Step 3: Exporting Data
|
||||||
|
|
||||||
|
### Export Single Trial
|
||||||
|
|
||||||
|
1. Open trial details
|
||||||
|
2. Click **Export**
|
||||||
|
3. Select format
|
||||||
|
|
||||||
|
### Export Study Data
|
||||||
|
|
||||||
|
1. Open study
|
||||||
|
2. Go to **Data** tab
|
||||||
|
3. Click **Export All**
|
||||||
|
4. Select options:
|
||||||
|
- Date range
|
||||||
|
- Trial status
|
||||||
|
- Include forms
|
||||||
|
|
||||||
|
### Export Formats
|
||||||
|
|
||||||
|
#### CSV Format
|
||||||
|
|
||||||
|
```csv
|
||||||
|
trial_id,participant,experiment,started_at,duration,status,steps_completed
|
||||||
|
trial_abc,P001,Interactive Storyteller,2024-03-15T14:00:00Z,323,completed,6
|
||||||
|
trial_def,P002,Interactive Storyteller,2024-03-15T14:20:00Z,298,completed,6
|
||||||
|
trial_ghi,P003,Interactive Storyteller,2024-03-15T14:40:00Z,0,failed,1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exportDate": "2024-03-15T15:00:00Z",
|
||||||
|
"studyName": "Robot Trust Study",
|
||||||
|
"trials": [...],
|
||||||
|
"forms": [...],
|
||||||
|
"metadata": {
|
||||||
|
"totalTrials": 20,
|
||||||
|
"completedTrials": 18,
|
||||||
|
"averageDuration": 312
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Event Log CSV
|
||||||
|
|
||||||
|
```csv
|
||||||
|
timestamp,event_type,step_name,action_name,parameters,duration,status
|
||||||
|
2024-03-15T14:00:00.123Z,trial_started,,,,,
|
||||||
|
2024-03-15T14:00:02.456Z,step_changed,The Hook,,,,
|
||||||
|
2024-03-15T14:00:03.789Z,action_executed,The Hook,Say Text,"{""text"":""Hello!""}",2300,completed
|
||||||
|
2024-03-15T14:00:08.012Z,action_executed,The Hook,Wave,,1500,completed
|
||||||
|
2024-03-15T14:02:30.123Z,intervention,The Narrative,Note,"{""note"":""Participant laughed""}",,,
|
||||||
|
2024-03-15T14:03:00.456Z,wizard_response,Comprehension Check,Correct,,,,
|
||||||
|
2024-03-15T14:05:23.789Z,trial_completed,,,,323,
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Data Dashboard
|
||||||
|
|
||||||
|
### Study Dashboard
|
||||||
|
|
||||||
|
View aggregate statistics:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Study Dashboard: Robot Trust Study │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Overview │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ 20 │ │ 18 │ │ 5m12s │ │ 2 │ │
|
||||||
|
│ │ Trials │ │ Complete│ │ Avg Time│ │ Failed │ │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Completion Rate │
|
||||||
|
│ ████████████████████████████████████░░░░ 90% │
|
||||||
|
│ │
|
||||||
|
│ Timeline │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ P001 ████████████████████████████████ 5:23 │ │
|
||||||
|
│ │ P002 ██████████████████████████████ 5:02 │ │
|
||||||
|
│ │ P003 ██████████████████████████ 4:45 │ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
| Metric | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Total Trials | Number of scheduled trials |
|
||||||
|
| Completed | Successfully completed trials |
|
||||||
|
| Average Duration | Mean trial time |
|
||||||
|
| Completion Rate | % of trials completed |
|
||||||
|
| Failed | Trials that failed |
|
||||||
|
| Average Steps | Mean steps per trial |
|
||||||
|
|
||||||
|
## Step 5: Analyzing Event Data
|
||||||
|
|
||||||
|
### Timing Analysis
|
||||||
|
|
||||||
|
Calculate action durations:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
with open('trial_events.json') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Calculate action durations
|
||||||
|
for event in data['events']:
|
||||||
|
if event['type'] == 'action_executed':
|
||||||
|
duration = event.get('duration', 0)
|
||||||
|
print(f"{event['actionName']}: {duration/1000:.1f}s")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intervention Analysis
|
||||||
|
|
||||||
|
Track wizard interventions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Count interventions by type
|
||||||
|
interventions = [
|
||||||
|
e for e in data['events']
|
||||||
|
if e['type'] == 'intervention'
|
||||||
|
]
|
||||||
|
|
||||||
|
by_type = {}
|
||||||
|
for i in interventions:
|
||||||
|
itype = i['data'].get('type', 'unknown')
|
||||||
|
by_type[itype] = by_type.get(itype, 0) + 1
|
||||||
|
|
||||||
|
print(by_type)
|
||||||
|
# {'note': 15, 'pause': 3, 'alert': 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch Selection Analysis
|
||||||
|
|
||||||
|
Analyze wizard decisions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get wizard responses
|
||||||
|
responses = [
|
||||||
|
e for e in data['events']
|
||||||
|
if e['type'] == 'wizard_response'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Count by value
|
||||||
|
by_value = {}
|
||||||
|
for r in responses:
|
||||||
|
value = r.get('selectedValue', 'unknown')
|
||||||
|
by_value[value] = by_value.get(value, 0) + 1
|
||||||
|
|
||||||
|
print(by_value)
|
||||||
|
# {'correct': 12, 'incorrect': 6}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Form Data Analysis
|
||||||
|
|
||||||
|
### Response Aggregation
|
||||||
|
|
||||||
|
Aggregate survey responses:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Calculate average rating
|
||||||
|
ratings = [
|
||||||
|
r['responses']['engagement_rating']
|
||||||
|
for r in form_responses
|
||||||
|
]
|
||||||
|
|
||||||
|
avg_rating = sum(ratings) / len(ratings)
|
||||||
|
print(f"Average engagement: {avg_rating:.2f}/5")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Tabulation
|
||||||
|
|
||||||
|
Compare responses across conditions:
|
||||||
|
|
||||||
|
```
|
||||||
|
| Condition A | Condition B | Total
|
||||||
|
--------------------|------------|-------------|-------
|
||||||
|
Robot engaged | 4.2 | 4.5 | 4.35
|
||||||
|
Natural interaction | 3.8 | 4.1 | 3.95
|
||||||
|
Would use again | 78% | 85% | 81%
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Data Visualization
|
||||||
|
|
||||||
|
### Trial Timeline
|
||||||
|
|
||||||
|
Visualize trial progression:
|
||||||
|
|
||||||
|
```
|
||||||
|
P001: ████████████████░░░░░░░░░░░░░░░░░ 5:23
|
||||||
|
P002: ███████████████░░░░░░░░░░░░░░░░░░ 4:58
|
||||||
|
P003: ██████████████████████████████░░░░ 6:02
|
||||||
|
P004: ████████████████░░░░░░░░░░░░░░░░░░ 5:15
|
||||||
|
```
|
||||||
|
|
||||||
|
### Action Distribution
|
||||||
|
|
||||||
|
```
|
||||||
|
Action Frequency
|
||||||
|
────────────────
|
||||||
|
Say Text ████████████████████ 45
|
||||||
|
Wave ████████████ 25
|
||||||
|
Turn Head ████████████ 25
|
||||||
|
Move Arm ████ 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch Outcomes
|
||||||
|
|
||||||
|
```
|
||||||
|
Branch Selection
|
||||||
|
────────────────
|
||||||
|
Correct Response (A): ██████████████████████████ 67%
|
||||||
|
Incorrect Response (B): █████████████ 33%
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 8: Generating Reports
|
||||||
|
|
||||||
|
### Trial Summary Report
|
||||||
|
|
||||||
|
Generate PDF summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
TRIAL SUMMARY REPORT
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Study: Robot Trust Study
|
||||||
|
Participant: P001
|
||||||
|
Date: March 15, 2024
|
||||||
|
Experiment: Interactive Storyteller v1
|
||||||
|
|
||||||
|
EXECUTIVE SUMMARY
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Duration: 5 minutes 23 seconds
|
||||||
|
Status: Completed successfully
|
||||||
|
Steps Completed: 6/6
|
||||||
|
Interventions: 2
|
||||||
|
|
||||||
|
TIMELINE
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
14:00:00 Trial started
|
||||||
|
14:00:02 Step 1: The Hook
|
||||||
|
14:00:08 Step 2: The Narrative
|
||||||
|
14:02:30 Wizard note: "Participant engaged"
|
||||||
|
14:03:00 Step 3: Comprehension Check
|
||||||
|
14:03:28 Branch selected: Correct
|
||||||
|
14:03:30 Step 4a: Correct Response
|
||||||
|
14:05:23 Trial completed
|
||||||
|
|
||||||
|
METRICS
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Actions Executed: 12
|
||||||
|
Action Success Rate: 100%
|
||||||
|
Average Action Duration: 2.1s
|
||||||
|
Wizard Intervention Rate: 0.37/min
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
### Study Report
|
||||||
|
|
||||||
|
Aggregate across participants:
|
||||||
|
|
||||||
|
```
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
STUDY REPORT
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Study: Robot Trust Study
|
||||||
|
Date Range: March 1-15, 2024
|
||||||
|
Participants: 20
|
||||||
|
|
||||||
|
PARTICIPATION
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Enrolled: 20
|
||||||
|
Completed: 18 (90%)
|
||||||
|
Withdrew: 1 (5%)
|
||||||
|
Failed: 1 (5%)
|
||||||
|
|
||||||
|
TIMING
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Mean Duration: 5m 12s ± 28s
|
||||||
|
Min Duration: 4m 45s
|
||||||
|
Max Duration: 6m 02s
|
||||||
|
|
||||||
|
INTERVENTIONS
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Total Interventions: 34
|
||||||
|
Notes: 25 (73%)
|
||||||
|
Pauses: 7 (21%)
|
||||||
|
Alerts: 2 (6%)
|
||||||
|
|
||||||
|
BRANCH SELECTION
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
Branch A (Correct): 12 (67%)
|
||||||
|
Branch B (Incorrect): 6 (33%)
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 9: Data Privacy
|
||||||
|
|
||||||
|
### Anonymization
|
||||||
|
|
||||||
|
Remove identifying information:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Replace participant codes with anonymous IDs
|
||||||
|
participant_map = {
|
||||||
|
'P001': 'S001',
|
||||||
|
'P002': 'S002',
|
||||||
|
'P003': 'S003',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Settings
|
||||||
|
|
||||||
|
Configure export options:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| Include participant codes | Keep or anonymize |
|
||||||
|
| Include timestamps | Full or relative |
|
||||||
|
| Include notes | Include/exclude |
|
||||||
|
| Include form responses | Include/exclude |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Data Collection
|
||||||
|
|
||||||
|
- [ ] Enable all event logging
|
||||||
|
- [ ] Configure sensor data capture
|
||||||
|
- [ ] Set up automatic backups
|
||||||
|
- [ ] Test data export before study
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
- [ ] Export regularly (daily/weekly)
|
||||||
|
- [ ] Store in secure location
|
||||||
|
- [ ] Follow IRB data retention
|
||||||
|
- [ ] Backup critical data
|
||||||
|
|
||||||
|
### Data Analysis
|
||||||
|
|
||||||
|
- [ ] Document analysis methods
|
||||||
|
- [ ] Track protocol versions
|
||||||
|
- [ ] Note data quality issues
|
||||||
|
- [ ] Share data dictionary
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you understand data collection:
|
||||||
|
|
||||||
|
1. **[Your First Study](02-your-first-study.md)** - Apply data practices
|
||||||
|
2. **[Simulation Mode](09-simulation-mode.md)** - Test data collection
|
||||||
|
3. **[Running Trials](04-running-trials.md)** - Practice with data capture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Forms & Surveys](07-forms-and-surveys.md) | **Next**: [Simulation Mode](09-simulation-mode.md)
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
# Tutorial 9: Simulation Mode
|
||||||
|
|
||||||
|
Learn how to test HRIStudio experiments without a physical robot.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
- Enable simulation mode
|
||||||
|
- Use the mock robot server
|
||||||
|
- Test experiments end-to-end
|
||||||
|
- Practice trial execution
|
||||||
|
|
||||||
|
## Why Simulation Mode?
|
||||||
|
|
||||||
|
Simulation mode allows you to:
|
||||||
|
|
||||||
|
- **Test protocols** without a robot
|
||||||
|
- **Train wizards** before live sessions
|
||||||
|
- **Debug experiments** in development
|
||||||
|
- **Run pilots** without robot access
|
||||||
|
- **Develop** on any computer
|
||||||
|
|
||||||
|
## Understanding Simulation Options
|
||||||
|
|
||||||
|
HRIStudio offers two simulation approaches:
|
||||||
|
|
||||||
|
| Approach | Pros | Cons |
|
||||||
|
|----------|------|------|
|
||||||
|
| **Client-side** | No server needed, instant | Limited robot simulation |
|
||||||
|
| **Mock Server** | Full rosbridge protocol | Requires running server |
|
||||||
|
|
||||||
|
### Client-Side Simulation
|
||||||
|
|
||||||
|
Simulates robot locally in the browser:
|
||||||
|
- No network required
|
||||||
|
- Instant startup
|
||||||
|
- Basic action timing
|
||||||
|
- Fake sensor data
|
||||||
|
|
||||||
|
### Mock Server
|
||||||
|
|
||||||
|
Full WebSocket server simulating rosbridge:
|
||||||
|
- Complete protocol support
|
||||||
|
- Realistic timing
|
||||||
|
- Sensor data simulation
|
||||||
|
- Better for integration testing
|
||||||
|
|
||||||
|
## Step 1: Enable Client-Side Simulation
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. Create or edit `hristudio/.env.local`
|
||||||
|
2. Add:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_SIMULATION_MODE=true
|
||||||
|
```
|
||||||
|
3. Restart the dev server:
|
||||||
|
```bash
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Enabled
|
||||||
|
|
||||||
|
Look for the simulation indicator in the UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Wizard Interface [🔵 SIMULATION MODE] │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features Available
|
||||||
|
|
||||||
|
In simulation mode:
|
||||||
|
|
||||||
|
- ✅ All robot actions execute (simulated timing)
|
||||||
|
- ✅ Speech actions show estimated duration
|
||||||
|
- ✅ Movement actions track position
|
||||||
|
- ✅ Sensor data is simulated
|
||||||
|
- ✅ Trial execution works normally
|
||||||
|
- ❌ Real robot not controlled
|
||||||
|
- ❌ Physical interactions not possible
|
||||||
|
|
||||||
|
## Step 2: Start Mock Robot Server
|
||||||
|
|
||||||
|
For more complete testing, use the mock server:
|
||||||
|
|
||||||
|
### Option 1: Standalone Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hristudio/scripts/mock-robot
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Server starts on `ws://localhost:9090`
|
||||||
|
|
||||||
|
### Option 2: Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd nao6-hristudio-integration
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.mock.yml --profile mock up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Server Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Should show:
|
||||||
|
# CONTAINER ID IMAGE STATUS
|
||||||
|
# abc123def456 hristudio-mock-robot Up 2 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Connect to Mock Server
|
||||||
|
|
||||||
|
1. Go to the **NAO Test Page**: `/nao-test`
|
||||||
|
2. Ensure `NEXT_PUBLIC_SIMULATION_MODE` is NOT set (or set to false)
|
||||||
|
3. Click **Connect**
|
||||||
|
4. You should see:
|
||||||
|
```
|
||||||
|
Connected to rosbridge
|
||||||
|
Subscribed to: /joint_states, /bumper, /sonar/left, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Test Robot Actions
|
||||||
|
|
||||||
|
### From NAO Test Page
|
||||||
|
|
||||||
|
1. **Speech Test**
|
||||||
|
- Enter text: "Hello, this is a test"
|
||||||
|
- Click **Say**
|
||||||
|
- See simulated speech duration
|
||||||
|
|
||||||
|
2. **Movement Test**
|
||||||
|
- Set walk speed: 0.1 m/s
|
||||||
|
- Click **Walk Forward**
|
||||||
|
- Watch position update
|
||||||
|
|
||||||
|
3. **Head Control**
|
||||||
|
- Set yaw: 1.0, pitch: 0.0
|
||||||
|
- Click **Move Head**
|
||||||
|
- See joint angles update
|
||||||
|
|
||||||
|
### From Wizard Interface
|
||||||
|
|
||||||
|
1. Start a trial
|
||||||
|
2. Execute actions as normal
|
||||||
|
3. Actions are sent to mock server
|
||||||
|
4. Mock server responds with simulated data
|
||||||
|
|
||||||
|
## Step 5: Simulated Actions Reference
|
||||||
|
|
||||||
|
### Speech Actions
|
||||||
|
|
||||||
|
| Action | Simulation Behavior |
|
||||||
|
|--------|---------------------|
|
||||||
|
| `say_text` | Duration = 1.5s + 300ms × word_count |
|
||||||
|
| `say_with_emotion` | Duration = 1.5s + 300ms × word_count + emotion_overhead |
|
||||||
|
| `wave_goodbye` | Duration = 3.0s |
|
||||||
|
|
||||||
|
### Movement Actions
|
||||||
|
|
||||||
|
| Action | Simulation Behavior |
|
||||||
|
|--------|---------------------|
|
||||||
|
| `walk_forward` | Position updates over 500ms |
|
||||||
|
| `walk_backward` | Position updates over 500ms |
|
||||||
|
| `turn_left` | Angle decreases over 500ms |
|
||||||
|
| `turn_right` | Angle increases over 500ms |
|
||||||
|
| `stop` | Velocity set to 0 |
|
||||||
|
|
||||||
|
### Sensor Simulation
|
||||||
|
|
||||||
|
| Sensor | Simulated Value |
|
||||||
|
|--------|-----------------|
|
||||||
|
| Battery | 85% ± 2% variation |
|
||||||
|
| Joint States | Random positions ±0.1 rad |
|
||||||
|
| Bumper | False (no contact) |
|
||||||
|
| Sonar | 0.5-1.0m (random) |
|
||||||
|
| Touch | False (no touch) |
|
||||||
|
|
||||||
|
## Step 6: Testing Experiment Protocols
|
||||||
|
|
||||||
|
### Full Protocol Test
|
||||||
|
|
||||||
|
1. Enable simulation mode
|
||||||
|
2. Create or open experiment
|
||||||
|
3. Schedule trial
|
||||||
|
4. Start trial in wizard interface
|
||||||
|
5. Execute through all steps
|
||||||
|
6. Verify timing and flow
|
||||||
|
|
||||||
|
### Test Checklist
|
||||||
|
|
||||||
|
- [ ] All steps execute in order
|
||||||
|
- [ ] Branching decisions work
|
||||||
|
- [ ] Timing estimates are accurate
|
||||||
|
- [ ] Event log captures everything
|
||||||
|
- [ ] No errors or warnings
|
||||||
|
- [ ] Trial completes successfully
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable verbose logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In browser console, run:
|
||||||
|
localStorage.setItem('debug', 'true')
|
||||||
|
|
||||||
|
# Refresh page
|
||||||
|
# Now see detailed action logs in console
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Training Wizards
|
||||||
|
|
||||||
|
Simulation mode is perfect for training:
|
||||||
|
|
||||||
|
### Training Scenario 1: Basic Operation
|
||||||
|
|
||||||
|
1. Enable simulation mode
|
||||||
|
2. Load simple experiment
|
||||||
|
3. Practice:
|
||||||
|
- Starting/pausing trials
|
||||||
|
- Executing quick actions
|
||||||
|
- Adding notes
|
||||||
|
|
||||||
|
### Training Scenario 2: Decision Making
|
||||||
|
|
||||||
|
1. Load branching experiment
|
||||||
|
2. Practice:
|
||||||
|
- Observing participant cues
|
||||||
|
- Selecting appropriate branches
|
||||||
|
- Documenting decisions
|
||||||
|
|
||||||
|
### Training Scenario 3: Handling Issues
|
||||||
|
|
||||||
|
1. Practice:
|
||||||
|
- Pausing for breaks
|
||||||
|
- Responding to alerts
|
||||||
|
- Stopping trials early
|
||||||
|
|
||||||
|
## Step 8: Development Workflow
|
||||||
|
|
||||||
|
### TDD with Simulation
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Development Cycle │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Design experiment in UI │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 2. Enable simulation mode │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 3. Run test trial │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 4. Review event log │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 5. Fix issues found │
|
||||||
|
│ │ │
|
||||||
|
│ └────────────┐ │
|
||||||
|
│ │ │
|
||||||
|
│ └ (repeat) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Checklist
|
||||||
|
|
||||||
|
Before running real trials:
|
||||||
|
|
||||||
|
- [ ] Experiment works in simulation
|
||||||
|
- [ ] All actions execute correctly
|
||||||
|
- [ ] Timing is acceptable
|
||||||
|
- [ ] Branching works as expected
|
||||||
|
- [ ] Wizard notes function properly
|
||||||
|
- [ ] Data exports correctly
|
||||||
|
|
||||||
|
## Step 9: Transitioning to Real Robot
|
||||||
|
|
||||||
|
When ready to test with real robot:
|
||||||
|
|
||||||
|
### Step 1: Disable Simulation
|
||||||
|
|
||||||
|
Remove or set to false:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_SIMULATION_MODE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Connect Robot
|
||||||
|
|
||||||
|
1. Start Docker services
|
||||||
|
2. Verify robot connection
|
||||||
|
3. Test with NAO Test Page
|
||||||
|
|
||||||
|
### Step 3: Run Comparison Trial
|
||||||
|
|
||||||
|
1. Run same experiment on real robot
|
||||||
|
2. Compare timing and behavior
|
||||||
|
3. Adjust parameters as needed
|
||||||
|
|
||||||
|
### Step 4: Document Differences
|
||||||
|
|
||||||
|
Note any protocol adjustments needed:
|
||||||
|
- Timing differences
|
||||||
|
- Action parameter changes
|
||||||
|
- Branch criteria updates
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Simulation Actions Not Working
|
||||||
|
|
||||||
|
1. Check `NEXT_PUBLIC_SIMULATION_MODE=true` is set
|
||||||
|
2. Verify no errors in browser console
|
||||||
|
3. Try refreshing the page
|
||||||
|
|
||||||
|
### Mock Server Connection Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if server is running
|
||||||
|
docker ps | grep mock
|
||||||
|
|
||||||
|
# Check server logs
|
||||||
|
docker compose logs mock_robot
|
||||||
|
|
||||||
|
# Restart if needed
|
||||||
|
docker compose restart mock_robot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actions Execute But Nothing Happens
|
||||||
|
|
||||||
|
1. Check WebSocket URL is correct
|
||||||
|
2. Verify port 9090 is not blocked
|
||||||
|
3. Try client-side simulation instead
|
||||||
|
|
||||||
|
## Comparison: Simulation vs Real
|
||||||
|
|
||||||
|
| Aspect | Simulation | Real Robot |
|
||||||
|
|--------|------------|------------|
|
||||||
|
| Setup time | 1 min | 30+ min |
|
||||||
|
| Availability | Always | Requires robot |
|
||||||
|
| Cost | Free | Robot access needed |
|
||||||
|
| Timing accuracy | Estimated | Actual |
|
||||||
|
| Physical interaction | ✗ | ✓ |
|
||||||
|
| Sensor accuracy | Fake | Real |
|
||||||
|
| Network dependent | No | Yes |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### When to Use Simulation
|
||||||
|
|
||||||
|
- During experiment design
|
||||||
|
- While robot unavailable
|
||||||
|
- For wizard training
|
||||||
|
- For debugging protocols
|
||||||
|
- For quick iteration
|
||||||
|
|
||||||
|
### When to Use Real Robot
|
||||||
|
|
||||||
|
- Final protocol validation
|
||||||
|
- Timing accuracy critical
|
||||||
|
- Physical interaction matters
|
||||||
|
- Sensor data needed
|
||||||
|
- Pre-study pilot
|
||||||
|
|
||||||
|
### Transition Checklist
|
||||||
|
|
||||||
|
Before real trials:
|
||||||
|
- [ ] Protocol tested in simulation
|
||||||
|
- [ ] Timing verified
|
||||||
|
- [ ] Actions calibrated
|
||||||
|
- [ ] Wizard team trained
|
||||||
|
- [ ] Backup plan ready
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you've mastered simulation:
|
||||||
|
|
||||||
|
1. **[Robot Integration](06-robot-integration.md)** - Connect real robot
|
||||||
|
2. **[Running Trials](04-running-trials.md)** - Execute live trials
|
||||||
|
3. **[Your First Study](02-your-first-study.md)** - Run complete study
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Previous**: [Data & Analysis](08-data-and-analysis.md) | **Back**: [Tutorials Overview](../tutorials/README.md)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# HRIStudio Tutorials
|
||||||
|
|
||||||
|
Welcome to the HRIStudio tutorials! These guides will help you get up and running with the platform for your HRI research.
|
||||||
|
|
||||||
|
## Tutorial Overview
|
||||||
|
|
||||||
|
| Tutorial | Description | Time |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| **[Getting Started](tutorials/01-getting-started.md)** | Installation, setup, and first login | 10 min |
|
||||||
|
| **[Your First Study](tutorials/02-your-first-study.md)** | Creating a study and adding team members | 15 min |
|
||||||
|
| **[Designing Experiments](tutorials/03-designing-experiments.md)** | Building experiment protocols with blocks | 25 min |
|
||||||
|
| **[Running Trials](tutorials/04-running-trials.md)** | Executing trials and managing participants | 20 min |
|
||||||
|
| **[Wizard Interface](tutorials/05-wizard-interface.md)** | Real-time trial control and monitoring | 15 min |
|
||||||
|
| **[Robot Integration](tutorials/06-robot-integration.md)** | Connecting NAO6 and other robots | 20 min |
|
||||||
|
| **[Forms & Surveys](tutorials/07-forms-and-surveys.md)** | Creating consent forms and questionnaires | 15 min |
|
||||||
|
| **[Data & Analysis](tutorials/08-data-and-analysis.md)** | Collecting and exporting trial data | 15 min |
|
||||||
|
| **[Simulation Mode](tutorials/09-simulation-mode.md)** | Testing without a physical robot | 10 min |
|
||||||
|
|
||||||
|
## Quick Navigation
|
||||||
|
|
||||||
|
### For Researchers
|
||||||
|
1. [Getting Started](tutorials/01-getting-started.md) - Set up your environment
|
||||||
|
2. [Your First Study](tutorials/02-your-first-study.md) - Create your study
|
||||||
|
3. [Designing Experiments](tutorials/03-designing-experiments.md) - Build your protocol
|
||||||
|
4. [Running Trials](tutorials/04-running-trials.md) - Execute your study
|
||||||
|
5. [Data & Analysis](tutorials/08-data-and-analysis.md) - Analyze results
|
||||||
|
|
||||||
|
### For Wizards
|
||||||
|
1. [Getting Started](tutorials/01-getting-started.md) - Basic setup
|
||||||
|
2. [Wizard Interface](tutorials/05-wizard-interface.md) - Control trials
|
||||||
|
3. [Robot Integration](tutorials/06-robot-integration.md) - Connect to robot
|
||||||
|
|
||||||
|
### For Administrators
|
||||||
|
1. [Getting Started](tutorials/01-getting-started.md) - Full setup
|
||||||
|
2. [Robot Integration](tutorials/06-robot-integration.md) - Configure robots
|
||||||
|
3. [Forms & Surveys](tutorials/07-forms-and-surveys.md) - Manage templates
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Basic HRI Experiment
|
||||||
|
```
|
||||||
|
Create Study → Design Experiment → Add Participants → Run Trials → Collect Data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wizard-of-Oz Study
|
||||||
|
```
|
||||||
|
Create Study → Design Experiment with Wizard Blocks → Configure Robot →
|
||||||
|
Add Wizards → Run Trials with Live Control → Collect Data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pilot Testing
|
||||||
|
```
|
||||||
|
Create Study → Design Experiment → Enable Simulation Mode → Run Test Trials →
|
||||||
|
Refine Protocol → Connect Real Robot → Run Study
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **For local development**: Bun, Docker, PostgreSQL
|
||||||
|
- **For robot studies**: NAO6 robot or compatible robot
|
||||||
|
- **For cloud deployment**: Vercel, Cloudflare R2, PostgreSQL database
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Check the [Quick Reference](../quick-reference.md) for common commands
|
||||||
|
- Review the [Implementation Guide](../implementation-guide.md) for technical details
|
||||||
|
- Visit the [NAO6 Integration](../nao6-quick-reference.md) for robot-specific help
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next**: [Getting Started](tutorials/01-getting-started.md)
|
||||||
@@ -9,4 +9,5 @@ export default {
|
|||||||
url: env.DATABASE_URL,
|
url: env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
tablesFilter: ["hs_*"],
|
tablesFilter: ["hs_*"],
|
||||||
|
out: "./migrations",
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
+9
-37
@@ -1,55 +1,27 @@
|
|||||||
import type { Session } from "next-auth";
|
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "./src/server/auth";
|
|
||||||
|
|
||||||
export default auth((req: NextRequest & { auth: Session | null }) => {
|
export default async function middleware(request: NextRequest) {
|
||||||
const { nextUrl } = req;
|
const { nextUrl } = request;
|
||||||
const isLoggedIn = !!req.auth;
|
|
||||||
|
|
||||||
// Define route patterns
|
// Skip session checks for now to debug the auth issue
|
||||||
const isApiAuthRoute = nextUrl.pathname.startsWith("/api/auth");
|
const isApiRoute = nextUrl.pathname.startsWith("/api");
|
||||||
const isPublicRoute = ["/", "/auth/signin", "/auth/signup"].includes(
|
|
||||||
nextUrl.pathname,
|
|
||||||
);
|
|
||||||
const isAuthRoute = nextUrl.pathname.startsWith("/auth");
|
const isAuthRoute = nextUrl.pathname.startsWith("/auth");
|
||||||
|
|
||||||
// Allow API auth routes to pass through
|
if (isApiRoute) {
|
||||||
if (isApiAuthRoute) {
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user is on auth pages and already logged in, redirect to dashboard
|
// Allow auth routes through for now
|
||||||
if (isAuthRoute && isLoggedIn) {
|
if (isAuthRoute) {
|
||||||
return NextResponse.redirect(new URL("/", nextUrl));
|
return NextResponse.next();
|
||||||
}
|
|
||||||
|
|
||||||
// If user is not logged in and trying to access protected routes
|
|
||||||
if (!isLoggedIn && !isPublicRoute && !isAuthRoute) {
|
|
||||||
let callbackUrl = nextUrl.pathname;
|
|
||||||
if (nextUrl.search) {
|
|
||||||
callbackUrl += nextUrl.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedCallbackUrl = encodeURIComponent(callbackUrl);
|
|
||||||
return NextResponse.redirect(
|
|
||||||
new URL(`/auth/signin?callbackUrl=${encodedCallbackUrl}`, nextUrl),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
});
|
}
|
||||||
|
|
||||||
// Configure which routes the middleware should run on
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
/*
|
|
||||||
* Match all request paths except for the ones starting with:
|
|
||||||
* - _next/static (static files)
|
|
||||||
* - _next/image (image optimization files)
|
|
||||||
* - favicon.ico (favicon file)
|
|
||||||
* - public files (images, etc.)
|
|
||||||
*/
|
|
||||||
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,605 @@
|
|||||||
|
CREATE TYPE "public"."block_category" AS ENUM('wizard', 'robot', 'control', 'sensor', 'logic', 'event');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."block_shape" AS ENUM('action', 'control', 'value', 'boolean', 'hat', 'cap');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."communication_protocol" AS ENUM('rest', 'ros2', 'custom');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."experiment_status" AS ENUM('draft', 'testing', 'ready', 'deprecated');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."export_status" AS ENUM('pending', 'processing', 'completed', 'failed');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."form_field_type" AS ENUM('text', 'textarea', 'multiple_choice', 'checkbox', 'rating', 'yes_no', 'date', 'signature');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."form_response_status" AS ENUM('pending', 'completed', 'rejected');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."form_type" AS ENUM('consent', 'survey', 'questionnaire');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."media_type" AS ENUM('video', 'audio', 'image');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."plugin_status" AS ENUM('active', 'deprecated', 'disabled');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."step_type" AS ENUM('wizard', 'robot', 'parallel', 'conditional');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."study_member_role" AS ENUM('owner', 'researcher', 'wizard', 'observer');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."study_status" AS ENUM('draft', 'active', 'completed', 'archived');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."system_role" AS ENUM('administrator', 'researcher', 'wizard', 'observer');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."trial_status" AS ENUM('scheduled', 'in_progress', 'completed', 'aborted', 'failed');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."trust_level" AS ENUM('official', 'verified', 'community');--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_account" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"provider_id" varchar(255) NOT NULL,
|
||||||
|
"account_id" varchar(255) NOT NULL,
|
||||||
|
"refresh_token" text,
|
||||||
|
"access_token" text,
|
||||||
|
"expires_at" timestamp with time zone,
|
||||||
|
"scope" varchar(255),
|
||||||
|
"password" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_account_provider_id_account_id_unique" UNIQUE("provider_id","account_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_action" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"step_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"type" varchar(100) NOT NULL,
|
||||||
|
"order_index" integer NOT NULL,
|
||||||
|
"parameters" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"validation_schema" jsonb,
|
||||||
|
"timeout" integer,
|
||||||
|
"retry_count" integer DEFAULT 0 NOT NULL,
|
||||||
|
"source_kind" varchar(20),
|
||||||
|
"plugin_id" varchar(255),
|
||||||
|
"plugin_version" varchar(50),
|
||||||
|
"robot_id" varchar(255),
|
||||||
|
"base_action_id" varchar(255),
|
||||||
|
"category" varchar(50),
|
||||||
|
"transport" varchar(20),
|
||||||
|
"ros2_config" jsonb,
|
||||||
|
"rest_config" jsonb,
|
||||||
|
"retryable" boolean,
|
||||||
|
"parameter_schema_raw" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_action_step_id_order_index_unique" UNIQUE("step_id","order_index")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_activity_log" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid,
|
||||||
|
"user_id" text,
|
||||||
|
"action" varchar(100) NOT NULL,
|
||||||
|
"resource_type" varchar(50),
|
||||||
|
"resource_id" uuid,
|
||||||
|
"description" text,
|
||||||
|
"ip_address" "inet",
|
||||||
|
"user_agent" text,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_annotation" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"annotator_id" text NOT NULL,
|
||||||
|
"timestamp_start" timestamp with time zone NOT NULL,
|
||||||
|
"timestamp_end" timestamp with time zone,
|
||||||
|
"category" varchar(100),
|
||||||
|
"label" varchar(100),
|
||||||
|
"description" text,
|
||||||
|
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_attachment" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"resource_type" varchar(50) NOT NULL,
|
||||||
|
"resource_id" uuid NOT NULL,
|
||||||
|
"file_name" varchar(255) NOT NULL,
|
||||||
|
"file_size" bigint NOT NULL,
|
||||||
|
"file_path" text NOT NULL,
|
||||||
|
"content_type" varchar(100),
|
||||||
|
"description" text,
|
||||||
|
"uploaded_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_audit_log" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" text,
|
||||||
|
"action" varchar(100) NOT NULL,
|
||||||
|
"resource_type" varchar(50),
|
||||||
|
"resource_id" uuid,
|
||||||
|
"changes" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"ip_address" "inet",
|
||||||
|
"user_agent" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_block_registry" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"block_type" varchar(100) NOT NULL,
|
||||||
|
"plugin_id" uuid,
|
||||||
|
"shape" "block_shape" NOT NULL,
|
||||||
|
"category" "block_category" NOT NULL,
|
||||||
|
"display_name" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"icon" varchar(100),
|
||||||
|
"color" varchar(50),
|
||||||
|
"config" jsonb NOT NULL,
|
||||||
|
"parameter_schema" jsonb NOT NULL,
|
||||||
|
"execution_handler" varchar(100),
|
||||||
|
"timeout" integer,
|
||||||
|
"retry_policy" jsonb,
|
||||||
|
"requires_connection" boolean DEFAULT false,
|
||||||
|
"preview_mode" boolean DEFAULT true,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_block_registry_block_type_plugin_id_unique" UNIQUE("block_type","plugin_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_comment" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"parent_id" uuid,
|
||||||
|
"resource_type" varchar(50) NOT NULL,
|
||||||
|
"resource_id" uuid NOT NULL,
|
||||||
|
"author_id" text NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_consent_form" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"version" integer DEFAULT 1 NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"storage_path" text,
|
||||||
|
CONSTRAINT "hs_consent_form_study_id_version_unique" UNIQUE("study_id","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_experiment" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"version" integer DEFAULT 1 NOT NULL,
|
||||||
|
"robot_id" uuid,
|
||||||
|
"status" "experiment_status" DEFAULT 'draft' NOT NULL,
|
||||||
|
"estimated_duration" integer,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"visual_design" jsonb,
|
||||||
|
"execution_graph" jsonb,
|
||||||
|
"plugin_dependencies" text[],
|
||||||
|
"integrity_hash" varchar(128),
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
CONSTRAINT "hs_experiment_study_id_name_version_unique" UNIQUE("study_id","name","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_export_job" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"requested_by" text NOT NULL,
|
||||||
|
"export_type" varchar(50) NOT NULL,
|
||||||
|
"format" varchar(20) NOT NULL,
|
||||||
|
"filters" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"status" "export_status" DEFAULT 'pending' NOT NULL,
|
||||||
|
"storage_path" text,
|
||||||
|
"expires_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"completed_at" timestamp with time zone,
|
||||||
|
"error_message" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_form_response" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"form_id" uuid NOT NULL,
|
||||||
|
"participant_id" uuid NOT NULL,
|
||||||
|
"responses" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"status" "form_response_status" DEFAULT 'pending',
|
||||||
|
"signature_data" text,
|
||||||
|
"signed_at" timestamp with time zone,
|
||||||
|
"ip_address" "inet",
|
||||||
|
"submitted_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_form_response_form_id_participant_id_unique" UNIQUE("form_id","participant_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_form" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"type" "form_type" NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"version" integer DEFAULT 1 NOT NULL,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"is_template" boolean DEFAULT false NOT NULL,
|
||||||
|
"template_name" varchar(100),
|
||||||
|
"fields" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"settings" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_form_study_id_version_unique" UNIQUE("study_id","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_media_capture" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"media_type" "media_type",
|
||||||
|
"storage_path" text NOT NULL,
|
||||||
|
"file_size" bigint,
|
||||||
|
"duration" integer,
|
||||||
|
"format" varchar(20),
|
||||||
|
"resolution" varchar(20),
|
||||||
|
"start_timestamp" timestamp with time zone,
|
||||||
|
"end_timestamp" timestamp with time zone,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_participant_consent" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"participant_id" uuid NOT NULL,
|
||||||
|
"consent_form_id" uuid NOT NULL,
|
||||||
|
"signed_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"signature_data" text,
|
||||||
|
"ip_address" "inet",
|
||||||
|
"storage_path" text,
|
||||||
|
CONSTRAINT "hs_participant_consent_participant_id_consent_form_id_unique" UNIQUE("participant_id","consent_form_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_participant_document" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"participant_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"type" varchar(100),
|
||||||
|
"storage_path" text NOT NULL,
|
||||||
|
"file_size" integer,
|
||||||
|
"uploaded_by" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_participant" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"participant_code" varchar(50) NOT NULL,
|
||||||
|
"email" varchar(255),
|
||||||
|
"name" varchar(255),
|
||||||
|
"demographics" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"consent_given" boolean DEFAULT false NOT NULL,
|
||||||
|
"consent_date" timestamp with time zone,
|
||||||
|
"notes" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_participant_study_id_participant_code_unique" UNIQUE("study_id","participant_code")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_permission" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"resource" varchar(50) NOT NULL,
|
||||||
|
"action" varchar(50) NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_permission_name_unique" UNIQUE("name")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_plugin_repository" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"url" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"trust_level" "trust_level" DEFAULT 'community' NOT NULL,
|
||||||
|
"is_enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"is_official" boolean DEFAULT false NOT NULL,
|
||||||
|
"last_sync_at" timestamp with time zone,
|
||||||
|
"sync_status" varchar(50) DEFAULT 'pending',
|
||||||
|
"sync_error" text,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
CONSTRAINT "hs_plugin_repository_url_unique" UNIQUE("url")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_plugin" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"robot_id" uuid,
|
||||||
|
"identifier" varchar(100) NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"version" varchar(50) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"author" varchar(255),
|
||||||
|
"repository_url" text,
|
||||||
|
"trust_level" "trust_level",
|
||||||
|
"status" "plugin_status" DEFAULT 'active' NOT NULL,
|
||||||
|
"configuration_schema" jsonb,
|
||||||
|
"action_definitions" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
CONSTRAINT "hs_plugin_identifier_unique" UNIQUE("identifier"),
|
||||||
|
CONSTRAINT "hs_plugin_name_version_unique" UNIQUE("name","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_robot_plugin" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"version" varchar(50) NOT NULL,
|
||||||
|
"manufacturer" varchar(255),
|
||||||
|
"description" text,
|
||||||
|
"robot_id" uuid,
|
||||||
|
"communication_protocol" "communication_protocol",
|
||||||
|
"status" "plugin_status" DEFAULT 'active' NOT NULL,
|
||||||
|
"config_schema" jsonb,
|
||||||
|
"capabilities" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"trust_level" "trust_level" DEFAULT 'community' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_robot" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"manufacturer" varchar(255),
|
||||||
|
"model" varchar(255),
|
||||||
|
"description" text,
|
||||||
|
"capabilities" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"communication_protocol" "communication_protocol",
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_role_permission" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"role" "system_role" NOT NULL,
|
||||||
|
"permission_id" uuid NOT NULL,
|
||||||
|
CONSTRAINT "hs_role_permission_role_permission_id_unique" UNIQUE("role","permission_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_sensor_data" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"sensor_type" varchar(50) NOT NULL,
|
||||||
|
"timestamp" timestamp with time zone NOT NULL,
|
||||||
|
"data" jsonb NOT NULL,
|
||||||
|
"robot_state" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_session" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"token" varchar(255) NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"expires_at" timestamp with time zone NOT NULL,
|
||||||
|
"ip_address" text,
|
||||||
|
"user_agent" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_session_token_unique" UNIQUE("token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_shared_resource" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"resource_type" varchar(50) NOT NULL,
|
||||||
|
"resource_id" uuid NOT NULL,
|
||||||
|
"shared_by" text NOT NULL,
|
||||||
|
"share_token" varchar(255),
|
||||||
|
"permissions" jsonb DEFAULT '["read"]'::jsonb,
|
||||||
|
"expires_at" timestamp with time zone,
|
||||||
|
"access_count" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_shared_resource_share_token_unique" UNIQUE("share_token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_step" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"experiment_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"type" "step_type" NOT NULL,
|
||||||
|
"order_index" integer NOT NULL,
|
||||||
|
"duration_estimate" integer,
|
||||||
|
"required" boolean DEFAULT true NOT NULL,
|
||||||
|
"conditions" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_step_experiment_id_order_index_unique" UNIQUE("experiment_id","order_index")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_study" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"institution" varchar(255),
|
||||||
|
"irb_protocol" varchar(100),
|
||||||
|
"status" "study_status" DEFAULT 'draft' NOT NULL,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"settings" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_study_member" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"role" "study_member_role" NOT NULL,
|
||||||
|
"permissions" jsonb DEFAULT '[]'::jsonb,
|
||||||
|
"joined_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"invited_by" text,
|
||||||
|
CONSTRAINT "hs_study_member_study_id_user_id_unique" UNIQUE("study_id","user_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_study_plugin" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"study_id" uuid NOT NULL,
|
||||||
|
"plugin_id" uuid NOT NULL,
|
||||||
|
"configuration" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"installed_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"installed_by" text NOT NULL,
|
||||||
|
CONSTRAINT "hs_study_plugin_study_id_plugin_id_unique" UNIQUE("study_id","plugin_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_system_setting" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"key" varchar(100) NOT NULL,
|
||||||
|
"value" jsonb NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"updated_by" text,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_system_setting_key_unique" UNIQUE("key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_trial_event" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"event_type" varchar(50) NOT NULL,
|
||||||
|
"action_id" uuid,
|
||||||
|
"timestamp" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"data" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_by" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_trial" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"experiment_id" uuid NOT NULL,
|
||||||
|
"participant_id" uuid,
|
||||||
|
"wizard_id" text,
|
||||||
|
"session_number" integer DEFAULT 1 NOT NULL,
|
||||||
|
"status" "trial_status" DEFAULT 'scheduled' NOT NULL,
|
||||||
|
"scheduled_at" timestamp with time zone,
|
||||||
|
"started_at" timestamp with time zone,
|
||||||
|
"completed_at" timestamp with time zone,
|
||||||
|
"duration" integer,
|
||||||
|
"notes" text,
|
||||||
|
"parameters" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_user_system_role" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"role" "system_role" NOT NULL,
|
||||||
|
"granted_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"granted_by" text,
|
||||||
|
CONSTRAINT "hs_user_system_role_user_id_role_unique" UNIQUE("user_id","role")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_user" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar(255),
|
||||||
|
"email" varchar(255) NOT NULL,
|
||||||
|
"email_verified" boolean DEFAULT false NOT NULL,
|
||||||
|
"image" text,
|
||||||
|
"password" varchar(255),
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone,
|
||||||
|
CONSTRAINT "hs_user_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_verification_token" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"identifier" varchar(255) NOT NULL,
|
||||||
|
"value" varchar(255) NOT NULL,
|
||||||
|
"expires_at" timestamp with time zone NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_verification_token_value_unique" UNIQUE("value")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_wizard_intervention" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"wizard_id" text NOT NULL,
|
||||||
|
"intervention_type" varchar(100) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"timestamp" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
"parameters" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"reason" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "hs_ws_connection" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"trial_id" uuid NOT NULL,
|
||||||
|
"client_id" text NOT NULL,
|
||||||
|
"user_id" text,
|
||||||
|
"connected_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT "hs_ws_connection_client_id_unique" UNIQUE("client_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_account" ADD CONSTRAINT "hs_account_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_action" ADD CONSTRAINT "hs_action_step_id_hs_step_id_fk" FOREIGN KEY ("step_id") REFERENCES "public"."hs_step"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_activity_log" ADD CONSTRAINT "hs_activity_log_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_activity_log" ADD CONSTRAINT "hs_activity_log_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_annotation" ADD CONSTRAINT "hs_annotation_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_annotation" ADD CONSTRAINT "hs_annotation_annotator_id_hs_user_id_fk" FOREIGN KEY ("annotator_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_attachment" ADD CONSTRAINT "hs_attachment_uploaded_by_hs_user_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_audit_log" ADD CONSTRAINT "hs_audit_log_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_block_registry" ADD CONSTRAINT "hs_block_registry_plugin_id_hs_robot_plugin_id_fk" FOREIGN KEY ("plugin_id") REFERENCES "public"."hs_robot_plugin"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_comment" ADD CONSTRAINT "hs_comment_author_id_hs_user_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_consent_form" ADD CONSTRAINT "hs_consent_form_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_consent_form" ADD CONSTRAINT "hs_consent_form_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_experiment" ADD CONSTRAINT "hs_experiment_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_experiment" ADD CONSTRAINT "hs_experiment_robot_id_hs_robot_id_fk" FOREIGN KEY ("robot_id") REFERENCES "public"."hs_robot"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_experiment" ADD CONSTRAINT "hs_experiment_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_export_job" ADD CONSTRAINT "hs_export_job_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_export_job" ADD CONSTRAINT "hs_export_job_requested_by_hs_user_id_fk" FOREIGN KEY ("requested_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_form_response" ADD CONSTRAINT "hs_form_response_form_id_hs_form_id_fk" FOREIGN KEY ("form_id") REFERENCES "public"."hs_form"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_form_response" ADD CONSTRAINT "hs_form_response_participant_id_hs_participant_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."hs_participant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_form" ADD CONSTRAINT "hs_form_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_form" ADD CONSTRAINT "hs_form_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_media_capture" ADD CONSTRAINT "hs_media_capture_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_participant_consent" ADD CONSTRAINT "hs_participant_consent_participant_id_hs_participant_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."hs_participant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_participant_consent" ADD CONSTRAINT "hs_participant_consent_consent_form_id_hs_consent_form_id_fk" FOREIGN KEY ("consent_form_id") REFERENCES "public"."hs_consent_form"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_participant_document" ADD CONSTRAINT "hs_participant_document_participant_id_hs_participant_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."hs_participant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_participant_document" ADD CONSTRAINT "hs_participant_document_uploaded_by_hs_user_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_participant" ADD CONSTRAINT "hs_participant_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_plugin_repository" ADD CONSTRAINT "hs_plugin_repository_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_plugin" ADD CONSTRAINT "hs_plugin_robot_id_hs_robot_id_fk" FOREIGN KEY ("robot_id") REFERENCES "public"."hs_robot"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_robot_plugin" ADD CONSTRAINT "hs_robot_plugin_robot_id_hs_robot_id_fk" FOREIGN KEY ("robot_id") REFERENCES "public"."hs_robot"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_role_permission" ADD CONSTRAINT "hs_role_permission_permission_id_hs_permission_id_fk" FOREIGN KEY ("permission_id") REFERENCES "public"."hs_permission"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_sensor_data" ADD CONSTRAINT "hs_sensor_data_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_session" ADD CONSTRAINT "hs_session_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_shared_resource" ADD CONSTRAINT "hs_shared_resource_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_shared_resource" ADD CONSTRAINT "hs_shared_resource_shared_by_hs_user_id_fk" FOREIGN KEY ("shared_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_step" ADD CONSTRAINT "hs_step_experiment_id_hs_experiment_id_fk" FOREIGN KEY ("experiment_id") REFERENCES "public"."hs_experiment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study" ADD CONSTRAINT "hs_study_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_member" ADD CONSTRAINT "hs_study_member_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_member" ADD CONSTRAINT "hs_study_member_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_member" ADD CONSTRAINT "hs_study_member_invited_by_hs_user_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_plugin" ADD CONSTRAINT "hs_study_plugin_study_id_hs_study_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."hs_study"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_plugin" ADD CONSTRAINT "hs_study_plugin_plugin_id_hs_plugin_id_fk" FOREIGN KEY ("plugin_id") REFERENCES "public"."hs_plugin"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_study_plugin" ADD CONSTRAINT "hs_study_plugin_installed_by_hs_user_id_fk" FOREIGN KEY ("installed_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_system_setting" ADD CONSTRAINT "hs_system_setting_updated_by_hs_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial_event" ADD CONSTRAINT "hs_trial_event_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial_event" ADD CONSTRAINT "hs_trial_event_action_id_hs_action_id_fk" FOREIGN KEY ("action_id") REFERENCES "public"."hs_action"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial_event" ADD CONSTRAINT "hs_trial_event_created_by_hs_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial" ADD CONSTRAINT "hs_trial_experiment_id_hs_experiment_id_fk" FOREIGN KEY ("experiment_id") REFERENCES "public"."hs_experiment"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial" ADD CONSTRAINT "hs_trial_participant_id_hs_participant_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."hs_participant"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_trial" ADD CONSTRAINT "hs_trial_wizard_id_hs_user_id_fk" FOREIGN KEY ("wizard_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_user_system_role" ADD CONSTRAINT "hs_user_system_role_user_id_hs_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."hs_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_user_system_role" ADD CONSTRAINT "hs_user_system_role_granted_by_hs_user_id_fk" FOREIGN KEY ("granted_by") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_wizard_intervention" ADD CONSTRAINT "hs_wizard_intervention_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_wizard_intervention" ADD CONSTRAINT "hs_wizard_intervention_wizard_id_hs_user_id_fk" FOREIGN KEY ("wizard_id") REFERENCES "public"."hs_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "hs_ws_connection" ADD CONSTRAINT "hs_ws_connection_trial_id_hs_trial_id_fk" FOREIGN KEY ("trial_id") REFERENCES "public"."hs_trial"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "account_user_id_idx" ON "hs_account" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_logs_study_created_idx" ON "hs_activity_log" USING btree ("study_id","created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_logs_created_idx" ON "hs_audit_log" USING btree ("created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "block_registry_category_idx" ON "hs_block_registry" USING btree ("category");--> statement-breakpoint
|
||||||
|
CREATE INDEX "experiment_visual_design_idx" ON "hs_experiment" USING gin ("visual_design");--> statement-breakpoint
|
||||||
|
CREATE INDEX "participant_document_participant_idx" ON "hs_participant_document" USING btree ("participant_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "sensor_data_trial_timestamp_idx" ON "hs_sensor_data" USING btree ("trial_id","timestamp");--> statement-breakpoint
|
||||||
|
CREATE INDEX "session_user_id_idx" ON "hs_session" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "trial_events_trial_timestamp_idx" ON "hs_trial_event" USING btree ("trial_id","timestamp");--> statement-breakpoint
|
||||||
|
CREATE INDEX "verification_token_identifier_idx" ON "hs_verification_token" USING btree ("identifier");--> statement-breakpoint
|
||||||
|
CREATE INDEX "verification_token_value_idx" ON "hs_verification_token" USING btree ("value");
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- Migration 0001: Minimal Seed Data
|
||||||
|
-- HRIStudio - Only essential data needed for auth
|
||||||
|
|
||||||
|
-- ======================
|
||||||
|
-- USERS & AUTH
|
||||||
|
-- ======================
|
||||||
|
|
||||||
|
-- Users (using valid UUID v4 format)
|
||||||
|
INSERT INTO "hs_user" ("id", "name", "email", "email_verified", "image", "created_at", "updated_at")
|
||||||
|
VALUES
|
||||||
|
('11111111-1111-4111-8111-111111111111', 'Sean O''Connor', 'sean@soconnor.dev', true, 'https://www.gravatar.com/avatar/4b20f4a15f9a0e0f5e5e5a0f5e5e5a0f?d=identicon', NOW(), NOW()),
|
||||||
|
('22222222-2222-4222-8222-222222222222', 'Dr. Felipe Perrone', 'felipe.perrone@bucknell.edu', true, 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe', NOW(), NOW())
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Accounts
|
||||||
|
INSERT INTO "hs_account" ("id", "user_id", "provider_id", "account_id", "password", "created_at", "updated_at")
|
||||||
|
VALUES
|
||||||
|
('aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa', '11111111-1111-4111-8111-111111111111', 'credential', '11111111-1111-4111-8111-111111111111', '$2b$12$50kgpkp.qZrZXCWjHuVSHOZBjAQUrX50VdtWc6WBj27HnzUYFwwPm', NOW(), NOW()),
|
||||||
|
('bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb', '22222222-2222-4222-8222-222222222222', 'credential', '22222222-2222-4222-8222-222222222222', '$2b$12$50kgpkp.qZrZXCWjHuVSHOZBjAQUrX50VdtWc6WBj27HnzUYFwwPm', NOW(), NOW())
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- System Roles
|
||||||
|
INSERT INTO "hs_user_system_role" ("id", "user_id", "role", "granted_at", "granted_by")
|
||||||
|
VALUES
|
||||||
|
(gen_random_uuid(), '11111111-1111-4111-8111-111111111111', 'administrator', NOW(), '11111111-1111-4111-8111-111111111111'),
|
||||||
|
(gen_random_uuid(), '22222222-2222-4222-8222-222222222222', 'researcher', NOW(), '11111111-1111-4111-8111-111111111111')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Minimal seed migration complete';
|
||||||
|
RAISE NOTICE 'Admin: sean@soconnor.dev / password123';
|
||||||
|
RAISE NOTICE 'Use bun db:seed for full demo data';
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 0,
|
||||||
|
"tag": "0000_init_schema",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1,
|
||||||
|
"tag": "0001_seed_data",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+5
-2
@@ -5,6 +5,9 @@
|
|||||||
import "./src/env.js";
|
import "./src/env.js";
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {};
|
const nextConfig = {
|
||||||
|
// Mark server-only packages as external to prevent bundling in client
|
||||||
|
serverExternalPackages: ["postgres", "minio", "child_process"],
|
||||||
|
};
|
||||||
|
|
||||||
export default config;
|
export default nextConfig;
|
||||||
|
|||||||
+25
-14
@@ -5,27 +5,33 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"check": "next lint && tsc --noEmit",
|
"check": "eslint . && tsc --noEmit",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:seed": "bun db:push && bun scripts/seed-dev.ts",
|
"db:seed": "bun db:push && bun scripts/seed-dev.ts",
|
||||||
"dev": "next dev --turbo",
|
"db:reset": "docker compose rm -s -f -v db && docker compose up -d db && sleep 2 && bun db:seed",
|
||||||
|
"db:restart": "docker compose restart db",
|
||||||
|
"dev": "bun run dev:ws & next dev",
|
||||||
|
"dev:ws": "bun run ws-server.ts",
|
||||||
"docker:up": "if [ \"$(uname)\" = \"Darwin\" ]; then colima start; fi && docker compose up -d",
|
"docker:up": "if [ \"$(uname)\" = \"Darwin\" ]; then colima start; fi && docker compose up -d",
|
||||||
"docker:down": "docker compose down && if [ \"$(uname)\" = \"Darwin\" ]; then colima stop; fi",
|
"docker:down": "docker compose down && if [ \"$(uname)\" = \"Darwin\" ]; then colima stop; fi",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"lint": "next lint",
|
"lint": "eslint .",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
"start": "next start",
|
"start": "bun run start:ws & next start",
|
||||||
|
"start:ws": "bun run ws-server.ts",
|
||||||
|
"start:web": "next start",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^1.11.1",
|
"@auth/drizzle-adapter": "^1.11.1",
|
||||||
"@aws-sdk/client-s3": "^3.989.0",
|
"@aws-sdk/client-s3": "^3.989.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.989.0",
|
"@aws-sdk/s3-request-presigner": "^3.989.0",
|
||||||
|
"@better-auth/drizzle-adapter": "^1.5.5",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -67,24 +73,25 @@
|
|||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"better-auth": "^1.5.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"driver.js": "^1.4.0",
|
"driver.js": "^1.4.0",
|
||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.45.2",
|
||||||
"html2pdf.js": "^0.14.0",
|
"html2pdf.js": "^0.14.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.536.0",
|
"lucide-react": "^0.536.0",
|
||||||
"minio": "^8.0.6",
|
"minio": "^8.0.6",
|
||||||
"next": "^16.1.6",
|
"next": "16.2.6",
|
||||||
"next-auth": "^5.0.0-beta.30",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
@@ -106,12 +113,12 @@
|
|||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^20.19.33",
|
"@types/node": "^20.19.33",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-next": "^15.5.12",
|
"eslint-config-next": "16.2.1",
|
||||||
"eslint-plugin-drizzle": "^0.2.3",
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
@@ -131,5 +138,9 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"sharp",
|
"sharp",
|
||||||
"unrs-resolver"
|
"unrs-resolver"
|
||||||
]
|
],
|
||||||
}
|
"overrides": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Homepage Screenshots
|
||||||
|
|
||||||
|
Add your app screenshots here. The homepage will display them automatically.
|
||||||
|
|
||||||
|
## Required Screenshots
|
||||||
|
|
||||||
|
1. **experiment-designer.png** - Visual experiment designer showing block-based workflow
|
||||||
|
2. **wizard-interface.png** - Wizard execution interface with trial controls
|
||||||
|
3. **dashboard.png** - Study dashboard showing experiments and trials
|
||||||
|
|
||||||
|
## Recommended Size
|
||||||
|
|
||||||
|
- Width: 1200px
|
||||||
|
- Format: PNG or WebP
|
||||||
|
- Quality: High (screenshot at 2x for retina displays)
|
||||||
|
|
||||||
|
## Preview in Browser
|
||||||
|
|
||||||
|
After adding screenshots, uncomment the `<Image>` component in `src/app/page.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Image
|
||||||
|
src={screenshot.src}
|
||||||
|
alt={screenshot.alt}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
```
|
||||||
+1
-1
Submodule robot-plugins updated: f83a207b16...8334b809f2
@@ -564,6 +564,7 @@ async function seedNAO6Plugin() {
|
|||||||
|
|
||||||
const pluginData: InsertPlugin = {
|
const pluginData: InsertPlugin = {
|
||||||
robotId: robotId,
|
robotId: robotId,
|
||||||
|
identifier: "nao6-ros2",
|
||||||
name: "NAO6 Robot (Enhanced ROS2 Integration)",
|
name: "NAO6 Robot (Enhanced ROS2 Integration)",
|
||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import * as schema from "../../src/server/db/schema";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
const connectionString = process.env.DATABASE_URL!;
|
||||||
|
const connection = postgres(connectionString);
|
||||||
|
const db = drizzle(connection, { schema });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("🌱 Seeding 'Story: Red Rock' experiment...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Find Admin User & Study
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: (users, { eq }) => eq(users.email, "sean@soconnor.dev"),
|
||||||
|
});
|
||||||
|
if (!user) throw new Error("Admin user 'sean@soconnor.dev' not found.");
|
||||||
|
|
||||||
|
const study = await db.query.studies.findFirst({
|
||||||
|
where: (studies, { eq }) => eq(studies.name, "Comparative WoZ Study"),
|
||||||
|
});
|
||||||
|
if (!study) throw new Error("Study 'Comparative WoZ Study' not found.");
|
||||||
|
|
||||||
|
const robot = await db.query.robots.findFirst({
|
||||||
|
where: (robots, { eq }) => eq(robots.name, "NAO6"),
|
||||||
|
});
|
||||||
|
if (!robot) throw new Error("Robot 'NAO6' not found.");
|
||||||
|
|
||||||
|
// 2. Create Experiment
|
||||||
|
const [experiment] = await db
|
||||||
|
.insert(schema.experiments)
|
||||||
|
.values({
|
||||||
|
studyId: study.id,
|
||||||
|
name: "Story: Red Rock",
|
||||||
|
description:
|
||||||
|
"A story about a red rock on Mars with comprehension check and branching.",
|
||||||
|
version: 1,
|
||||||
|
status: "draft",
|
||||||
|
robotId: robot.id,
|
||||||
|
createdBy: user.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!experiment) throw new Error("Failed to create experiment");
|
||||||
|
console.log(`✅ Created Experiment: ${experiment.id}`);
|
||||||
|
|
||||||
|
// 3. Create Steps (in reverse for ID references if needed, but we'll use uuid placeholders)
|
||||||
|
const conclusionId = uuidv4();
|
||||||
|
const branchAId = uuidv4();
|
||||||
|
const branchBId = uuidv4();
|
||||||
|
const checkId = uuidv4();
|
||||||
|
|
||||||
|
// Step 1: The Hook
|
||||||
|
const [step1] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "The Hook",
|
||||||
|
type: "wizard",
|
||||||
|
orderIndex: 0,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Step 2: The Narrative
|
||||||
|
const [step2] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "The Narrative",
|
||||||
|
type: "wizard",
|
||||||
|
orderIndex: 1,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Step 3: Comprehension Check (Conditional)
|
||||||
|
const [step3] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
id: checkId,
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "Comprehension Check",
|
||||||
|
type: "conditional",
|
||||||
|
orderIndex: 2,
|
||||||
|
conditions: {
|
||||||
|
variable: "last_wizard_response",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Answer: Red (Correct)",
|
||||||
|
value: "Red",
|
||||||
|
variant: "default",
|
||||||
|
nextStepId: branchAId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Answer: Other (Incorrect)",
|
||||||
|
value: "Incorrect",
|
||||||
|
variant: "destructive",
|
||||||
|
nextStepId: branchBId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Step 4: Branch A (Correct)
|
||||||
|
const [step4] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
id: branchAId,
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "Branch A: Correct Response",
|
||||||
|
type: "wizard",
|
||||||
|
orderIndex: 3,
|
||||||
|
conditions: { nextStepId: conclusionId }, // SKIP BRANCH B
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Step 5: Branch B (Incorrect)
|
||||||
|
const [step5] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
id: branchBId,
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "Branch B: Incorrect Response",
|
||||||
|
type: "wizard",
|
||||||
|
orderIndex: 4,
|
||||||
|
conditions: { nextStepId: conclusionId },
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Step 6: Conclusion
|
||||||
|
const [step6] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
id: conclusionId,
|
||||||
|
experimentId: experiment.id,
|
||||||
|
name: "Conclusion",
|
||||||
|
type: "wizard",
|
||||||
|
orderIndex: 5,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// 4. Create Actions
|
||||||
|
|
||||||
|
// The Hook
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step1!.id,
|
||||||
|
name: "Say Hello",
|
||||||
|
type: "nao6-ros2.say_text",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: { text: "Hello! Are you ready for a story?" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step1!.id,
|
||||||
|
name: "Wave",
|
||||||
|
type: "nao6-ros2.move_arm",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: { arm: "right", shoulder_pitch: 0.5 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// The Narrative
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step2!.id,
|
||||||
|
name: "The Story",
|
||||||
|
type: "nao6-ros2.say_text",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: {
|
||||||
|
text: "Once, a traveler went to Mars. He found a bright red rock that glowed.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step2!.id,
|
||||||
|
name: "Look Left",
|
||||||
|
type: "nao6-ros2.turn_head",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: { yaw: 0.5, speed: 0.3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step2!.id,
|
||||||
|
name: "Look Right",
|
||||||
|
type: "nao6-ros2.turn_head",
|
||||||
|
orderIndex: 2,
|
||||||
|
parameters: { yaw: -0.5, speed: 0.3 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Comprehension Check
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step3!.id,
|
||||||
|
name: "Ask Color",
|
||||||
|
type: "nao6-ros2.say_text",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: { text: "What color was the rock I found on Mars?" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step3!.id,
|
||||||
|
name: "Wait for Color",
|
||||||
|
type: "wizard_wait_for_response",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: {
|
||||||
|
options: ["Red", "Blue", "Green", "Incorrect"],
|
||||||
|
prompt_text: "What color did the participant say?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Branch A (Using say_with_emotion)
|
||||||
|
await db
|
||||||
|
.insert(schema.actions)
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
stepId: step4!.id,
|
||||||
|
name: "Happy Response",
|
||||||
|
type: "nao6-ros2.say_with_emotion",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: {
|
||||||
|
text: "Exacty! It was a glowing red rock.",
|
||||||
|
emotion: "happy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Branch B
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step5!.id,
|
||||||
|
name: "Correct them",
|
||||||
|
type: "nao6-ros2.say_text",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: { text: "Actually, it was red." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step5!.id,
|
||||||
|
name: "Shake Head",
|
||||||
|
type: "nao6-ros2.turn_head",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: { yaw: 0.3, speed: 0.5 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Conclusion
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step6!.id,
|
||||||
|
name: "Final Goodbye",
|
||||||
|
type: "nao6-ros2.say_text",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: { text: "That is all for today. Goodbye!" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step6!.id,
|
||||||
|
name: "Rest",
|
||||||
|
type: "nao6-ros2.move_arm",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: { shoulder_pitch: 1.5 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("✅ Seed completed successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Seed failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await connection.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -42,7 +42,7 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await caller.experiments.get({ id: exp.id });
|
const result = await caller.experiments!.get({ id: exp.id });
|
||||||
|
|
||||||
console.log(`✅ Fetched experiment: ${result.name} (${result.id})`);
|
console.log(`✅ Fetched experiment: ${result.name} (${result.id})`);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { db } from "~/server/db";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
async function migrate() {
|
||||||
|
console.log("Adding identifier column to hs_plugin...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE hs_plugin ADD COLUMN identifier varchar(100)`,
|
||||||
|
);
|
||||||
|
console.log("✓ Added identifier column");
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Column may already exist:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(
|
||||||
|
sql`UPDATE hs_plugin SET identifier = name WHERE identifier IS NULL`,
|
||||||
|
);
|
||||||
|
console.log("✓ Copied name to identifier");
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Error copying:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE hs_plugin ADD CONSTRAINT hs_plugin_identifier_unique UNIQUE (identifier)`,
|
||||||
|
);
|
||||||
|
console.log("✓ Added unique constraint");
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Constraint may already exist:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migration complete!");
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate().catch(console.error);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Mock Robot Configuration
|
||||||
|
# Copy this file to .env and adjust as needed
|
||||||
|
|
||||||
|
# Port for mock robot WebSocket server (default: 9090, same as rosbridge)
|
||||||
|
MOCK_ROBOT_PORT=9090
|
||||||
|
|
||||||
|
# How often to publish robot state (ms)
|
||||||
|
MOCK_PUBLISH_INTERVAL=100
|
||||||
|
|
||||||
|
# Robot configuration
|
||||||
|
MOCK_ROBOT_NAME=MOCK-NAO6
|
||||||
|
MOCK_ROBOT_VERSION=6.0
|
||||||
|
MOCK_BATTERY_LEVEL=85
|
||||||
|
|
||||||
|
# Enable simulation features
|
||||||
|
MOCK_ENABLE_SPEECH=true
|
||||||
|
MOCK_ENABLE_MOVEMENT=true
|
||||||
|
MOCK_ENABLE_SENSORS=true
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@hristudio/mock-robot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Mock robot server for simulating NAO6 robot connections",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/server.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/server.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import { WebSocketServer, WebSocket } from "ws";
|
||||||
|
|
||||||
|
interface RosMessage {
|
||||||
|
op: string;
|
||||||
|
topic?: string;
|
||||||
|
type?: string;
|
||||||
|
id?: string;
|
||||||
|
msg?: Record<string, unknown>;
|
||||||
|
service?: string;
|
||||||
|
args?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Subscriber {
|
||||||
|
id: string;
|
||||||
|
topic: string;
|
||||||
|
type: string;
|
||||||
|
ws: WebSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.MOCK_ROBOT_PORT || "9090", 10);
|
||||||
|
const PUBLISH_INTERVAL = parseInt(process.env.MOCK_PUBLISH_INTERVAL || "100", 10);
|
||||||
|
|
||||||
|
const subscribers: Map<string, Subscriber> = new Map();
|
||||||
|
let subscriberIdCounter = 0;
|
||||||
|
|
||||||
|
const mockRobotState = {
|
||||||
|
battery: 85,
|
||||||
|
position: { x: 0, y: 0, theta: 0 },
|
||||||
|
joints: [
|
||||||
|
"HeadYaw",
|
||||||
|
"HeadPitch",
|
||||||
|
"LShoulderPitch",
|
||||||
|
"LShoulderRoll",
|
||||||
|
"LElbowYaw",
|
||||||
|
"LElbowRoll",
|
||||||
|
"LWristYaw",
|
||||||
|
"LHand",
|
||||||
|
"RShoulderPitch",
|
||||||
|
"RShoulderRoll",
|
||||||
|
"RElbowYaw",
|
||||||
|
"RElbowRoll",
|
||||||
|
"RWristYaw",
|
||||||
|
"RHand",
|
||||||
|
"LHipYawPitch",
|
||||||
|
"LHipRoll",
|
||||||
|
"LHipPitch",
|
||||||
|
"LKneePitch",
|
||||||
|
"LAnklePitch",
|
||||||
|
"LAnkleRoll",
|
||||||
|
"RHipYawPitch",
|
||||||
|
"RHipRoll",
|
||||||
|
"RHipPitch",
|
||||||
|
"RKneePitch",
|
||||||
|
"RAnklePitch",
|
||||||
|
"RAnkleRoll",
|
||||||
|
],
|
||||||
|
jointPositions: new Array(26).fill(0).map(() => (Math.random() - 0.5) * 0.1),
|
||||||
|
bumperLeft: false,
|
||||||
|
bumperRight: false,
|
||||||
|
handTouchLeft: false,
|
||||||
|
handTouchRight: false,
|
||||||
|
headTouchFront: false,
|
||||||
|
headTouchMiddle: false,
|
||||||
|
headTouchRear: false,
|
||||||
|
sonarLeft: 0.5 + Math.random() * 0.5,
|
||||||
|
sonarRight: 0.5 + Math.random() * 0.5,
|
||||||
|
lastSpeechText: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function broadcastToSubscribers(topic: string, msg: Record<string, unknown>, type: string): void {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
op: "publish",
|
||||||
|
topic,
|
||||||
|
type,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
subscribers.forEach((sub) => {
|
||||||
|
if (sub.topic === topic && sub.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
sub.ws.send(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to send to subscriber ${sub.id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishRobotState(): void {
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/joint_states",
|
||||||
|
{
|
||||||
|
header: { stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, frame_id: "" },
|
||||||
|
name: mockRobotState.joints,
|
||||||
|
position: mockRobotState.jointPositions,
|
||||||
|
velocity: new Array(26).fill(0),
|
||||||
|
effort: new Array(26).fill(0),
|
||||||
|
},
|
||||||
|
"sensor_msgs/JointState"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/naoqi_driver/battery",
|
||||||
|
{ header: {}, percentage: mockRobotState.battery, charging: false, plug: false },
|
||||||
|
"naoqi_bridge_msgs/Bumper"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/bumper",
|
||||||
|
{ left: mockRobotState.bumperLeft, right: mockRobotState.bumperRight },
|
||||||
|
"naoqi_bridge_msgs/Bumper"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/hand_touch",
|
||||||
|
{
|
||||||
|
leftHand: mockRobotState.handTouchLeft,
|
||||||
|
rightHand: mockRobotState.handTouchRight,
|
||||||
|
},
|
||||||
|
"naoqi_bridge_msgs/HandTouch"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/head_touch",
|
||||||
|
{
|
||||||
|
front: mockRobotState.headTouchFront,
|
||||||
|
middle: mockRobotState.headTouchMiddle,
|
||||||
|
rear: mockRobotState.headTouchRear,
|
||||||
|
},
|
||||||
|
"naoqi_bridge_msgs/HeadTouch"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/sonar/left",
|
||||||
|
{ header: {}, radiation_type: 1, field_of_view: 0.5, min_range: 0.1, max_range: 5.0, range: mockRobotState.sonarLeft },
|
||||||
|
"sensor_msgs/Range"
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/sonar/right",
|
||||||
|
{ header: {}, radiation_type: 1, field_of_view: 0.5, min_range: 0.1, max_range: 5.0, range: mockRobotState.sonarRight },
|
||||||
|
"sensor_msgs/Range"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(ws: WebSocket, data: string): void {
|
||||||
|
try {
|
||||||
|
const message: RosMessage = JSON.parse(data);
|
||||||
|
console.log(`[MockRobot] Received: ${message.op} ${message.topic || message.service || ""}`);
|
||||||
|
|
||||||
|
switch (message.op) {
|
||||||
|
case "subscribe":
|
||||||
|
handleSubscribe(ws, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "unsubscribe":
|
||||||
|
handleUnsubscribe(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "publish":
|
||||||
|
handlePublish(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "call_service":
|
||||||
|
handleServiceCall(ws, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "advertise":
|
||||||
|
console.log(`[MockRobot] Client advertising: ${message.topic}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "unadvertise":
|
||||||
|
console.log(`[MockRobot] Client unadvertising: ${message.topic}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auth":
|
||||||
|
ws.send(JSON.stringify({ op: "auth_result", result: true }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[MockRobot] Unknown operation: ${message.op}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[MockRobot] Failed to parse message:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubscribe(ws: WebSocket, message: RosMessage): void {
|
||||||
|
if (!message.topic) return;
|
||||||
|
|
||||||
|
const id = `sub_${subscriberIdCounter++}`;
|
||||||
|
const subscriber: Subscriber = {
|
||||||
|
id,
|
||||||
|
topic: message.topic,
|
||||||
|
type: message.type || "unknown",
|
||||||
|
ws,
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribers.set(id, subscriber);
|
||||||
|
console.log(`[MockRobot] Subscribed to ${message.topic} (${id})`);
|
||||||
|
|
||||||
|
if (message.id) {
|
||||||
|
ws.send(JSON.stringify({ op: "subscribe", id: message.id, values: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUnsubscribe(message: RosMessage): void {
|
||||||
|
if (!message.id) return;
|
||||||
|
|
||||||
|
const subscriber = subscribers.get(message.id);
|
||||||
|
if (subscriber) {
|
||||||
|
console.log(`[MockRobot] Unsubscribed from ${subscriber.topic}`);
|
||||||
|
subscribers.delete(message.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePublish(message: RosMessage): void {
|
||||||
|
if (!message.topic || !message.msg) return;
|
||||||
|
|
||||||
|
console.log(`[MockRobot] Publish to ${message.topic}:`, JSON.stringify(message.msg).slice(0, 200));
|
||||||
|
|
||||||
|
if (message.topic === "/cmd_vel") {
|
||||||
|
handleCmdVel(message.msg);
|
||||||
|
} else if (message.topic === "/speech") {
|
||||||
|
handleSpeech(message.msg);
|
||||||
|
} else if (message.topic === "/joint_angles") {
|
||||||
|
handleJointAngles(message.msg);
|
||||||
|
} else if (message.topic === "/autonomous_life/control") {
|
||||||
|
handleAutonomousLife(message.msg);
|
||||||
|
} else if (message.topic === "/leds") {
|
||||||
|
handleLEDs(message.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCmdVel(msg: Record<string, unknown>): void {
|
||||||
|
const twist = msg as { linear?: { x?: number; y?: number; z?: number }; angular?: { x?: number; y?: number; z?: number } };
|
||||||
|
const linear = twist.linear || {};
|
||||||
|
const angular = twist.angular || {};
|
||||||
|
|
||||||
|
if (angular.z !== undefined && angular.z !== 0) {
|
||||||
|
mockRobotState.position.theta += angular.z * (PUBLISH_INTERVAL / 1000);
|
||||||
|
console.log(`[MockRobot] Turning: angular.z=${angular.z}, new theta=${mockRobotState.position.theta.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linear.x !== undefined && linear.x !== 0) {
|
||||||
|
const dx = linear.x * Math.cos(mockRobotState.position.theta) * (PUBLISH_INTERVAL / 1000);
|
||||||
|
const dy = linear.x * Math.sin(mockRobotState.position.theta) * (PUBLISH_INTERVAL / 1000);
|
||||||
|
mockRobotState.position.x += dx;
|
||||||
|
mockRobotState.position.y += dy;
|
||||||
|
console.log(`[MockRobot] Walking: linear.x=${linear.x}, pos=(${mockRobotState.position.x.toFixed(2)}, ${mockRobotState.position.y.toFixed(2)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSpeech(msg: Record<string, unknown>): void {
|
||||||
|
const text = (msg as { data?: string }).data || "";
|
||||||
|
mockRobotState.lastSpeechText = text;
|
||||||
|
console.log(`[MockRobot] Speaking: "${text}"`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
broadcastToSubscribers(
|
||||||
|
"/speech/status",
|
||||||
|
{ state: "done", text },
|
||||||
|
"std_msgs/String"
|
||||||
|
);
|
||||||
|
console.log(`[MockRobot] Speech complete: "${text}"`);
|
||||||
|
}, Math.max(500, text.split(/\s+/).length * 300 + 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJointAngles(msg: Record<string, unknown>): void {
|
||||||
|
const data = msg as {
|
||||||
|
joint_names?: string[];
|
||||||
|
joint_angles?: number[];
|
||||||
|
speed?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.joint_names && data.joint_angles && Array.isArray(data.joint_angles)) {
|
||||||
|
const jointAngles = data.joint_angles;
|
||||||
|
data.joint_names.forEach((name, i) => {
|
||||||
|
const idx = mockRobotState.joints.indexOf(name);
|
||||||
|
const angle = jointAngles[i];
|
||||||
|
if (idx >= 0 && angle !== undefined) {
|
||||||
|
mockRobotState.jointPositions[idx] = angle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`[MockRobot] Joint angles updated: ${data.joint_names.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAutonomousLife(msg: Record<string, unknown>): void {
|
||||||
|
const state = (msg as { data?: string }).data || "disabled";
|
||||||
|
console.log(`[MockRobot] Autonomous life: ${state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLEDs(msg: Record<string, unknown>): void {
|
||||||
|
const ledName = (msg as { name?: string }).name || "unknown";
|
||||||
|
const color = (msg as { color?: string }).color || "unknown";
|
||||||
|
console.log(`[MockRobot] LED ${ledName} set to ${color}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleServiceCall(ws: WebSocket, message: RosMessage): void {
|
||||||
|
const service = message.service || "";
|
||||||
|
const id = message.id || `svc_${Date.now()}`;
|
||||||
|
const args = message.args || {};
|
||||||
|
|
||||||
|
console.log(`[MockRobot] Service call: ${service}`, args);
|
||||||
|
|
||||||
|
let response: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case "/rosapi/get_param":
|
||||||
|
response = { value: args.param || "" };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/rosapi/topics_for_type":
|
||||||
|
response = { topics: [] };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/rosapi/get_topic_type":
|
||||||
|
response = { type: "" };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/rosapi/get_node_details":
|
||||||
|
response = { node_api: "", publications: [], subscriptions: [], services: [] };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/get_robot_info":
|
||||||
|
response = {
|
||||||
|
robotName: "MOCK-NAO6",
|
||||||
|
robotVersion: "6.0",
|
||||||
|
bodyType: "nao",
|
||||||
|
headTiltAngle: 0,
|
||||||
|
time: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/get_joint_names":
|
||||||
|
response = { joint_names: mockRobotState.joints };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/get_position":
|
||||||
|
response = {
|
||||||
|
x: mockRobotState.position.x,
|
||||||
|
y: mockRobotState.position.y,
|
||||||
|
theta: mockRobotState.position.theta,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/is_waking_up":
|
||||||
|
response = { success: true, is_waking_up: false, is_webots: false };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/robot_supports":
|
||||||
|
response = { supports_service: true };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/set_autonomous_state":
|
||||||
|
response = { success: true };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/toggle_autonomous":
|
||||||
|
response = { success: true };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/call_button_action":
|
||||||
|
response = { success: true, button_id: (args as { button_id?: string }).button_id };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/naoqi_driver/robot_batch_request":
|
||||||
|
response = { success: true };
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[MockRobot] Unknown service: ${service}`);
|
||||||
|
response = { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
op: "service_response",
|
||||||
|
id,
|
||||||
|
service,
|
||||||
|
result: true,
|
||||||
|
values: response,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: PORT });
|
||||||
|
|
||||||
|
console.log(`[MockRobot] Mock Robot Server starting on ws://localhost:${PORT}`);
|
||||||
|
console.log(`[MockRobot] Publish interval: ${PUBLISH_INTERVAL}ms`);
|
||||||
|
console.log("[MockRobot] Simulating NAO6 robot with rosbridge protocol\n");
|
||||||
|
|
||||||
|
wss.on("connection", (ws: WebSocket) => {
|
||||||
|
console.log("[MockRobot] Client connected");
|
||||||
|
|
||||||
|
ws.on("message", (data: Buffer) => {
|
||||||
|
handleMessage(ws, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("[MockRobot] Client disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (error) => {
|
||||||
|
console.error("[MockRobot] WebSocket error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ op: "connected", id: "mock_robot_server" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(publishRobotState, PUBLISH_INTERVAL);
|
||||||
|
|
||||||
|
console.log(`[MockRobot] Server ready. Connect via WebSocket to ws://localhost:${PORT}`);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
+238
-41
@@ -14,31 +14,31 @@ const db = drizzle(connection, { schema });
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
// Function to load plugin definition (Remote -> Local Fallback)
|
// Function to load plugin definition (Local first -> Remote fallback)
|
||||||
async function loadNaoPluginDef() {
|
async function loadNaoPluginDef() {
|
||||||
const REMOTE_URL = "https://repo.hristudio.com/plugins/nao6-ros2.json";
|
|
||||||
const LOCAL_PATH = path.join(
|
const LOCAL_PATH = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../robot-plugins/plugins/nao6-ros2.json",
|
"../robot-plugins/plugins/nao6-ros2.json",
|
||||||
);
|
);
|
||||||
|
const REMOTE_URL = "https://repo.hristudio.com/plugins/nao6-ros2.json";
|
||||||
|
|
||||||
|
// Always load from local file first (has latest fixes)
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(`📁 Loading plugin definition from local file...`);
|
||||||
`🌐 Attempting to fetch plugin definition from ${REMOTE_URL}...`,
|
const rawPlugin = fs.readFileSync(LOCAL_PATH, "utf-8");
|
||||||
|
console.log("✅ Successfully loaded local plugin definition.");
|
||||||
|
return JSON.parse(rawPlugin);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Local file load failed. Falling back to remote: ${REMOTE_URL}`,
|
||||||
);
|
);
|
||||||
const response = await fetch(REMOTE_URL, {
|
const response = await fetch(REMOTE_URL, {
|
||||||
signal: AbortSignal.timeout(3000),
|
signal: AbortSignal.timeout(5000),
|
||||||
}); // 3s timeout
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("✅ Successfully fetched plugin definition from remote.");
|
console.log("✅ Successfully fetched plugin definition from remote.");
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Remote fetch failed (${err instanceof Error ? err.message : String(err)}). Falling back to local file.`,
|
|
||||||
);
|
|
||||||
const rawPlugin = fs.readFileSync(LOCAL_PATH, "utf-8");
|
|
||||||
return JSON.parse(rawPlugin);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +76,9 @@ async function main() {
|
|||||||
|
|
||||||
// 1. Clean existing data (Full Wipe)
|
// 1. Clean existing data (Full Wipe)
|
||||||
console.log("🧹 Cleaning existing data...");
|
console.log("🧹 Cleaning existing data...");
|
||||||
|
await db.delete(schema.sessions).where(sql`1=1`);
|
||||||
|
await db.delete(schema.accounts).where(sql`1=1`);
|
||||||
|
await db.delete(schema.verificationTokens).where(sql`1=1`);
|
||||||
await db.delete(schema.mediaCaptures).where(sql`1=1`);
|
await db.delete(schema.mediaCaptures).where(sql`1=1`);
|
||||||
await db.delete(schema.trialEvents).where(sql`1=1`);
|
await db.delete(schema.trialEvents).where(sql`1=1`);
|
||||||
await db.delete(schema.trials).where(sql`1=1`);
|
await db.delete(schema.trials).where(sql`1=1`);
|
||||||
@@ -85,7 +88,8 @@ async function main() {
|
|||||||
await db.delete(schema.participants).where(sql`1=1`);
|
await db.delete(schema.participants).where(sql`1=1`);
|
||||||
await db.delete(schema.studyPlugins).where(sql`1=1`);
|
await db.delete(schema.studyPlugins).where(sql`1=1`);
|
||||||
await db.delete(schema.studyMembers).where(sql`1=1`);
|
await db.delete(schema.studyMembers).where(sql`1=1`);
|
||||||
await db.delete(schema.studies).where(sql`1=1`);
|
await db.delete(schema.formResponses).where(sql`1=1`);
|
||||||
|
await db.delete(schema.forms).where(sql`1=1`);
|
||||||
await db.delete(schema.studies).where(sql`1=1`);
|
await db.delete(schema.studies).where(sql`1=1`);
|
||||||
await db.delete(schema.plugins).where(sql`1=1`);
|
await db.delete(schema.plugins).where(sql`1=1`);
|
||||||
await db.delete(schema.pluginRepositories).where(sql`1=1`);
|
await db.delete(schema.pluginRepositories).where(sql`1=1`);
|
||||||
@@ -93,20 +97,24 @@ async function main() {
|
|||||||
await db.delete(schema.users).where(sql`1=1`);
|
await db.delete(schema.users).where(sql`1=1`);
|
||||||
await db.delete(schema.robots).where(sql`1=1`);
|
await db.delete(schema.robots).where(sql`1=1`);
|
||||||
|
|
||||||
// 2. Create Users
|
// 2. Create Users (Better Auth manages credentials)
|
||||||
console.log("👥 Creating users...");
|
console.log("👥 Creating users...");
|
||||||
const hashedPassword = await bcrypt.hash("password123", 12);
|
const hashedPassword = await bcrypt.hash("password123", 12);
|
||||||
|
|
||||||
const gravatarUrl = (email: string) =>
|
const gravatarUrl = (email: string) =>
|
||||||
`https://www.gravatar.com/avatar/${createHash("md5").update(email.toLowerCase().trim()).digest("hex")}?d=identicon`;
|
`https://www.gravatar.com/avatar/${createHash("md5").update(email.toLowerCase().trim()).digest("hex")}?d=identicon`;
|
||||||
|
|
||||||
|
// Generate text IDs (Better Auth uses text-based IDs)
|
||||||
|
const adminId = `admin_${randomUUID()}`;
|
||||||
|
const researcherId = `researcher_${randomUUID()}`;
|
||||||
|
|
||||||
const [adminUser] = await db
|
const [adminUser] = await db
|
||||||
.insert(schema.users)
|
.insert(schema.users)
|
||||||
.values({
|
.values({
|
||||||
|
id: adminId,
|
||||||
name: "Sean O'Connor",
|
name: "Sean O'Connor",
|
||||||
email: "sean@soconnor.dev",
|
email: "sean@soconnor.dev",
|
||||||
password: hashedPassword,
|
emailVerified: true,
|
||||||
emailVerified: new Date(),
|
|
||||||
image: gravatarUrl("sean@soconnor.dev"),
|
image: gravatarUrl("sean@soconnor.dev"),
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -114,16 +122,39 @@ async function main() {
|
|||||||
const [researcherUser] = await db
|
const [researcherUser] = await db
|
||||||
.insert(schema.users)
|
.insert(schema.users)
|
||||||
.values({
|
.values({
|
||||||
|
id: researcherId,
|
||||||
name: "Dr. Felipe Perrone",
|
name: "Dr. Felipe Perrone",
|
||||||
email: "felipe.perrone@bucknell.edu",
|
email: "felipe.perrone@bucknell.edu",
|
||||||
password: hashedPassword,
|
emailVerified: true,
|
||||||
emailVerified: new Date(),
|
|
||||||
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe",
|
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe",
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!adminUser) throw new Error("Failed to create admin user");
|
if (!adminUser) throw new Error("Failed to create admin user");
|
||||||
|
|
||||||
|
// Create credential accounts for Better Auth (accountId = userId for credential provider)
|
||||||
|
await db.insert(schema.accounts).values({
|
||||||
|
id: `acc_${randomUUID()}`,
|
||||||
|
userId: adminUser.id,
|
||||||
|
providerId: "credential",
|
||||||
|
accountId: adminUser.id,
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (researcherUser) {
|
||||||
|
await db.insert(schema.accounts).values({
|
||||||
|
id: `acc_${randomUUID()}`,
|
||||||
|
userId: researcherUser.id,
|
||||||
|
providerId: "credential",
|
||||||
|
accountId: researcherUser.id,
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(schema.userSystemRoles)
|
||||||
|
.values({ userId: researcherUser.id, role: "researcher" });
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(schema.userSystemRoles)
|
.insert(schema.userSystemRoles)
|
||||||
.values({ userId: adminUser.id, role: "administrator" });
|
.values({ userId: adminUser.id, role: "administrator" });
|
||||||
@@ -159,6 +190,7 @@ async function main() {
|
|||||||
.insert(schema.plugins)
|
.insert(schema.plugins)
|
||||||
.values({
|
.values({
|
||||||
robotId: naoRobot!.id,
|
robotId: naoRobot!.id,
|
||||||
|
identifier: NAO_PLUGIN_DEF.robotId,
|
||||||
name: NAO_PLUGIN_DEF.name,
|
name: NAO_PLUGIN_DEF.name,
|
||||||
version: NAO_PLUGIN_DEF.version,
|
version: NAO_PLUGIN_DEF.version,
|
||||||
description: NAO_PLUGIN_DEF.description,
|
description: NAO_PLUGIN_DEF.description,
|
||||||
@@ -192,10 +224,107 @@ async function main() {
|
|||||||
{ studyId: study!.id, userId: researcherUser!.id, role: "researcher" },
|
{ studyId: study!.id, userId: researcherUser!.id, role: "researcher" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Create Forms & Templates
|
||||||
|
console.log("📝 Creating forms and templates...");
|
||||||
|
|
||||||
|
// Templates (system-wide templates)
|
||||||
|
const [consentTemplate] = await db
|
||||||
|
.insert(schema.forms)
|
||||||
|
.values({
|
||||||
|
studyId: study!.id,
|
||||||
|
type: "consent",
|
||||||
|
title: "Standard Informed Consent",
|
||||||
|
description: "A comprehensive informed consent document template for HRI research studies.",
|
||||||
|
isTemplate: true,
|
||||||
|
templateName: "Informed Consent",
|
||||||
|
version: 100,
|
||||||
|
fields: [
|
||||||
|
{ id: "1", type: "text", label: "Study Title", required: true },
|
||||||
|
{ id: "2", type: "text", label: "Principal Investigator Name", required: true },
|
||||||
|
{ id: "3", type: "text", label: "Institution", required: true },
|
||||||
|
{ id: "4", type: "textarea", label: "Purpose of the Study", required: true },
|
||||||
|
{ id: "5", type: "textarea", label: "Procedures", required: true },
|
||||||
|
{ id: "6", type: "textarea", label: "Risks and Benefits", required: true },
|
||||||
|
{ id: "7", type: "textarea", label: "Confidentiality", required: true },
|
||||||
|
{ id: "8", type: "yes_no", label: "I consent to participate in this study", required: true },
|
||||||
|
{ id: "9", type: "signature", label: "Participant Signature", required: true },
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
createdBy: adminUser.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const [surveyTemplate] = await db
|
||||||
|
.insert(schema.forms)
|
||||||
|
.values({
|
||||||
|
studyId: study!.id,
|
||||||
|
type: "survey",
|
||||||
|
title: "Post-Session Questionnaire",
|
||||||
|
description: "Standard questionnaire to collect participant feedback after HRI sessions.",
|
||||||
|
isTemplate: true,
|
||||||
|
templateName: "Post-Session Survey",
|
||||||
|
version: 101,
|
||||||
|
fields: [
|
||||||
|
{ id: "1", type: "rating", label: "How engaging was the robot?", required: true, settings: { scale: 5 } },
|
||||||
|
{ id: "2", type: "rating", label: "How understandable was the robot's speech?", required: true, settings: { scale: 5 } },
|
||||||
|
{ id: "3", type: "rating", label: "How natural did the interaction feel?", required: true, settings: { scale: 5 } },
|
||||||
|
{ id: "4", type: "multiple_choice", label: "Did the robot respond appropriately to your actions?", required: true, options: ["Yes, always", "Yes, mostly", "Sometimes", "Rarely", "No"] },
|
||||||
|
{ id: "5", type: "textarea", label: "What did you like most about the interaction?", required: false },
|
||||||
|
{ id: "6", type: "textarea", label: "What could be improved?", required: false },
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
createdBy: adminUser.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const [questionnaireTemplate] = await db
|
||||||
|
.insert(schema.forms)
|
||||||
|
.values({
|
||||||
|
studyId: study!.id,
|
||||||
|
type: "questionnaire",
|
||||||
|
title: "Demographics Form",
|
||||||
|
description: "Basic demographic information collection form.",
|
||||||
|
isTemplate: true,
|
||||||
|
templateName: "Demographics",
|
||||||
|
version: 102,
|
||||||
|
fields: [
|
||||||
|
{ id: "1", type: "text", label: "Age", required: true },
|
||||||
|
{ id: "2", type: "multiple_choice", label: "Gender", required: true, options: ["Male", "Female", "Non-binary", "Prefer not to say"] },
|
||||||
|
{ id: "3", type: "multiple_choice", label: "Experience with robots", required: true, options: ["None", "A little", "Moderate", "Extensive"] },
|
||||||
|
{ id: "4", type: "multiple_choice", label: "Experience with HRI research", required: true, options: ["Never participated", "Participated once", "Participated several times"] },
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
createdBy: adminUser.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Study-specific form (not a template)
|
||||||
|
const [consentForm] = await db
|
||||||
|
.insert(schema.forms)
|
||||||
|
.values({
|
||||||
|
studyId: study!.id,
|
||||||
|
type: "consent",
|
||||||
|
title: "Interactive Storyteller Consent",
|
||||||
|
description: "Consent form for the Comparative WoZ Study - Interactive Storyteller scenario.",
|
||||||
|
version: 1,
|
||||||
|
active: true,
|
||||||
|
fields: [
|
||||||
|
{ id: "1", type: "text", label: "Participant Name", required: true },
|
||||||
|
{ id: "2", type: "date", label: "Date", required: true },
|
||||||
|
{ id: "3", type: "textarea", label: "I understand that I will interact with a robot storyteller and may be asked to respond to questions.", required: true },
|
||||||
|
{ id: "4", type: "yes_no", label: "I consent to participate in this study", required: true },
|
||||||
|
{ id: "5", type: "signature", label: "Signature", required: true },
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
createdBy: adminUser.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
// Insert System Plugins
|
// Insert System Plugins
|
||||||
const [corePlugin] = await db
|
const [corePlugin] = await db
|
||||||
.insert(schema.plugins)
|
.insert(schema.plugins)
|
||||||
.values({
|
.values({
|
||||||
|
identifier: CORE_PLUGIN_DEF.id,
|
||||||
name: CORE_PLUGIN_DEF.name,
|
name: CORE_PLUGIN_DEF.name,
|
||||||
version: CORE_PLUGIN_DEF.version,
|
version: CORE_PLUGIN_DEF.version,
|
||||||
description: CORE_PLUGIN_DEF.description,
|
description: CORE_PLUGIN_DEF.description,
|
||||||
@@ -211,6 +340,7 @@ async function main() {
|
|||||||
const [wozPlugin] = await db
|
const [wozPlugin] = await db
|
||||||
.insert(schema.plugins)
|
.insert(schema.plugins)
|
||||||
.values({
|
.values({
|
||||||
|
identifier: WOZ_PLUGIN_DEF.id,
|
||||||
name: WOZ_PLUGIN_DEF.name,
|
name: WOZ_PLUGIN_DEF.name,
|
||||||
version: WOZ_PLUGIN_DEF.version,
|
version: WOZ_PLUGIN_DEF.version,
|
||||||
description: WOZ_PLUGIN_DEF.description,
|
description: WOZ_PLUGIN_DEF.description,
|
||||||
@@ -262,6 +392,35 @@ async function main() {
|
|||||||
// 5. Create Steps & Actions (The Interactive Storyteller Protocol)
|
// 5. Create Steps & Actions (The Interactive Storyteller Protocol)
|
||||||
console.log("🎬 Creating experiment steps (Interactive Storyteller)...");
|
console.log("🎬 Creating experiment steps (Interactive Storyteller)...");
|
||||||
|
|
||||||
|
// Pre-create steps that will be referenced before they're defined
|
||||||
|
// --- Step 5: Story Continues (Convergence point for both branches) ---
|
||||||
|
const [step5] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
experimentId: experiment!.id,
|
||||||
|
name: "Story Continues",
|
||||||
|
description: "Both branches converge here",
|
||||||
|
type: "robot",
|
||||||
|
orderIndex: 5,
|
||||||
|
required: true,
|
||||||
|
durationEstimate: 15,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// --- Step 6: Conclusion ---
|
||||||
|
const [step6] = await db
|
||||||
|
.insert(schema.steps)
|
||||||
|
.values({
|
||||||
|
experimentId: experiment!.id,
|
||||||
|
name: "Conclusion",
|
||||||
|
description: "End the story and thank participant",
|
||||||
|
type: "robot",
|
||||||
|
orderIndex: 6,
|
||||||
|
required: true,
|
||||||
|
durationEstimate: 25,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
// --- Step 1: The Hook ---
|
// --- Step 1: The Hook ---
|
||||||
const [step1] = await db
|
const [step1] = await db
|
||||||
.insert(schema.steps)
|
.insert(schema.steps)
|
||||||
@@ -363,10 +522,6 @@ async function main() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- Step 3: Comprehension Check (Wizard Decision Point) ---
|
|
||||||
// Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect)
|
|
||||||
// --- Step 3: Comprehension Check (Wizard Decision Point) ---
|
|
||||||
// Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect)
|
|
||||||
// --- Step 4a: Correct Response Branch ---
|
// --- Step 4a: Correct Response Branch ---
|
||||||
const [step4a] = await db
|
const [step4a] = await db
|
||||||
.insert(schema.steps)
|
.insert(schema.steps)
|
||||||
@@ -378,6 +533,9 @@ async function main() {
|
|||||||
orderIndex: 3,
|
orderIndex: 3,
|
||||||
required: false,
|
required: false,
|
||||||
durationEstimate: 20,
|
durationEstimate: 20,
|
||||||
|
conditions: {
|
||||||
|
nextStepId: step5!.id, // Jump to Story Continues after completing
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -392,11 +550,13 @@ async function main() {
|
|||||||
orderIndex: 4,
|
orderIndex: 4,
|
||||||
required: false,
|
required: false,
|
||||||
durationEstimate: 20,
|
durationEstimate: 20,
|
||||||
|
conditions: {
|
||||||
|
nextStepId: step5!.id, // Jump to Story Continues after completing
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// --- Step 3: Comprehension Check (Wizard Decision Point) ---
|
// --- Step 3: Comprehension Check (Wizard Decision Point) ---
|
||||||
// Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect)
|
|
||||||
const [step3] = await db
|
const [step3] = await db
|
||||||
.insert(schema.steps)
|
.insert(schema.steps)
|
||||||
.values({
|
.values({
|
||||||
@@ -445,10 +605,12 @@ async function main() {
|
|||||||
name: "Wait for Choice",
|
name: "Wait for Choice",
|
||||||
type: "wizard_wait_for_response",
|
type: "wizard_wait_for_response",
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
// Define the options that will be presented to the Wizard
|
|
||||||
parameters: {
|
parameters: {
|
||||||
prompt_text: "Did participant answer 'Red' correctly?",
|
prompt_text: "Did participant answer 'Red' correctly?",
|
||||||
options: ["Correct", "Incorrect"],
|
options: [
|
||||||
|
{ label: "Correct", value: "Correct", nextStepId: step4a!.id },
|
||||||
|
{ label: "Incorrect", value: "Incorrect", nextStepId: step4b!.id },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
sourceKind: "core",
|
sourceKind: "core",
|
||||||
pluginId: "hristudio-woz", // Explicit link
|
pluginId: "hristudio-woz", // Explicit link
|
||||||
@@ -553,23 +715,42 @@ async function main() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- Step 5: Conclusion ---
|
// --- Step 5 actions: Story Continues ---
|
||||||
const [step5] = await db
|
|
||||||
.insert(schema.steps)
|
|
||||||
.values({
|
|
||||||
experimentId: experiment!.id,
|
|
||||||
name: "Conclusion",
|
|
||||||
description: "End the story and thank participant",
|
|
||||||
type: "robot",
|
|
||||||
orderIndex: 5,
|
|
||||||
required: true,
|
|
||||||
durationEstimate: 25,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
await db.insert(schema.actions).values([
|
await db.insert(schema.actions).values([
|
||||||
{
|
{
|
||||||
stepId: step5!.id,
|
stepId: step5!.id,
|
||||||
|
name: "Excited Continuation",
|
||||||
|
type: "nao6-ros2.say_with_emotion",
|
||||||
|
orderIndex: 0,
|
||||||
|
parameters: {
|
||||||
|
text: "And so the adventure continues! The traveler kept the glowing rock as a precious treasure.",
|
||||||
|
emotion: "excited",
|
||||||
|
speed: 1.1,
|
||||||
|
},
|
||||||
|
pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2",
|
||||||
|
pluginVersion: "2.2.0",
|
||||||
|
category: "interaction",
|
||||||
|
retryable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepId: step5!.id,
|
||||||
|
name: "Wave Goodbye",
|
||||||
|
type: "nao6-ros2.wave_goodbye",
|
||||||
|
orderIndex: 1,
|
||||||
|
parameters: {
|
||||||
|
text: "See you later!",
|
||||||
|
},
|
||||||
|
pluginId: NAO_PLUGIN_DEF.robotId || "nao6-ros2",
|
||||||
|
pluginVersion: "2.2.0",
|
||||||
|
category: "interaction",
|
||||||
|
retryable: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Step 6 actions: Conclusion ---
|
||||||
|
await db.insert(schema.actions).values([
|
||||||
|
{
|
||||||
|
stepId: step6!.id,
|
||||||
name: "End Story",
|
name: "End Story",
|
||||||
type: "nao6-ros2.say_text",
|
type: "nao6-ros2.say_text",
|
||||||
orderIndex: 0,
|
orderIndex: 0,
|
||||||
@@ -580,7 +761,7 @@ async function main() {
|
|||||||
retryable: true,
|
retryable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stepId: step5!.id,
|
stepId: step6!.id,
|
||||||
name: "Bow Gesture",
|
name: "Bow Gesture",
|
||||||
type: "nao6-ros2.move_arm",
|
type: "nao6-ros2.move_arm",
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
@@ -843,6 +1024,22 @@ async function main() {
|
|||||||
.values(participants)
|
.values(participants)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
// 7. Pre-create a pending trial for immediate testing
|
||||||
|
console.log("🧪 Creating a pre-seeded pending trial for testing...");
|
||||||
|
const p001 = insertedParticipants.find((p) => p.participantCode === "P101");
|
||||||
|
|
||||||
|
const [pendingTrial] = await db
|
||||||
|
.insert(schema.trials)
|
||||||
|
.values({
|
||||||
|
experimentId: experiment!.id,
|
||||||
|
participantId: p001?.id,
|
||||||
|
status: "scheduled",
|
||||||
|
scheduledAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
console.log(` Created pending trial: ${pendingTrial?.id}`);
|
||||||
|
|
||||||
console.log("\n✅ Database seeded successfully!");
|
console.log("\n✅ Database seeded successfully!");
|
||||||
console.log(`Summary:`);
|
console.log(`Summary:`);
|
||||||
console.log(`- 1 Admin User (sean@soconnor.dev)`);
|
console.log(`- 1 Admin User (sean@soconnor.dev)`);
|
||||||
@@ -1024,7 +1221,7 @@ async function main() {
|
|||||||
trialId: analyticsTrial!.id,
|
trialId: analyticsTrial!.id,
|
||||||
eventType: "step_changed",
|
eventType: "step_changed",
|
||||||
timestamp: new Date(currentTime),
|
timestamp: new Date(currentTime),
|
||||||
data: { stepId: step5!.id, stepName: "Conclusion" },
|
data: { stepId: step6!.id, stepName: "Conclusion" },
|
||||||
});
|
});
|
||||||
|
|
||||||
advance(2);
|
advance(2);
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ export default function HelpCenterPage() {
|
|||||||
description: "Learn the basics of HRIStudio and set up your first study.",
|
description: "Learn the basics of HRIStudio and set up your first study.",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
items: [
|
items: [
|
||||||
{ label: "Platform Overview", href: "#" },
|
{ label: "Tutorials Overview", href: "/help/tutorials" },
|
||||||
{ label: "Creating a New Study", href: "#" },
|
{ label: "Getting Started Guide", href: "/help/tutorials/getting-started" },
|
||||||
{ label: "Managing Team Members", href: "#" },
|
{ label: "Your First Study", href: "/help/tutorials/your-first-study" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,9 +36,9 @@ export default function HelpCenterPage() {
|
|||||||
description: "Master the visual experiment designer and flow control.",
|
description: "Master the visual experiment designer and flow control.",
|
||||||
icon: FlaskConical,
|
icon: FlaskConical,
|
||||||
items: [
|
items: [
|
||||||
{ label: "Using the Visual Designer", href: "#" },
|
{ label: "Visual Designer Guide", href: "/help/tutorials/designing-experiments" },
|
||||||
{ label: "Robot Actions & Plugins", href: "#" },
|
{ label: "Robot Actions & Plugins", href: "/help/tutorials/robot-integration" },
|
||||||
{ label: "Variables & Logic", href: "#" },
|
{ label: "Wizard Interface", href: "/help/tutorials/wizard-interface" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,9 +46,9 @@ export default function HelpCenterPage() {
|
|||||||
description: "Execute experiments and manage Wizard of Oz sessions.",
|
description: "Execute experiments and manage Wizard of Oz sessions.",
|
||||||
icon: PlayCircle,
|
icon: PlayCircle,
|
||||||
items: [
|
items: [
|
||||||
{ label: "Wizard Interface Guide", href: "#" },
|
{ label: "Running Trials Guide", href: "/help/tutorials/running-trials" },
|
||||||
{ label: "Participant Management", href: "#" },
|
{ label: "Participant Management", href: "/help/tutorials/your-first-study" },
|
||||||
{ label: "Handling Robot Errors", href: "#" },
|
{ label: "Simulation Mode", href: "/help/tutorials/simulation-mode" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,9 +56,9 @@ export default function HelpCenterPage() {
|
|||||||
description: "Analyze trial results and export research data.",
|
description: "Analyze trial results and export research data.",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
items: [
|
items: [
|
||||||
{ label: "Understanding Analytics", href: "#" },
|
{ label: "Data & Analysis Guide", href: "/help/tutorials/data-and-analysis" },
|
||||||
{ label: "Exporting Data (CSV/JSON)", href: "#" },
|
{ label: "Forms & Surveys", href: "/help/tutorials/forms-and-surveys" },
|
||||||
{ label: "Video Replay & Annotation", href: "#" },
|
{ label: "Exporting Data", href: "/help/tutorials/data-and-analysis" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function DataAndAnalysisTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Data & Analysis"
|
||||||
|
description="Collect and export trial data"
|
||||||
|
duration="15 min"
|
||||||
|
level="Intermediate"
|
||||||
|
steps={[
|
||||||
|
{ title: "Understand data collection", description: "" },
|
||||||
|
{ title: "Access trial data", description: "" },
|
||||||
|
{ title: "Export data formats", description: "" },
|
||||||
|
{ title: "Use the analytics dashboard", description: "" },
|
||||||
|
{ title: "Generate reports", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Forms & Surveys",
|
||||||
|
href: "/help/tutorials/forms-and-surveys",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Simulation Mode",
|
||||||
|
href: "/help/tutorials/simulation-mode",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Data Collection Overview</h2>
|
||||||
|
<p>HRIStudio automatically captures comprehensive data during trials:</p>
|
||||||
|
<pre><code>Trial Data
|
||||||
|
├── Trial Metadata
|
||||||
|
│ ├── Start/End times
|
||||||
|
│ ├── Duration
|
||||||
|
│ ├── Participant info
|
||||||
|
│ └── Experiment version
|
||||||
|
├── Event Log (Timestamped)
|
||||||
|
│ ├── Step changes
|
||||||
|
│ ├── Action executions
|
||||||
|
│ ├── Robot responses
|
||||||
|
│ └── Wizard interventions
|
||||||
|
├── Form Responses
|
||||||
|
│ ├── Consent forms
|
||||||
|
│ ├── Surveys
|
||||||
|
│ └── Questionnaires
|
||||||
|
└── Sensor Data
|
||||||
|
├── Joint positions
|
||||||
|
├── Touch events
|
||||||
|
└── Audio/video (if enabled)</code></pre>
|
||||||
|
|
||||||
|
<h2>Event Types</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Event Type</th><th>Description</th><th>Data Captured</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>trial_started</td><td>Trial began</td><td>Timestamp</td></tr>
|
||||||
|
<tr><td>step_changed</td><td>New step began</td><td>Step ID, name</td></tr>
|
||||||
|
<tr><td>action_executed</td><td>Robot action</td><td>Action details, duration</td></tr>
|
||||||
|
<tr><td>wizard_response</td><td>Wizard decision</td><td>Selected option</td></tr>
|
||||||
|
<tr><td>intervention</td><td>Wizard intervention</td><td>Type, note</td></tr>
|
||||||
|
<tr><td>trial_completed</td><td>Trial finished</td><td>Summary</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 1: Accessing Trial Data</h2>
|
||||||
|
|
||||||
|
<h3>From Trial List</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Trials</strong> tab</li>
|
||||||
|
<li>Find completed trial</li>
|
||||||
|
<li>Click <strong>View Details</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>From Study Dashboard</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open your study</li>
|
||||||
|
<li>Go to <strong>Data</strong> tab</li>
|
||||||
|
<li>Select trial or view aggregate</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 2: Exporting Data</h2>
|
||||||
|
|
||||||
|
<h3>Export Single Trial</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open trial details</li>
|
||||||
|
<li>Click <strong>Export</strong></li>
|
||||||
|
<li>Select format</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Export Study Data</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open study</li>
|
||||||
|
<li>Go to <strong>Data</strong> tab</li>
|
||||||
|
<li>Click <strong>Export All</strong></li>
|
||||||
|
<li>Select options:
|
||||||
|
<ul>
|
||||||
|
<li>Date range</li>
|
||||||
|
<li>Trial status</li>
|
||||||
|
<li>Include forms</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Export Formats</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Format</th><th>Contents</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>CSV</td><td>Tabular data for spreadsheets</td></tr>
|
||||||
|
<tr><td>JSON</td><td>Full event log with metadata</td></tr>
|
||||||
|
<tr><td>Video</td><td>Screen recording (if enabled)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 3: Analytics Dashboard</h2>
|
||||||
|
<p>View aggregate statistics:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Total Trials</strong> - Number of scheduled trials</li>
|
||||||
|
<li><strong>Completed</strong> - Successfully completed trials</li>
|
||||||
|
<li><strong>Average Duration</strong> - Mean trial time</li>
|
||||||
|
<li><strong>Completion Rate</strong> - % of trials completed</li>
|
||||||
|
<li><strong>Failed</strong> - Trials that failed</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 4: Analyzing Event Data</h2>
|
||||||
|
|
||||||
|
<h3>Timing Analysis</h3>
|
||||||
|
<p>Calculate action durations from event log:</p>
|
||||||
|
<pre><code>{`for event in events:
|
||||||
|
if event.type == 'action_executed':
|
||||||
|
duration = event.get('duration', 0)
|
||||||
|
print(f"{event.actionName}: {duration/1000:.1f}s")`}</code></pre>
|
||||||
|
|
||||||
|
<h3>Intervention Analysis</h3>
|
||||||
|
<p>Track wizard interventions:</p>
|
||||||
|
<pre><code>{`interventions = [e for e in events if e.type == 'intervention']
|
||||||
|
|
||||||
|
by_type = {}
|
||||||
|
for i in interventions:
|
||||||
|
itype = i.data.get('type', 'unknown')
|
||||||
|
by_type[itype] = by_type.get(itype, 0) + 1`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 5: Generating Reports</h2>
|
||||||
|
|
||||||
|
<h3>Trial Summary Report</h3>
|
||||||
|
<p>Generate PDF summary with:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Executive summary</li>
|
||||||
|
<li>Timeline of events</li>
|
||||||
|
<li>Metrics and statistics</li>
|
||||||
|
<li>Intervention summary</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Study Report</h3>
|
||||||
|
<p>Aggregate across participants:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Participation rates</li>
|
||||||
|
<li>Timing statistics</li>
|
||||||
|
<li>Intervention totals</li>
|
||||||
|
<li>Branch selection distribution</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Data Privacy</h2>
|
||||||
|
|
||||||
|
<h3>Anonymization</h3>
|
||||||
|
<p>Remove identifying information:</p>
|
||||||
|
<pre><code>{`participant_map = {
|
||||||
|
'P001': 'S001',
|
||||||
|
'P002': 'S002',
|
||||||
|
'P003': 'S003',
|
||||||
|
}`}</code></pre>
|
||||||
|
|
||||||
|
<h2>Best Practices</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Export data regularly (daily/weekly)</li>
|
||||||
|
<li>Store in secure location</li>
|
||||||
|
<li>Follow IRB data retention</li>
|
||||||
|
<li>Backup critical data</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/forms-and-surveys">
|
||||||
|
Previous: Forms & Surveys
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/simulation-mode">
|
||||||
|
Next: Simulation Mode
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function DesigningExperimentsTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Designing Experiments"
|
||||||
|
description="Build experiment protocols with the visual designer"
|
||||||
|
duration="25 min"
|
||||||
|
level="Intermediate"
|
||||||
|
steps={[
|
||||||
|
{ title: "Understand the experiment structure", description: "" },
|
||||||
|
{ title: "Navigate the visual designer", description: "" },
|
||||||
|
{ title: "Use core blocks", description: "" },
|
||||||
|
{ title: "Build branching protocols", description: "" },
|
||||||
|
{ title: "Test your experiment", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Your First Study",
|
||||||
|
href: "/help/tutorials/your-first-study",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Running Trials",
|
||||||
|
href: "/help/tutorials/running-trials",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>What is an Experiment?</h2>
|
||||||
|
<p>An <strong>Experiment</strong> defines the protocol for your study:</p>
|
||||||
|
<pre><code>Experiment
|
||||||
|
├── Steps (ordered sequence)
|
||||||
|
│ ├── Actions (robot behaviors)
|
||||||
|
│ ├── Wizard Blocks (human decisions)
|
||||||
|
│ └── Control Flow (loops, branches)
|
||||||
|
├── Robot Actions (from plugins)
|
||||||
|
└── Parameters (configurable values)</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 1: Create an Experiment</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Open your study</li>
|
||||||
|
<li>Go to <strong>Experiments</strong> tab</li>
|
||||||
|
<li>Click <strong>New Experiment</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 2: The Visual Designer</h2>
|
||||||
|
<p>The designer has three main areas:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Block Library</strong> (left) - Drag blocks from here</li>
|
||||||
|
<li><strong>Canvas</strong> (center) - Design your protocol visually</li>
|
||||||
|
<li><strong>Properties Panel</strong> (right) - Configure selected elements</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 3: Block Categories</h2>
|
||||||
|
|
||||||
|
<h3>Events (Triggers)</h3>
|
||||||
|
<p>Start your experiment with these blocks:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Block</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Trial Start</td><td>Triggers when trial begins</td></tr>
|
||||||
|
<tr><td>Wizard Button</td><td>Waits for wizard to press a button</td></tr>
|
||||||
|
<tr><td>Timer</td><td>Waits for a specified duration</td></tr>
|
||||||
|
<tr><td>Participant Response</td><td>Waits for participant input</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Wizard Actions</h3>
|
||||||
|
<p>Blocks the wizard can control:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Block</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Say Text</td><td>Robot speaks text</td></tr>
|
||||||
|
<tr><td>Play Animation</td><td>Play a predefined animation</td></tr>
|
||||||
|
<tr><td>Show Image</td><td>Display image on robot screen</td></tr>
|
||||||
|
<tr><td>Move Robot</td><td>Move robot to position</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Control Flow</h3>
|
||||||
|
<p>Control experiment progression:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Block</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Branch</td><td>Split into multiple paths</td></tr>
|
||||||
|
<tr><td>Loop</td><td>Repeat a sequence</td></tr>
|
||||||
|
<tr><td>Wait</td><td>Pause for duration</td></tr>
|
||||||
|
<tr><td>Converge</td><td>Merge multiple paths back</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 4: Building a Branching Protocol</h2>
|
||||||
|
<p>Let's build "The Interactive Storyteller" - a simple storytelling experiment:</p>
|
||||||
|
|
||||||
|
<h3>Step 1: The Hook (Start)</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>+ Add Step</strong></li>
|
||||||
|
<li>Name it "The Hook"</li>
|
||||||
|
<li>Set type to <strong>Robot</strong></li>
|
||||||
|
<li>Drag <strong>Say Text</strong> block</li>
|
||||||
|
<li>Configure: <code>{`{ text: "Hello! I have a story to tell you." }`}</code></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Step 2: Comprehension Check (Branching)</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Add new step "Comprehension Check"</li>
|
||||||
|
<li>Set type to <strong>Conditional</strong></li>
|
||||||
|
<li>Add <strong>Ask Question</strong> block</li>
|
||||||
|
<li>Configure options:
|
||||||
|
<pre><code>{`{
|
||||||
|
question: "What color was the rock?",
|
||||||
|
options: [
|
||||||
|
{ label: "Correct", value: "red" },
|
||||||
|
{ label: "Incorrect", value: "other" }
|
||||||
|
]
|
||||||
|
}`}</code></pre>
|
||||||
|
</li>
|
||||||
|
<li>This creates two paths automatically</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Step 3: Converge Paths</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Add new step "Story Continues"</li>
|
||||||
|
<li>Set type to <strong>Converge</strong></li>
|
||||||
|
<li>Connect both branches to this step</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 5: Testing Your Experiment</h2>
|
||||||
|
|
||||||
|
<h3>Preview Mode</h3>
|
||||||
|
<p>Test your experiment without running a real trial:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Preview</strong> button</li>
|
||||||
|
<li>Step through each block</li>
|
||||||
|
<li>See timing and flow</li>
|
||||||
|
<li>Test branching decisions</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Simulation Mode</h3>
|
||||||
|
<p>Run with a simulated robot:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Enable <code>NEXT_PUBLIC_SIMULATION_MODE=true</code></li>
|
||||||
|
<li>Start a trial</li>
|
||||||
|
<li>Robot actions are logged but not executed</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Common Patterns</h2>
|
||||||
|
|
||||||
|
<h3>Linear Protocol</h3>
|
||||||
|
<pre><code>Start → Step 1 → Step 2 → Step 3 → End</code></pre>
|
||||||
|
|
||||||
|
<h3>Branching Protocol</h3>
|
||||||
|
<pre><code>Start → Step 1
|
||||||
|
├── Condition A → Step 2a
|
||||||
|
└── Condition B → Step 2b</code></pre>
|
||||||
|
|
||||||
|
<h3>Loop Protocol</h3>
|
||||||
|
<pre><code>Start → Step 1 → Loop (3x) → Step 2 → End
|
||||||
|
↑
|
||||||
|
└── (back to Step 1)</code></pre>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/your-first-study">
|
||||||
|
Previous: Your First Study
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/running-trials">
|
||||||
|
Next: Running Trials
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function FormsAndSurveysTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Forms & Surveys"
|
||||||
|
description="Create consent forms and questionnaires"
|
||||||
|
duration="15 min"
|
||||||
|
level="Intermediate"
|
||||||
|
steps={[
|
||||||
|
{ title: "Understand form types", description: "" },
|
||||||
|
{ title: "Create a new form", description: "" },
|
||||||
|
{ title: "Add form fields", description: "" },
|
||||||
|
{ title: "Use form templates", description: "" },
|
||||||
|
{ title: "Collect responses", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Robot Integration",
|
||||||
|
href: "/help/tutorials/robot-integration",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Data & Analysis",
|
||||||
|
href: "/help/tutorials/data-and-analysis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Form Types</h2>
|
||||||
|
<p>HRIStudio supports three form types:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Type</th><th>Purpose</th><th>When</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Consent</td><td>Informed consent for participation</td><td>Before trial</td></tr>
|
||||||
|
<tr><td>Survey</td><td>Collect feedback and observations</td><td>After trial</td></tr>
|
||||||
|
<tr><td>Questionnaire</td><td>Demographic data collection</td><td>Any time</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 1: Access Forms</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Go to your <strong>Study</strong></li>
|
||||||
|
<li>Click <strong>Forms</strong> tab</li>
|
||||||
|
<li>View existing forms and templates</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 2: Create a Form</h2>
|
||||||
|
|
||||||
|
<h3>Using a Template</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Create Form</strong></li>
|
||||||
|
<li>Select <strong>Use Template</strong></li>
|
||||||
|
<li>Choose template:
|
||||||
|
<ul>
|
||||||
|
<li>Informed Consent</li>
|
||||||
|
<li>Post-Session Survey</li>
|
||||||
|
<li>Demographics</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Customize as needed</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>From Scratch</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Create Form</strong></li>
|
||||||
|
<li>Select <strong>Blank Form</strong></li>
|
||||||
|
<li>Choose form type</li>
|
||||||
|
<li>Build fields manually</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 3: Form Field Types</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Field Type</th><th>Description</th><th>Example</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Text</td><td>Single line text input</td><td>Participant name</td></tr>
|
||||||
|
<tr><td>Text Area</td><td>Multi-line text</td><td>Open-ended feedback</td></tr>
|
||||||
|
<tr><td>Rating</td><td>Scale rating</td><td>Rate 1-5</td></tr>
|
||||||
|
<tr><td>Multiple Choice</td><td>Select one option</td><td>Gender selection</td></tr>
|
||||||
|
<tr><td>Yes/No</td><td>Binary choice</td><td>Consent checkbox</td></tr>
|
||||||
|
<tr><td>Date</td><td>Date picker</td><td>Session date</td></tr>
|
||||||
|
<tr><td>Signature</td><td>Digital signature</td><td>Consent signature</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 4: Consent Forms</h2>
|
||||||
|
<p>For IRB compliance, consent forms must include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Study title and purpose</li>
|
||||||
|
<li>Principal investigator</li>
|
||||||
|
<li>Procedures description</li>
|
||||||
|
<li>Risks and benefits</li>
|
||||||
|
<li>Confidentiality statement</li>
|
||||||
|
<li>Voluntary participation note</li>
|
||||||
|
<li>Signature and date fields</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 5: Distributing Forms</h2>
|
||||||
|
|
||||||
|
<h3>Automatic Distribution</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open form settings</li>
|
||||||
|
<li>Enable <strong>Auto-distribute</strong></li>
|
||||||
|
<li>Set trigger:
|
||||||
|
<ul>
|
||||||
|
<li>Before trial (consent)</li>
|
||||||
|
<li>After trial (survey)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Select participants</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Manual Distribution</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open form</li>
|
||||||
|
<li>Click <strong>Distribute</strong></li>
|
||||||
|
<li>Select participants</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 6: Collecting Responses</h2>
|
||||||
|
|
||||||
|
<h3>View Responses</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open form</li>
|
||||||
|
<li>Click <strong>Responses</strong> tab</li>
|
||||||
|
<li>View individual submissions</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Export Responses</h3>
|
||||||
|
<p>Download collected data:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Format</th><th>Contents</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>CSV</td><td>Tabular data</td></tr>
|
||||||
|
<tr><td>JSON</td><td>Full response objects</td></tr>
|
||||||
|
<tr><td>PDF</td><td>Printed consent forms</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Form Templates</h2>
|
||||||
|
<p>Pre-built templates available:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Template</th><th>Use Case</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Standard Consent</td><td>Generic research consent</td></tr>
|
||||||
|
<tr><td>Post-Session Survey</td><td>Post-session feedback</td></tr>
|
||||||
|
<tr><td>Demographics</td><td>Participant information</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/robot-integration">
|
||||||
|
Previous: Robot Integration
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/data-and-analysis">
|
||||||
|
Next: Data & Analysis
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function GettingStartedTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Getting Started"
|
||||||
|
description="Set up HRIStudio and learn the basics"
|
||||||
|
duration="10 min"
|
||||||
|
level="Beginner"
|
||||||
|
steps={[
|
||||||
|
{ title: "Clone and install the repository", description: "" },
|
||||||
|
{ title: "Start the database with Docker", description: "" },
|
||||||
|
{ title: "Seed the database with sample data", description: "" },
|
||||||
|
{ title: "Start the development server", description: "" },
|
||||||
|
{ title: "Log in and explore the interface", description: "" },
|
||||||
|
]}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Your First Study",
|
||||||
|
href: "/help/tutorials/your-first-study",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Prerequisites</h2>
|
||||||
|
<p>Before you begin, make sure you have the following installed:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Bun</strong> - The package manager for HRIStudio</li>
|
||||||
|
<li><strong>Docker</strong> - For running PostgreSQL and MinIO</li>
|
||||||
|
<li><strong>Git</strong> - For version control</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 1: Clone the Repository</h2>
|
||||||
|
<p>Start by cloning the HRIStudio repository:</p>
|
||||||
|
<pre><code>git clone https://github.com/soconnor0919/hristudio.git
|
||||||
|
cd hristudio</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 2: Install Dependencies</h2>
|
||||||
|
<p>HRIStudio uses Bun as its package manager:</p>
|
||||||
|
<pre><code>bun install</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 3: Start the Database</h2>
|
||||||
|
<p>HRIStudio requires PostgreSQL. The easiest way is using Docker:</p>
|
||||||
|
<pre><code># Start PostgreSQL and MinIO (for file storage)
|
||||||
|
bun run docker:up
|
||||||
|
|
||||||
|
# Push database schema
|
||||||
|
bun db:push
|
||||||
|
|
||||||
|
# Seed with sample data
|
||||||
|
bun db:seed</code></pre>
|
||||||
|
<p className="bg-muted p-4 rounded-lg border">
|
||||||
|
<strong>Note:</strong> This creates the database schema and populates it with
|
||||||
|
sample users, studies, and experiments so you can explore the platform immediately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Step 4: Start the Development Server</h2>
|
||||||
|
<pre><code>bun dev</code></pre>
|
||||||
|
<p>The application will be available at <code>http://localhost:3000</code>.</p>
|
||||||
|
|
||||||
|
<h2>Step 5: Log In</h2>
|
||||||
|
<p>Use one of the default accounts:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Password</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Administrator</td>
|
||||||
|
<td><code>sean@soconnor.dev</code></td>
|
||||||
|
<td><code>password123</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Researcher</td>
|
||||||
|
<td><code>felipe.perrone@bucknell.edu</code></td>
|
||||||
|
<td><code>password123</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Wizard</td>
|
||||||
|
<td><code>emily.watson@lab.edu</code></td>
|
||||||
|
<td><code>password123</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Observer</td>
|
||||||
|
<td><code>maria.santos@tech.edu</code></td>
|
||||||
|
<td><code>password123</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Exploring the Interface</h2>
|
||||||
|
<p>After logging in, you'll see the main dashboard with navigation to:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Studies</strong> - View and manage your research studies</li>
|
||||||
|
<li><strong>Trials</strong> - Monitor and manage experiment trials</li>
|
||||||
|
<li><strong>Plugins</strong> - Manage robot integrations</li>
|
||||||
|
<li><strong>Admin</strong> - System administration (admins only)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Using Simulation Mode</h2>
|
||||||
|
<p>If you don't have a physical robot, enable simulation mode:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Create or edit <code>hristudio/.env.local</code></li>
|
||||||
|
<li>Add: <code>NEXT_PUBLIC_SIMULATION_MODE=true</code></li>
|
||||||
|
<li>Restart the dev server</li>
|
||||||
|
</ol>
|
||||||
|
<p>Simulation mode allows you to test experiments without connecting to a real robot.</p>
|
||||||
|
|
||||||
|
<h2>Troubleshooting</h2>
|
||||||
|
|
||||||
|
<h3>Database Connection Failed</h3>
|
||||||
|
<pre><code># Check if Docker is running
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Restart the database
|
||||||
|
bun run docker:down
|
||||||
|
bun run docker:up
|
||||||
|
bun db:push</code></pre>
|
||||||
|
|
||||||
|
<h3>Port Already in Use</h3>
|
||||||
|
<p>If port 3000 is in use:</p>
|
||||||
|
<pre><code>PORT=3001 bun dev</code></pre>
|
||||||
|
|
||||||
|
<h3>Seed Script Fails</h3>
|
||||||
|
<pre><code># Reset the database
|
||||||
|
bun run docker:down -v
|
||||||
|
bun run docker:up
|
||||||
|
bun db:push
|
||||||
|
bun db:seed</code></pre>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-end">
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/your-first-study">
|
||||||
|
Next: Your First Study
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
FlaskConical,
|
||||||
|
PlayCircle,
|
||||||
|
BarChart3,
|
||||||
|
Bot,
|
||||||
|
FileText,
|
||||||
|
ClipboardList,
|
||||||
|
Layers,
|
||||||
|
ArrowRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { PageLayout } from "~/components/ui/page-layout";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const tutorials = [
|
||||||
|
{
|
||||||
|
slug: "getting-started",
|
||||||
|
title: "Getting Started",
|
||||||
|
description: "Set up HRIStudio and learn the basics",
|
||||||
|
icon: BookOpen,
|
||||||
|
duration: "10 min",
|
||||||
|
level: "Beginner",
|
||||||
|
href: "/help/tutorials/getting-started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "your-first-study",
|
||||||
|
title: "Your First Study",
|
||||||
|
description: "Create a research study and manage team members",
|
||||||
|
icon: Layers,
|
||||||
|
duration: "15 min",
|
||||||
|
level: "Beginner",
|
||||||
|
href: "/help/tutorials/your-first-study",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "designing-experiments",
|
||||||
|
title: "Designing Experiments",
|
||||||
|
description: "Build experiment protocols with the visual designer",
|
||||||
|
icon: FlaskConical,
|
||||||
|
duration: "25 min",
|
||||||
|
level: "Intermediate",
|
||||||
|
href: "/help/tutorials/designing-experiments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "running-trials",
|
||||||
|
title: "Running Trials",
|
||||||
|
description: "Execute experiments and manage participants",
|
||||||
|
icon: PlayCircle,
|
||||||
|
duration: "20 min",
|
||||||
|
level: "Intermediate",
|
||||||
|
href: "/help/tutorials/running-trials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "wizard-interface",
|
||||||
|
title: "Wizard Interface",
|
||||||
|
description: "Real-time trial control and monitoring",
|
||||||
|
icon: Bot,
|
||||||
|
duration: "15 min",
|
||||||
|
level: "Intermediate",
|
||||||
|
href: "/help/tutorials/wizard-interface",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "robot-integration",
|
||||||
|
title: "Robot Integration",
|
||||||
|
description: "Connect NAO6 and configure robot plugins",
|
||||||
|
icon: ClipboardList,
|
||||||
|
duration: "20 min",
|
||||||
|
level: "Advanced",
|
||||||
|
href: "/help/tutorials/robot-integration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "forms-and-surveys",
|
||||||
|
title: "Forms & Surveys",
|
||||||
|
description: "Create consent forms and questionnaires",
|
||||||
|
icon: FileText,
|
||||||
|
duration: "15 min",
|
||||||
|
level: "Intermediate",
|
||||||
|
href: "/help/tutorials/forms-and-surveys",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "data-and-analysis",
|
||||||
|
title: "Data & Analysis",
|
||||||
|
description: "Collect and export trial data",
|
||||||
|
icon: BarChart3,
|
||||||
|
duration: "15 min",
|
||||||
|
level: "Intermediate",
|
||||||
|
href: "/help/tutorials/data-and-analysis",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const levelColors: Record<string, string> = {
|
||||||
|
Beginner: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300",
|
||||||
|
Intermediate: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300",
|
||||||
|
Advanced: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TutorialsPage() {
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
title="Tutorials"
|
||||||
|
description="Step-by-step guides for learning HRIStudio"
|
||||||
|
breadcrumb={[
|
||||||
|
{ label: "Help", href: "/help" },
|
||||||
|
{ label: "Tutorials" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="mb-2 text-lg font-semibold">Quick Start Path</h2>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Follow this sequence to go from setup to running your first trial.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{tutorials.slice(0, 5).map((tutorial, index) => (
|
||||||
|
<div key={tutorial.slug} className="flex items-center gap-2">
|
||||||
|
<Link href={tutorial.href}>
|
||||||
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
{tutorial.title}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
{index < 4 && <ArrowRight className="text-muted-foreground h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{tutorials.map((tutorial) => (
|
||||||
|
<Link key={tutorial.slug} href={tutorial.href}>
|
||||||
|
<Card className="h-full transition-all hover:border-primary hover:shadow-md">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div className="bg-primary/10 rounded-lg p-2">
|
||||||
|
<tutorial.icon className="text-primary h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-2 py-0.5 text-xs font-medium ${levelColors[tutorial.level]}`}
|
||||||
|
>
|
||||||
|
{tutorial.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">{tutorial.title}</CardTitle>
|
||||||
|
<CardDescription>{tutorial.description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{tutorial.duration}
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="text-muted-foreground h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold">By Role</h2>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Researchers</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<Link href="/help/tutorials/getting-started" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Getting Started
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/your-first-study" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Your First Study
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/designing-experiments" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Designing Experiments
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/data-and-analysis" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Data & Analysis
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Wizards</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<Link href="/help/tutorials/getting-started" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Getting Started
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/wizard-interface" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Wizard Interface
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/robot-integration" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Robot Integration
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Administrators</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<Link href="/help/tutorials/getting-started" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Getting Started
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/robot-integration" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Robot Integration
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/forms-and-surveys" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Forms & Surveys
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Pilot Testing</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<Link href="/help/tutorials/getting-started" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Getting Started
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/designing-experiments" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Designing Experiments
|
||||||
|
</Link>
|
||||||
|
<Link href="/help/tutorials/running-trials" className="block text-muted-foreground hover:text-foreground">
|
||||||
|
Running Trials
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function RobotIntegrationTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Robot Integration"
|
||||||
|
description="Connect NAO6 and configure robot plugins"
|
||||||
|
duration="20 min"
|
||||||
|
level="Advanced"
|
||||||
|
steps={[
|
||||||
|
{ title: "Set up the NAO6 robot", description: "" },
|
||||||
|
{ title: "Start Docker services", description: "" },
|
||||||
|
{ title: "Configure HRIStudio", description: "" },
|
||||||
|
{ title: "Test the connection", description: "" },
|
||||||
|
{ title: "Troubleshoot common issues", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Wizard Interface",
|
||||||
|
href: "/help/tutorials/wizard-interface",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Forms & Surveys",
|
||||||
|
href: "/help/tutorials/forms-and-surveys",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Supported Robots</h2>
|
||||||
|
<p>HRIStudio supports multiple robot platforms:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Robot</th><th>Protocol</th><th>Capabilities</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>NAO6</td><td>ROS2</td><td>Speech, movement, gestures, sensors</td></tr>
|
||||||
|
<tr><td>TurtleBot3</td><td>ROS2</td><td>Navigation, sensors</td></tr>
|
||||||
|
<tr><td>Mock Robot</td><td>WebSocket</td><td>All actions (simulation)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 1: Set Up NAO6 Robot</h2>
|
||||||
|
|
||||||
|
<h3>Network Configuration</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Connect NAO6 to your network</li>
|
||||||
|
<li>Note the robot's IP address:
|
||||||
|
<pre><code># On the robot, say "What is my IP address?"
|
||||||
|
# Or check robot's network settings</code></pre>
|
||||||
|
</li>
|
||||||
|
<li>Verify network access:
|
||||||
|
<pre><code>ping nao.local
|
||||||
|
# Or ping the IP directly:
|
||||||
|
ping 192.168.1.100</code></pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Wake Up Robot</h3>
|
||||||
|
<p>Before connecting, wake up the robot:</p>
|
||||||
|
<pre><code>ssh nao@192.168.1.100
|
||||||
|
# Enter password when prompted
|
||||||
|
|
||||||
|
# Wake up the robot
|
||||||
|
python -c "from naoqi import ALProxy; proxy = ALProxy('ALMotion', '192.168.1.100', 9559); proxy.wakeUp()"</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 2: Start Docker Services</h2>
|
||||||
|
|
||||||
|
<pre><code>cd ~/nao6-hristudio-integration
|
||||||
|
|
||||||
|
# Set robot IP
|
||||||
|
export NAO_IP=192.168.1.100
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker compose up -d</code></pre>
|
||||||
|
|
||||||
|
<h3>Services Overview</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Service</th><th>Port</th><th>Purpose</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>nao_driver</td><td>-</td><td>ROS2 driver for NAO</td></tr>
|
||||||
|
<tr><td>ros_bridge</td><td>9090</td><td>WebSocket bridge</td></tr>
|
||||||
|
<tr><td>ros_api</td><td>-</td><td>Topic introspection</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 3: Configure HRIStudio</h2>
|
||||||
|
|
||||||
|
<h3>Install Robot Plugin</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Plugins</strong> in sidebar</li>
|
||||||
|
<li>Select your study</li>
|
||||||
|
<li>Click <strong>Browse Plugins</strong></li>
|
||||||
|
<li>Find <strong>NAO6 Robot (ROS2 Integration)</strong></li>
|
||||||
|
<li>Click <strong>Install</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Configure Plugin</h3>
|
||||||
|
<pre><code>Robot Name: NAO6-Lab
|
||||||
|
Robot IP: 192.168.1.100
|
||||||
|
WebSocket URL: ws://localhost:9090</code></pre>
|
||||||
|
|
||||||
|
<h3>Environment Variables</h3>
|
||||||
|
<p>Create <code>hristudio/.env.local</code>:</p>
|
||||||
|
<pre><code># Robot connection
|
||||||
|
NAO_ROBOT_IP=192.168.1.100
|
||||||
|
NAO_PASSWORD=robolab
|
||||||
|
NAO_USERNAME=nao
|
||||||
|
|
||||||
|
# WebSocket bridge
|
||||||
|
NEXT_PUBLIC_ROS_BRIDGE_URL=ws://localhost:9090</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 4: Test Connection</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Navigate to: <code>http://localhost:3000/nao-test</code></li>
|
||||||
|
<li>Click <strong>Connect</strong></li>
|
||||||
|
<li>Verify connection status shows "Connected"</li>
|
||||||
|
<li>Test basic actions (Say, Wave, Move)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Robot Actions Reference</h2>
|
||||||
|
|
||||||
|
<h3>Speech Actions</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Action</th><th>Parameters</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>say_text</td><td>text</td><td>Speak text</td></tr>
|
||||||
|
<tr><td>say_with_emotion</td><td>text, emotion</td><td>Emotional speech</td></tr>
|
||||||
|
<tr><td>set_volume</td><td>level</td><td>Set speech volume</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Movement Actions</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Action</th><th>Parameters</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>walk_forward</td><td>speed, duration</td><td>Walk forward</td></tr>
|
||||||
|
<tr><td>walk_backward</td><td>speed</td><td>Walk backward</td></tr>
|
||||||
|
<tr><td>turn_left</td><td>speed</td><td>Turn left</td></tr>
|
||||||
|
<tr><td>turn_right</td><td>speed</td><td>Turn right</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Troubleshooting</h2>
|
||||||
|
|
||||||
|
<h3>Robot Not Found</h3>
|
||||||
|
<pre><code>Error: Cannot connect to robot at 192.168.1.100
|
||||||
|
|
||||||
|
Solutions:
|
||||||
|
1. Verify IP address: ping 192.168.1.100
|
||||||
|
2. Check robot is powered on
|
||||||
|
3. Verify network connectivity
|
||||||
|
4. Try nao.local hostname</code></pre>
|
||||||
|
|
||||||
|
<h3>WebSocket Connection Failed</h3>
|
||||||
|
<pre><code>Error: WebSocket connection to ws://localhost:9090 failed
|
||||||
|
|
||||||
|
Solutions:
|
||||||
|
1. Check Docker is running: docker ps
|
||||||
|
2. Verify ros_bridge container
|
||||||
|
3. Check port 9090 is not blocked
|
||||||
|
4. Restart services: docker compose restart</code></pre>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/wizard-interface">
|
||||||
|
Previous: Wizard Interface
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/forms-and-surveys">
|
||||||
|
Next: Forms & Surveys
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function RunningTrialsTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Running Trials"
|
||||||
|
description="Execute experiments and manage participant trials"
|
||||||
|
duration="20 min"
|
||||||
|
level="Intermediate"
|
||||||
|
steps={[
|
||||||
|
{ title: "Schedule a trial", description: "" },
|
||||||
|
{ title: "Prepare for trial execution", description: "" },
|
||||||
|
{ title: "Start and monitor the trial", description: "" },
|
||||||
|
{ title: "Handle interventions", description: "" },
|
||||||
|
{ title: "Complete and review the trial", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Designing Experiments",
|
||||||
|
href: "/help/tutorials/designing-experiments",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Wizard Interface",
|
||||||
|
href: "/help/tutorials/wizard-interface",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>What is a Trial?</h2>
|
||||||
|
<p>A <strong>Trial</strong> is a single execution of an experiment with one participant:</p>
|
||||||
|
<pre><code>Trial
|
||||||
|
├── Participant (who took part)
|
||||||
|
├── Experiment (which protocol)
|
||||||
|
├── Status (scheduled, in_progress, completed)
|
||||||
|
├── Events (timestamped actions)
|
||||||
|
└── Data (collected responses)</code></pre>
|
||||||
|
|
||||||
|
<h2>Trial Lifecycle</h2>
|
||||||
|
<pre><code>Scheduled → In Progress → Completed
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Aborted ◄────────┤
|
||||||
|
│ │ │
|
||||||
|
└────────► Failed ◄───────┘</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 1: Schedule a Trial</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Go to your <strong>Study</strong></li>
|
||||||
|
<li>Open <strong>Trials</strong> tab</li>
|
||||||
|
<li>Click <strong>Schedule Trial</strong></li>
|
||||||
|
<li>Select:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Participant</strong>: P001</li>
|
||||||
|
<li><strong>Experiment</strong>: The Interactive Storyteller</li>
|
||||||
|
<li><strong>Scheduled Time</strong>: Today, 2:00 PM</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 2: Prepare for Trial</h2>
|
||||||
|
<p>Before starting:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Verify Robot Connection</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Check robot is powered on</li>
|
||||||
|
<li>Verify network connection</li>
|
||||||
|
<li>Test WebSocket connection</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>Review Experiment</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Ensure experiment is "Ready" status</li>
|
||||||
|
<li>Check step count and timing</li>
|
||||||
|
<li>Verify all actions are configured</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>Prepare Environment</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Ensure participant consent is obtained</li>
|
||||||
|
<li>Set up recording equipment (if needed)</li>
|
||||||
|
<li>Remove distractions</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 3: Start a Trial</h2>
|
||||||
|
<p>From Trials List:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Find the scheduled trial</li>
|
||||||
|
<li>Click <strong>Start Trial</strong></li>
|
||||||
|
<li>Confirm participant is ready</li>
|
||||||
|
<li>Click <strong>Begin</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 4: During the Trial</h2>
|
||||||
|
<p>The wizard interface provides:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Timeline View</strong> - Visual step progression</li>
|
||||||
|
<li><strong>Current Step</strong> - Highlighted current step</li>
|
||||||
|
<li><strong>Progress</strong> - Estimated time remaining</li>
|
||||||
|
<li><strong>Event Log</strong> - Timestamped events</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 5: Wizard Interventions</h2>
|
||||||
|
<p>During Wizard-of-Oz studies, wizards can intervene:</p>
|
||||||
|
|
||||||
|
<h3>Add Intervention</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>+ Intervention</strong></li>
|
||||||
|
<li>Select type:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Pause</strong>: Temporarily stop trial</li>
|
||||||
|
<li><strong>Resume</strong>: Continue after pause</li>
|
||||||
|
<li><strong>Note</strong>: Add observation</li>
|
||||||
|
<li><strong>Alert</strong>: Send alert notification</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Branch Selection</h3>
|
||||||
|
<p>When reaching a conditional step:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Observe participant response</li>
|
||||||
|
<li>Select appropriate branch</li>
|
||||||
|
<li>Selection is logged for analysis</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 6: Trial Completion</h2>
|
||||||
|
|
||||||
|
<h3>Automatic Completion</h3>
|
||||||
|
<p>When all steps complete:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Final step executes</li>
|
||||||
|
<li>Trial status → "Completed"</li>
|
||||||
|
<li>Data is saved automatically</li>
|
||||||
|
<li>Summary shown</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Manual Completion</h3>
|
||||||
|
<p>To end early:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Stop Trial</strong></li>
|
||||||
|
<li>Confirm completion</li>
|
||||||
|
<li>Select reason</li>
|
||||||
|
<li>Save partial data</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Best Practices</h2>
|
||||||
|
|
||||||
|
<h3>Before Trials</h3>
|
||||||
|
<ul className="list-disc pl-6">
|
||||||
|
<li>Robot connected and tested</li>
|
||||||
|
<li>Experiment verified</li>
|
||||||
|
<li>Participant consent obtained</li>
|
||||||
|
<li>Recording equipment ready</li>
|
||||||
|
<li>Wizard briefed on protocol</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>During Trials</h3>
|
||||||
|
<ul className="list-disc pl-6">
|
||||||
|
<li>Monitor timeline progress</li>
|
||||||
|
<li>Take timestamped notes</li>
|
||||||
|
<li>Document interventions</li>
|
||||||
|
<li>Watch for issues</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/designing-experiments">
|
||||||
|
Previous: Designing Experiments
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/wizard-interface">
|
||||||
|
Next: Wizard Interface
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function SimulationModeTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Simulation Mode"
|
||||||
|
description="Test experiments without a physical robot"
|
||||||
|
duration="10 min"
|
||||||
|
level="Beginner"
|
||||||
|
steps={[
|
||||||
|
{ title: "Enable simulation mode", description: "" },
|
||||||
|
{ title: "Test robot actions", description: "" },
|
||||||
|
{ title: "Run test trials", description: "" },
|
||||||
|
{ title: "Practice wizard controls", description: "" },
|
||||||
|
{ title: "Transition to real robot", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Data & Analysis",
|
||||||
|
href: "/help/tutorials/data-and-analysis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Why Simulation Mode?</h2>
|
||||||
|
<p>Simulation mode allows you to:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Test protocols</strong> without a robot</li>
|
||||||
|
<li><strong>Train wizards</strong> before live sessions</li>
|
||||||
|
<li><strong>Debug experiments</strong> in development</li>
|
||||||
|
<li><strong>Run pilots</strong> without robot access</li>
|
||||||
|
<li><strong>Develop</strong> on any computer</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Simulation Options</h2>
|
||||||
|
<p>HRIStudio offers two simulation approaches:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Approach</th><th>Pros</th><th>Cons</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Client-side</td>
|
||||||
|
<td>No server needed, instant</td>
|
||||||
|
<td>Limited robot simulation</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Mock Server</td>
|
||||||
|
<td>Full rosbridge protocol</td>
|
||||||
|
<td>Requires running server</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 1: Enable Client-Side Simulation</h2>
|
||||||
|
|
||||||
|
<h3>Quick Start</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Create or edit <code>hristudio/.env.local</code></li>
|
||||||
|
<li>Add: <code>NEXT_PUBLIC_SIMULATION_MODE=true</code></li>
|
||||||
|
<li>Restart the dev server:
|
||||||
|
<pre><code>bun dev</code></pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Verify Enabled</h3>
|
||||||
|
<p>Look for the simulation indicator in the UI:</p>
|
||||||
|
<pre><code>Wizard Interface [🔵 SIMULATION MODE]</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 2: Start Mock Server (Optional)</h2>
|
||||||
|
<p>For more complete testing, use the mock server:</p>
|
||||||
|
|
||||||
|
<h3>Standalone Server</h3>
|
||||||
|
<pre><code>cd hristudio/scripts/mock-robot
|
||||||
|
bun install
|
||||||
|
bun dev</code></pre>
|
||||||
|
|
||||||
|
<h3>Docker</h3>
|
||||||
|
<pre><code>cd nao6-hristudio-integration
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.mock.yml --profile mock up -d</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 3: Test Robot Actions</h2>
|
||||||
|
|
||||||
|
<h3>From NAO Test Page</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Navigate to: <code>/nao-test</code></li>
|
||||||
|
<li>Click <strong>Connect</strong></li>
|
||||||
|
<li>Test actions:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Speech</strong> - Enter text, click Say</li>
|
||||||
|
<li><strong>Movement</strong> - Set speed, click Walk</li>
|
||||||
|
<li><strong>Head</strong> - Set angles, click Move</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Simulated Actions</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Action</th><th>Simulation Behavior</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>say_text</td><td>Duration = 1.5s + 300ms × word_count</td></tr>
|
||||||
|
<tr><td>walk_forward</td><td>Position updates over 500ms</td></tr>
|
||||||
|
<tr><td>turn_left/right</td><td>Angle changes over 500ms</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 4: Run Test Trials</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Enable simulation mode</li>
|
||||||
|
<li>Create or open experiment</li>
|
||||||
|
<li>Schedule trial</li>
|
||||||
|
<li>Start trial in wizard interface</li>
|
||||||
|
<li>Execute through all steps</li>
|
||||||
|
<li>Verify timing and flow</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Test Checklist</h3>
|
||||||
|
<ul>
|
||||||
|
<li>All steps execute in order</li>
|
||||||
|
<li>Branching decisions work</li>
|
||||||
|
<li>Timing estimates are accurate</li>
|
||||||
|
<li>Event log captures everything</li>
|
||||||
|
<li>No errors or warnings</li>
|
||||||
|
<li>Trial completes successfully</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 5: Training Wizards</h2>
|
||||||
|
<p>Simulation mode is perfect for training:</p>
|
||||||
|
|
||||||
|
<h3>Training Scenarios</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Basic Operation</strong> - Start/pause trials, execute actions</li>
|
||||||
|
<li><strong>Decision Making</strong> - Select appropriate branches</li>
|
||||||
|
<li><strong>Handling Issues</strong> - Pause, respond to alerts, stop early</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Transitioning to Real Robot</h2>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Disable Simulation</strong>
|
||||||
|
<pre><code>NEXT_PUBLIC_SIMULATION_MODE=false</code></pre>
|
||||||
|
</li>
|
||||||
|
<li><strong>Connect Robot</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Start Docker services</li>
|
||||||
|
<li>Verify robot connection</li>
|
||||||
|
<li>Test with NAO Test Page</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>Run Comparison Trial</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Run same experiment on real robot</li>
|
||||||
|
<li>Compare timing and behavior</li>
|
||||||
|
<li>Adjust parameters as needed</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Comparison: Simulation vs Real</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Aspect</th><th>Simulation</th><th>Real Robot</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Setup time</td><td>1 min</td><td>30+ min</td></tr>
|
||||||
|
<tr><td>Availability</td><td>Always</td><td>Requires robot</td></tr>
|
||||||
|
<tr><td>Cost</td><td>Free</td><td>Robot access needed</td></tr>
|
||||||
|
<tr><td>Timing accuracy</td><td>Estimated</td><td>Actual</td></tr>
|
||||||
|
<tr><td>Physical interaction</td><td>✗</td><td>✓</td></tr>
|
||||||
|
<tr><td>Sensor accuracy</td><td>Fake</td><td>Real</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Best Practices</h2>
|
||||||
|
|
||||||
|
<h3>When to Use Simulation</h3>
|
||||||
|
<ul>
|
||||||
|
<li>During experiment design</li>
|
||||||
|
<li>While robot unavailable</li>
|
||||||
|
<li>For wizard training</li>
|
||||||
|
<li>For debugging protocols</li>
|
||||||
|
<li>For quick iteration</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>When to Use Real Robot</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Final protocol validation</li>
|
||||||
|
<li>Timing accuracy critical</li>
|
||||||
|
<li>Physical interaction matters</li>
|
||||||
|
<li>Sensor data needed</li>
|
||||||
|
<li>Pre-study pilot</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-start">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/data-and-analysis">
|
||||||
|
Previous: Data & Analysis
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function WizardInterfaceTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Wizard Interface"
|
||||||
|
description="Real-time trial control and monitoring"
|
||||||
|
duration="15 min"
|
||||||
|
level="Intermediate"
|
||||||
|
steps={[
|
||||||
|
{ title: "Access the wizard interface", description: "" },
|
||||||
|
{ title: "Understand the layout", description: "" },
|
||||||
|
{ title: "Control robot actions", description: "" },
|
||||||
|
{ title: "Make branching decisions", description: "" },
|
||||||
|
{ title: "Handle interruptions", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Running Trials",
|
||||||
|
href: "/help/tutorials/running-trials",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Robot Integration",
|
||||||
|
href: "/help/tutorials/robot-integration",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>What is the Wizard Interface?</h2>
|
||||||
|
<p>The <strong>Wizard Interface</strong> is your control center during trials. It provides:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Real-time trial monitoring</li>
|
||||||
|
<li>Robot action controls</li>
|
||||||
|
<li>Decision-making tools</li>
|
||||||
|
<li>Intervention capabilities</li>
|
||||||
|
<li>Event logging</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 1: Accessing the Interface</h2>
|
||||||
|
|
||||||
|
<h3>Method 1: From Trials List</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Trials</strong> in sidebar</li>
|
||||||
|
<li>Find your scheduled trial</li>
|
||||||
|
<li>Click <strong>Open Wizard</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Method 2: Direct URL</h3>
|
||||||
|
<pre><code>{`/trials/{trialId}/wizard`}</code></pre>
|
||||||
|
|
||||||
|
<h3>Method 3: Trial Queue</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Wizard Queue</strong></li>
|
||||||
|
<li>See all pending trials</li>
|
||||||
|
<li>Click <strong>Start</strong> on any trial</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 2: Understanding the Layout</h2>
|
||||||
|
|
||||||
|
<h3>Left Panel: Trial Controls</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Control</th><th>Function</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Play/Pause</td><td>Start or pause trial</td></tr>
|
||||||
|
<tr><td>Stop</td><td>End trial early</td></tr>
|
||||||
|
<tr><td>Notes</td><td>Add timestamped observations</td></tr>
|
||||||
|
<tr><td>Alert</td><td>Send alert to researchers</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Center Panel: Timeline</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Visual Progress</strong> - See step progression</li>
|
||||||
|
<li><strong>Current Position</strong> - Highlighted current step</li>
|
||||||
|
<li><strong>Time Display</strong> - Elapsed and estimated remaining</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Right Panel: Robot Control</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Status Section</strong> - Connection, battery, position</li>
|
||||||
|
<li><strong>Action Section</strong> - Quick action buttons</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Step 3: Controlling the Robot</h2>
|
||||||
|
|
||||||
|
<h3>Quick Actions</h3>
|
||||||
|
<p>Pre-configured robot actions:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Action</th><th>Description</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Say Text</td><td>Make robot speak</td></tr>
|
||||||
|
<tr><td>Wave</td><td>Wave gesture</td></tr>
|
||||||
|
<tr><td>Look at Me</td><td>Turn head toward participant</td></tr>
|
||||||
|
<tr><td>Nod</td><td>Confirmation nod</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Custom Say Text</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Say Text</strong></li>
|
||||||
|
<li>Enter text in popup</li>
|
||||||
|
<li>Select options (speed, emotion)</li>
|
||||||
|
<li>Click <strong>Execute</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 4: Making Decisions</h2>
|
||||||
|
<p>When the experiment reaches a branching point:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Observe</strong> participant's actual response</li>
|
||||||
|
<li><strong>Consider</strong> protocol criteria</li>
|
||||||
|
<li><strong>Select</strong> appropriate branch</li>
|
||||||
|
<li><strong>Confirm</strong> selection</li>
|
||||||
|
</ol>
|
||||||
|
<p>Decision is logged with timestamp and trial continues.</p>
|
||||||
|
|
||||||
|
<h2>Step 5: Handling Interruptions</h2>
|
||||||
|
|
||||||
|
<h3>Pause Trial</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Pause</strong> button</li>
|
||||||
|
<li>Add reason (optional)</li>
|
||||||
|
<li>Trial pauses, robot holds position</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Resume Trial</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Play</strong> button</li>
|
||||||
|
<li>Trial resumes from pause point</li>
|
||||||
|
<li>Pause duration is logged</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Stop Trial</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Click <strong>Stop</strong> button</li>
|
||||||
|
<li>Select reason</li>
|
||||||
|
<li>Confirm stop</li>
|
||||||
|
<li>Partial data is saved</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Keyboard Shortcuts</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Key</th><th>Action</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Space</td><td>Play/Pause toggle</td></tr>
|
||||||
|
<tr><td>Escape</td><td>Stop trial</td></tr>
|
||||||
|
<tr><td>N</td><td>Add note</td></tr>
|
||||||
|
<tr><td>A</td><td>Send alert</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Event Logging</h2>
|
||||||
|
<p>All actions are logged automatically:</p>
|
||||||
|
<pre><code>[14:32:05] Trial started
|
||||||
|
[14:32:08] Step 1: The Hook
|
||||||
|
[14:32:10] Action: Say Text "Hello!"
|
||||||
|
[14:33:28] Wizard Note: "Participant engaged"
|
||||||
|
[14:33:30] Branch: Correct selected
|
||||||
|
[14:34:05] Trial completed</code></pre>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/running-trials">
|
||||||
|
Previous: Running Trials
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/robot-integration">
|
||||||
|
Next: Robot Integration
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { TutorialPage } from "~/components/ui/tutorial-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function YourFirstStudyTutorial() {
|
||||||
|
return (
|
||||||
|
<TutorialPage
|
||||||
|
title="Your First Study"
|
||||||
|
description="Create a research study and manage team members"
|
||||||
|
duration="15 min"
|
||||||
|
level="Beginner"
|
||||||
|
steps={[
|
||||||
|
{ title: "Understand the Study structure", description: "" },
|
||||||
|
{ title: "Create a new study", description: "" },
|
||||||
|
{ title: "Add team members", description: "" },
|
||||||
|
{ title: "Install robot plugins", description: "" },
|
||||||
|
{ title: "Add participants", description: "" },
|
||||||
|
]}
|
||||||
|
prevTutorial={{
|
||||||
|
title: "Getting Started",
|
||||||
|
href: "/help/tutorials/getting-started",
|
||||||
|
}}
|
||||||
|
nextTutorial={{
|
||||||
|
title: "Designing Experiments",
|
||||||
|
href: "/help/tutorials/designing-experiments",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>What is a Study?</h2>
|
||||||
|
<p>In HRIStudio, a <strong>Study</strong> is the top-level container for your research:</p>
|
||||||
|
<pre><code>Study
|
||||||
|
├── Experiments (multiple protocols)
|
||||||
|
├── Participants (study participants)
|
||||||
|
├── Team Members (collaborators)
|
||||||
|
├── Forms & Surveys (consent, questionnaires)
|
||||||
|
└── Trials (individual experiment runs)</code></pre>
|
||||||
|
|
||||||
|
<h2>Step 1: Create a New Study</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Log in as <strong>Researcher</strong> or <strong>Administrator</strong></li>
|
||||||
|
<li>Click <strong>Studies</strong> in the sidebar</li>
|
||||||
|
<li>Click <strong>Create Study</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Study Settings</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>Study title</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Description</td>
|
||||||
|
<td>Brief overview of research goals</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Institution</td>
|
||||||
|
<td>University or organization</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IRB Protocol</td>
|
||||||
|
<td>Protocol number (e.g., 2024-HRI-001)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>Draft, Active, Completed, Archived</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Step 2: Add Team Members</h2>
|
||||||
|
<p>Studies can have multiple collaborators with different roles:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Owner</td>
|
||||||
|
<td>Full access, can delete study</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Researcher</td>
|
||||||
|
<td>Create/edit experiments, manage participants</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Wizard</td>
|
||||||
|
<td>Execute trials, control robot during trials</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Observer</td>
|
||||||
|
<td>View-only access, add annotations</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Adding a Wizard</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open your study</li>
|
||||||
|
<li>Go to <strong>Team</strong> tab</li>
|
||||||
|
<li>Click <strong>Add Member</strong></li>
|
||||||
|
<li>Enter the wizard's email</li>
|
||||||
|
<li>Select <strong>Wizard</strong> role</li>
|
||||||
|
<li>Click <strong>Invite</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 3: Install Robot Plugins</h2>
|
||||||
|
<p>For studies involving robots, you need to install the appropriate plugin:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Plugins</strong> in the sidebar</li>
|
||||||
|
<li>Select your study from the dropdown</li>
|
||||||
|
<li>Click <strong>Browse Plugins</strong></li>
|
||||||
|
<li>Find your robot (e.g., "NAO6 Robot")</li>
|
||||||
|
<li>Click <strong>Install</strong></li>
|
||||||
|
<li>Configure robot settings (IP address, etc.)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Step 4: Add Participants</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Go to <strong>Participants</strong> tab</li>
|
||||||
|
<li>Click <strong>Add Participant</strong></li>
|
||||||
|
<li>Enter participant code (e.g., "P001")</li>
|
||||||
|
<li>Fill in optional details</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Batch Import</h3>
|
||||||
|
<p>For large studies, import from CSV:</p>
|
||||||
|
<pre><code>participantCode,name,email,notes
|
||||||
|
P001,John Smith,john@email.com,Condition A
|
||||||
|
P002,Jane Doe,jane@email.com,Condition B</code></pre>
|
||||||
|
|
||||||
|
<h2>Study Workflow</h2>
|
||||||
|
<pre><code>Draft → Active → Recruiting → In Progress → Completed
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ └── All trials done
|
||||||
|
│ │ │ └── Trials running
|
||||||
|
│ │ └── Recruiting participants
|
||||||
|
│ └── Ready to collect data
|
||||||
|
└── Setting up study</code></pre>
|
||||||
|
|
||||||
|
<h2>Common Tasks</h2>
|
||||||
|
|
||||||
|
<h3>Clone a Study</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Open the study</li>
|
||||||
|
<li>Click <strong>Settings</strong> (gear icon)</li>
|
||||||
|
<li>Select <strong>Duplicate Study</strong></li>
|
||||||
|
<li>Enter new study name</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Archive a Study</h3>
|
||||||
|
<p>When a study is complete:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Go to study settings</li>
|
||||||
|
<li>Change status to <strong>Archived</strong></li>
|
||||||
|
<li>Data is preserved but study is read-only</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/help/tutorials/getting-started">
|
||||||
|
Previous: Getting Started
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/help/tutorials/designing-experiments">
|
||||||
|
Next: Designing Experiments
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TutorialPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cookies } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "~/components/ui/sidebar";
|
} from "~/components/ui/sidebar";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { AppSidebar } from "~/components/dashboard/app-sidebar";
|
import { AppSidebar } from "~/components/dashboard/app-sidebar";
|
||||||
import { auth } from "~/server/auth";
|
import { auth } from "~/lib/auth";
|
||||||
import {
|
import {
|
||||||
BreadcrumbProvider,
|
BreadcrumbProvider,
|
||||||
BreadcrumbDisplay,
|
BreadcrumbDisplay,
|
||||||
@@ -22,16 +22,15 @@ interface DashboardLayoutProps {
|
|||||||
export default async function DashboardLayout({
|
export default async function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
}: DashboardLayoutProps) {
|
}: DashboardLayoutProps) {
|
||||||
const session = await auth();
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRole =
|
const userRole = "researcher"; // Default role for dashboard access
|
||||||
typeof session.user.roles?.[0] === "string"
|
|
||||||
? session.user.roles[0]
|
|
||||||
: (session.user.roles?.[0]?.role ?? "observer");
|
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
|
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
|
||||||
|
|||||||
@@ -1,10 +1,35 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { PasswordChangeForm } from "~/components/profile/password-change-form";
|
import Link from "next/link";
|
||||||
import { ProfileEditForm } from "~/components/profile/profile-edit-form";
|
import { useSession } from "~/lib/auth-client";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { api } from "~/trpc/react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
|
Shield,
|
||||||
|
Lock,
|
||||||
|
Settings,
|
||||||
|
Building,
|
||||||
|
Calendar,
|
||||||
|
ChevronRight,
|
||||||
|
Loader2,
|
||||||
|
Save,
|
||||||
|
X,
|
||||||
|
Crown,
|
||||||
|
FlaskConical,
|
||||||
|
Eye,
|
||||||
|
UserCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -12,200 +37,375 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import { PageHeader } from "~/components/ui/page-header";
|
|
||||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
|
||||||
import { formatRole, getRoleDescription } from "~/lib/auth-client";
|
|
||||||
import {
|
import {
|
||||||
User,
|
Dialog,
|
||||||
Shield,
|
DialogContent,
|
||||||
Download,
|
DialogDescription,
|
||||||
Trash2,
|
DialogFooter,
|
||||||
ExternalLink,
|
DialogHeader,
|
||||||
Lock,
|
DialogTitle,
|
||||||
UserCog,
|
DialogTrigger,
|
||||||
Mail,
|
} from "~/components/ui/dialog";
|
||||||
Fingerprint,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
|
|
||||||
interface ProfileUser {
|
interface Membership {
|
||||||
id: string;
|
studyId: string;
|
||||||
name: string | null;
|
role: string;
|
||||||
email: string;
|
joinedAt: Date;
|
||||||
image: string | null;
|
|
||||||
roles?: Array<{
|
|
||||||
role: "administrator" | "researcher" | "wizard" | "observer";
|
|
||||||
grantedAt: string | Date;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileContent({ user }: { user: ProfileUser }) {
|
function getMemberRole(memberships: Membership[], studyId: string): string {
|
||||||
|
const membership = memberships.find((m) => m.studyId === studyId);
|
||||||
|
return membership?.role ?? "observer";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfilePageContent() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
|
const [name, setName] = React.useState(session?.user?.name ?? "");
|
||||||
|
const [email, setEmail] = React.useState(session?.user?.email ?? "");
|
||||||
|
const [passwordOpen, setPasswordOpen] = React.useState(false);
|
||||||
|
const [currentPassword, setCurrentPassword] = React.useState("");
|
||||||
|
const [newPassword, setNewPassword] = React.useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = React.useState("");
|
||||||
|
|
||||||
|
const { data: userData } = api.users.get.useQuery(
|
||||||
|
{ id: session?.user?.id ?? "" },
|
||||||
|
{ enabled: !!session?.user?.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: userStudies } = api.studies.list.useQuery({
|
||||||
|
memberOnly: true,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: membershipsData } = api.studies.getMyMemberships.useQuery();
|
||||||
|
|
||||||
|
const studyMemberships = membershipsData ?? [];
|
||||||
|
|
||||||
|
const updateProfile = api.users.update.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Profile updated successfully");
|
||||||
|
void utils.users.get.invalidate();
|
||||||
|
setIsEditing(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to update profile", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const changePassword = api.users.changePassword.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Password changed successfully");
|
||||||
|
setPasswordOpen(false);
|
||||||
|
setCurrentPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to change password", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
toast.error("Name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateProfile.mutate({ id: session?.user?.id ?? "", name, email });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
toast.error("Passwords don't match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
toast.error("Password must be at least 8 characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changePassword.mutate({
|
||||||
|
currentPassword,
|
||||||
|
newPassword,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = userData ?? session?.user;
|
||||||
|
const roles = (userData as any)?.systemRoles ?? [];
|
||||||
|
const initials = (user?.name ?? user?.email ?? "U").charAt(0).toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in space-y-8 duration-500">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
{/* Header */}
|
||||||
title={user.name ?? "User"}
|
<div className="flex items-center justify-between">
|
||||||
description={user.email}
|
<div className="flex items-center gap-4">
|
||||||
icon={User}
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-xl font-bold text-primary-foreground">
|
||||||
badges={[
|
{initials}
|
||||||
{ label: `ID: ${user.id}`, variant: "outline" },
|
</div>
|
||||||
...(user.roles?.map((r) => ({
|
<div>
|
||||||
label: formatRole(r.role),
|
<h1 className="text-2xl font-bold">{user?.name ?? "User"}</h1>
|
||||||
variant: "secondary" as const,
|
<p className="text-muted-foreground">{user?.email}</p>
|
||||||
})) ?? []),
|
{roles.length > 0 && (
|
||||||
]}
|
<div className="mt-1 flex gap-2">
|
||||||
/>
|
{roles.map((role: any) => (
|
||||||
|
<Badge key={role.role} variant="secondary" className="text-xs">
|
||||||
|
{role.role}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||||
|
<X className="mr-2 h-4 w-4" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={updateProfile.isPending}>
|
||||||
|
{updateProfile.isPending ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" onClick={() => setIsEditing(true)}>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
Edit Profile
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
{/* Main Content */}
|
||||||
{/* Main Content (Left Column) */}
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="space-y-8 lg:col-span-2">
|
{/* Left Column - Profile Info */}
|
||||||
|
<div className="space-y-6 lg:col-span-2">
|
||||||
{/* Personal Information */}
|
{/* Personal Information */}
|
||||||
<section className="space-y-4">
|
<Card>
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
<CardHeader>
|
||||||
<User className="text-primary h-5 w-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold">Personal Information</h3>
|
<User className="h-5 w-5 text-primary" />
|
||||||
</div>
|
Personal Information
|
||||||
<Card className="border-border/60 hover:border-border transition-colors">
|
</CardTitle>
|
||||||
<CardHeader>
|
<CardDescription>
|
||||||
<CardTitle className="text-base">Contact Details</CardTitle>
|
Your public profile information
|
||||||
<CardDescription>
|
</CardDescription>
|
||||||
Update your public profile information
|
</CardHeader>
|
||||||
</CardDescription>
|
<CardContent className="space-y-4">
|
||||||
</CardHeader>
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<CardContent>
|
<div className="space-y-2">
|
||||||
<ProfileEditForm
|
<Label htmlFor="name">Full Name</Label>
|
||||||
user={{
|
{isEditing ? (
|
||||||
id: user.id,
|
<Input
|
||||||
name: user.name,
|
id="name"
|
||||||
email: user.email,
|
value={name}
|
||||||
image: user.image,
|
onChange={(e) => setName(e.target.value)}
|
||||||
}}
|
placeholder="Your name"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
) : (
|
||||||
</Card>
|
<div className="flex items-center gap-2 rounded-md border bg-muted/50 p-2">
|
||||||
</section>
|
<User className="text-muted-foreground h-4 w-4" />
|
||||||
|
<span>{name || "Not set"}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email Address</Label>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border bg-muted/50 p-2">
|
||||||
|
<Mail className="text-muted-foreground h-4 w-4" />
|
||||||
|
<span>{email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>User ID</Label>
|
||||||
|
<div className="rounded-md border bg-muted/50 p-2 font-mono text-sm">
|
||||||
|
{user?.id ?? session?.user?.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Security */}
|
{/* Recent Activity */}
|
||||||
<section className="space-y-4">
|
<Card>
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
<CardHeader>
|
||||||
<Lock className="text-primary h-5 w-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold">Security</h3>
|
<Calendar className="h-5 w-5 text-primary" />
|
||||||
</div>
|
Recent Activity
|
||||||
<Card className="border-border/60 hover:border-border transition-colors">
|
</CardTitle>
|
||||||
<CardHeader>
|
<CardDescription>
|
||||||
<CardTitle className="text-base">Password</CardTitle>
|
Your recent actions across the platform
|
||||||
<CardDescription>
|
</CardDescription>
|
||||||
Ensure your account stays secure
|
</CardHeader>
|
||||||
</CardDescription>
|
<CardContent>
|
||||||
</CardHeader>
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<CardContent>
|
<Calendar className="text-muted-foreground/50 mb-3 h-12 w-12" />
|
||||||
<PasswordChangeForm />
|
<p className="font-medium">No recent activity</p>
|
||||||
</CardContent>
|
<p className="text-muted-foreground text-sm">
|
||||||
</Card>
|
Your recent actions will appear here
|
||||||
</section>
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar (Right Column) */}
|
{/* Right Column - Settings */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
{/* Permissions */}
|
{/* Security */}
|
||||||
<section className="space-y-4">
|
<Card>
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
<CardHeader>
|
||||||
<Shield className="text-primary h-5 w-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold">Permissions</h3>
|
<Shield className="h-5 w-5 text-primary" />
|
||||||
</div>
|
Security
|
||||||
<Card>
|
</CardTitle>
|
||||||
<CardContent className="pt-6">
|
</CardHeader>
|
||||||
{user.roles && user.roles.length > 0 ? (
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
{user.roles.map((roleInfo, index) => (
|
<div className="flex items-center gap-3">
|
||||||
<div key={index} className="space-y-2">
|
<Lock className="text-muted-foreground h-4 w-4" />
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<span className="text-sm font-medium">
|
<p className="text-sm font-medium">Password</p>
|
||||||
{formatRole(roleInfo.role)}
|
<p className="text-muted-foreground text-xs">Last changed: Never</p>
|
||||||
</span>
|
</div>
|
||||||
<span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px]">
|
</div>
|
||||||
Since{" "}
|
<Dialog open={passwordOpen} onOpenChange={setPasswordOpen}>
|
||||||
{new Date(roleInfo.grantedAt).toLocaleDateString()}
|
<DialogTrigger asChild>
|
||||||
</span>
|
<Button variant="ghost" size="sm">
|
||||||
</div>
|
Change
|
||||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
</Button>
|
||||||
{getRoleDescription(roleInfo.role)}
|
</DialogTrigger>
|
||||||
</p>
|
<DialogContent>
|
||||||
{index < (user.roles?.length || 0) - 1 && (
|
<DialogHeader>
|
||||||
<Separator className="my-2" />
|
<DialogTitle>Change Password</DialogTitle>
|
||||||
)}
|
<DialogDescription>
|
||||||
|
Enter your current password and choose a new one.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="current">Current Password</Label>
|
||||||
|
<Input
|
||||||
|
id="current"
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="space-y-2">
|
||||||
<div className="text-muted-foreground mt-4 rounded-lg border border-blue-100 bg-blue-50/50 p-3 text-xs dark:border-blue-900/30 dark:bg-blue-900/10">
|
<Label htmlFor="new">New Password</Label>
|
||||||
<div className="text-primary mb-1 flex items-center gap-2 font-medium">
|
<Input
|
||||||
<Shield className="h-3 w-3" />
|
id="new"
|
||||||
<span>Role Management</span>
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
System roles are managed by administrators. Contact
|
<div className="space-y-2">
|
||||||
support if you need access adjustments.
|
<Label htmlFor="confirm">Confirm Password</Label>
|
||||||
|
<Input
|
||||||
|
id="confirm"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setPasswordOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={changePassword.isPending}>
|
||||||
|
{changePassword.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-destructive/5 p-3">
|
||||||
|
<p className="text-sm font-medium text-destructive">Danger Zone</p>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
Account deletion is not available. Contact an administrator for assistance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Studies Access */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Building className="h-5 w-5 text-primary" />
|
||||||
|
Studies Access
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Studies you have access to
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{userStudies?.studies.slice(0, 5).map((study) => (
|
||||||
|
<Link
|
||||||
|
key={study.id}
|
||||||
|
href={`/studies/${study.id}`}
|
||||||
|
className="hover:bg-accent/50 flex items-center justify-between rounded-md border p-3 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
|
||||||
|
<span className="text-xs font-medium text-primary">
|
||||||
|
{(study.name ?? "S").charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{study.name}</p>
|
||||||
|
<p className="text-muted-foreground text-xs capitalize">
|
||||||
|
{getMemberRole(studyMemberships, study.id)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="py-4 text-center">
|
</Link>
|
||||||
<p className="text-sm font-medium">No Roles Assigned</p>
|
))}
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
{(!userStudies?.studies.length) && (
|
||||||
Contact an admin to request access.
|
<div className="flex flex-col items-center justify-center py-4 text-center">
|
||||||
</p>
|
<Building className="text-muted-foreground/50 mb-2 h-8 w-8" />
|
||||||
<Button size="sm" variant="outline" className="mt-3 w-full">
|
<p className="text-sm">No studies yet</p>
|
||||||
Request Access
|
<Button variant="link" size="sm" asChild className="mt-1">
|
||||||
</Button>
|
<Link href="/studies/new">Create a study</Link>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Data & Privacy */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<div className="flex items-center gap-2 border-b pb-2">
|
|
||||||
<Download className="text-primary h-5 w-5" />
|
|
||||||
<h3 className="text-lg font-semibold">Data & Privacy</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="border-destructive/10 bg-destructive/5 overflow-hidden">
|
|
||||||
<CardContent className="space-y-4 pt-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="mb-1 text-sm font-semibold">Export Data</h4>
|
|
||||||
<p className="text-muted-foreground mb-3 text-xs">
|
|
||||||
Download a copy of your personal data.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="bg-background w-full"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Download className="mr-2 h-3 w-3" />
|
|
||||||
Download Archive
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="bg-destructive/10" />
|
)}
|
||||||
<div>
|
{userStudies && userStudies.studies.length > 5 && (
|
||||||
<h4 className="text-destructive mb-1 text-sm font-semibold">
|
<Button variant="ghost" size="sm" asChild className="w-full">
|
||||||
Delete Account
|
<Link href="/studies">
|
||||||
</h4>
|
View all {userStudies.studies.length} studies <ChevronRight className="ml-1 h-3 w-3" />
|
||||||
<p className="text-muted-foreground mb-3 text-xs">
|
</Link>
|
||||||
This action is irreversible.
|
</Button>
|
||||||
</p>
|
)}
|
||||||
<Button
|
</CardContent>
|
||||||
variant="destructive"
|
</Card>
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-3 w-3" />
|
|
||||||
Delete Account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,17 +413,12 @@ function ProfileContent({ user }: { user: ProfileUser }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, isPending } = useSession();
|
||||||
|
|
||||||
useBreadcrumbsEffect([
|
if (isPending) {
|
||||||
{ label: "Dashboard", href: "/dashboard" },
|
|
||||||
{ label: "Profile" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (status === "loading") {
|
|
||||||
return (
|
return (
|
||||||
<div className="text-muted-foreground animate-pulse p-8">
|
<div className="flex items-center justify-center p-12">
|
||||||
Loading profile...
|
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -232,7 +427,5 @@ export default function ProfilePage() {
|
|||||||
redirect("/auth/signin");
|
redirect("/auth/signin");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = session.user;
|
return <ProfilePageContent />;
|
||||||
|
|
||||||
return <ProfileContent user={user} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import type {
|
|||||||
} from "~/lib/experiment-designer/types";
|
} from "~/lib/experiment-designer/types";
|
||||||
import { api } from "~/trpc/server";
|
import { api } from "~/trpc/server";
|
||||||
import { DesignerPageClient } from "./DesignerPageClient";
|
import { DesignerPageClient } from "./DesignerPageClient";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { studyPlugins, plugins } from "~/server/db/schema";
|
||||||
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
interface ExperimentDesignerPageProps {
|
interface ExperimentDesignerPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -74,10 +77,20 @@ export default async function ExperimentDesignerPage({
|
|||||||
actionDefinitions: Array<{ id: string }> | null;
|
actionDefinitions: Array<{ id: string }> | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const rawInstalledPluginsUnknown: unknown =
|
const installedPluginsResult = await db
|
||||||
await api.robots.plugins.getStudyPlugins({
|
.select({
|
||||||
studyId: experiment.study.id,
|
plugin: {
|
||||||
});
|
id: plugins.id,
|
||||||
|
name: plugins.name,
|
||||||
|
version: plugins.version,
|
||||||
|
actionDefinitions: plugins.actionDefinitions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.from(studyPlugins)
|
||||||
|
.innerJoin(plugins, eq(studyPlugins.pluginId, plugins.id))
|
||||||
|
.where(eq(studyPlugins.studyId, experiment.study.id))
|
||||||
|
.orderBy(desc(studyPlugins.installedAt));
|
||||||
|
const rawInstalledPluginsUnknown = installedPluginsResult;
|
||||||
|
|
||||||
function asRecord(v: unknown): Record<string, unknown> | null {
|
function asRecord(v: unknown): Record<string, unknown> | null {
|
||||||
return v && typeof v === "object"
|
return v && typeof v === "object"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "~/components/ui/entity-view";
|
} from "~/components/ui/entity-view";
|
||||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "~/lib/auth-client";
|
||||||
import { useStudyManagement } from "~/hooks/useStudyManagement";
|
import { useStudyManagement } from "~/hooks/useStudyManagement";
|
||||||
|
|
||||||
interface ExperimentDetailPageProps {
|
interface ExperimentDetailPageProps {
|
||||||
@@ -99,6 +99,9 @@ export default function ExperimentDetailPage({
|
|||||||
params,
|
params,
|
||||||
}: ExperimentDetailPageProps) {
|
}: ExperimentDetailPageProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const { data: userData } = api.auth.me.useQuery(undefined, {
|
||||||
|
enabled: !!session?.user,
|
||||||
|
});
|
||||||
const [experiment, setExperiment] = useState<Experiment | null>(null);
|
const [experiment, setExperiment] = useState<Experiment | null>(null);
|
||||||
const [trials, setTrials] = useState<Trial[]>([]);
|
const [trials, setTrials] = useState<Trial[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -181,7 +184,7 @@ export default function ExperimentDetailPage({
|
|||||||
const description = experiment.description;
|
const description = experiment.description;
|
||||||
|
|
||||||
// Check if user can edit this experiment
|
// Check if user can edit this experiment
|
||||||
const userRoles = session?.user?.roles?.map((r) => r.role) ?? [];
|
const userRoles = userData?.roles ?? [];
|
||||||
const canEdit =
|
const canEdit =
|
||||||
userRoles.includes("administrator") || userRoles.includes("researcher");
|
userRoles.includes("administrator") || userRoles.includes("researcher");
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default function StudyExperimentsPage() {
|
|||||||
}
|
}
|
||||||
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
||||||
|
|
||||||
|
const canManage = study?.userRole === "owner" || study?.userRole === "researcher";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -38,12 +40,14 @@ export default function StudyExperimentsPage() {
|
|||||||
description="Design and manage experiment protocols for this study"
|
description="Design and manage experiment protocols for this study"
|
||||||
icon={FlaskConical}
|
icon={FlaskConical}
|
||||||
actions={
|
actions={
|
||||||
<Button asChild>
|
canManage ? (
|
||||||
<a href={`/studies/${studyId}/experiments/new`}>
|
<Button asChild>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<a href={`/studies/${studyId}/experiments/new`}>
|
||||||
Create Experiment
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
</a>
|
Create Experiment
|
||||||
</Button>
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,961 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useSession } from "~/lib/auth-client";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
ArrowLeft,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
GripVertical,
|
||||||
|
FileSignature,
|
||||||
|
ClipboardList,
|
||||||
|
FileQuestion,
|
||||||
|
Save,
|
||||||
|
Eye,
|
||||||
|
Edit2,
|
||||||
|
Users,
|
||||||
|
CheckCircle,
|
||||||
|
Printer,
|
||||||
|
Pencil,
|
||||||
|
X,
|
||||||
|
FileDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
|
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "~/components/ui/select";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { FormField, FormFieldType } from "~/lib/types/forms";
|
||||||
|
import { FORM_FIELD_TYPES } from "~/lib/types/forms";
|
||||||
|
import { formStatusColors } from "~/lib/constants";
|
||||||
|
import { FormBuilder } from "~/components/forms/FormBuilder";
|
||||||
|
import { FormFieldRenderer } from "~/components/forms/FormFieldRenderer";
|
||||||
|
|
||||||
|
const formTypeIcons = {
|
||||||
|
consent: FileSignature,
|
||||||
|
survey: ClipboardList,
|
||||||
|
questionnaire: FileQuestion,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FormViewPageProps {
|
||||||
|
params: Promise<{
|
||||||
|
id: string;
|
||||||
|
formId: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormViewPage({ params }: FormViewPageProps) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [resolvedParams, setResolvedParams] = useState<{
|
||||||
|
id: string;
|
||||||
|
formId: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isEnteringData, setIsEnteringData] = useState(false);
|
||||||
|
const [selectedParticipantId, setSelectedParticipantId] =
|
||||||
|
useState<string>("");
|
||||||
|
const [formResponses, setFormResponses] = useState<Record<string, any>>({});
|
||||||
|
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [fields, setFields] = useState<FormField[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resolveParams = async () => {
|
||||||
|
const resolved = await params;
|
||||||
|
setResolvedParams(resolved);
|
||||||
|
};
|
||||||
|
void resolveParams();
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
const { data: participants } = api.participants.list.useQuery(
|
||||||
|
{ studyId: resolvedParams?.id ?? "" },
|
||||||
|
{ enabled: !!resolvedParams?.id && isEnteringData },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: study } = api.studies.get.useQuery(
|
||||||
|
{ id: resolvedParams?.id ?? "" },
|
||||||
|
{ enabled: !!resolvedParams?.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: form, isLoading } = api.forms.get.useQuery(
|
||||||
|
{ id: resolvedParams?.formId ?? "" },
|
||||||
|
{ enabled: !!resolvedParams?.formId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: responsesData } = api.forms.getResponses.useQuery(
|
||||||
|
{ formId: resolvedParams?.formId ?? "", limit: 50 },
|
||||||
|
{ enabled: !!resolvedParams?.formId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const userRole = (study as any)?.userRole;
|
||||||
|
const canManage = userRole === "owner" || userRole === "researcher";
|
||||||
|
|
||||||
|
const updateForm = api.forms.update.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Form updated successfully!");
|
||||||
|
setIsEditing(false);
|
||||||
|
void utils.forms.get.invalidate({ id: resolvedParams?.formId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to update form", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitResponse = api.forms.submitResponse.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Response submitted successfully!");
|
||||||
|
setIsEnteringData(false);
|
||||||
|
setSelectedParticipantId("");
|
||||||
|
setFormResponses({});
|
||||||
|
void utils.forms.getResponses.invalidate({
|
||||||
|
formId: resolvedParams?.formId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to submit response", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportCsv = api.forms.exportCsv.useQuery(
|
||||||
|
{ formId: resolvedParams?.formId ?? "" },
|
||||||
|
{ enabled: !!resolvedParams?.formId && canManage },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExportCsv = () => {
|
||||||
|
if (exportCsv.data) {
|
||||||
|
const blob = new Blob([exportCsv.data.csv], { type: "text/csv" });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = exportCsv.data.filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
toast.success("CSV exported successfully!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePdf = async () => {
|
||||||
|
if (!study || !form) return;
|
||||||
|
setIsGeneratingPdf(true);
|
||||||
|
const { downloadPdfFromHtml } = await import("~/lib/pdf-generator");
|
||||||
|
|
||||||
|
const fieldsHtml = fields
|
||||||
|
.map((field, index) => {
|
||||||
|
const requiredMark = field.required
|
||||||
|
? '<span style="color: red">*</span>'
|
||||||
|
: "";
|
||||||
|
let inputField = "";
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case "text":
|
||||||
|
inputField =
|
||||||
|
'<input type="text" style="width: 100%; padding: 8px; border: 1px solid #ccc; margin-top: 4px;" placeholder="________________________" />';
|
||||||
|
break;
|
||||||
|
case "textarea":
|
||||||
|
inputField =
|
||||||
|
'<textarea style="width: 100%; height: 80px; padding: 8px; border: 1px solid #ccc; margin-top: 4px;" placeholder=""></textarea>';
|
||||||
|
break;
|
||||||
|
case "multiple_choice":
|
||||||
|
inputField = `<div style="margin-top: 4px;">${field.options
|
||||||
|
?.map((opt) => `<div><input type="checkbox" /> ${opt}</div>`)
|
||||||
|
.join("")}</div>`;
|
||||||
|
break;
|
||||||
|
case "checkbox":
|
||||||
|
inputField =
|
||||||
|
'<div style="margin-top: 4px;"><input type="checkbox" /> Yes</div>';
|
||||||
|
break;
|
||||||
|
case "yes_no":
|
||||||
|
inputField =
|
||||||
|
'<div style="margin-top: 4px;"><input type="radio" name="yn" /> Yes <input type="radio" name="yn" /> No</div>';
|
||||||
|
break;
|
||||||
|
case "rating":
|
||||||
|
const scale = (field.settings?.scale as number) || 5;
|
||||||
|
inputField = `<div style="margin-top: 4px;">${Array.from(
|
||||||
|
{ length: scale },
|
||||||
|
(_, i) => `<input type="radio" name="rating" /> ${i + 1} `,
|
||||||
|
).join("")}</div>`;
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
inputField =
|
||||||
|
'<input type="text" style="padding: 8px; border: 1px solid #ccc; margin-top: 4px;" placeholder="MM/DD/YYYY" />';
|
||||||
|
break;
|
||||||
|
case "signature":
|
||||||
|
inputField =
|
||||||
|
'<div style="height: 60px; border: 1px solid #ccc; margin-top: 4px;"></div><div style="font-size: 12px; color: #666; margin-top: 4px;">Signature: _________________________ Date: ____________</div>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<p style="margin: 0; font-weight: 500;">${index + 1}. ${field.label} ${requiredMark}</p>
|
||||||
|
${inputField}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join(
|
||||||
|
"<hr style='border: none; border-top: 1px solid #eee; margin: 16px 0;' />",
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
|
<h1 style="margin-bottom: 8px;">${title}</h1>
|
||||||
|
${description ? `<p style="color: #666; margin-bottom: 24px;">${description}</p>` : ""}
|
||||||
|
<p style="color: #666; font-size: 12px; margin-bottom: 24px;">
|
||||||
|
<strong>Study:</strong> ${study?.name || ""} |
|
||||||
|
<strong>Form Type:</strong> ${form?.type} |
|
||||||
|
<strong>Version:</strong> ${form?.version}
|
||||||
|
</p>
|
||||||
|
<hr style="border: none; border-top: 2px solid #333; margin-bottom: 24px;" />
|
||||||
|
${fieldsHtml}
|
||||||
|
<hr style="border: none; border-top: 2px solid #333; margin-top: 24px;" />
|
||||||
|
<p style="font-size: 10px; color: #999; margin-top: 24px;">
|
||||||
|
Generated by HRIStudio | ${new Date().toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await downloadPdfFromHtml(html, {
|
||||||
|
filename: `${title.replace(/\s+/g, "_")}_form.pdf`,
|
||||||
|
});
|
||||||
|
setIsGeneratingPdf(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataEntry = () => {
|
||||||
|
if (!selectedParticipantId || !form) {
|
||||||
|
toast.error("Please select a participant");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const answers: Record<string, any> = {};
|
||||||
|
fields.forEach((field) => {
|
||||||
|
answers[field.id] = formResponses[field.id] ?? "";
|
||||||
|
});
|
||||||
|
submitResponse.mutate({
|
||||||
|
formId: form.id,
|
||||||
|
participantId: selectedParticipantId,
|
||||||
|
responses: answers,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (form) {
|
||||||
|
setTitle(form.title);
|
||||||
|
setDescription(form.description || "");
|
||||||
|
setFields((form.fields as FormField[]) || []);
|
||||||
|
}
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
useBreadcrumbsEffect([
|
||||||
|
{ label: "Dashboard", href: "/dashboard" },
|
||||||
|
{ label: "Studies", href: "/studies" },
|
||||||
|
{ label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` },
|
||||||
|
{ label: "Forms", href: `/studies/${resolvedParams?.id}/forms` },
|
||||||
|
{ label: form?.title ?? "Form" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || !form) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
const TypeIcon =
|
||||||
|
formTypeIcons[form.type as keyof typeof formTypeIcons] || FileText;
|
||||||
|
const responses = responsesData?.responses ?? [];
|
||||||
|
|
||||||
|
const addField = (type: string) => {
|
||||||
|
const newField: FormField = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: type as FormFieldType,
|
||||||
|
label: `New ${FORM_FIELD_TYPES.find((f) => f.value === type)?.label || "Field"}`,
|
||||||
|
required: false,
|
||||||
|
options:
|
||||||
|
type === "multiple_choice" ? ["Option 1", "Option 2"] : undefined,
|
||||||
|
};
|
||||||
|
setFields([...fields, newField]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeField = (id: string) => {
|
||||||
|
setFields(fields.filter((f) => f.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateField = (id: string, updates: Partial<FormField>) => {
|
||||||
|
setFields(fields.map((f) => (f.id === id ? { ...f, ...updates } : f)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
updateForm.mutate({
|
||||||
|
id: form.id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
fields,
|
||||||
|
settings: form.settings as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={`/studies/${resolvedParams?.id}/forms`}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TypeIcon className="text-muted-foreground h-5 w-5" />
|
||||||
|
<h1 className="text-2xl font-bold">{form.title}</h1>
|
||||||
|
{form.active && (
|
||||||
|
<Badge variant="default" className="text-xs">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm capitalize">
|
||||||
|
{form.type} • Version {form.version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{canManage && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={updateForm.isPending}>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={generatePdf}
|
||||||
|
disabled={isGeneratingPdf}
|
||||||
|
>
|
||||||
|
<Printer className="mr-2 h-4 w-4" />
|
||||||
|
{isGeneratingPdf ? "Generating..." : "Print PDF"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsEditing(true)}>
|
||||||
|
<Edit2 className="mr-2 h-4 w-4" />
|
||||||
|
Edit Form
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="fields" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="fields">Fields</TabsTrigger>
|
||||||
|
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||||
|
{canManage && (
|
||||||
|
<TabsTrigger value="data-entry">Data Entry</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="responses">
|
||||||
|
Responses ({responses.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="fields">
|
||||||
|
{isEditing ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Form Fields</CardTitle>
|
||||||
|
<Select onValueChange={addField}>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Add field..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FORM_FIELD_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
<span className="mr-2">{type.icon}</span>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<FileText className="mb-2 h-8 w-8" />
|
||||||
|
<p>No fields added yet</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex items-start gap-3 rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div className="text-muted-foreground flex cursor-grab items-center">
|
||||||
|
<GripVertical className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{
|
||||||
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
|
?.icon
|
||||||
|
}{" "}
|
||||||
|
{
|
||||||
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
|
?.label
|
||||||
|
}
|
||||||
|
</Badge>
|
||||||
|
<Input
|
||||||
|
value={field.label}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(field.id, { label: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Field label"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.required}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(field.id, {
|
||||||
|
required: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
Required
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{field.type === "multiple_choice" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">Options</Label>
|
||||||
|
{field.options?.map((opt, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={opt}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newOptions = [
|
||||||
|
...(field.options || []),
|
||||||
|
];
|
||||||
|
newOptions[i] = e.target.value;
|
||||||
|
updateField(field.id, {
|
||||||
|
options: newOptions,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder={`Option ${i + 1}`}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = field.options?.filter(
|
||||||
|
(_, idx) => idx !== i,
|
||||||
|
);
|
||||||
|
updateField(field.id, {
|
||||||
|
options: newOptions,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = [
|
||||||
|
...(field.options || []),
|
||||||
|
`Option ${(field.options?.length || 0) + 1}`,
|
||||||
|
];
|
||||||
|
updateField(field.id, {
|
||||||
|
options: newOptions,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
|
Add Option
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeField(field.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="text-destructive h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Fields</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No fields defined</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex items-center gap-3 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<span className="bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">{field.label}</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{
|
||||||
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
|
?.label
|
||||||
|
}
|
||||||
|
{field.required && " • Required"}
|
||||||
|
{field.type === "multiple_choice" &&
|
||||||
|
` • ${field.options?.length} options`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="preview">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Preview</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-semibold">{title}</h2>
|
||||||
|
{description && (
|
||||||
|
<p className="text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No fields to preview</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="space-y-2">
|
||||||
|
<Label>
|
||||||
|
{index + 1}. {field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-destructive"> *</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
{field.type === "text" && (
|
||||||
|
<Input placeholder="Enter your response..." disabled />
|
||||||
|
)}
|
||||||
|
{field.type === "textarea" && (
|
||||||
|
<Textarea
|
||||||
|
placeholder="Enter your response..."
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === "multiple_choice" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{field.options?.map((opt, i) => (
|
||||||
|
<label key={i} className="flex items-center gap-2">
|
||||||
|
<input type="radio" disabled /> {opt}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === "checkbox" && (
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input type="checkbox" disabled /> Yes
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{field.type === "yes_no" && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input type="radio" disabled /> Yes
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input type="radio" disabled /> No
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === "rating" && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{Array.from(
|
||||||
|
{ length: (field.settings?.scale as number) || 5 },
|
||||||
|
(_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
className="disabled h-8 w-8 rounded border"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === "date" && <Input type="date" disabled />}
|
||||||
|
{field.type === "signature" && (
|
||||||
|
<div className="bg-muted/50 text-muted-foreground flex h-24 items-center justify-center rounded border">
|
||||||
|
Signature pad (disabled in preview)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="data-entry">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>Manual Data Entry</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEnteringData(!isEnteringData);
|
||||||
|
setSelectedParticipantId("");
|
||||||
|
setFormResponses({});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEnteringData ? (
|
||||||
|
<>
|
||||||
|
<X className="mr-2 h-4 w-4" />
|
||||||
|
Cancel
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Enter Data
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{isEnteringData ? (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Select Participant</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedParticipantId}
|
||||||
|
onValueChange={setSelectedParticipantId}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Choose a participant..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{participants?.participants?.map((p) => (
|
||||||
|
<SelectItem key={p.id} value={p.id}>
|
||||||
|
{p.name || p.participantCode || p.email || p.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedParticipantId && (
|
||||||
|
<div className="space-y-6 border-t pt-4">
|
||||||
|
<h3 className="font-semibold">Form Responses</h3>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="space-y-2">
|
||||||
|
<Label>
|
||||||
|
{index + 1}. {field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-destructive"> *</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
{field.type === "text" && (
|
||||||
|
<Input
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Enter response..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === "textarea" && (
|
||||||
|
<Textarea
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Enter response..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === "multiple_choice" && (
|
||||||
|
<Select
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an option..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{field.options?.map((opt, i) => (
|
||||||
|
<SelectItem key={i} value={opt}>
|
||||||
|
{opt}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{field.type === "checkbox" && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formResponses[field.id] || false}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span>Yes</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{field.type === "yes_no" && (
|
||||||
|
<Select
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="yes">Yes</SelectItem>
|
||||||
|
<SelectItem value="no">No</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{field.type === "rating" && (
|
||||||
|
<Select
|
||||||
|
value={String(formResponses[field.id] || "")}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: parseInt(val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select rating..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from(
|
||||||
|
{ length: (field.settings?.scale as number) || 5 },
|
||||||
|
(_, i) => (
|
||||||
|
<SelectItem key={i} value={String(i + 1)}>
|
||||||
|
{i + 1}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{field.type === "date" && (
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.type === "signature" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={formResponses[field.id] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormResponses({
|
||||||
|
...formResponses,
|
||||||
|
[field.id]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Type name as signature..."
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
By entering your name above, you confirm that
|
||||||
|
the information provided is accurate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 border-t pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEnteringData(false);
|
||||||
|
setSelectedParticipantId("");
|
||||||
|
setFormResponses({});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDataEntry}
|
||||||
|
disabled={submitResponse.isPending}
|
||||||
|
>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{submitResponse.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: "Save Response"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<Pencil className="mb-2 h-8 w-8" />
|
||||||
|
<p>Manual data entry</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Enter responses directly for participants who completed the
|
||||||
|
form on paper
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="responses">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Form Responses</CardTitle>
|
||||||
|
{canManage && responses.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleExportCsv}
|
||||||
|
disabled={exportCsv.isFetching}
|
||||||
|
>
|
||||||
|
<FileDown className="mr-2 h-4 w-4" />
|
||||||
|
{exportCsv.isFetching ? "Exporting..." : "Export CSV"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{responses.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<Users className="mb-2 h-8 w-8" />
|
||||||
|
<p>No responses yet</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{responses.map((response) => (
|
||||||
|
<div key={response.id} className="rounded-lg border p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="text-muted-foreground h-4 w-4" />
|
||||||
|
<span className="font-medium">
|
||||||
|
{response.participant?.name ||
|
||||||
|
response.participant?.participantCode ||
|
||||||
|
"Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
className={`text-xs ${formStatusColors[response.status as keyof typeof formStatusColors]}`}
|
||||||
|
>
|
||||||
|
{response.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
{Object.entries(
|
||||||
|
response.responses as Record<string, any>,
|
||||||
|
).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex gap-2">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{key}:
|
||||||
|
</span>
|
||||||
|
<span>{String(value)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{response.signedAt && (
|
||||||
|
<div className="text-muted-foreground mt-2 border-t pt-2 text-xs">
|
||||||
|
Signed: {new Date(response.signedAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useSession } from "~/lib/auth-client";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
ArrowLeft,
|
||||||
|
Save,
|
||||||
|
LayoutTemplate,
|
||||||
|
FileSignature,
|
||||||
|
ClipboardList,
|
||||||
|
FileQuestion,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "~/components/ui/select";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { FormField, FormType } from "~/lib/types/forms";
|
||||||
|
import { FORM_FIELD_TYPES } from "~/lib/types/forms";
|
||||||
|
import { FormBuilder } from "~/components/forms/FormBuilder";
|
||||||
|
|
||||||
|
const formTypes = [
|
||||||
|
{ value: "consent", label: "Consent Form", icon: FileSignature, description: "Legal/IRB consent documents" },
|
||||||
|
{ value: "survey", label: "Survey", icon: ClipboardList, description: "Multi-question questionnaires" },
|
||||||
|
{ value: "questionnaire", label: "Questionnaire", icon: FileQuestion, description: "Custom data collection forms" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function NewFormPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const studyId = typeof params.id === "string" ? params.id : "";
|
||||||
|
|
||||||
|
const [formType, setFormType] = useState<string>("");
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [fields, setFields] = useState<FormField[]>([]);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const { data: study } = api.studies.get.useQuery(
|
||||||
|
{ id: studyId },
|
||||||
|
{ enabled: !!studyId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: templates } = api.forms.listTemplates.useQuery();
|
||||||
|
|
||||||
|
const createFromTemplate = api.forms.createFromTemplate.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success("Form created from template!");
|
||||||
|
router.push(`/studies/${studyId}/forms/${data.id}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to create from template", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createForm = api.forms.create.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success("Form created successfully!");
|
||||||
|
router.push(`/studies/${studyId}/forms/${data.id}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to create form", { description: error.message });
|
||||||
|
setIsSubmitting(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useBreadcrumbsEffect([
|
||||||
|
{ label: "Dashboard", href: "/dashboard" },
|
||||||
|
{ label: "Studies", href: "/studies" },
|
||||||
|
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||||
|
{ label: "Forms", href: `/studies/${studyId}/forms` },
|
||||||
|
{ label: "Create Form" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formType || !title) {
|
||||||
|
toast.error("Please select a form type and enter a title");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
createForm.mutate({
|
||||||
|
studyId,
|
||||||
|
type: formType as FormType,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
fields,
|
||||||
|
settings: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={`/studies/${studyId}/forms`}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">Create New Form</h1>
|
||||||
|
<p className="text-muted-foreground">Design a consent form, survey, or questionnaire</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{templates && templates.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<LayoutTemplate className="h-5 w-5" />
|
||||||
|
Start from Template
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
{templates.map((template) => {
|
||||||
|
const TypeIcon = formTypes.find(t => t.value === template.type)?.icon || FileText;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={template.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
createFromTemplate.mutate({
|
||||||
|
studyId,
|
||||||
|
templateId: template.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={createFromTemplate.isPending}
|
||||||
|
className="flex flex-col items-start rounded-lg border p-4 text-left transition-all hover:bg-muted/50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<TypeIcon className="mb-2 h-5 w-5 text-muted-foreground" />
|
||||||
|
<span className="font-medium">{template.templateName}</span>
|
||||||
|
<span className="text-muted-foreground text-xs capitalize">{template.type}</span>
|
||||||
|
<span className="text-muted-foreground text-xs mt-1 line-clamp-2">
|
||||||
|
{template.description}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||||
|
Or design from scratch below
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form Details</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Form Type</Label>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
{formTypes.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormType(type.value)}
|
||||||
|
className={`flex flex-col items-start rounded-lg border p-4 text-left transition-all hover:bg-muted/50 ${
|
||||||
|
formType === type.value
|
||||||
|
? "border-primary bg-primary/5 ring-1 ring-primary"
|
||||||
|
: "border-border"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<type.icon className={`mb-2 h-5 w-5 ${formType === type.value ? "text-primary" : "text-muted-foreground"}`} />
|
||||||
|
<span className="font-medium">{type.label}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">{type.description}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Title</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Enter form title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">Description (optional)</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Brief description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Form Fields</CardTitle>
|
||||||
|
<Select onValueChange={(type) => {
|
||||||
|
const newField: FormField = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: type as FormField["type"],
|
||||||
|
label: `New ${FORM_FIELD_TYPES.find(f => f.value === type)?.label || "Field"}`,
|
||||||
|
required: false,
|
||||||
|
options: type === "multiple_choice" ? ["Option 1", "Option 2"] : undefined,
|
||||||
|
};
|
||||||
|
setFields([...fields, newField]);
|
||||||
|
}}>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Add field..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FORM_FIELD_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
<span className="mr-2">{type.icon}</span>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<FormBuilder fields={fields} onFieldsChange={setFields} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href={`/studies/${studyId}/forms`}>Cancel</Link>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !formType || !title}>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{isSubmitting ? "Creating..." : "Create Form"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,317 +1,270 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useSession } from "~/lib/auth-client";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { FileText, Loader2, Plus, Download, Edit2, Eye, Save } from "lucide-react";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
EntityView,
|
FileText,
|
||||||
EntityViewHeader,
|
Plus,
|
||||||
EntityViewSection,
|
Search,
|
||||||
EmptyState,
|
ClipboardList,
|
||||||
} from "~/components/ui/entity-view";
|
FileQuestion,
|
||||||
|
FileSignature,
|
||||||
|
MoreHorizontal,
|
||||||
|
Trash2,
|
||||||
|
Eye,
|
||||||
|
CheckCircle,
|
||||||
|
} from "lucide-react";
|
||||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PageHeader } from "~/components/ui/page-header";
|
import { PageHeader } from "~/components/ui/page-header";
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
import { Markdown } from 'tiptap-markdown';
|
|
||||||
import { Table } from '@tiptap/extension-table';
|
|
||||||
import { TableRow } from '@tiptap/extension-table-row';
|
|
||||||
import { TableCell } from '@tiptap/extension-table-cell';
|
|
||||||
import { TableHeader } from '@tiptap/extension-table-header';
|
|
||||||
import { Bold, Italic, List, ListOrdered, Heading1, Heading2, Quote, Table as TableIcon } from "lucide-react";
|
|
||||||
import { downloadPdfFromHtml } from "~/lib/pdf-generator";
|
|
||||||
|
|
||||||
const Toolbar = ({ editor }: { editor: any }) => {
|
const formTypeIcons = {
|
||||||
if (!editor) {
|
consent: FileSignature,
|
||||||
return null;
|
survey: ClipboardList,
|
||||||
}
|
questionnaire: FileQuestion,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const formTypeColors = {
|
||||||
<div className="border border-input bg-transparent rounded-tr-md rounded-tl-md p-1 flex items-center gap-1 flex-wrap">
|
consent:
|
||||||
<Button
|
"bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400",
|
||||||
variant="ghost"
|
survey: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
||||||
size="sm"
|
questionnaire:
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
"bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
|
||||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
|
||||||
className={editor.isActive('bold') ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<Bold className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
|
||||||
className={editor.isActive('italic') ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<Italic className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-[1px] h-6 bg-border mx-1" />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
||||||
className={editor.isActive('heading', { level: 1 }) ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<Heading1 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
||||||
className={editor.isActive('heading', { level: 2 }) ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<Heading2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-[1px] h-6 bg-border mx-1" />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
className={editor.isActive('bulletList') ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<List className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
className={editor.isActive('orderedList') ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<ListOrdered className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
||||||
className={editor.isActive('blockquote') ? 'bg-muted' : ''}
|
|
||||||
>
|
|
||||||
<Quote className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-[1px] h-6 bg-border mx-1" />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
|
|
||||||
>
|
|
||||||
<TableIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StudyFormsPageProps {
|
interface StudyFormsPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StudyFormsPage({ params }: StudyFormsPageProps) {
|
export default function StudyFormsPage({ params }: StudyFormsPageProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const utils = api.useUtils();
|
const router = useRouter();
|
||||||
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(null);
|
const utils = api.useUtils();
|
||||||
const [editorTarget, setEditorTarget] = useState<string>("");
|
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resolveParams = async () => {
|
const resolveParams = async () => {
|
||||||
const resolved = await params;
|
const resolved = await params;
|
||||||
setResolvedParams(resolved);
|
setResolvedParams(resolved);
|
||||||
};
|
|
||||||
void resolveParams();
|
|
||||||
}, [params]);
|
|
||||||
|
|
||||||
const { data: study } = api.studies.get.useQuery(
|
|
||||||
{ id: resolvedParams?.id ?? "" },
|
|
||||||
{ enabled: !!resolvedParams?.id },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: activeConsentForm, refetch: refetchConsentForm } =
|
|
||||||
api.studies.getActiveConsentForm.useQuery(
|
|
||||||
{ studyId: resolvedParams?.id ?? "" },
|
|
||||||
{ enabled: !!resolvedParams?.id },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only sync once when form loads to avoid resetting user edits
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeConsentForm && !editorTarget) {
|
|
||||||
setEditorTarget(activeConsentForm.content);
|
|
||||||
}
|
|
||||||
}, [activeConsentForm, editorTarget]);
|
|
||||||
|
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Table.configure({
|
|
||||||
resizable: true,
|
|
||||||
}),
|
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
|
||||||
TableCell,
|
|
||||||
Markdown.configure({
|
|
||||||
transformPastedText: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
content: editorTarget || '',
|
|
||||||
immediatelyRender: false,
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
setEditorTarget(editor.storage.markdown.getMarkdown());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync Tiptap when editorTarget is set (e.g., from DB) but make sure not to overwrite active edits
|
|
||||||
useEffect(() => {
|
|
||||||
if (editor && editorTarget && editor.isEmpty) {
|
|
||||||
editor.commands.setContent(editorTarget);
|
|
||||||
}
|
|
||||||
}, [editorTarget, editor]);
|
|
||||||
|
|
||||||
const generateConsentMutation = api.studies.generateConsentForm.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast.success("Default Consent Form Generated!");
|
|
||||||
setEditorTarget(data.content);
|
|
||||||
editor?.commands.setContent(data.content);
|
|
||||||
void refetchConsentForm();
|
|
||||||
void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Error generating consent form", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateConsentMutation = api.studies.updateConsentForm.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Consent Form Saved Successfully!");
|
|
||||||
void refetchConsentForm();
|
|
||||||
void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Error saving consent form", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDownloadConsent = async () => {
|
|
||||||
if (!activeConsentForm || !study || !editor) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
toast.loading("Generating Document...", { id: "pdf-gen" });
|
|
||||||
await downloadPdfFromHtml(editor.getHTML(), {
|
|
||||||
filename: `Consent_Form_${study.name.replace(/\s+/g, "_")}_v${activeConsentForm.version}.pdf`
|
|
||||||
});
|
|
||||||
toast.success("Document Downloaded Successfully!", { id: "pdf-gen" });
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Error generating PDF", { id: "pdf-gen" });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
void resolveParams();
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
useBreadcrumbsEffect([
|
const { data: study } = api.studies.get.useQuery(
|
||||||
{ label: "Dashboard", href: "/dashboard" },
|
{ id: resolvedParams?.id ?? "" },
|
||||||
{ label: "Studies", href: "/studies" },
|
{ enabled: !!resolvedParams?.id },
|
||||||
{ label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` },
|
);
|
||||||
{ label: "Forms" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
const { data: formsData, isLoading } = api.forms.list.useQuery(
|
||||||
return notFound();
|
{ studyId: resolvedParams?.id ?? "", search: search || undefined },
|
||||||
}
|
{ enabled: !!resolvedParams?.id },
|
||||||
|
);
|
||||||
|
|
||||||
if (!study) return <div>Loading...</div>;
|
const userRole = (study as any)?.userRole;
|
||||||
|
const canManage = userRole === "owner" || userRole === "researcher";
|
||||||
|
|
||||||
return (
|
const deleteMutation = api.forms.delete.useMutation({
|
||||||
<EntityView>
|
onSuccess: () => {
|
||||||
<PageHeader
|
toast.success("Form deleted successfully");
|
||||||
title="Study Forms"
|
void utils.forms.list.invalidate({ studyId: resolvedParams?.id });
|
||||||
description="Manage consent forms and future questionnaires for this study"
|
},
|
||||||
icon={FileText}
|
onError: (error) => {
|
||||||
/>
|
toast.error("Failed to delete form", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8">
|
const setActiveMutation = api.forms.setActive.useMutation({
|
||||||
<EntityViewSection
|
onSuccess: () => {
|
||||||
title="Consent Document"
|
toast.success("Form set as active");
|
||||||
icon="FileText"
|
void utils.forms.list.invalidate({ studyId: resolvedParams?.id });
|
||||||
description="Design and manage the consent form that participants must sign before participating in your trials."
|
},
|
||||||
actions={
|
onError: (error) => {
|
||||||
<div className="flex gap-2">
|
toast.error("Failed to set active", { description: error.message });
|
||||||
<Button
|
},
|
||||||
variant="outline"
|
});
|
||||||
size="sm"
|
|
||||||
onClick={() => generateConsentMutation.mutate({ studyId: study.id })}
|
|
||||||
disabled={generateConsentMutation.isPending || updateConsentMutation.isPending}
|
|
||||||
>
|
|
||||||
{generateConsentMutation.isPending ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Generate Default Template
|
|
||||||
</Button>
|
|
||||||
{activeConsentForm && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => updateConsentMutation.mutate({ studyId: study.id, content: editorTarget })}
|
|
||||||
disabled={updateConsentMutation.isPending || editorTarget === activeConsentForm.content}
|
|
||||||
>
|
|
||||||
{updateConsentMutation.isPending ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{activeConsentForm ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm font-medium leading-none">
|
|
||||||
{activeConsentForm.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
v{activeConsentForm.version} • Status: Active
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleDownloadConsent}
|
|
||||||
>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
Download PDF
|
|
||||||
</Button>
|
|
||||||
<Badge variant="outline" className="bg-green-50 text-green-700 hover:bg-green-50">Active</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex justify-center bg-muted/30 p-8 rounded-md border border-border overflow-hidden">
|
useBreadcrumbsEffect([
|
||||||
<div className="max-w-4xl w-full bg-white dark:bg-card shadow-xl ring-1 ring-border rounded-sm flex flex-col">
|
{ label: "Dashboard", href: "/dashboard" },
|
||||||
<div className="border-b border-border bg-muted/50 dark:bg-muted/10">
|
{ label: "Studies", href: "/studies" },
|
||||||
<Toolbar editor={editor} />
|
{ label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` },
|
||||||
</div>
|
{ label: "Forms" },
|
||||||
<div className="min-h-[850px] px-16 py-20 text-sm editor-container bg-white dark:bg-card">
|
]);
|
||||||
<EditorContent editor={editor} className="prose prose-sm dark:prose-invert max-w-none h-full outline-none focus:outline-none focus-visible:outline-none" />
|
|
||||||
</div>
|
if (!session?.user) {
|
||||||
</div>
|
return notFound();
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
) : (
|
if (!study) return <div>Loading...</div>;
|
||||||
<EmptyState
|
|
||||||
icon="FileText"
|
const forms = formsData?.forms ?? [];
|
||||||
title="No Consent Form"
|
|
||||||
description="Generate a boilerplate consent form for this study to download and collect signatures."
|
return (
|
||||||
/>
|
<div className="space-y-6">
|
||||||
)}
|
<PageHeader
|
||||||
</EntityViewSection>
|
title="Forms"
|
||||||
|
description="Manage consent forms, surveys, and questionnaires for this study"
|
||||||
|
icon={FileText}
|
||||||
|
actions={
|
||||||
|
canManage && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={`/studies/${resolvedParams?.id}/forms/new`}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Form
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{forms.length === 0 && !isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<FileText className="text-muted-foreground mb-4 h-12 w-12" />
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">No Forms Yet</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Create consent forms, surveys, or questionnaires to collect data
|
||||||
|
from participants
|
||||||
|
</p>
|
||||||
|
{canManage && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href={`/studies/${resolvedParams?.id}/forms/new`}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Your First Form
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="relative max-w-sm flex-1">
|
||||||
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search forms..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EntityView>
|
</div>
|
||||||
);
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{forms.map((form) => {
|
||||||
|
const TypeIcon =
|
||||||
|
formTypeIcons[form.type as keyof typeof formTypeIcons] ||
|
||||||
|
FileText;
|
||||||
|
const typeColor =
|
||||||
|
formTypeColors[form.type as keyof typeof formTypeColors] ||
|
||||||
|
"bg-gray-100";
|
||||||
|
const isActive = form.active;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={form.id} className="overflow-hidden">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`rounded-md p-2 ${typeColor}`}>
|
||||||
|
<TypeIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
{form.title}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-muted-foreground text-xs capitalize">
|
||||||
|
{form.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isActive && (
|
||||||
|
<Badge variant="default" className="text-xs">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-3">
|
||||||
|
{form.description && (
|
||||||
|
<p className="text-muted-foreground mb-3 line-clamp-2 text-sm">
|
||||||
|
{form.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="text-muted-foreground flex items-center justify-between text-xs">
|
||||||
|
<span>v{form.version}</span>
|
||||||
|
<span>
|
||||||
|
{(form as any)._count?.responses ?? 0} responses
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<div className="bg-muted/30 flex items-center justify-between border-t px-4 py-2">
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link
|
||||||
|
href={`/studies/${resolvedParams?.id}/forms/${form.id}`}
|
||||||
|
>
|
||||||
|
<Eye className="mr-1 h-3 w-3" />
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{canManage && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{!isActive && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
setActiveMutation.mutate({ id: form.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckCircle className="mr-2 h-4 w-4" />
|
||||||
|
Set Active
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to delete this form?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation.mutate({ id: form.id });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
} from "~/components/ui/entity-view";
|
} from "~/components/ui/entity-view";
|
||||||
import { PageHeader } from "~/components/ui/page-header";
|
import { PageHeader } from "~/components/ui/page-header";
|
||||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "~/lib/auth-client";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
|
import { AddMemberDialog } from "~/components/studies/add-member-dialog";
|
||||||
|
|
||||||
interface StudyDetailPageProps {
|
interface StudyDetailPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -59,6 +60,7 @@ type Study = {
|
|||||||
irbProtocol: string | null;
|
irbProtocol: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
userRole?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Member = {
|
type Member = {
|
||||||
@@ -156,6 +158,10 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
|||||||
).length;
|
).length;
|
||||||
const totalTrials = trials.length;
|
const totalTrials = trials.length;
|
||||||
|
|
||||||
|
const userRole = (studyData as any)?.userRole;
|
||||||
|
const canManage = userRole === "owner" || userRole === "researcher";
|
||||||
|
const canRunTrials = userRole === "owner" || userRole === "researcher" || userRole === "wizard";
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
experiments: experiments.length,
|
experiments: experiments.length,
|
||||||
totalTrials: totalTrials,
|
totalTrials: totalTrials,
|
||||||
@@ -181,18 +187,22 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
|||||||
]}
|
]}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button asChild variant="outline">
|
{canManage && (
|
||||||
<Link href={`/studies/${study.id}/edit`}>
|
<Button asChild variant="outline">
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Link href={`/studies/${study.id}/edit`}>
|
||||||
Edit Study
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
</Link>
|
Edit Study
|
||||||
</Button>
|
</Link>
|
||||||
<Button asChild>
|
</Button>
|
||||||
<Link href={`/studies/${study.id}/experiments/new`}>
|
)}
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
{canManage && (
|
||||||
New Experiment
|
<Button asChild>
|
||||||
</Link>
|
<Link href={`/studies/${study.id}/experiments/new`}>
|
||||||
</Button>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New Experiment
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -234,12 +244,14 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
|||||||
icon="FlaskConical"
|
icon="FlaskConical"
|
||||||
description="Design and manage experimental protocols for this study"
|
description="Design and manage experimental protocols for this study"
|
||||||
actions={
|
actions={
|
||||||
<Button asChild variant="outline" size="sm">
|
canManage ? (
|
||||||
<Link href={`/studies/${study.id}/experiments/new`}>
|
<Button asChild variant="outline" size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Link href={`/studies/${study.id}/experiments/new`}>
|
||||||
Add Experiment
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
</Link>
|
Add Experiment
|
||||||
</Button>
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{experiments.length === 0 ? (
|
{experiments.length === 0 ? (
|
||||||
@@ -273,12 +285,13 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</h4>
|
</h4>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${experiment.status === "draft"
|
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||||
? "bg-gray-100 text-gray-800"
|
experiment.status === "draft"
|
||||||
: experiment.status === "ready"
|
? "bg-gray-100 text-gray-800"
|
||||||
? "bg-green-100 text-green-800"
|
: experiment.status === "ready"
|
||||||
: "bg-blue-100 text-blue-800"
|
? "bg-green-100 text-green-800"
|
||||||
}`}
|
: "bg-blue-100 text-blue-800"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{experiment.status}
|
{experiment.status}
|
||||||
</span>
|
</span>
|
||||||
@@ -390,10 +403,14 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
|||||||
icon="Users"
|
icon="Users"
|
||||||
description={`${members.length} team member${members.length !== 1 ? "s" : ""}`}
|
description={`${members.length} team member${members.length !== 1 ? "s" : ""}`}
|
||||||
actions={
|
actions={
|
||||||
<Button variant="outline" size="sm">
|
canManage ? (
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<AddMemberDialog studyId={study.id}>
|
||||||
Invite
|
<Button variant="outline" size="sm">
|
||||||
</Button>
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
</AddMemberDialog>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default function StudyParticipantsPage() {
|
|||||||
}
|
}
|
||||||
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
||||||
|
|
||||||
|
const canManage = study?.userRole === "owner" || study?.userRole === "researcher";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -38,12 +40,14 @@ export default function StudyParticipantsPage() {
|
|||||||
description="Manage participant registration, consent, and trial assignments for this study"
|
description="Manage participant registration, consent, and trial assignments for this study"
|
||||||
icon={Users}
|
icon={Users}
|
||||||
actions={
|
actions={
|
||||||
<Button asChild>
|
canManage ? (
|
||||||
<a href={`/studies/${studyId}/participants/new`}>
|
<Button asChild>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<a href={`/studies/${studyId}/participants/new`}>
|
||||||
Add Participant
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
</a>
|
Add Participant
|
||||||
</Button>
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { WizardView } from "~/components/trials/views/WizardView";
|
|||||||
import { ObserverView } from "~/components/trials/views/ObserverView";
|
import { ObserverView } from "~/components/trials/views/ObserverView";
|
||||||
import { ParticipantView } from "~/components/trials/views/ParticipantView";
|
import { ParticipantView } from "~/components/trials/views/ParticipantView";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "~/lib/auth-client";
|
||||||
|
|
||||||
function WizardPageContent() {
|
function WizardPageContent() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -25,6 +25,11 @@ function WizardPageContent() {
|
|||||||
const { study } = useSelectedStudyDetails();
|
const { study } = useSelectedStudyDetails();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
// Get user roles
|
||||||
|
const { data: userData } = api.auth.me.useQuery(undefined, {
|
||||||
|
enabled: !!session?.user,
|
||||||
|
});
|
||||||
|
|
||||||
// Get trial data
|
// Get trial data
|
||||||
const {
|
const {
|
||||||
data: trial,
|
data: trial,
|
||||||
@@ -67,7 +72,7 @@ function WizardPageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default role logic based on user
|
// Default role logic based on user
|
||||||
const userRole = session.user.roles?.[0]?.role ?? "observer";
|
const userRole = userData?.roles?.[0] ?? "observer";
|
||||||
if (userRole === "administrator" || userRole === "researcher") {
|
if (userRole === "administrator" || userRole === "researcher") {
|
||||||
return "wizard";
|
return "wizard";
|
||||||
}
|
}
|
||||||
@@ -188,6 +193,7 @@ function WizardPageContent() {
|
|||||||
name: trial.experiment.name,
|
name: trial.experiment.name,
|
||||||
description: trial.experiment.description,
|
description: trial.experiment.description,
|
||||||
studyId: trial.experiment.studyId,
|
studyId: trial.experiment.studyId,
|
||||||
|
robotId: trial.experiment.robotId,
|
||||||
},
|
},
|
||||||
participant: {
|
participant: {
|
||||||
id: trial.participant.id,
|
id: trial.participant.id,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user