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

This commit is contained in:
2026-03-25 22:48:42 -04:00
parent 3959cf23f7
commit 1c7f0297a6
34 changed files with 6298 additions and 139 deletions
+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>
);
}
+9 -119
View File
@@ -11,6 +11,7 @@ import {
Building,
ChevronDown,
FlaskConical,
GraduationCap,
Home,
LogOut,
MoreHorizontal,
@@ -59,7 +60,6 @@ import { Logo } from "~/components/ui/logo";
import { useStudyManagement } from "~/hooks/useStudyManagement";
import { handleAuthError, isAuthError } from "~/lib/auth-error-handler";
import { api } from "~/trpc/react";
// Global items - always available
const globalItems = [
@@ -129,10 +129,9 @@ const helpItems = [
icon: BookOpen,
},
{
title: "Interactive Tour",
url: "#tour",
title: "Tutorials",
url: "/help/tutorials",
icon: PlayCircle,
action: "tour",
},
];
@@ -183,12 +182,6 @@ export function AppSidebar({
}
}, [isLoadingUserStudies, selectedStudyId, userStudies, selectStudy]);
// Debug API call
const { data: debugData } = api.dashboard.debug.useQuery(undefined, {
enabled: process.env.NODE_ENV === "development",
staleTime: 1000 * 30, // 30 seconds
});
type Study = {
id: string;
name: string;
@@ -285,9 +278,6 @@ export function AppSidebar({
return () => clearInterval(interval);
}, [refreshStudyData]);
// Show debug info in development
const showDebug = process.env.NODE_ENV === "development";
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
@@ -600,23 +590,14 @@ export function AppSidebar({
{helpItems.map((item) => {
const isActive = pathname.startsWith(item.url);
const menuButton =
item.action === "tour" ? (
<SidebarMenuButton
onClick={() => startTour("full_platform")}
isActive={false}
>
const menuButton = (
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.url}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</SidebarMenuButton>
) : (
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.url}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
);
</Link>
</SidebarMenuButton>
);
return (
<SidebarMenuItem key={item.title}>
@@ -639,99 +620,8 @@ export function AppSidebar({
</SidebarGroupContent>
</SidebarGroup>
{/* Debug info moved to footer tooltip button */}
<SidebarFooter>
<SidebarMenu>
{showDebug && (
<SidebarMenuItem>
{isCollapsed ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-xs"
aria-label="Debug info"
>
<BarChart3 className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent
side="right"
className="space-y-1 p-2 text-[10px]"
>
<div>Session: {session?.user?.email ?? "No session"}</div>
<div>Role: {userRole ?? "No role"}</div>
<div>Studies: {userStudies.length}</div>
<div>Selected: {selectedStudy?.name ?? "None"}</div>
<div>Auth: {session ? "✓" : "✗"}</div>
{debugData && (
<>
<div>DB User: {debugData.user?.email ?? "None"}</div>
<div>
System Roles:{" "}
{debugData.systemRoles.join(", ") || "None"}
</div>
<div>
Memberships: {debugData.studyMemberships.length}
</div>
<div>All Studies: {debugData.allStudies.length}</div>
<div>
Session ID: {debugData.session.userId.slice(0, 8)}
...
</div>
</>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton className="w-full justify-start">
<BarChart3 className="h-4 w-4" />
<span className="truncate">Debug</span>
<ChevronDown className="ml-auto h-4 w-4 flex-shrink-0" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-popper-anchor-width] max-w-72"
align="start"
>
<DropdownMenuLabel className="text-xs font-medium">
Debug Info
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="space-y-1 px-2 py-1 text-[11px] leading-tight">
<div>Session: {session?.user?.email ?? "No session"}</div>
<div>Role: {userRole ?? "No role"}</div>
<div>Studies: {userStudies.length}</div>
<div>Selected: {selectedStudy?.name ?? "None"}</div>
<div>Auth: {session ? "✓" : "✗"}</div>
{debugData && (
<>
<div>DB User: {debugData.user?.email ?? "None"}</div>
<div>
System Roles:{" "}
{debugData.systemRoles.join(", ") || "None"}
</div>
<div>
Memberships: {debugData.studyMemberships.length}
</div>
<div>All Studies: {debugData.allStudies.length}</div>
<div>
Session ID: {debugData.session.userId.slice(0, 8)}
...
</div>
</>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
)}
<SidebarMenuItem>
{isCollapsed ? (
<TooltipProvider>
+126
View File
@@ -0,0 +1,126 @@
import { type ReactNode } from "react";
import Link from "next/link";
import { Button } from "~/components/ui/button";
import {
ChevronLeft,
ChevronRight,
CheckCircle2,
} from "lucide-react";
import { PageLayout } from "~/components/ui/page-layout";
interface TutorialStep {
title: string;
description: string;
}
interface TutorialPageProps {
children: ReactNode;
title: string;
description: string;
duration: string;
level: string;
steps: TutorialStep[];
prevTutorial?: {
title: string;
href: string;
};
nextTutorial?: {
title: string;
href: string;
};
}
export function TutorialPage({
children,
title,
description,
duration,
level,
steps,
prevTutorial,
nextTutorial,
}: TutorialPageProps) {
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",
};
return (
<PageLayout
title={title}
description={description}
breadcrumb={[
{ label: "Help", href: "/help" },
{ label: "Tutorials", href: "/help/tutorials" },
{ label: title },
]}
>
<div className="grid gap-8 lg:grid-cols-[1fr_250px]">
<div className="prose prose-slate dark:prose-invert max-w-none">
{children}
</div>
<aside className="hidden lg:block">
<div className="sticky top-4 space-y-6">
<Card className="p-4">
<div className="mb-4 flex items-center justify-between">
<span className="text-sm font-medium">Tutorial Info</span>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${levelColors[level]}`}
>
{level}
</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Duration</span>
<span>{duration}</span>
</div>
</div>
</Card>
<div>
<h3 className="mb-3 text-sm font-medium">In This Tutorial</h3>
<ul className="space-y-2">
{steps.map((step, index) => (
<li key={index} className="flex items-start gap-2 text-sm">
<CheckCircle2 className="text-primary mt-0.5 h-4 w-4 flex-shrink-0" />
<span>{step.title}</span>
</li>
))}
</ul>
</div>
<div className="flex flex-col gap-2">
{prevTutorial && (
<Button variant="outline" size="sm" className="justify-start" asChild>
<Link href={prevTutorial.href}>
<ChevronLeft className="mr-1 h-4 w-4" />
{prevTutorial.title}
</Link>
</Button>
)}
{nextTutorial && (
<Button variant="outline" size="sm" className="justify-start" asChild>
<Link href={nextTutorial.href}>
{nextTutorial.title}
<ChevronRight className="ml-1 h-4 w-4" />
</Link>
</Button>
)}
</div>
</div>
</aside>
</div>
</PageLayout>
);
}
function Card({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={`rounded-lg border bg-card text-card-foreground shadow-sm ${className}`}>
{children}
</div>
);
}
+20 -2
View File
@@ -10,6 +10,7 @@ import {
export interface UseWizardRosOptions {
autoConnect?: boolean;
simulationMode?: boolean;
onConnected?: () => void;
onDisconnected?: () => void;
onError?: (error: unknown) => void;
@@ -24,6 +25,7 @@ export interface UseWizardRosOptions {
export interface UseWizardRosReturn {
isConnected: boolean;
isConnecting: boolean;
isSimulationMode: boolean;
connectionError: string | null;
robotStatus: RobotStatus;
activeActions: RobotActionExecution[];
@@ -48,6 +50,7 @@ export interface UseWizardRosReturn {
args?: Record<string, unknown>,
) => Promise<any>;
setAutonomousLife: (enabled: boolean) => Promise<boolean>;
setSimulationMode: (enabled: boolean) => void;
}
export function useWizardRos(
@@ -55,6 +58,7 @@ export function useWizardRos(
): UseWizardRosReturn {
const {
autoConnect = true,
simulationMode = false,
onConnected,
onDisconnected,
onError,
@@ -101,14 +105,17 @@ export function useWizardRos(
// Initialize service (only once)
useEffect(() => {
if (!isInitializedRef.current) {
serviceRef.current = getWizardRosService();
serviceRef.current = getWizardRosService(simulationMode);
if (simulationMode) {
serviceRef.current.setSimulationMode(true);
}
isInitializedRef.current = true;
}
return () => {
mountedRef.current = false;
};
}, []);
}, [simulationMode]);
// Set up event listeners with stable callbacks
useEffect(() => {
@@ -381,9 +388,19 @@ export function useWizardRos(
[isConnected],
);
const setSimulationMode = useCallback((enabled: boolean) => {
const service = serviceRef.current;
if (service) {
service.setSimulationMode(enabled);
}
}, []);
const isSimulationMode = serviceRef.current?.isSimulationMode() ?? simulationMode;
return {
isConnected,
isConnecting,
isSimulationMode,
connectionError,
robotStatus,
activeActions,
@@ -392,5 +409,6 @@ export function useWizardRos(
executeRobotAction,
callService,
setAutonomousLife,
setSimulationMode,
};
}
+260 -5
View File
@@ -60,6 +60,10 @@ export class WizardRosService extends EventEmitter {
private maxReconnectAttempts = 5;
private isConnecting = false;
// Simulation mode
private simulationMode: boolean;
private simulationInterval: NodeJS.Timeout | null = null;
// Robot state
private robotStatus: RobotStatus = {
connected: false,
@@ -73,15 +77,40 @@ export class WizardRosService extends EventEmitter {
// Active action tracking
private activeActions: Map<string, RobotActionExecution> = new Map();
constructor(url: string = "ws://localhost:9090") {
constructor(url: string = "ws://localhost:9090", simulationMode: boolean = false) {
super();
this.url = url;
this.simulationMode = simulationMode ||
(typeof window !== "undefined" && process.env.NEXT_PUBLIC_SIMULATION_MODE === "true");
}
/**
* Check if running in simulation mode
*/
isSimulationMode(): boolean {
return this.simulationMode;
}
/**
* Enable or disable simulation mode
*/
setSimulationMode(enabled: boolean): void {
this.simulationMode = enabled;
if (!enabled && this.simulationInterval) {
clearInterval(this.simulationInterval);
this.simulationInterval = null;
}
}
/**
* Connect to ROS bridge WebSocket
*/
async connect(): Promise<void> {
// Simulation mode - fake connection
if (this.simulationMode) {
return this.connectSimulation();
}
return new Promise((resolve, reject) => {
if (
this.isConnected ||
@@ -167,6 +196,11 @@ export class WizardRosService extends EventEmitter {
disconnect(): void {
this.clearReconnectTimer();
if (this.simulationMode) {
this.disconnectSimulation();
return;
}
if (this.ws) {
this.ws.close(1000, "Manual disconnect");
this.ws = null;
@@ -178,10 +212,173 @@ export class WizardRosService extends EventEmitter {
this.emit("disconnected");
}
/**
* Simulation mode connection - simulates robot responses
*/
private async connectSimulation(): Promise<void> {
console.log(`[WizardROS] SIMULATION MODE - Connecting to mock robot`);
this.isConnected = true;
this.isConnecting = false;
this.connectionAttempts = 0;
// Initialize mock robot state
const mockStates = this.getMockJointStates();
this.robotStatus = {
connected: true,
battery: 85,
position: { x: 0, y: 0, theta: 0 },
joints: mockStates.names.reduce((acc, name, i) => {
acc[name] = mockStates.positions[i] ?? 0;
return acc;
}, {} as Record<string, number>),
sensors: {},
lastUpdate: new Date(),
};
// Start publishing simulated sensor data
this.simulationInterval = setInterval(() => {
this.publishSimulationData();
}, 100);
this.emit("connected");
console.log(`[WizardROS] SIMULATION MODE - Connected to mock robot`);
}
/**
* Simulation mode disconnection
*/
private disconnectSimulation(): void {
console.log(`[WizardROS] SIMULATION MODE - Disconnecting`);
if (this.simulationInterval) {
clearInterval(this.simulationInterval);
this.simulationInterval = null;
}
this.isConnected = false;
this.robotStatus.connected = false;
this.emit("disconnected");
}
/**
* Publish simulated sensor data
*/
private publishSimulationData(): void {
if (!this.simulationMode || !this.isConnected) return;
const mockData = this.getMockJointStates();
this.updateJointStates(mockData);
this.robotStatus.battery = 85 + Math.random() * 2 - 1; // Slight variation
this.robotStatus.sensors = {
"/bumper": { left: false, right: false },
"/hand_touch": { leftHand: false, rightHand: false },
"/head_touch": { front: false, middle: false, rear: false },
"/sonar/left": { range: 0.5 + Math.random() * 0.5 },
"/sonar/right": { range: 0.5 + Math.random() * 0.5 },
};
this.robotStatus.lastUpdate = new Date();
this.emit("robot_status_updated", this.robotStatus);
}
/**
* Get mock joint states for simulation
*/
private getMockJointStates(): { names: string[]; positions: number[] } {
const names = [
"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",
];
const positions = names.map(() => (Math.random() - 0.5) * 0.1);
return { names, positions };
}
/**
* Execute action in simulation mode
*/
private async executeSimulationAction(
pluginName: string,
actionId: string,
parameters: Record<string, unknown>,
actionConfig?: {
topic: string;
messageType: string;
payloadMapping: {
type: string;
payload?: Record<string, unknown>;
transformFn?: string;
};
},
): Promise<RobotActionExecution> {
const executionId = `${pluginName}_${actionId}_${Date.now()}`;
const execution: RobotActionExecution = {
id: executionId,
actionId,
pluginName,
parameters,
status: "pending",
startTime: new Date(),
};
this.activeActions.set(executionId, execution);
this.emit("action_started", execution);
try {
execution.status = "executing";
this.activeActions.set(executionId, execution);
console.log(`[WizardROS] SIMULATION MODE - Executing ${actionId}:`, parameters);
// Simulate action execution based on action type
let duration = 500;
if (actionId === "say_text" || actionId === "say_with_emotion" || actionConfig?.topic === "/speech") {
const text = String(parameters.text || parameters.data || "Hello");
const wordCount = text.split(/\s+/).filter(Boolean).length;
duration = 1500 + Math.max(1000, wordCount * 300);
} else if (actionId.includes("walk") || actionId.includes("turn") || actionConfig?.topic === "/cmd_vel") {
duration = 500;
// Simulate position change
const speed = Number(parameters.speed) || 0.1;
if (actionId === "walk_forward") {
this.robotStatus.position.x += speed * 0.5;
} else if (actionId === "walk_backward") {
this.robotStatus.position.x -= speed * 0.5;
} else if (actionId === "turn_left") {
this.robotStatus.position.theta -= 0.5;
} else if (actionId === "turn_right") {
this.robotStatus.position.theta += 0.5;
}
} else if (actionId.includes("head") || actionId.includes("move") || actionConfig?.topic === "/joint_angles") {
duration = 1000;
}
// Simulate async execution
await new Promise((resolve) => setTimeout(resolve, duration));
execution.status = "completed";
execution.endTime = new Date();
this.emit("action_completed", execution);
} catch (error) {
execution.status = "failed";
execution.error = error instanceof Error ? error.message : String(error);
execution.endTime = new Date();
this.emit("action_failed", execution);
}
this.activeActions.set(executionId, execution);
return execution;
}
/**
* Check if connected to ROS bridge
*/
getConnectionStatus(): boolean {
if (this.simulationMode) {
return this.isConnected;
}
return this.isConnected && this.ws?.readyState === WebSocket.OPEN;
}
@@ -213,6 +410,11 @@ export class WizardRosService extends EventEmitter {
throw new Error("Not connected to ROS bridge");
}
// Simulation mode - simulate action execution
if (this.simulationMode) {
return this.executeSimulationAction(pluginName, actionId, parameters, actionConfig);
}
const executionId = `${pluginName}_${actionId}_${Date.now()}`;
const execution: RobotActionExecution = {
id: executionId,
@@ -587,6 +789,42 @@ export class WizardRosService extends EventEmitter {
throw new Error("Not connected to ROS bridge");
}
// Simulation mode - return mock responses
if (this.simulationMode) {
console.log(`[WizardROS] SIMULATION MODE - Service call: ${service}`, args);
const mockResponses: Record<string, ServiceResponse> = {
"/naoqi_driver/get_robot_info": {
result: true,
values: {
robotName: "MOCK-NAO6",
robotVersion: "6.0",
bodyType: "nao",
},
},
"/naoqi_driver/get_joint_names": {
result: true,
values: {
joint_names: [
"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",
],
},
},
"/naoqi_driver/get_position": {
result: true,
values: this.robotStatus.position,
},
};
return mockResponses[service] || { result: true };
}
const id = `call_${this.messageId++}`;
return new Promise((resolve, reject) => {
@@ -892,7 +1130,7 @@ let isCreatingInstance = false;
/**
* Get or create the global wizard ROS service (true singleton)
*/
export function getWizardRosService(): WizardRosService {
export function getWizardRosService(simulationMode?: boolean): WizardRosService {
// Prevent multiple instances during creation
if (isCreatingInstance && !wizardRosService) {
throw new Error("WizardRosService is being initialized, please wait");
@@ -901,7 +1139,10 @@ export function getWizardRosService(): WizardRosService {
if (!wizardRosService) {
isCreatingInstance = true;
try {
wizardRosService = new WizardRosService();
const url = typeof window !== "undefined"
? (process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090")
: "ws://localhost:9090";
wizardRosService = new WizardRosService(url, simulationMode);
} finally {
isCreatingInstance = false;
}
@@ -912,8 +1153,12 @@ export function getWizardRosService(): WizardRosService {
/**
* Initialize wizard ROS service with connection
*/
export async function initWizardRosService(): Promise<WizardRosService> {
const service = getWizardRosService();
export async function initWizardRosService(simulationMode?: boolean): Promise<WizardRosService> {
const service = getWizardRosService(simulationMode);
if (simulationMode !== undefined) {
service.setSimulationMode(simulationMode);
}
if (!service.getConnectionStatus()) {
await service.connect();
@@ -921,3 +1166,13 @@ export async function initWizardRosService(): Promise<WizardRosService> {
return service;
}
/**
* Reset the global wizard ROS service (useful for testing or reinitializing)
*/
export function resetWizardRosService(): void {
if (wizardRosService) {
wizardRosService.disconnect();
wizardRosService = null;
}
}