72 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] b147f81322 chore: bump next to 16.2.6 for additional CVE fixes
Agent-Logs-Url: https://github.com/soconnor0919/hristudio/sessions/7450cc95-9c00-40dc-b652-fca3024486d9

Co-authored-by: soconnor0919 <44291434+soconnor0919@users.noreply.github.com>
2026-05-13 15:45:42 +00:00
copilot-swe-agent[bot] 880980dccd chore: upgrade next and drizzle-orm for security fixes
Agent-Logs-Url: https://github.com/soconnor0919/hristudio/sessions/7450cc95-9c00-40dc-b652-fca3024486d9

Co-authored-by: soconnor0919 <44291434+soconnor0919@users.noreply.github.com>
2026-05-13 15:42:32 +00:00
copilot-swe-agent[bot] 356084a3f1 Initial plan 2026-05-13 15:39:11 +00:00
soconnor 14182bf078 fix: resolve all three functional issues in trial execution
- WebSocket broadcasts: Next.js tRPC router now routes broadcasts via
  POST /internal/broadcast on the Bun ws-server process, which holds
  the actual client connections. Broadcasts were previously silently
  dropped due to the split singleton across processes.

- ws-server stubs: request_trial_status and request_trial_events now
  use the real async DB methods instead of the stub getTrialStatusSync/
  getTrialEventsSync that always returned null/[].

- Duplicate branch case: removed the unreachable second case "branch"
  block in executeAction switch; server-side branching is a pass-through
  since routing is client-orchestrated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:24:32 -04:00
soconnor 943c7bd963 fix(wizard): skip unchosen branch steps during linear progression
When the wizard makes a branch choice, mark all other branch targets as
skipped. Linear progression now advances past skipped steps, so path
1→2→4 no longer executes step 3 when branch A was chosen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:15:58 -04:00
soconnor 6b54724171 feat(wizard): enhance branching logic and add next step selection in PropertiesPanel 2026-04-08 12:08:33 -04:00
soconnor 86c1f35537 fix: SSH actions in experiment runner, branch ID serialization, and branch UI
- robot-communication.ts: add sshCommand to payloadMapping type
- trial-execution.ts: fix executeRobotActionWithComm to use ros2 key as
  implementation fallback and skip ROS connection for SSH actions
- route.ts: move studyId membership check inside initialize/executeSystemAction
  cases so executeSSH works without studyId; fix command param location
- experiments.ts: build tempId→dbUUID map on step insert and replace branch
  nextStepId references after all steps are saved
- WizardInterface.tsx: stop filtering branch actions from step action list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:31:44 -04:00
soconnor 5b5490cb90 fix(wizard-ros): use executeSSH for animations to bypass studyId requirement 2026-04-01 19:48:40 -04:00
soconnor 6b98cad53e fix(api): add missing posture actions (stand, stand_init, sit, crouch) 2026-04-01 19:46:31 -04:00
soconnor 3e2aa894a0 fix(trial-execution): handle SSH actions without requiring ROS connection
- Add startTime parameter to executeRobotActionInternal for proper duration tracking
- SSH actions (animations, posture commands) now work without ROS bridge connection
- Refactor executeAction to handle SSH and ROS paths separately
2026-04-01 19:44:08 -04:00
soconnor 27f633fb4b feat(nao6): add SSH-based posture actions (wake_up, rest, stand, sit, crouch)
- Update plugin with sshCommand payloadMapping type
- Add server-side SSH command execution in robot-communication.ts
- Add client-side SSH command execution in wizard-ros-service.ts
- Update API route to handle executeSSH action
2026-04-01 19:37:28 -04:00
soconnor 6243b62d3b Fix robot action ID namespacing for animation detection 2026-04-01 19:34:27 -04:00
soconnor f16dd4aa22 fix: handle namespaced action IDs in animation execution 2026-04-01 19:30:51 -04:00
soconnor 7483e4a72b fix: remove double-escaped NAOqi markup from speech transforms 2026-04-01 19:30:21 -04:00
soconnor 426b5e761b fix: allow timeoutMs=0 for wait blocks 2026-04-01 19:28:09 -04:00
soconnor cf21a27995 fix(ui): add number input for sliders, use textarea for text inputs
- Allow typing numbers directly in slider inputs
- Use textarea for text parameters like say_text
2026-04-01 19:20:42 -04:00
soconnor 74b5507769 fix: add transformToEmotionSpeech alias 2026-04-01 19:16:36 -04:00
soconnor 5c67fc106e chore: update robot-plugins submodule 2026-04-01 19:11:30 -04:00
soconnor 4b04f2c415 fix(nao6): route /animation via SSH, clean up working animations
- Fix executeWithConfig to route play_animation actions through SSH
- Remove broken animations: friendly, think, show_sole
- Keep working: bow, hey, show_floor, enthusiastic, yes, no, idontknow
2026-04-01 19:06:21 -04:00
soconnor c959e61f95 fix(wizard): use API route for animations instead of ROS topic
- Add executeAnimationSSH that calls /api/robots/command
- Remove ROS topic publishing for animations
- Fix play_animation_show_sole -> play_animation_friendly
2026-04-01 18:58:26 -04:00
soconnor de1b125b13 feat(nao6): add SSH-based animation execution for NAO6 robot
- Add play_animation actions to robots/command API using qicli SSH
- Add SSH-based animation execution to robot-communication service
- Animations: bow, hey, show_floor, show_sole, enthusiastic, think, yes, no, idontknow

This bypasses ROS2 cross-container issues by using direct SSH connection.
2026-04-01 18:51:40 -04:00
soconnor 143cf2ce50 chore: update robot-plugins with animation actions 2026-04-01 18:35:50 -04:00
soconnor 61c7cc1e94 feat(ros): add animation topic handler for play_animation actions 2026-04-01 18:35:45 -04:00
soconnor 8f330cf5f0 feat(ros): add animation handler and fix gesture action pipeline
- Add AnimationMovement interface: { joint_names, joint_angles, speed?,
  delay_after? } for describing individual frames in a joint animation
- Add executeAnimationSequence() public method: steps through frames,
  publishing each to /joint_angles with configurable per-frame delays
- Add executeSimulationAnimationSequence() for mock/sim mode
- Fix subscribeToRobotTopics: advertise /joint_angles as
  naoqi_bridge_msgs/msg/JointAnglesWithSpeed — without this rosbridge
  silently dropped every gesture publish (root cause of broken gestures)
- Fix executeBuiltinAction: correct ROS2 message type for /joint_angles
  (was naoqi_bridge_msgs/JointAnglesWithSpeed, ROS1 format)
- Add service payloadMapping type to executeWithConfig: routes actions
  to callService() instead of publish() — used by wake_up/rest
- Add wake_up/rest fallback service call chains in executeBuiltinAction
- Route gesture_sequence payloads through executeAnimationSequence
  instead of the old inline loop (which used a broken delay formula)
- Improve sim mode to handle gesture_sequence configs with realistic timing
- Update robot-plugins submodule pointer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 18:25:32 -04:00
soconnor 254805008e feat: add gesture sequence support in wizard-ros-service
- Add transformToGestureSequence for multi-movement gestures
- Update executeWithConfig to handle gesture sequences
- Add sequence execution with delays between movements
- Fix experiment description to be optional
2026-04-01 17:09:33 -04:00
soconnor c923c63099 chore: update robot-plugins submodule 2026-04-01 17:06:07 -04:00
soconnor c05384d1a0 feat: Add Test Action button, fix ros2 config copying, fix transform functions
- Add Test Action button in experiment designer properties panel
- Fix DesignerRoot to copy full ros2 config when adding actions
- Add transformToWaveGoodbye and transformToAnimation cases
- Fix escape sequences for NAOqi markup
- Update TrialForm with FormSection, sidebar, and visible validation
- Add db:reset and db:restart scripts
- Update docker-compose with configurable PostgreSQL and MinIO vars
2026-04-01 17:00:03 -04:00
soconnor c0e5a4ffb8 chore: update robot-plugins with beckon joint angle fix 2026-04-01 16:33:27 -04:00
soconnor 51aaaa5208 chore: update robot-plugins with valid animation names 2026-04-01 16:28:09 -04:00
soconnor e402c51483 chore: update robot-plugins submodule to latest with gesture actions 2026-04-01 16:05:03 -04:00
soconnor 7c360dc860 feat: add initial seed data migration and form builder components
- Created migration 0001_seed_data.sql to insert minimal seed data for users, accounts, and roles.
- Added meta journal for migration tracking.
- Implemented FormBuilder component for dynamic form field creation and management.
- Developed FormFieldRenderer component to render various types of form fields based on user input.
- Introduced constants for trust levels and status configurations.
- Defined types for form fields and trial data structures to enhance type safety and clarity.
2026-03-26 14:56:00 -04:00
soconnor 1c7f0297a6 feat(tutorials): add comprehensive tutorials for HRIStudio including Getting Started, Your First Study, Designing Experiments, Running Trials, Wizard Interface, Robot Integration, Forms & Surveys, Data & Analysis, and Simulation Mode 2026-03-25 22:48:42 -04:00
soconnor 3959cf23f7 feat(forms): add public form access and response submission for participants
- Implemented public access to forms with `getPublic` procedure.
- Added `submitPublic` procedure for participants to submit responses.
- Created a new participant form page to handle form display and submission.
- Enhanced form validation and error handling for required fields.
- Introduced CSV export functionality for form responses.
- Updated form listing and template creation procedures.
- Added README for homepage screenshots.
2026-03-23 11:07:02 -04:00
soconnor 3270e3f8fe docs: update documentation for forms system and role-based access
- Add forms system to README key features
- Update router/table counts to reflect new forms router
- Add forms section to quick-reference with types, templates, routes
- Clarify study-level user roles (owner/researcher/wizard/observer)
2026-03-22 18:11:42 -04:00
soconnor bfd1924897 fix: update forms pages to use proper page layout
- Remove EntityView wrapper, use standard space-y-6 pattern
- Full page width instead of container max-w
- Consistent with experiments/participants pages
2026-03-22 18:09:01 -04:00
soconnor 0827a791c6 fix: standardize MinIO bucket name to hristudio-data
- Update docker-compose to create hristudio-data bucket instead of hristudio
- Fix files.ts, storage.ts, trials.ts, lib/storage/minio.ts to use consistent bucket name
- All now default to hristudio-data matching compose bucket creation
2026-03-22 17:57:34 -04:00
soconnor ecf0ab9103 feat: add form templates
- Add isTemplate and templateName fields to forms
- Add listTemplates and createFromTemplate API endpoints
- Add template selection to new form page UI
- Add sample templates and forms to seed script:
  - Informed Consent template
  - Post-Session Survey template
  - Demographics questionnaire template
2026-03-22 17:53:16 -04:00
soconnor 49e0df016a feat: complete forms system overhaul
- Add new forms table with type (consent/survey/questionnaire)
- Add formResponses table for submissions
- Add forms API router with full CRUD:
  - list, get, create, update, delete
  - setActive, createVersion
  - getResponses, submitResponse
- Add forms list page with card-based UI
- Add form builder with field types (text, textarea, multiple_choice, checkbox, rating, yes_no, date, signature)
- Add form viewer with edit mode and preview
- Add responses viewing with participant info
2026-03-22 17:43:12 -04:00
soconnor 8529d0ef89 fix: add role-based permissions to forms page
- Hide Generate Default Template and Save Changes buttons for wizard/observer
- Backend already enforces owner/researcher for mutations
- UI now provides cleaner experience for read-only roles
2026-03-22 17:26:52 -04:00
soconnor 67ad904f62 feat: add role-based permissions and profile page improvements
- Add getMyMemberships API endpoint for user role lookup
- Add getMemberRole helper for profile page display
- Add role-based UI controls to study page (owner/researcher only)
- Add canManage checks to experiments, participants, trials pages
- Hide management actions for wizard/observer roles

Backend already enforces permissions; UI now provides cleaner UX
2026-03-22 17:25:04 -04:00
soconnor 519e6a2606 ui: complete profile page redesign
- Modern card-based layout with large avatar
- Inline editing for name/email with save/cancel
- Password change dialog with validation
- Security section with danger zone
- Studies access quick link
- Consistent teal theme colors
- Uses PageHeader pattern
- Better loading states
2026-03-22 17:08:50 -04:00
soconnor b353ef7c9f ui: dashboard redesign, member management, dark mode fixes
- Simplified dashboard using PageHeader/page-layout
- Fixed dark mode live session banner
- Added AddMemberDialog component for study team management
- Study page now has working member add/remove
- Fixed toast imports to use sonner
2026-03-22 17:05:28 -04:00
soconnor cbd31e9aa4 ui: complete dashboard redesign
- New modern dashboard layout with gradient background
- Quick action cards with teal glow effects
- Live trials banner with pulsing indicator
- Stats grid with colored icons
- Study list with bot icons
- Quick links sidebar
- Recent trials section with status badges
- Proper null safety and type checking
2026-03-22 16:55:41 -04:00
soconnor 37feea8df3 ui: fix dead links in dashboard, update theme to teal/cyan
- Fix broken /trials links to use study-scoped routes
- Fix /wizard/ link to proper wizard URL with studyId
- Add studyId to getLiveTrials API response
- Update theme colors to teal/cyan sci-fi style
- Add custom CSS utilities for glow effects and chamfered corners
- Consolidate docs and archive outdated files
2026-03-22 16:49:20 -04:00
soconnor cf3597881b docs: consolidate and archive documentation
- Move 30+ outdated docs to docs/_archive/
- Move obsolete root files to _archive/
- Update README.md (Better Auth, current features)
- Update docs/README.md (new architecture diagram)
- Update docs/quick-reference.md (consolidated)
- Update docs/project-status.md (March 2026 state)
- Update docs/nao6-quick-reference.md (14 actions, Docker services)
- Update docs/implementation-guide.md (Better Auth, git submodule)
- Update docs/proposal.tex (timeline updates)
- Archive errors.txt, plugin_dump.json, test HTML files
2026-03-22 16:38:28 -04:00
soconnor add3380307 fix: upgrade to Next.js 16.2.1 and resolve bundling issues
- Fixed client bundle contamination by moving child_process-dependent code
- Created standalone /api/robots/command route for SSH robot commands
- Created plugins router to replace robots.plugins for plugin management
- Added getStudyPlugins procedure to studies router
- Fixed trial.studyId references to trial.experiment.studyId
- Updated WizardInterface to use REST API for robot commands
2026-03-22 01:08:13 -04:00
soconnor 79bb298756 revert: stay on Next.js 16.1.6 due to bundling issues in 16.2.1 2026-03-22 00:50:29 -04:00
soconnor a5762ec935 feat: implement WebSocket for real-time trial updates
- Create standalone WebSocket server (ws-server.ts) on port 3001 using Bun
- Add ws_connections table to track active connections in database
- Create global WebSocket manager that persists across component unmounts
- Fix useWebSocket hook to prevent infinite re-renders and use refs
- Fix TrialForm Select components with proper default values
- Add trialId to WebSocket URL for server-side tracking
- Update package.json with dev:ws script for separate WS server
2026-03-22 00:48:43 -04:00
soconnor 20d6d3de1a migrate: replace NextAuth.js with Better Auth
- Install better-auth and @better-auth/drizzle-adapter
- Create src/lib/auth.ts with Better Auth configuration using bcrypt
- Update database schema: change auth table IDs from uuid to text
- Update route handler from /api/auth/[...nextauth] to /api/auth/[...all]
- Update tRPC context and middleware for Better Auth session handling
- Update client components to use Better Auth APIs (signIn, signOut)
- Update seed script with text-based IDs and correct account schema
- Fix type errors in wizard components (robotId, optional chaining)
- Fix API paths: api.robots.initialize -> api.robots.plugins.initialize
- Update auth router to use text IDs for Better Auth compatibility

Note: Auth tables were reset - users will need to re-register.
2026-03-21 23:03:55 -04:00
soconnor 4bed537943 Update docs: add March 2026 session summary, NAO6 Docker integration docs, and quick reference updates
- Add MARCH-2026-SESSION.md with complete summary of work done
- Update nao6-quick-reference.md for Docker-based deployment
- Update quick-reference.md with NAO6 Docker integration section
2026-03-21 20:51:08 -04:00
soconnor 73f70f6550 Add nextStepId conditions to Branch A and B to jump to Story Continues 2026-03-21 20:44:47 -04:00
soconnor 3fafd61553 Fix onClick handlers passing event object to handleNextStep
The issue was that onClick={onNextStep} passes the click event as the first argument,
making targetIndex an object instead of undefined. This caused handleNextStep to fall
through to linear progression instead of properly checking branching logic.

Fixed by wrapping with arrow function: onClick={() => onNextStep()}
2026-03-21 20:35:54 -04:00
soconnor 3491bf4463 Add debug logging for branching flow 2026-03-21 20:26:55 -04:00
soconnor cc58593891 Update robot-plugins submodule 2026-03-21 20:21:38 -04:00
soconnor bbbe397ba8 Various improvements: study forms, participant management, PDF generator, robot integration 2026-03-21 20:21:18 -04:00
soconnor bbc34921b5 Fix branching logic and robot action timing
- Remove onCompleted() call after branch selection to prevent count increment
- Add proper duration estimation for speech actions (word count + emotion overhead)
- Add say_with_emotion and wave_goodbye to built-in actions
- Add identifier field to admin.ts plugin creation
2026-03-21 20:15:39 -04:00
soconnor 8e647c958e Fix seed script to include identifier for system plugins 2026-03-21 20:04:46 -04:00
soconnor 4e86546311 Add identifier column to plugins table for cleaner plugin lookup
- Added 'identifier' column (unique) for machine-readable plugin ID
- 'name' now used for display name only
- Updated trial-execution to look up by identifier first, then name
- Added migration script for existing databases
2026-03-21 20:03:33 -04:00
soconnor e84c794962 Load plugin from local file first (not remote) 2026-03-21 19:32:13 -04:00
soconnor 70064f487e Fix say_with_emotion with proper NAOqi markup, add transform functions, update seed script for linear branching 2026-03-21 19:29:28 -04:00
soconnor 91d03a789d Redesign experiment structure and add pending trial
- Both branch choices now jump to Story Continues (convergence point)
- Add Story Continues step with expressive actions
- Add pre-seeded pending trial for immediate testing
- Fix duplicate comments in seed script
- Update step ordering (Conclusion now step6)
2026-03-21 19:15:41 -04:00
soconnor 31d2173703 Fix branching and add move_arm builtin
- Branching: mark source step as completed when jumping to prevent revisiting
- Add move_arm as builtin for arm control
2026-03-21 19:09:26 -04:00
soconnor 4a9abf4ff1 Restore builtins for standard ROS actions
- Re-add say_text, walk_forward, walk_backward, turn_left, turn_right, move_head, turn_head as builtins
- These use standard ROS topics (/speech, /cmd_vel, /joint_angles) that work with most robots
- Plugin-specific actions should still be defined in plugin config
2026-03-21 19:04:51 -04:00
soconnor 487f97c5c2 Update robot-plugins submodule 2026-03-21 18:58:29 -04:00
soconnor db147f2294 Update robot-plugins submodule 2026-03-21 18:57:00 -04:00
soconnor a705c720fb Make wizard-ros-service robot-agnostic
- Remove NAO-specific hardcoded action handlers
- Remove helper methods (executeMovementAction, executeTurnHead, executeMoveArm)
- Keep only generic emergency_stop as builtin
- All robot-specific actions should be defined in plugin config
2026-03-21 18:55:52 -04:00
soconnor e460c1b029 Update robot-plugins submodule 2026-03-21 18:54:18 -04:00
soconnor eb0d86f570 Clean up debug logs 2026-03-21 18:52:16 -04:00
soconnor e40c37cfd0 Fix branching logic and add combo robot actions
- Fix handleNextStep to handle both string and object options in conditions
- Add say_with_emotion, bow, wave, nod, shake_head, point combo actions
- Update seed data with nextStepId in wizard_wait_for_response options
2026-03-21 18:51:27 -04:00
soconnor f8e6fccae3 Update robot-plugins submodule 2026-03-21 18:28:07 -04:00
soconnor 3f87588fea fix: Update ROS topics and robot configuration
ROS Topic Fixes:
- wizard-ros-service.ts: Use correct ROS topics (/cmd_vel, /joint_angles, /speech)
- ros-bridge.ts: Update subscriptions to match naoqi_driver topics
- Fixes action execution (movement, speech, head control)

Robot Configuration:
- robots.ts: Use NAO_IP/NAO_ROBOT_IP env vars instead of hardcoded 'nao.local'
- robots.ts: Use NAO_PASSWORD env var for SSH authentication
- Improves Docker integration with NAO6

Wizard Interface:
- useWizardRos.ts: Enhanced wizard interface for robot control
- WizardInterface.tsx: Updated wizard controls
- Add comprehensive event listeners for robot actions
2026-03-21 17:58:29 -04:00
soconnor 18e5aab4a5 feat: Convert robot-plugins to proper git submodule
- Removes nested .git directory from robot-plugins
- Adds robot-plugins as a proper submodule of hristudio
- Points to main branch of soconnor0919/robot-plugins repository
- This enables proper version tracking and updates of robot plugins
2026-03-21 17:57:54 -04:00
177 changed files with 17476 additions and 5063 deletions
+10 -2
View File
@@ -16,11 +16,19 @@
AUTH_SECRET=""
# 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_ENDPOINT="http://localhost:9000"
MINIO_ENDPOINT="http://localhost:${MINIO_PORT_API:-9000}"
MINIO_REGION="us-east-1"
MINIO_ACCESS_KEY="minioadmin"
MINIO_SECRET_KEY="minioadmin"
MINIO_BUCKET_NAME="hristudio-data"
MINIO_PORT_API="9000"
MINIO_PORT_CONSOLE="9001"
+4
View File
@@ -0,0 +1,4 @@
[submodule "robot-plugins"]
path = robot-plugins
url = git@github.com:soconnor0919/robot-plugins.git
branch = main
+33 -16
View File
@@ -64,16 +64,15 @@ bun dev
## 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
- **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
- **UI**: Tailwind CSS + shadcn/ui (built on Radix UI primitives)
- **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
- **Real-time**: WebSocket with Edge Runtime compatibility
## Architecture
@@ -203,14 +202,12 @@ src/
Comprehensive documentation available in the `docs/` folder:
- **[Quick Reference](docs/quick-reference.md)**: 5-minute setup guide and essential commands
- **[Project Overview](docs/project-overview.md)**: Complete feature overview and architecture
- **[Implementation Details](docs/implementation-details.md)**: Architecture decisions and patterns
- **[Database Schema](docs/database-schema.md)**: Complete PostgreSQL schema documentation
- **[API Routes](docs/api-routes.md)**: Comprehensive tRPC API reference
- **[Core Blocks System](docs/core-blocks-system.md)**: Repository-based block architecture
- **[Plugin System](docs/plugin-system-implementation-guide.md)**: Robot integration guide
- **[Project Status](docs/project-status.md)**: Current completion status (98% complete)
- **[Tutorials](docs/tutorials/README.md)**: Step-by-step guides for new users
- **[Quick Reference](docs/quick-reference.md)**: Essential commands and setup
- **[Implementation Guide](docs/implementation-guide.md)**: Technical implementation details
- **[Project Status](docs/project-status.md)**: Current development state
- **[NAO6 Integration](docs/nao6-quick-reference.md)**: Robot setup and commands
- **[Archive](docs/_archive/)**: Historical documentation (outdated)
## Research Paper
@@ -234,19 +231,39 @@ Full paper available at: [docs/paper.md](docs/paper.md)
- **4 User Roles**: Complete role-based access control
- **Plugin System**: Extensible robot integration architecture
- **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
Complete NAO6 robot integration is available in the separate **[nao6-hristudio-integration](../nao6-hristudio-integration/)** repository.
### 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
- 9 robot actions: speech, movement, gestures, sensors, LEDs
- 14 robot actions: speech, movement, gestures, sensors, LEDs, animations
- 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
# Start NAO integration
cd ~/naoqi_ros2_ws
View File
+162 -25
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "hristudio",
@@ -7,6 +8,7 @@
"@auth/drizzle-adapter": "^1.11.1",
"@aws-sdk/client-s3": "^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/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -48,24 +50,25 @@
"@types/js-cookie": "^3.0.6",
"@types/ws": "^8.18.1",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"driver.js": "^1.4.0",
"drizzle-orm": "^0.41.0",
"drizzle-orm": "^0.45.2",
"html2pdf.js": "^0.14.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.536.0",
"minio": "^8.0.6",
"next": "^16.1.6",
"next": "16.2.6",
"next-auth": "^5.0.0-beta.30",
"next-themes": "^0.4.6",
"postgres": "^3.4.8",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react": "19.2.4",
"react-day-picker": "^9.13.2",
"react-dom": "^19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.71.1",
"react-resizable-panels": "^3.0.6",
"react-signature-canvas": "^1.1.0-alpha.2",
@@ -87,12 +90,12 @@
"@types/bun": "^1.3.9",
"@types/crypto-js": "^4.2.2",
"@types/node": "^20.19.33",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/uuid": "^11.0.0",
"drizzle-kit": "^0.30.6",
"eslint": "^9.39.2",
"eslint-config-next": "^15.5.12",
"eslint-config-next": "16.2.1",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
@@ -112,6 +115,10 @@
"sharp",
"unrs-resolver",
],
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
},
"packages": {
"@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=="],
"@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/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=="],
"@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=="],
"@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=="],
"@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=="],
@@ -585,8 +648,6 @@
"@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=="],
"@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/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=="],
"@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=="],
"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=="],
"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=="],
"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-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=="],
"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=="],
@@ -1045,6 +1118,8 @@
"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=="],
"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=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"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-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=="],
"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=="],
"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=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"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-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=="],
@@ -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-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=="],
@@ -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=="],
"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-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=="],
"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=="],
"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=="],
"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=="],
@@ -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=="],
"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-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=="],
"kysely": ["kysely@0.28.14", "", {}, "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA=="],
"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=="],
@@ -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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
@@ -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-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=="],
"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=="],
"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=="],
"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=="],
"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-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=="],
"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=="],
"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=="],
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"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=="],
@@ -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=="],
"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=="],
"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=="],
"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-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=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"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=="],
"@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/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=="],
"@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=="],
"@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=="],
"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-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=="],
"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/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
+11 -11
View File
@@ -2,13 +2,13 @@ services:
db:
image: postgres:15
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hristudio
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-hristudio}
PGSSLMODE: disable
command: -c ssl=off
ports:
- "5140:5432"
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -20,11 +20,11 @@ services:
minio:
image: minio/minio
ports:
- "9000:9000" # API
- "9001:9001" # Console
- "${MINIO_PORT_API:-9000}:9000" # API
- "${MINIO_PORT_CONSOLE:-9001}:9001" # Console
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
volumes:
- minio_data:/data
command: server --console-address ":9001" /data
@@ -35,9 +35,9 @@ services:
- minio
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
/usr/bin/mc mb myminio/hristudio;
/usr/bin/mc anonymous set public myminio/hristudio;
/usr/bin/mc alias set myminio http://minio:9000 ${MINIO_ACCESS_KEY:-minioadmin} ${MINIO_SECRET_KEY:-minioadmin};
/usr/bin/mc mb myminio/${MINIO_BUCKET_NAME:-hristudio-data};
/usr/bin/mc anonymous set public myminio/${MINIO_BUCKET_NAME:-hristudio-data};
exit 0;
"
+180 -283
View File
@@ -1,307 +1,204 @@
# 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:
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
New to HRIStudio? Start with our comprehensive tutorials:
### **📋 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**
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)**
## Getting Started
### 1. Clone & Install
```bash
# Clone and install
git clone <repo-url> hristudio
git clone https://github.com/soconnor0919/hristudio.git
cd hristudio
git submodule update --init --recursive
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
- **Visual Experiment Designer**: Repository-based plugin architecture with 26 core blocks
- **Core Block Categories**: Events, wizard actions, control flow, observation blocks
- **Real-time Trial Execution**: Live wizard control with data capture
- **Multi-role Collaboration**: Administrator, Researcher, Wizard, Observer
- **Comprehensive Data Management**: Synchronized multi-modal capture
- **Visual Designer**: 26+ core blocks (events, wizard actions, control flow, observation)
- **Conditional Branching**: Wizard choices with convergence paths
- **WebSocket Real-time**: Trial updates with auto-reconnect
- **Plugin System**: Robot-agnostic via identifier lookup
- **Docker NAO6**: Three-service ROS2 integration
- **Forms System**: Consent forms, surveys, questionnaires with templates
- **Role-based Access**: Owner, Researcher, Wizard, Observer permissions
### **Technical Excellence**
- **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
## System Components
### **Development Experience**
- **Unified Components**: Significant reduction in code duplication
- **Panel Architecture**: 90% code sharing between experiment designer and wizard interface
- **Consolidated Wizard**: 3-panel design with trial controls, horizontal timeline, and unified robot controls
- **Enterprise DataTables**: Advanced filtering, export, pagination
- **Comprehensive Testing**: Realistic seed data with complete scenarios
- **Developer Friendly**: Clear patterns and extensive documentation
### Backend (src/server/)
- `api/routers/` - 13 tRPC routers (studies, experiments, trials, participants, forms, etc.)
- `db/schema.ts` - Drizzle schema (33 tables)
- `services/trial-execution.ts` - Trial execution engine
- `services/websocket-manager.ts` - Real-time connections
### **Robot Integration**
- **NAO6 Full Support**: Complete ROS2 integration with movement, speech, and sensor control
- **Real-time Control**: WebSocket-based robot control through web interface
- **Safety Features**: Emergency stops, movement limits, and comprehensive monitoring
- **Production Ready**: Tested with NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble
- **Troubleshooting Guides**: Complete documentation for setup and problem resolution
### Frontend (src/)
- `app/` - Next.js App Router pages
- `components/trials/wizard/` - Wizard interface
- `components/trials/forms/` - Form builder and viewer
- `hooks/useWebSocket.ts` - Real-time trial updates
- `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**
**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.
**Last Updated**: March 22, 2026
+159
View File
@@ -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*
+9 -2
View File
@@ -2,7 +2,7 @@
## 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
@@ -25,7 +25,14 @@ This guide provides step-by-step technical instructions for implementing HRIStud
### 1. Initialize Project
```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 \
--nextjs \
--tailwind \
+182
View File
@@ -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
View File
@@ -2,88 +2,112 @@
Essential commands for using NAO6 robots with HRIStudio.
## Quick Start
## Quick Start (Docker)
### 1. Start NAO Integration
### 1. Start Docker Integration
```bash
cd ~/naoqi_ros2_ws
source install/setup.bash
ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab
cd ~/Documents/Projects/nao6-hristudio-integration
docker compose up -d
```
### 2. Wake Robot
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)
```
The robot will automatically wake up and autonomous life will be disabled on startup.
### 3. Start HRIStudio
### 2. Start HRIStudio
```bash
cd ~/Documents/Projects/hristudio
bun dev
```
### 4. Test Connection
- Open: `http://localhost:3000/nao-test`
- Click "Connect"
- Test robot commands
### 3. Verify Connection
- Open: `http://localhost:3000`
- Navigate to trial wizard
- WebSocket should connect automatically
## Essential Commands
## Docker Services
### Test Connectivity
```bash
ping nao.local # Test network
ros2 topic list | grep naoqi # Check ROS topics
```
| Service | Port | Description |
|---------|------|-------------|
| nao_driver | - | NAOqi driver + robot init |
| ros_bridge | 9090 | WebSocket bridge |
| ros_api | - | ROS API services |
### Manual Control
```bash
# Speech
ros2 topic pub --once /speech std_msgs/String "data: 'Hello world'"
# 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
```
**Auto-initialization**: On Docker startup, `init_robot.sh` runs automatically via SSH to:
- Wake up the robot (`ALMotion.wakeUp`)
- Disable autonomous life (`ALAutonomousLife.setState disabled`)
- Ensure robot is ready for commands
## ROS Topics
**Commands (Input):**
- `/speech` - Text-to-speech
- `/cmd_vel` - Movement
- `/joint_angles` - Joint control
**Commands (Publish to these):**
```
/speech - Text-to-speech
/cmd_vel - Velocity commands (movement)
/joint_angles - Joint position commands
```
**Sensors (Output):**
- `/naoqi_driver/joint_states` - Joint data
- `/naoqi_driver/battery` - Battery level
- `/naoqi_driver/bumper` - Foot sensors
- `/naoqi_driver/sonar/*` - Distance sensors
- `/naoqi_driver/camera/*` - Camera feeds
**Sensors (Subscribe to these):**
```
/camera/front/image_raw - Front camera
/camera/bottom/image_raw - Bottom camera
/joint_states - Joint positions
/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
@@ -99,79 +123,76 @@ ros2 run rosbridge_server rosbridge_websocket
}
```
## More Information
## Troubleshooting
See **[nao6-hristudio-integration](../../nao6-hristudio-integration/)** repository for:
- Complete installation guide
- Detailed usage instructions
- Full troubleshooting guide
- Plugin definitions
- Launch file configurations
**Robot not moving:**
- Check robot is awake: `qicli call ALMotion.isWakeUp` → returns `true`
- If not: `qicli call ALMotion.wakeUp`
## Common Use Cases
### Make Robot Speak
**WebSocket fails:**
```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
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
```bash
ros2 topic pub --once /joint_angles naoqi_bridge_msgs/msg/JointAnglesWithSpeed '{joint_names: ["HeadYaw"], joint_angles: [0.8], speed: 0.2}'
```
## Environment Variables
### Emergency Stop
```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}}'
Create `nao6-hristudio-integration/.env`:
```
NAO_IP=10.0.0.42
NAO_USERNAME=nao
NAO_PASSWORD=nao
BRIDGE_PORT=9090
```
## 🚨 Safety Notes
- **Always wake up robot before movement commands**
- **Keep emergency stop accessible**
- **Always verify robot is awake before movement commands**
- **Keep emergency stop accessible** (`qicli call ALMotion.rest()`)
- **Start with small movements (0.05 m/s)**
- **Monitor battery level during experiments**
- **Monitor battery level**
- **Ensure clear space around robot**
## 📝 Credentials
## Credentials
**Default NAO Login:**
**NAO Robot:**
- IP: `10.0.0.42` (configurable)
- Username: `nao`
- Password: `robolab` (institution-specific)
- Password: `nao`
**HRIStudio Login:**
**HRIStudio:**
- Email: `sean@soconnor.dev`
- Password: `password123`
## 🔄 Complete Restart Procedure
## Complete Restart
```bash
# 1. Kill all processes
sudo fuser -k 9090/tcp
pkill -f "rosbridge\|naoqi\|ros2"
# 1. Restart Docker integration
cd ~/Documents/Projects/nao6-hristudio-integration
docker compose down
docker compose up -d
# 2. Restart database
sudo docker compose down && sudo docker compose up -d
# 2. Verify robot is awake (check logs)
docker compose logs nao_driver | grep -i "wake\|autonomous"
# 3. Start ROS integration
cd ~/naoqi_ros2_ws && source install/setup.bash
ros2 launch install/nao_launch/share/nao_launch/launch/nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab
# 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
# 3. Start HRIStudio
cd ~/Documents/Projects/hristudio
bun dev
```
---
**📖 For detailed setup instructions, see:** [NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)
**✅ 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
View File
@@ -1,402 +1,189 @@
# HRIStudio Project Status
## 🎯 **Current Status: Production Ready**
## Current Status: Active Development
**Project Version**: 1.0.0
**Last Updated**: December 2024
**Overall Completion**: Complete ✅
**Status**: Ready for Production Deployment
### **🎉 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.
**Last Updated**: March 2026
**Overall Completion**: 98%
**Status**: Thesis research phase
---
## 📊 **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**
-**Complete Backend Infrastructure** - Full API with 12 tRPC routers
-**Complete Frontend Implementation** - Professional UI with unified experiences
-**Full Type Safety** - Zero TypeScript errors in production code
-**Complete Authentication** - Role-based access control system
-**Visual Experiment Designer** - Repository-based plugin architecture
-**Core Blocks System** - 26 blocks across 4 categories (events, wizard, control, observation)
-**Production Database** - 31 tables with comprehensive relationships
-**Development Environment** - Realistic seed data and testing scenarios
-**Trial System Overhaul** - Unified EntityView patterns with real-time execution
-**WebSocket Integration** - Real-time updates with polling fallback
-**Route Consolidation** - Study-scoped architecture with eliminated duplicate components
-**Multi-View Trial Interface** - Role-based Wizard, Observer, and Participant views for thesis research
-**Dashboard Resolution** - Fixed routing issues and implemented proper layout structure
### Recent Updates (March 2026)
-WebSocket real-time trial updates implemented
-Better Auth migration complete (replaced NextAuth.js)
-Docker integration for NAO6 (3 services: nao_driver, ros_bridge, ros_api)
-Conditional branching with wizard choices and convergence
-14 NAO6 robot actions (speech, movement, gestures, sensors, LEDs, animations)
-Plugin identifier system for clean plugin lookup
-Seed script with branching experiment structure
### Key Achievements
-Complete backend with 12 tRPC routers
-Professional UI with unified experiences
-Full TypeScript coverage (strict mode)
-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
- **Repository Management**: Admin tools for plugin repositories and trust levels
- **Plugin Store**: Study-scoped plugin installation and configuration
- **Block Categories**: Events, wizard actions, control flow, observation blocks
- **Type Safety**: Full TypeScript support for all plugin definitions
- **Documentation**: Complete guides for core blocks and robot plugins
**Database Schema**
- ✅ 31 tables covering all research workflows
- ✅ Complete relationships with foreign keys and indexes
- ✅ Audit logging and soft deletes implemented
- ✅ Performance optimizations with strategic indexing
- ✅ JSONB support for flexible metadata storage
**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[];
}
```
┌─────────────────────────────────────────────────────┐
│ User Interface Layer │
│ ├── Experiment Designer (visual programming) │
│ ├── Wizard Interface (trial execution) │
│ ├── Observer View (live monitoring) │
│ └── Participant View (thesis study) │
├─────────────────────────────────────────────────────┤
│ Data Management Layer │
│ ├── PostgreSQL + Drizzle ORM │
│ ├── tRPC API (12 routers) │
│ └── Better Auth (role-based auth) │
├─────────────────────────────────────────────────────┤
│ Robot Integration Layer │
│ ├── Plugin system (robot-agnostic) │
│ ├── ROS2 via rosbridge WebSocket │
│ └── Docker deployment (nao_driver, ros_bridge) │
└─────────────────────────────────────────────────────┘
```
### **Key Fixes Applied**
-**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
-**State Management**: Zustand store working correctly with all actions
-**UI Layout**: Three-panel layout with Action Library, Step Flow, and Properties
### Plugin Identifier System
```
plugins table:
- id: UUID (primary key)
- 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)**
**Theme**: Production Deployment Preparation
Experiment steps support conditional branching with wizard choices:
**Goals**:
1. ✅ Complete experiment designer redesign
2. ✅ Fix step addition functionality
3. ✅ Resolve TypeScript compilation issues
4. ⏳ Final code quality improvements
```
Step 3 (Comprehension Check)
└── wizard_wait_for_response
├── Click "Correct" → setLastResponse("Correct") → nextStepId=step4a
└── Click "Incorrect" → setLastResponse("Incorrect") → nextStepId=step4b
**Sprint Metrics**:
- **Story Points**: 34 total
- **Completed**: 30 points
- **In Progress**: 4 points
- **Planned**: 0 points
Step 4a/4b (Branches)
└── conditions.nextStepId: step5.id → convergence point
### **Development Velocity**
- **Sprint 1**: 28 story points completed
- **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
Step 5 (Story Continues)
└── Linear progression to Step 6
```
---
## 🎯 **Success Criteria Validation**
## NAO6 Robot Actions (14 total)
### **Technical Requirements** ✅ **Met**
- ✅ End-to-end type safety throughout platform
- ✅ Role-based access control with 4 distinct roles
- ✅ Comprehensive API covering all research workflows
- ✅ Visual experiment designer with drag-and-drop interface
- ✅ Real-time trial execution framework ready
- ✅ Scalable architecture built for research teams
### **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
| Category | Actions |
|----------|---------|
| Speech | say, say_with_emotion, wave_goodbye |
| Movement | walk, turn, move_to_posture |
| Gestures | play_animation, gesture |
| Sensors | get_sensors, bumper_state, touch_state |
| LEDs | set_eye_leds, set_breathing_lights |
---
## 🚀 **Production Readiness**
## Tech Stack
### **Deployment Checklist** ✅ **Complete**
- ✅ Environment variables configured for Vercel
- ✅ Database migrations ready for production
- ✅ Security headers and CSRF protection configured
- ✅ Error tracking and performance monitoring setup
- ✅ Build process optimized for Edge Runtime
- ✅ Static assets and CDN configuration ready
### **Performance Validation** ✅ **Passed**
- ✅ Page load time < 2 seconds (Currently optimal)
- ✅ API response time < 200ms (Currently optimal)
- ✅ Database query time < 50ms (Currently optimal)
- ✅ 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
| Component | Technology | Version |
|-----------|------------|---------|
| Framework | Next.js | 15-16.x |
| Language | TypeScript | 5.x (strict) |
| Database | PostgreSQL | 14+ |
| ORM | Drizzle | latest |
| Auth | NextAuth.js | v5 |
| API | tRPC | latest |
| UI | Tailwind + shadcn/ui | latest |
| Real-time | WebSocket | with polling fallback |
| Robot | ROS2 Humble | via rosbridge |
| Package Manager | Bun | latest |
---
## 📈 **Platform Capabilities**
## Development Status
### **Research Workflow Support**
- **Study Management**: Complete lifecycle from creation to analysis
- **Team Collaboration**: Multi-user support with role-based permissions
- **Experiment Design**: Visual programming interface for protocol creation
- **Trial Execution**: Panel-based wizard interface matching experiment designer architecture
- **Real-time Updates**: WebSocket integration with intelligent polling fallback
- **Data Capture**: Synchronized multi-modal data streams with comprehensive event logging
- **Robot Integration**: Plugin-based support for multiple platforms
### Completed Features
| Feature | Status | Notes |
|---------|--------|-------|
| Database Schema | ✅ | 31 tables |
| Authentication | ✅ | 4 roles |
| Experiment Designer | ✅ | 26+ blocks |
| Wizard Interface | ✅ | 3-panel design |
| Real-time Updates | ✅ | WebSocket |
| Plugin System | ✅ | Robot-agnostic |
| NAO6 Integration | ✅ | Docker deployment |
| Conditional Branching | ✅ | Wizard choices |
| Mock Robot | ✅ | Development mode |
### **Technical Capabilities**
- **Scalability**: Architecture supporting large research institutions
- **Performance**: Optimized for concurrent multi-user environments
- **Security**: Research-grade data protection and access control
- **Flexibility**: Customizable workflows for diverse methodologies
- **Integration**: Robot platform agnostic with plugin architecture
- **Compliance**: Research ethics and data protection compliance
### Known Issues
| Issue | Status | Notes |
|-------|--------|-------|
| robots.executeSystemAction | Known error | Fallback works |
---
## 🔮 **Roadmap & Future Work**
## SSH Deployment Commands
### **Immediate Priorities** (Next 30 days)
- **Wizard Interface Development** - Complete rebuild of trial execution interface
- **Robot Control Implementation** - NAO6 integration with WebSocket communication
- **Trial Execution Engine** - Step-by-step protocol execution with real-time data capture
- **User Experience Testing** - Validate study-scoped workflows with target users
```bash
# Local development
bun dev
### **Short-term Goals** (Next 60 days)
- **IRB Application Preparation** - Complete documentation and study protocols
- **Reference Experiment Implementation** - Well-documented HRI experiment for comparison study
- **Training Materials Development** - Comprehensive materials for both HRIStudio and Choregraphe
- **Platform Validation** - Extensive testing and reliability verification
# Database
bun db:push # Push schema changes
bun db:seed # Seed with test data
bun run docker:up # Start PostgreSQL
### **Long-term Vision** (Next 90+ days)
- **User Study Execution** - Comparative study with 10-12 non-engineering participants
- **Thesis Research Completion** - Data analysis and academic paper preparation
- **Platform Refinement** - Post-study improvements based on real user feedback
- **Community Release** - Open source release for broader HRI research community
# Quality
bun typecheck # TypeScript validation
bun lint # ESLint
```
---
## 🎊 **Project Success Declaration**
## Thesis Timeline
**HRIStudio is officially ready for production deployment.**
Current phase: **March 2026** - Implementation complete, preparing user study
### **Completion Summary**
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.
### **Key Success Metrics**
- **Development Velocity**: Consistently meeting sprint goals with 30+ story points
- **Code Quality**: Zero production TypeScript errors, fully functional designer
- **Architecture Quality**: Clean study-scoped hierarchy with eliminated code duplication
- **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.**
| Phase | Status | Date |
|-------|--------|------|
| Proposal | ✅ | Sept 2025 |
| IRB Application | ✅ | Dec 2025 |
| Implementation | ✅ | Feb 2026 |
| User Study | 🔄 In Progress | Mar-Apr 2026 |
| Defense | Scheduled | April 2026 |
---
## 🔧 **Development Notes**
## Next Steps
### **Technical Debt Status**
- **High Priority**: None identified
- **Medium Priority**: Minor database query optimizations possible
- **Low Priority**: Some older components could benefit from modern React patterns
### **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
1. Complete user study (10-12 participants)
2. Data analysis and thesis writing
3. Final defense April 2026
4. Open source release
---
*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
View File
@@ -107,7 +107,7 @@ This work addresses a significant bottleneck in HRI research. By creating HRIStu
\hline
September & Finalize and submit this proposal (Due: Sept. 20).
Submit IRB application for the user study. \\
Submit IRB application for the study. \\
\hline
Oct -- Nov & Complete final implementation of core HRIStudio features.
@@ -119,13 +119,15 @@ Begin recruiting participants. \\
\hline
\multicolumn{2}{|l|}{\textbf{Spring 2026: Execution, Analysis, and Writing}} \\
\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
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
April & Submit completed thesis draft to the defense committee (Due: April 1).
+108 -508
View File
@@ -1,566 +1,166 @@
# 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
# Clone and install
git clone <repo-url> hristudio
# Clone with submodules
git clone https://github.com/soconnor0919/hristudio.git
cd hristudio
git submodule update --init --recursive
# Install and setup
bun install
# Start database
bun run docker:up
# Setup database
bun db:push
bun db:seed
# Single command now syncs all repositories:
# - Core blocks from localhost:3000/hristudio-core
# - Robot plugins from https://repo.hristudio.com
# Start development
# Start
bun dev
```
### Default Login
- **Admin**: `sean@soconnor.dev` / `password123`
- **Researcher**: `alice.rodriguez@university.edu` / `password123`
- **Wizard**: `emily.watson@lab.edu` / `password123`
**Login**: `sean@soconnor.dev` / `password123`
---
## 📁 **Project Structure**
## Key Concepts
```
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
### Hierarchy
```
Study → Experiment → Trial → Step → Action
```
### User Roles
- **Administrator**: Full system access
- **Researcher**: Create studies, design experiments
- **Wizard**: Execute trials, control robots
- **Observer**: Read-only access
### User Roles (Study-level)
- **Owner**: Full study control, manage members
- **Researcher**: Design experiments, manage participants
- **Wizard**: Execute trials, control robot during sessions
- **Observer**: Read-only access to study data
### Core Workflows
1. **Study Creation** → Team setup → Participant recruitment
2. **Experiment Design** → Visual designer → Protocol validation
3. **Trial Execution** → Wizard interface → Data capture
4. **Data Analysis** → Export → Insights
### Plugin Identifier System
- `identifier`: Machine-readable key (e.g., `nao6-ros2`)
- `name`: Display name (e.g., `NAO6 Robot (ROS2 Integration)`)
- Lookup order: identifier → name → fallback
---
## 🛠 **Development Commands**
## Development Commands
| Command | Purpose |
|---------|---------|
| `bun dev` | Start development server |
| `bun build` | Build for production |
| Command | Description |
|---------|-------------|
| `bun dev` | Start dev server |
| `bun build` | Production build |
| `bun typecheck` | TypeScript validation |
| `bun lint` | Code quality checks |
| `bun db:push` | Push schema changes |
| `bun db:seed` | Seed data & sync repositories |
| `bun db:studio` | Open database GUI |
| `bun db:seed` | Seed data + sync plugins + forms |
| `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
```
http://localhost:3000/api/trpc/
```bash
cd ~/Documents/Projects/nao6-hristudio-integration
docker compose up -d
```
### Key Routers
- **`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
**Services**: nao_driver, ros_bridge (:9090), ros_api
### Example Usage
```typescript
// Get user's studies
const studies = api.studies.getUserStudies.useQuery();
**Topics**:
- `/speech` - TTS
- `/cmd_vel` - Movement
- `/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
```sql
users -- Authentication & profiles
studies -- Research projects
experiments -- Protocol templates
participants -- Study participants
trials -- Experiment instances
steps -- Experiment phases
trial_events -- Execution logs
robots -- Available platforms
```
- `users` - Authentication
- `studies` - Research projects
- `experiments` - Protocol templates
- `trials` - Execution instances
- `steps` - Experiment phases
- `actions` - Atomic tasks
- `plugins` - Robot integrations (identifier column)
- `trial_events` - Execution logs
---
## Route Structure
### Key Relationships
```
studies → experiments → trials
studies → participants
trials → trial_events
experiments → steps
/dashboard - Global overview
/studies - Study list
/studies/[id] - Study details
/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
// Page wrapper with navigation
<PageLayout title="Studies" description="Manage research studies">
<StudiesTable />
</PageLayout>
// Loading a plugin by identifier
const plugin = await trialExecution.loadPlugin("nao6-ros2");
// Entity forms (unified pattern)
<EntityForm
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}`);
};
// Action execution
await robot.execute("nao6-ros2.say_with_emotion", { text: "Hello" });
```
---
## 🎯 **Route Structure**
### 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.*
Last updated: March 2026
+151
View File
@@ -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)
+222
View File
@@ -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)
+341
View File
@@ -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)
+366
View File
@@ -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)
+393
View File
@@ -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)
+386
View File
@@ -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)
+505
View File
@@ -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)
+505
View File
@@ -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)
+389
View File
@@ -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)
+71
View File
@@ -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)
+1
View File
@@ -9,4 +9,5 @@ export default {
url: env.DATABASE_URL,
},
tablesFilter: ["hs_*"],
out: "./migrations",
} satisfies Config;
+9 -37
View File
@@ -1,55 +1,27 @@
import type { Session } from "next-auth";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { auth } from "./src/server/auth";
export default auth((req: NextRequest & { auth: Session | null }) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
export default async function middleware(request: NextRequest) {
const { nextUrl } = request;
// Define route patterns
const isApiAuthRoute = nextUrl.pathname.startsWith("/api/auth");
const isPublicRoute = ["/", "/auth/signin", "/auth/signup"].includes(
nextUrl.pathname,
);
// Skip session checks for now to debug the auth issue
const isApiRoute = nextUrl.pathname.startsWith("/api");
const isAuthRoute = nextUrl.pathname.startsWith("/auth");
// Allow API auth routes to pass through
if (isApiAuthRoute) {
if (isApiRoute) {
return NextResponse.next();
}
// If user is on auth pages and already logged in, redirect to dashboard
if (isAuthRoute && isLoggedIn) {
return NextResponse.redirect(new URL("/", nextUrl));
}
// 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),
);
// Allow auth routes through for now
if (isAuthRoute) {
return NextResponse.next();
}
return NextResponse.next();
});
}
// Configure which routes the middleware should run on
export const config = {
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)$).*)",
],
};
+605
View File
@@ -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");
+34
View File
@@ -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 $$;
+20
View File
@@ -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
View File
@@ -5,6 +5,9 @@
import "./src/env.js";
/** @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;
+24 -13
View File
@@ -5,27 +5,33 @@
"type": "module",
"scripts": {
"build": "next build",
"check": "next lint && tsc --noEmit",
"check": "eslint . && tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"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:down": "docker compose down && if [ \"$(uname)\" = \"Darwin\" ]; then colima stop; fi",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"lint": "next lint",
"lint:fix": "next lint --fix",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"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"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.11.1",
"@aws-sdk/client-s3": "^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/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -67,24 +73,25 @@
"@types/js-cookie": "^3.0.6",
"@types/ws": "^8.18.1",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"driver.js": "^1.4.0",
"drizzle-orm": "^0.41.0",
"drizzle-orm": "^0.45.2",
"html2pdf.js": "^0.14.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.536.0",
"minio": "^8.0.6",
"next": "^16.1.6",
"next": "16.2.6",
"next-auth": "^5.0.0-beta.30",
"next-themes": "^0.4.6",
"postgres": "^3.4.8",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react": "19.2.4",
"react-day-picker": "^9.13.2",
"react-dom": "^19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.71.1",
"react-resizable-panels": "^3.0.6",
"react-signature-canvas": "^1.1.0-alpha.2",
@@ -106,12 +113,12 @@
"@types/bun": "^1.3.9",
"@types/crypto-js": "^4.2.2",
"@types/node": "^20.19.33",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/uuid": "^11.0.0",
"drizzle-kit": "^0.30.6",
"eslint": "^9.39.2",
"eslint-config-next": "^15.5.12",
"eslint-config-next": "16.2.1",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
@@ -131,5 +138,9 @@
"esbuild",
"sharp",
"unrs-resolver"
]
],
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3"
}
}
+28
View File
@@ -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
View File
@@ -564,6 +564,7 @@ async function seedNAO6Plugin() {
const pluginData: InsertPlugin = {
robotId: robotId,
identifier: "nao6-ros2",
name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0",
description:
+274
View File
@@ -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();
+1 -1
View File
@@ -42,7 +42,7 @@ async function main() {
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})`);
+37
View File
@@ -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);
+18
View File
@@ -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
+21
View File
@@ -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"
}
}
+412
View File
@@ -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}`);
+15
View File
@@ -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
View File
@@ -14,31 +14,31 @@ const db = drizzle(connection, { schema });
import * as fs from "fs";
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() {
const REMOTE_URL = "https://repo.hristudio.com/plugins/nao6-ros2.json";
const LOCAL_PATH = path.join(
__dirname,
"../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 {
console.log(
`🌐 Attempting to fetch plugin definition from ${REMOTE_URL}...`,
console.log(`📁 Loading plugin definition from local file...`);
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, {
signal: AbortSignal.timeout(3000),
}); // 3s timeout
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log("✅ Successfully fetched plugin definition from remote.");
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)
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.trialEvents).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.studyPlugins).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.plugins).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.robots).where(sql`1=1`);
// 2. Create Users
// 2. Create Users (Better Auth manages credentials)
console.log("👥 Creating users...");
const hashedPassword = await bcrypt.hash("password123", 12);
const gravatarUrl = (email: string) =>
`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
.insert(schema.users)
.values({
id: adminId,
name: "Sean O'Connor",
email: "sean@soconnor.dev",
password: hashedPassword,
emailVerified: new Date(),
emailVerified: true,
image: gravatarUrl("sean@soconnor.dev"),
})
.returning();
@@ -114,16 +122,39 @@ async function main() {
const [researcherUser] = await db
.insert(schema.users)
.values({
id: researcherId,
name: "Dr. Felipe Perrone",
email: "felipe.perrone@bucknell.edu",
password: hashedPassword,
emailVerified: new Date(),
emailVerified: true,
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe",
})
.returning();
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
.insert(schema.userSystemRoles)
.values({ userId: adminUser.id, role: "administrator" });
@@ -159,6 +190,7 @@ async function main() {
.insert(schema.plugins)
.values({
robotId: naoRobot!.id,
identifier: NAO_PLUGIN_DEF.robotId,
name: NAO_PLUGIN_DEF.name,
version: NAO_PLUGIN_DEF.version,
description: NAO_PLUGIN_DEF.description,
@@ -192,10 +224,107 @@ async function main() {
{ 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
const [corePlugin] = await db
.insert(schema.plugins)
.values({
identifier: CORE_PLUGIN_DEF.id,
name: CORE_PLUGIN_DEF.name,
version: CORE_PLUGIN_DEF.version,
description: CORE_PLUGIN_DEF.description,
@@ -211,6 +340,7 @@ async function main() {
const [wozPlugin] = await db
.insert(schema.plugins)
.values({
identifier: WOZ_PLUGIN_DEF.id,
name: WOZ_PLUGIN_DEF.name,
version: WOZ_PLUGIN_DEF.version,
description: WOZ_PLUGIN_DEF.description,
@@ -262,6 +392,35 @@ async function main() {
// 5. Create Steps & Actions (The Interactive Storyteller Protocol)
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 ---
const [step1] = await db
.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 ---
const [step4a] = await db
.insert(schema.steps)
@@ -378,6 +533,9 @@ async function main() {
orderIndex: 3,
required: false,
durationEstimate: 20,
conditions: {
nextStepId: step5!.id, // Jump to Story Continues after completing
},
})
.returning();
@@ -392,11 +550,13 @@ async function main() {
orderIndex: 4,
required: false,
durationEstimate: 20,
conditions: {
nextStepId: step5!.id, // Jump to Story Continues after completing
},
})
.returning();
// --- Step 3: Comprehension Check (Wizard Decision Point) ---
// Note: Wizard will choose to proceed to Step 4a (Correct) or 4b (Incorrect)
const [step3] = await db
.insert(schema.steps)
.values({
@@ -445,10 +605,12 @@ async function main() {
name: "Wait for Choice",
type: "wizard_wait_for_response",
orderIndex: 1,
// Define the options that will be presented to the Wizard
parameters: {
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",
pluginId: "hristudio-woz", // Explicit link
@@ -553,23 +715,42 @@ async function main() {
},
]);
// --- Step 5: Conclusion ---
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();
// --- Step 5 actions: Story Continues ---
await db.insert(schema.actions).values([
{
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",
type: "nao6-ros2.say_text",
orderIndex: 0,
@@ -580,7 +761,7 @@ async function main() {
retryable: true,
},
{
stepId: step5!.id,
stepId: step6!.id,
name: "Bow Gesture",
type: "nao6-ros2.move_arm",
orderIndex: 1,
@@ -843,6 +1024,22 @@ async function main() {
.values(participants)
.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(`Summary:`);
console.log(`- 1 Admin User (sean@soconnor.dev)`);
@@ -1024,7 +1221,7 @@ async function main() {
trialId: analyticsTrial!.id,
eventType: "step_changed",
timestamp: new Date(currentTime),
data: { stepId: step5!.id, stepName: "Conclusion" },
data: { stepId: step6!.id, stepName: "Conclusion" },
});
advance(2);
+12 -12
View File
@@ -26,9 +26,9 @@ export default function HelpCenterPage() {
description: "Learn the basics of HRIStudio and set up your first study.",
icon: BookOpen,
items: [
{ label: "Platform Overview", href: "#" },
{ label: "Creating a New Study", href: "#" },
{ label: "Managing Team Members", href: "#" },
{ label: "Tutorials Overview", href: "/help/tutorials" },
{ label: "Getting Started Guide", href: "/help/tutorials/getting-started" },
{ 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.",
icon: FlaskConical,
items: [
{ label: "Using the Visual Designer", href: "#" },
{ label: "Robot Actions & Plugins", href: "#" },
{ label: "Variables & Logic", href: "#" },
{ label: "Visual Designer Guide", href: "/help/tutorials/designing-experiments" },
{ label: "Robot Actions & Plugins", href: "/help/tutorials/robot-integration" },
{ 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.",
icon: PlayCircle,
items: [
{ label: "Wizard Interface Guide", href: "#" },
{ label: "Participant Management", href: "#" },
{ label: "Handling Robot Errors", href: "#" },
{ label: "Running Trials Guide", href: "/help/tutorials/running-trials" },
{ label: "Participant Management", href: "/help/tutorials/your-first-study" },
{ label: "Simulation Mode", href: "/help/tutorials/simulation-mode" },
],
},
{
@@ -56,9 +56,9 @@ export default function HelpCenterPage() {
description: "Analyze trial results and export research data.",
icon: BarChart3,
items: [
{ label: "Understanding Analytics", href: "#" },
{ label: "Exporting Data (CSV/JSON)", href: "#" },
{ label: "Video Replay & Annotation", href: "#" },
{ label: "Data & Analysis Guide", href: "/help/tutorials/data-and-analysis" },
{ label: "Forms & Surveys", href: "/help/tutorials/forms-and-surveys" },
{ 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&apos;s build &quot;The Interactive Storyteller&quot; - a simple storytelling experiment:</p>
<h3>Step 1: The Hook (Start)</h3>
<ol>
<li>Click <strong>+ Add Step</strong></li>
<li>Name it &quot;The Hook&quot;</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 &quot;Comprehension Check&quot;</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 &quot;Story Continues&quot;</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&apos;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&apos;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>
);
}
+241
View File
@@ -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&apos;s IP address:
<pre><code># On the robot, say &quot;What is my IP address?&quot;
# Or check robot&apos;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 &quot;from naoqi import ALProxy; proxy = ALProxy('ALMotion', '192.168.1.100', 9559); proxy.wakeUp()&quot;</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 &quot;Connected&quot;</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 &quot;Ready&quot; 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 &quot;Completed&quot;</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&apos;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 &quot;Hello!&quot;
[14:33:28] Wizard Note: &quot;Participant engaged&quot;
[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&apos;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., &quot;NAO6 Robot&quot;)</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., &quot;P001&quot;)</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>
);
}
+6 -7
View File
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { cookies, headers } from "next/headers";
import {
SidebarInset,
SidebarProvider,
@@ -7,7 +7,7 @@ import {
} from "~/components/ui/sidebar";
import { Separator } from "~/components/ui/separator";
import { AppSidebar } from "~/components/dashboard/app-sidebar";
import { auth } from "~/server/auth";
import { auth } from "~/lib/auth";
import {
BreadcrumbProvider,
BreadcrumbDisplay,
@@ -22,16 +22,15 @@ interface DashboardLayoutProps {
export default async function DashboardLayout({
children,
}: DashboardLayoutProps) {
const session = await auth();
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) {
redirect("/auth/signin");
}
const userRole =
typeof session.user.roles?.[0] === "string"
? session.user.roles[0]
: (session.user.roles?.[0]?.role ?? "observer");
const userRole = "researcher"; // Default role for dashboard access
const cookieStore = await cookies();
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
+384 -191
View File
@@ -1,10 +1,35 @@
"use client";
import * as React from "react";
import { redirect } from "next/navigation";
import { PasswordChangeForm } from "~/components/profile/password-change-form";
import { ProfileEditForm } from "~/components/profile/profile-edit-form";
import { Badge } from "~/components/ui/badge";
import Link from "next/link";
import { useSession } from "~/lib/auth-client";
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 { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import {
Card,
CardContent,
@@ -12,200 +37,375 @@ import {
CardHeader,
CardTitle,
} 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 {
User,
Shield,
Download,
Trash2,
ExternalLink,
Lock,
UserCog,
Mail,
Fingerprint,
} from "lucide-react";
import { useSession } from "next-auth/react";
import { cn } from "~/lib/utils";
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
interface ProfileUser {
id: string;
name: string | null;
email: string;
image: string | null;
roles?: Array<{
role: "administrator" | "researcher" | "wizard" | "observer";
grantedAt: string | Date;
}>;
interface Membership {
studyId: string;
role: string;
joinedAt: 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 (
<div className="animate-in fade-in space-y-8 duration-500">
<PageHeader
title={user.name ?? "User"}
description={user.email}
icon={User}
badges={[
{ label: `ID: ${user.id}`, variant: "outline" },
...(user.roles?.map((r) => ({
label: formatRole(r.role),
variant: "secondary" as const,
})) ?? []),
]}
/>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Main Content (Left Column) */}
<div className="space-y-8 lg:col-span-2">
{/* Personal Information */}
<section className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<User className="text-primary h-5 w-5" />
<h3 className="text-lg font-semibold">Personal Information</h3>
</div>
<Card className="border-border/60 hover:border-border transition-colors">
<CardHeader>
<CardTitle className="text-base">Contact Details</CardTitle>
<CardDescription>
Update your public profile information
</CardDescription>
</CardHeader>
<CardContent>
<ProfileEditForm
user={{
id: user.id,
name: user.name,
email: user.email,
image: user.image,
}}
/>
</CardContent>
</Card>
</section>
{/* Security */}
<section className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Lock className="text-primary h-5 w-5" />
<h3 className="text-lg font-semibold">Security</h3>
</div>
<Card className="border-border/60 hover:border-border transition-colors">
<CardHeader>
<CardTitle className="text-base">Password</CardTitle>
<CardDescription>
Ensure your account stays secure
</CardDescription>
</CardHeader>
<CardContent>
<PasswordChangeForm />
</CardContent>
</Card>
</section>
</div>
{/* Sidebar (Right Column) */}
<div className="space-y-8">
{/* Permissions */}
<section className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Shield className="text-primary h-5 w-5" />
<h3 className="text-lg font-semibold">Permissions</h3>
</div>
<Card>
<CardContent className="pt-6">
{user.roles && user.roles.length > 0 ? (
<div className="space-y-4">
{user.roles.map((roleInfo, index) => (
<div key={index} className="space-y-2">
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
{formatRole(roleInfo.role)}
</span>
<span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px]">
Since{" "}
{new Date(roleInfo.grantedAt).toLocaleDateString()}
</span>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">
{getRoleDescription(roleInfo.role)}
</p>
{index < (user.roles?.length || 0) - 1 && (
<Separator className="my-2" />
)}
<div className="flex items-center gap-4">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-xl font-bold text-primary-foreground">
{initials}
</div>
<div>
<h1 className="text-2xl font-bold">{user?.name ?? "User"}</h1>
<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 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">
<div className="text-primary mb-1 flex items-center gap-2 font-medium">
<Shield className="h-3 w-3" />
<span>Role Management</span>
</div>
System roles are managed by administrators. Contact
support if you need access adjustments.
)}
</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" />
) : (
<div className="py-4 text-center">
<p className="text-sm font-medium">No Roles Assigned</p>
<p className="text-muted-foreground mt-1 text-xs">
Contact an admin to request access.
<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>
{/* Main Content */}
<div className="grid gap-6 lg:grid-cols-3">
{/* Left Column - Profile Info */}
<div className="space-y-6 lg:col-span-2">
{/* Personal Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5 text-primary" />
Personal Information
</CardTitle>
<CardDescription>
Your public profile information
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
{isEditing ? (
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
) : (
<div className="flex items-center gap-2 rounded-md border bg-muted/50 p-2">
<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>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5 text-primary" />
Recent Activity
</CardTitle>
<CardDescription>
Your recent actions across the platform
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center py-8 text-center">
<Calendar className="text-muted-foreground/50 mb-3 h-12 w-12" />
<p className="font-medium">No recent activity</p>
<p className="text-muted-foreground text-sm">
Your recent actions will appear here
</p>
<Button size="sm" variant="outline" className="mt-3 w-full">
Request Access
</div>
</CardContent>
</Card>
</div>
{/* Right Column - Settings */}
<div className="space-y-6">
{/* Security */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
Security
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="flex items-center gap-3">
<Lock className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-sm font-medium">Password</p>
<p className="text-muted-foreground text-xs">Last changed: Never</p>
</div>
</div>
<Dialog open={passwordOpen} onOpenChange={setPasswordOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="sm">
Change
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<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 className="space-y-2">
<Label htmlFor="new">New Password</Label>
<Input
id="new"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
</div>
<div className="space-y-2">
<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>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</Link>
))}
{(!userStudies?.studies.length) && (
<div className="flex flex-col items-center justify-center py-4 text-center">
<Building className="text-muted-foreground/50 mb-2 h-8 w-8" />
<p className="text-sm">No studies yet</p>
<Button variant="link" size="sm" asChild className="mt-1">
<Link href="/studies/new">Create a study</Link>
</Button>
</div>
)}
{userStudies && userStudies.studies.length > 5 && (
<Button variant="ghost" size="sm" asChild className="w-full">
<Link href="/studies">
View all {userStudies.studies.length} studies <ChevronRight className="ml-1 h-3 w-3" />
</Link>
</Button>
)}
</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>
</div>
<Separator className="bg-destructive/10" />
<div>
<h4 className="text-destructive mb-1 text-sm font-semibold">
Delete Account
</h4>
<p className="text-muted-foreground mb-3 text-xs">
This action is irreversible.
</p>
<Button
variant="destructive"
size="sm"
className="w-full"
disabled
>
<Trash2 className="mr-2 h-3 w-3" />
Delete Account
</Button>
</div>
</CardContent>
</Card>
</section>
</div>
</div>
</div>
@@ -213,17 +413,12 @@ function ProfileContent({ user }: { user: ProfileUser }) {
}
export default function ProfilePage() {
const { data: session, status } = useSession();
const { data: session, isPending } = useSession();
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Profile" },
]);
if (status === "loading") {
if (isPending) {
return (
<div className="text-muted-foreground animate-pulse p-8">
Loading profile...
<div className="flex items-center justify-center p-12">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
);
}
@@ -232,7 +427,5 @@ export default function ProfilePage() {
redirect("/auth/signin");
}
const user = session.user;
return <ProfileContent user={user} />;
return <ProfilePageContent />;
}
@@ -8,6 +8,9 @@ import type {
} from "~/lib/experiment-designer/types";
import { api } from "~/trpc/server";
import { DesignerPageClient } from "./DesignerPageClient";
import { db } from "~/server/db";
import { studyPlugins, plugins } from "~/server/db/schema";
import { desc, eq } from "drizzle-orm";
interface ExperimentDesignerPageProps {
params: Promise<{
@@ -74,10 +77,20 @@ export default async function ExperimentDesignerPage({
actionDefinitions: Array<{ id: string }> | null;
};
};
const rawInstalledPluginsUnknown: unknown =
await api.robots.plugins.getStudyPlugins({
studyId: experiment.study.id,
});
const installedPluginsResult = await db
.select({
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 {
return v && typeof v === "object"
@@ -27,7 +27,7 @@ import {
} from "~/components/ui/entity-view";
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
import { api } from "~/trpc/react";
import { useSession } from "next-auth/react";
import { useSession } from "~/lib/auth-client";
import { useStudyManagement } from "~/hooks/useStudyManagement";
interface ExperimentDetailPageProps {
@@ -99,6 +99,9 @@ export default function ExperimentDetailPage({
params,
}: ExperimentDetailPageProps) {
const { data: session } = useSession();
const { data: userData } = api.auth.me.useQuery(undefined, {
enabled: !!session?.user,
});
const [experiment, setExperiment] = useState<Experiment | null>(null);
const [trials, setTrials] = useState<Trial[]>([]);
const [loading, setLoading] = useState(true);
@@ -181,7 +184,7 @@ export default function ExperimentDetailPage({
const description = experiment.description;
// Check if user can edit this experiment
const userRoles = session?.user?.roles?.map((r) => r.role) ?? [];
const userRoles = userData?.roles ?? [];
const canEdit =
userRoles.includes("administrator") || userRoles.includes("researcher");
@@ -31,6 +31,8 @@ export default function StudyExperimentsPage() {
}
}, [studyId, selectedStudyId, setSelectedStudyId]);
const canManage = study?.userRole === "owner" || study?.userRole === "researcher";
return (
<div className="space-y-6">
<PageHeader
@@ -38,12 +40,14 @@ export default function StudyExperimentsPage() {
description="Design and manage experiment protocols for this study"
icon={FlaskConical}
actions={
canManage ? (
<Button asChild>
<a href={`/studies/${studyId}/experiments/new`}>
<Plus className="mr-2 h-4 w-4" />
Create Experiment
</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 &nbsp; <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 || ""} &nbsp;|&nbsp;
<strong>Form Type:</strong> ${form?.type} &nbsp;|&nbsp;
<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>
);
}
+192 -239
View File
@@ -1,108 +1,49 @@
"use client";
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 { FileText, Loader2, Plus, Download, Edit2, Eye, Save } from "lucide-react";
import Link from "next/link";
import {
EntityView,
EntityViewHeader,
EntityViewSection,
EmptyState,
} from "~/components/ui/entity-view";
FileText,
Plus,
Search,
ClipboardList,
FileQuestion,
FileSignature,
MoreHorizontal,
Trash2,
Eye,
CheckCircle,
} from "lucide-react";
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
import { Button } from "~/components/ui/button";
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 { toast } from "sonner";
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 }) => {
if (!editor) {
return null;
}
const formTypeIcons = {
consent: FileSignature,
survey: ClipboardList,
questionnaire: FileQuestion,
};
return (
<div className="border border-input bg-transparent rounded-tr-md rounded-tl-md p-1 flex items-center gap-1 flex-wrap">
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleBold().run()}
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>
);
const formTypeColors = {
consent:
"bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400",
survey: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
questionnaire:
"bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
};
interface StudyFormsPageProps {
@@ -113,9 +54,12 @@ interface StudyFormsPageProps {
export default function StudyFormsPage({ params }: StudyFormsPageProps) {
const { data: session } = useSession();
const router = useRouter();
const utils = api.useUtils();
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(null);
const [editorTarget, setEditorTarget] = useState<string>("");
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(
null,
);
const [search, setSearch] = useState("");
useEffect(() => {
const resolveParams = async () => {
@@ -130,85 +74,33 @@ export default function StudyFormsPage({ params }: StudyFormsPageProps) {
{ enabled: !!resolvedParams?.id },
);
const { data: activeConsentForm, refetch: refetchConsentForm } =
api.studies.getActiveConsentForm.useQuery(
{ studyId: resolvedParams?.id ?? "" },
const { data: formsData, isLoading } = api.forms.list.useQuery(
{ studyId: resolvedParams?.id ?? "", search: search || undefined },
{ enabled: !!resolvedParams?.id },
);
// Only sync once when form loads to avoid resetting user edits
useEffect(() => {
if (activeConsentForm && !editorTarget) {
setEditorTarget(activeConsentForm.content);
}
}, [activeConsentForm, editorTarget]);
const userRole = (study as any)?.userRole;
const canManage = userRole === "owner" || userRole === "researcher";
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({
const deleteMutation = api.forms.delete.useMutation({
onSuccess: () => {
toast.success("Consent Form Saved Successfully!");
void refetchConsentForm();
void utils.studies.getActivity.invalidate({ studyId: resolvedParams?.id ?? "" });
toast.success("Form deleted successfully");
void utils.forms.list.invalidate({ studyId: resolvedParams?.id });
},
onError: (error) => {
toast.error("Error saving consent form", { description: error.message });
toast.error("Failed to delete 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`
const setActiveMutation = api.forms.setActive.useMutation({
onSuccess: () => {
toast.success("Form set as active");
void utils.forms.list.invalidate({ studyId: resolvedParams?.id });
},
onError: (error) => {
toast.error("Failed to set active", { description: error.message });
},
});
toast.success("Document Downloaded Successfully!", { id: "pdf-gen" });
} catch (error) {
toast.error("Error generating PDF", { id: "pdf-gen" });
console.error(error);
}
};
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
@@ -223,95 +115,156 @@ export default function StudyFormsPage({ params }: StudyFormsPageProps) {
if (!study) return <div>Loading...</div>;
const forms = formsData?.forms ?? [];
return (
<EntityView>
<div className="space-y-6">
<PageHeader
title="Study Forms"
description="Manage consent forms and future questionnaires for this study"
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>
)
}
/>
<div className="grid grid-cols-1 gap-8">
<EntityViewSection
title="Consent Document"
icon="FileText"
description="Design and manage the consent form that participants must sign before participating in your trials."
actions={
<div className="flex gap-2">
<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" />
) : (
{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" />
)}
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
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>
<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 })
}
>
{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">
<div className="max-w-4xl w-full bg-white dark:bg-card shadow-xl ring-1 ring-border rounded-sm flex flex-col">
<div className="border-b border-border bg-muted/50 dark:bg-muted/10">
<Toolbar editor={editor} />
</div>
<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>
</div>
</div>
</div>
) : (
<EmptyState
icon="FileText"
title="No Consent Form"
description="Generate a boilerplate consent form for this study to download and collect signatures."
/>
<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>
)}
</EntityViewSection>
</div>
</EntityView>
);
}
+20 -3
View File
@@ -18,8 +18,9 @@ import {
} from "~/components/ui/entity-view";
import { PageHeader } from "~/components/ui/page-header";
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 { AddMemberDialog } from "~/components/studies/add-member-dialog";
interface StudyDetailPageProps {
params: Promise<{
@@ -59,6 +60,7 @@ type Study = {
irbProtocol: string | null;
createdAt: Date;
updatedAt: Date;
userRole?: string;
};
type Member = {
@@ -156,6 +158,10 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
).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 = {
experiments: experiments.length,
totalTrials: totalTrials,
@@ -181,18 +187,22 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
]}
actions={
<div className="flex items-center gap-2">
{canManage && (
<Button asChild variant="outline">
<Link href={`/studies/${study.id}/edit`}>
<Settings className="mr-2 h-4 w-4" />
Edit Study
</Link>
</Button>
)}
{canManage && (
<Button asChild>
<Link href={`/studies/${study.id}/experiments/new`}>
<Plus className="mr-2 h-4 w-4" />
New Experiment
</Link>
</Button>
)}
</div>
}
/>
@@ -234,12 +244,14 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
icon="FlaskConical"
description="Design and manage experimental protocols for this study"
actions={
canManage ? (
<Button asChild variant="outline" size="sm">
<Link href={`/studies/${study.id}/experiments/new`}>
<Plus className="mr-2 h-4 w-4" />
Add Experiment
</Link>
</Button>
) : null
}
>
{experiments.length === 0 ? (
@@ -273,7 +285,8 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
</Link>
</h4>
<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 ${
experiment.status === "draft"
? "bg-gray-100 text-gray-800"
: experiment.status === "ready"
? "bg-green-100 text-green-800"
@@ -390,10 +403,14 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
icon="Users"
description={`${members.length} team member${members.length !== 1 ? "s" : ""}`}
actions={
canManage ? (
<AddMemberDialog studyId={study.id}>
<Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Invite
Manage
</Button>
</AddMemberDialog>
) : null
}
>
<div className="space-y-3">
@@ -31,6 +31,8 @@ export default function StudyParticipantsPage() {
}
}, [studyId, selectedStudyId, setSelectedStudyId]);
const canManage = study?.userRole === "owner" || study?.userRole === "researcher";
return (
<div className="space-y-6">
<PageHeader
@@ -38,12 +40,14 @@ export default function StudyParticipantsPage() {
description="Manage participant registration, consent, and trial assignments for this study"
icon={Users}
actions={
canManage ? (
<Button asChild>
<a href={`/studies/${studyId}/participants/new`}>
<Plus className="mr-2 h-4 w-4" />
Add Participant
</a>
</Button>
) : null
}
/>
@@ -13,7 +13,7 @@ import { WizardView } from "~/components/trials/views/WizardView";
import { ObserverView } from "~/components/trials/views/ObserverView";
import { ParticipantView } from "~/components/trials/views/ParticipantView";
import { api } from "~/trpc/react";
import { useSession } from "next-auth/react";
import { useSession } from "~/lib/auth-client";
function WizardPageContent() {
const params = useParams();
@@ -25,6 +25,11 @@ function WizardPageContent() {
const { study } = useSelectedStudyDetails();
const { data: session } = useSession();
// Get user roles
const { data: userData } = api.auth.me.useQuery(undefined, {
enabled: !!session?.user,
});
// Get trial data
const {
data: trial,
@@ -67,7 +72,7 @@ function WizardPageContent() {
}
// 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") {
return "wizard";
}
@@ -188,6 +193,7 @@ function WizardPageContent() {
name: trial.experiment.name,
description: trial.experiment.description,
studyId: trial.experiment.studyId,
robotId: trial.experiment.robotId,
},
participant: {
id: trial.participant.id,

Some files were not shown because too many files have changed in this diff Show More