feat(analytics): refine timeline visualization and add print support

This commit is contained in:
2026-02-17 21:17:11 -05:00
parent 568d408587
commit 72971a4b49
82 changed files with 6670 additions and 2448 deletions

View File

@@ -0,0 +1,46 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../../src/server/db/schema";
import { eq } from "drizzle-orm";
const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString);
const db = drizzle(connection, { schema });
async function main() {
console.log("🔍 Checking seeded actions...");
const actions = await db.query.actions.findMany({
where: (actions, { or, eq, like }) => or(
eq(actions.type, "sequence"),
eq(actions.type, "parallel"),
eq(actions.type, "loop"),
eq(actions.type, "branch"),
like(actions.type, "hristudio-core%")
),
limit: 10
});
console.log(`Found ${actions.length} control actions.`);
for (const action of actions) {
console.log(`\nAction: ${action.name} (${action.type})`);
console.log(`ID: ${action.id}`);
// Explicitly log parameters to check structure
console.log("Parameters:", JSON.stringify(action.parameters, null, 2));
const params = action.parameters as any;
if (params.children) {
console.log(`✅ Has ${params.children.length} children in parameters.`);
} else if (params.trueBranch || params.falseBranch) {
console.log(`✅ Has branches in parameters.`);
} else {
console.log(`❌ No children/branches found in parameters.`);
}
}
await connection.end();
}
main();

View File

@@ -0,0 +1,65 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../../src/server/db/schema";
import { eq } from "drizzle-orm";
const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString);
const db = drizzle(connection, { schema });
async function main() {
console.log("🔍 Checking Database State...");
// 1. Check Plugin
const plugins = await db.query.plugins.findMany();
console.log(`\nFound ${plugins.length} plugins.`);
const expectedKeys = new Set<string>();
for (const p of plugins) {
const meta = p.metadata as any;
const defs = p.actionDefinitions as any[];
console.log(`Plugin [${p.name}] (ID: ${p.id}):`);
console.log(` - Robot ID (Column): ${p.robotId}`);
console.log(` - Metadata.robotId: ${meta?.robotId}`);
console.log(` - Action Definitions: ${defs?.length ?? 0} found.`);
if (defs && meta?.robotId) {
defs.forEach(d => {
const key = `${meta.robotId}.${d.id}`;
expectedKeys.add(key);
// console.log(` -> Registers: ${key}`);
});
}
}
// 2. Check Actions
const actions = await db.query.actions.findMany();
console.log(`\nFound ${actions.length} actions.`);
let errorCount = 0;
for (const a of actions) {
// Only check plugin actions
if (a.sourceKind === 'plugin' || a.type.includes(".")) {
const isRegistered = expectedKeys.has(a.type);
const pluginIdMatch = a.pluginId === 'nao6-ros2';
console.log(`Action [${a.name}] (Type: ${a.type}):`);
console.log(` - PluginId: ${a.pluginId} ${pluginIdMatch ? '✅' : '❌'}`);
console.log(` - In Registry: ${isRegistered ? '✅' : '❌'}`);
if (!isRegistered || !pluginIdMatch) errorCount++;
}
}
if (errorCount > 0) {
console.log(`\n❌ Found ${errorCount} actions with issues.`);
} else {
console.log("\n✅ All plugin actions validated successfully against registry definitions.");
}
await connection.end();
}
main().catch(console.error);

View File

@@ -0,0 +1,58 @@
import { db } from "~/server/db";
import { steps, experiments, actions } from "~/server/db/schema";
import { eq, asc } from "drizzle-orm";
async function debugExperimentStructure() {
console.log("Debugging Experiment Structure for Interactive Storyteller...");
// Find the experiment
const experiment = await db.query.experiments.findFirst({
where: eq(experiments.name, "The Interactive Storyteller"),
with: {
steps: {
orderBy: [asc(steps.orderIndex)],
with: {
actions: {
orderBy: [asc(actions.orderIndex)],
}
}
}
}
});
if (!experiment) {
console.error("Experiment not found!");
return;
}
console.log(`Experiment: ${experiment.name} (${experiment.id})`);
console.log(`Plugin Dependencies:`, experiment.pluginDependencies);
console.log("---------------------------------------------------");
experiment.steps.forEach((step, index) => {
console.log(`Step ${index + 1}: ${step.name}`);
console.log(` ID: ${step.id}`);
console.log(` Type: ${step.type}`);
console.log(` Order: ${step.orderIndex}`);
console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2));
if (step.actions && step.actions.length > 0) {
console.log(` Actions (${step.actions.length}):`);
step.actions.forEach((action, actionIndex) => {
console.log(` ${actionIndex + 1}. [${action.type}] ${action.name}`);
if (action.type === 'wizard_wait_for_response') {
console.log(` Options:`, JSON.stringify((action.parameters as any)?.options, null, 2));
}
});
}
console.log("---------------------------------------------------");
});
}
debugExperimentStructure()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,42 @@
import { db } from "../../src/server/db";
import { experiments, steps } from "../../src/server/db/schema";
import { eq } from "drizzle-orm";
async function inspectAllSteps() {
const result = await db.query.experiments.findMany({
with: {
steps: {
orderBy: (steps, { asc }) => [asc(steps.orderIndex)],
columns: {
id: true,
name: true,
type: true,
orderIndex: true,
conditions: true,
}
}
}
});
console.log(`Found ${result.length} experiments.`);
for (const exp of result) {
console.log(`Experiment: ${exp.name} (${exp.id})`);
for (const step of exp.steps) {
// Only print conditional steps or the first step
if (step.type === 'conditional' || step.orderIndex === 0) {
console.log(` [${step.orderIndex}] ${step.name} (${step.type})`);
console.log(` Conditions: ${JSON.stringify(step.conditions)}`);
}
}
console.log('---');
}
}
inspectAllSteps()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,47 @@
import { db } from "~/server/db";
import { actions, steps } from "~/server/db/schema";
import { eq } from "drizzle-orm";
async function inspectAction() {
console.log("Inspecting Action 10851aef-e720-45fc-ba5e-05e1e3425dab...");
const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab";
const action = await db.query.actions.findFirst({
where: eq(actions.id, actionId),
with: {
step: {
columns: {
id: true,
name: true,
type: true,
conditions: true
}
}
}
});
if (!action) {
console.error("Action not found!");
return;
}
console.log("Action Found:");
console.log(" Name:", action.name);
console.log(" Type:", action.type);
console.log(" Parameters:", JSON.stringify(action.parameters, null, 2));
console.log("Parent Step:");
console.log(" ID:", action.step.id);
console.log(" Name:", action.step.name);
console.log(" Type:", action.step.type);
console.log(" Conditions:", JSON.stringify(action.step.conditions, null, 2));
}
inspectAction()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
import { db } from "~/server/db";
import { steps } from "~/server/db/schema";
import { eq, inArray } from "drizzle-orm";
async function inspectBranchSteps() {
console.log("Inspecting Steps 4 (Branch A) and 5 (Branch B)...");
const step4Id = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5";
const step5Id = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30";
const branchSteps = await db.query.steps.findMany({
where: inArray(steps.id, [step4Id, step5Id])
});
branchSteps.forEach(step => {
console.log(`Step: ${step.name} (${step.id})`);
console.log(` Type: ${step.type}`);
console.log(` Order: ${step.orderIndex}`);
console.log(` Conditions:`, JSON.stringify(step.conditions, null, 2));
console.log("---------------------------------------------------");
});
}
inspectBranchSteps()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,24 @@
import { db } from "../../src/server/db";
import { steps } from "../../src/server/db/schema";
import { eq, like } from "drizzle-orm";
async function checkSteps() {
const allSteps = await db.select().from(steps).where(like(steps.name, "%Comprehension Check%"));
console.log("Found steps:", allSteps.length);
for (const step of allSteps) {
console.log("Step Name:", step.name);
console.log("Type:", step.type);
console.log("Conditions (typeof):", typeof step.conditions);
console.log("Conditions (value):", JSON.stringify(step.conditions, null, 2));
}
}
checkSteps()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,61 @@
import { db } from "~/server/db";
import { steps, experiments } from "~/server/db/schema";
import { eq, asc } from "drizzle-orm";
async function inspectExperimentSteps() {
// Find experiment by ID
const experiment = await db.query.experiments.findFirst({
where: eq(experiments.id, "961d0cb1-256d-4951-8387-6d855a0ae603")
});
if (!experiment) {
console.log("Experiment not found!");
return;
}
console.log(`Inspecting Experiment: ${experiment.name} (${experiment.id})`);
const experimentSteps = await db.query.steps.findMany({
where: eq(steps.experimentId, experiment.id),
orderBy: [asc(steps.orderIndex)],
with: {
actions: {
orderBy: (actions, { asc }) => [asc(actions.orderIndex)]
}
}
});
console.log(`Found ${experimentSteps.length} steps.`);
for (const step of experimentSteps) {
console.log("--------------------------------------------------");
console.log(`Step [${step.orderIndex}] ID: ${step.id}`);
console.log(`Name: ${step.name}`);
console.log(`Type: ${step.type}`);
if (step.type === 'conditional') {
console.log("Conditions:", JSON.stringify(step.conditions, null, 2));
}
if (step.actions.length > 0) {
console.log("Actions:");
for (const action of step.actions) {
console.log(` - [${action.orderIndex}] ${action.name} (${action.type})`);
if (action.type === 'wizard_wait_for_response') {
console.log(" Parameters:", JSON.stringify(action.parameters, null, 2));
}
}
}
}
}
inspectExperimentSteps()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,33 @@
import { db } from "../../src/server/db";
import { experiments } from "../../src/server/db/schema";
import { eq } from "drizzle-orm";
async function inspectVisualDesign() {
const exps = await db.select().from(experiments);
for (const exp of exps) {
console.log(`Experiment: ${exp.name}`);
if (exp.visualDesign) {
const vd = exp.visualDesign as any;
console.log("Visual Design Steps:");
if (vd.steps && Array.isArray(vd.steps)) {
vd.steps.forEach((s: any, i: number) => {
console.log(` [${i}] ${s.name} (${s.type})`);
console.log(` Trigger: ${JSON.stringify(s.trigger)}`);
});
} else {
console.log(" No steps in visualDesign or invalid format.");
}
} else {
console.log(" No visualDesign blob.");
}
}
}
inspectVisualDesign()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,69 @@
import { db } from "~/server/db";
import { actions, steps } from "~/server/db/schema";
import { eq, sql } from "drizzle-orm";
async function patchActionParams() {
console.log("Patching Action Parameters for Interactive Storyteller...");
// Target Step IDs
const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check
const actionId = "10851aef-e720-45fc-ba5e-05e1e3425dab"; // Action: Wait for Choice
// 1. Get the authoritative conditions from the Step
const step = await db.query.steps.findFirst({
where: eq(steps.id, step3CondId)
});
if (!step) {
console.error("Step 3 not found!");
return;
}
const conditions = step.conditions as any;
const richOptions = conditions?.options;
if (!richOptions || !Array.isArray(richOptions)) {
console.error("Step 3 conditions are missing valid options!");
return;
}
console.log("Found rich options in Step:", JSON.stringify(richOptions, null, 2));
// 2. Get the Action
const action = await db.query.actions.findFirst({
where: eq(actions.id, actionId)
});
if (!action) {
console.error("Action not found!");
return;
}
console.log("Current Action Parameters:", JSON.stringify(action.parameters, null, 2));
// 3. Patch the Action Parameters
// We replace the simple string options with the rich object options
const currentParams = action.parameters as any;
const newParams = {
...currentParams,
options: richOptions // Overwrite with rich options from step
};
console.log("New Action Parameters:", JSON.stringify(newParams, null, 2));
await db.execute(sql`
UPDATE hs_action
SET parameters = ${JSON.stringify(newParams)}::jsonb
WHERE id = ${actionId}
`);
console.log("Action parameters successfully patched.");
}
patchActionParams()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,92 @@
import { db } from "~/server/db";
import { steps } from "~/server/db/schema";
import { eq, sql } from "drizzle-orm";
async function patchBranchSteps() {
console.log("Patching branch steps for Interactive Storyteller...");
// Target Step IDs (From debug output)
const step3CondId = "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85"; // Step 3: Comprehension Check
const stepBranchAId = "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5"; // Step 4: Branch A (Correct)
const stepBranchBId = "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30"; // Step 5: Branch B (Incorrect)
const stepConclusionId = "cc3fbc7f-29e5-45e0-8d46-e80813c54292"; // Step 6: Conclusion
// Update Step 3 (The Conditional Step)
console.log("Updating Step 3 (Conditional Step)...");
const step3Conditional = await db.query.steps.findFirst({
where: eq(steps.id, step3CondId)
});
if (step3Conditional) {
const currentConditions = (step3Conditional.conditions as any) || {};
const options = currentConditions.options || [];
// Patch options to point to real step IDs
const newOptions = options.map((opt: any) => {
if (opt.value === "Correct") return { ...opt, nextStepId: stepBranchAId };
if (opt.value === "Incorrect") return { ...opt, nextStepId: stepBranchBId };
return opt;
});
const newConditions = { ...currentConditions, options: newOptions };
await db.execute(sql`
UPDATE hs_step
SET conditions = ${JSON.stringify(newConditions)}::jsonb
WHERE id = ${step3CondId}
`);
console.log("Step 3 (Conditional) updated links.");
} else {
console.log("Step 3 (Conditional) not found.");
}
// Update Step 4 (Branch A)
console.log("Updating Step 4 (Branch A)...");
/*
Note: We already patched Step 4 in previous run but under wrong assumption?
Let's re-patch to be safe.
Debug output showed ID: 3a2dc0b7-a43e-4236-9b9e-f957abafc1e5
It should jump to Conclusion (cc3fbc7f...)
*/
const stepBranchA = await db.query.steps.findFirst({
where: eq(steps.id, stepBranchAId)
});
if (stepBranchA) {
const currentConditions = (stepBranchA.conditions as Record<string, unknown>) || {};
const newConditions = { ...currentConditions, nextStepId: stepConclusionId };
await db.execute(sql`
UPDATE hs_step
SET conditions = ${JSON.stringify(newConditions)}::jsonb
WHERE id = ${stepBranchAId}
`);
console.log("Step 4 (Branch A) updated jump target.");
}
// Update Step 5 (Branch B)
console.log("Updating Step 5 (Branch B)...");
const stepBranchB = await db.query.steps.findFirst({
where: eq(steps.id, stepBranchBId)
});
if (stepBranchB) {
const currentConditions = (stepBranchB.conditions as Record<string, unknown>) || {};
const newConditions = { ...currentConditions, nextStepId: stepConclusionId };
await db.execute(sql`
UPDATE hs_step
SET conditions = ${JSON.stringify(newConditions)}::jsonb
WHERE id = ${stepBranchBId}
`);
console.log("Step 5 (Branch B) updated jump target.");
}
}
patchBranchSteps()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,76 @@
import { convertDatabaseToSteps } from "../../src/lib/experiment-designer/block-converter";
import { type ExperimentStep } from "../../src/lib/experiment-designer/types";
// Mock DB Steps (simulating what experimentsRouter returns before conversion)
const mockDbSteps = [
{
id: "step-1",
name: "Step 1",
type: "wizard",
orderIndex: 0,
actions: [
{
id: "seq-1",
name: "Test Sequence",
type: "sequence",
parameters: {
children: [
{ id: "child-1", name: "Child 1", type: "wait", parameters: { duration: 1 } },
{ id: "child-2", name: "Child 2", type: "wait", parameters: { duration: 2 } }
]
}
}
]
}
];
// Mock Store Logic (simulating store.ts)
function cloneActions(actions: any[]): any[] {
return actions.map((a) => ({
...a,
children: a.children ? cloneActions(a.children) : undefined,
}));
}
function cloneSteps(steps: any[]): any[] {
return steps.map((s) => ({
...s,
actions: cloneActions(s.actions),
}));
}
console.log("🔹 Testing Hydration & Cloning...");
// 1. Convert DB -> Runtime
const runtimeSteps = convertDatabaseToSteps(mockDbSteps);
const seq = runtimeSteps[0]?.actions[0];
if (!seq) {
console.error("❌ Conversion Failed: Sequence action not found.");
process.exit(1);
}
console.log(`Runtime Children Count: ${seq.children?.length ?? "undefined"}`);
if (!seq.children || seq.children.length === 0) {
console.error("❌ Conversion Failed: Children not hydrated from parameters.");
process.exit(1);
}
// 2. Store Cloning
const clonedSteps = cloneSteps(runtimeSteps);
const clonedSeq = clonedSteps[0]?.actions[0];
if (!clonedSeq) {
console.error("❌ Cloning Failed: Sequence action lost.");
process.exit(1);
}
console.log(`Cloned Children Count: ${clonedSeq.children?.length ?? "undefined"}`);
if (clonedSeq.children?.length === 2) {
console.log("✅ SUCCESS: Data hydrated and cloned correctly.");
} else {
console.error("❌ CLONING FAILED: Children lost during clone.");
}

View File

@@ -0,0 +1,121 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../../src/server/db/schema";
import { sql } from "drizzle-orm";
// Database connection
const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString);
const db = drizzle(connection, { schema });
async function main() {
console.log("🌱 Seeding 'Control Flow Demo' 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. Run seed-dev.ts first.");
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. Run seed-dev.ts first.");
// Find Robot
const robot = await db.query.robots.findFirst({
where: (robots, { eq }) => eq(robots.name, "NAO6")
});
if (!robot) throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first.");
// 2. Create Experiment
const [experiment] = await db.insert(schema.experiments).values({
studyId: study.id,
name: "Control Flow Demo",
description: "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.",
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
// Step 1: Sequence & Parallel
const [step1] = await db.insert(schema.steps).values({
experimentId: experiment.id,
name: "Complex Action Structures",
description: "Demonstrating Sequence and Parallel groups",
type: "robot",
orderIndex: 0,
required: true,
durationEstimate: 30
}).returning();
// Step 2: Loops & Waits
const [step2] = await db.insert(schema.steps).values({
experimentId: experiment.id,
name: "Repetition & Delays",
description: "Demonstrating Loop and Wait actions",
type: "robot",
orderIndex: 1,
required: true,
durationEstimate: 45
}).returning();
// 4. Create Actions
// --- Step 1 Actions ---
// Top-level Sequence
const seqId = `seq-${Date.now()}`;
await db.insert(schema.actions).values({
stepId: step1!.id,
name: "Introduction Sequence",
type: "sequence", // New type
orderIndex: 0,
parameters: {},
pluginId: "hristudio-core",
category: "control",
// No explicit children column in schema?
// Wait, schema.actions has "children" as jsonb or it's a recursive relationship?
// Let's check schema/types.
// Looking at ActionChip, it expects `action.children`.
// In DB, it's likely stored in `children` jsonb column if it exists, OR we need to perform recursive inserts if schema supports parentId.
// Checking `types.ts` or schema...
// Assuming flat list references for now or JSONB.
// Wait, `ExperimentAction` in types has `children?: ExperimentAction[]`.
// If the DB schema `actions` table handles nesting via `parameters` or specific column, I need to know.
// Defaulting to "children" property in JSON parameter if DB doesn't have parentId.
// Checking `schema.ts`: "children" is likely NOT a column if I haven't seen it in seed-dev.
// However, `ActionChip` uses `action.children`. Steps map to `actions`.
// If `actions` table has `parentId` or `children` JSONB.
// I will assume `children` is part of the `parameters` or a simplified representation for now,
// BUT `FlowWorkspace` treats `action.children` as real actions.
// Let's check `schema.ts` quickly.
});
// I need to check schema.actions definition effectively.
// For this pass, I will insert them as flat actions since I can't confirm nesting storage without checking schema.
// But the user WANTS to see the nesting (Sequence, Parallel).
// The `SortableActionChip` renders `action.children`.
// The `TrialExecutionEngine` executes `action.children`.
// So the data MUST include children.
// Most likely `actions` table has a `children` JSONB column.
// I will insert a Parallel action with embedded children in the `children` column (if it exists) or `parameters`.
// Re-reading `scripts/seed-dev.ts`: It doesn't show any nested actions.
// I will read `src/server/db/schema.ts` to be sure.
} catch (err) {
console.error(err);
process.exit(1);
}
}
// I'll write the file AFTER checking schema to ensure I structure the nested actions correctly.

View File

@@ -0,0 +1,241 @@
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 'Control Flow Demo' 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. Run seed-dev.ts first.");
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. Run seed-dev.ts first.");
// Find Robot
const robot = await db.query.robots.findFirst({
where: (robots, { eq }) => eq(robots.name, "NAO6")
});
if (!robot) throw new Error("Robot 'NAO6' not found. Run seed-dev.ts first.");
// 2. Create Experiment
const [experiment] = await db.insert(schema.experiments).values({
studyId: study.id,
name: "Control Flow Demo",
description: "Demonstration of enhanced control flow actions: Sequence, Parallel, Wait, Loop, Branch.",
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
// Step 1: Sequence & Parallel
const [step1] = await db.insert(schema.steps).values({
experimentId: experiment.id,
name: "Complex Action Structures",
description: "Demonstrating Sequence and Parallel groups",
type: "robot",
orderIndex: 0,
required: true,
durationEstimate: 30
}).returning();
if (!step1) throw new Error("Failed to create step1");
// Step 2: Loops & Waits
const [step2] = await db.insert(schema.steps).values({
experimentId: experiment.id,
name: "Repetition & Delays",
description: "Demonstrating Loop and Wait actions",
type: "robot",
orderIndex: 1,
required: true,
durationEstimate: 45
}).returning();
if (!step2) throw new Error("Failed to create step2");
// 4. Create Actions
// --- Step 1 Actions ---
// Action 1: Sequence
// Note: Nested children are stored in 'children' property of the action object in frontend,
// but in DB 'parameters' is the JSONB field.
// However, looking at ActionChip, it expects `action.children`.
// The `ExperimentAction` type usually has `children` at top level.
// If the DB doesn't have it, the API must be hydrating it.
// BUT, for the purpose of this seed which writes to DB directly, I will put it in `parameters.children`
// and assume the frontend/API handles it or I'm missing a column.
// Actually, looking at schema again, `actions` table DOES NOT have children.
// So it MUST be in `parameters` or it's not persisted in this table structure yet (which would be a bug, but I'm seeding what exists).
// Wait, if I put it in parameters, does the UI read it?
// `ActionChip` reads `action.children`.
// I will try to put it in `parameters` and distinct `children` property in the JSON passed to `parameters`?
// No, `parameters` is jsonb.
// I will assume for now that the system expects it in parameters if it's not a column, OR it's not fully supported in DB yet.
// I will stick to what the UI likely consumes. `parameters: { children: [...] }`
// Sequence
await db.insert(schema.actions).values({
stepId: step1.id,
name: "Introduction Sequence",
type: "sequence",
orderIndex: 0,
// Embedding children here to demonstrate.
// Real implementation might vary if keys are strictly checked.
parameters: {
children: [
{
id: uuidv4(),
name: "Say Hello",
type: "nao6-ros2.say_text",
parameters: { text: "Hello there!" },
category: "interaction"
},
{
id: uuidv4(),
name: "Wave Hand",
type: "nao6-ros2.move_arm",
parameters: { arm: "right", action: "wave" },
category: "movement"
}
]
},
pluginId: "hristudio-core",
category: "control",
sourceKind: "core"
});
// Parallel
await db.insert(schema.actions).values({
stepId: step1.id,
name: "Parallel Actions",
type: "parallel",
orderIndex: 1,
parameters: {
children: [
{
id: uuidv4(),
name: "Say 'Moving'",
type: "nao6-ros2.say_text",
parameters: { text: "I am moving and talking." },
category: "interaction"
},
{
id: uuidv4(),
name: "Walk Forward",
type: "nao6-ros2.move_to",
parameters: { x: 0.5, y: 0 },
category: "movement"
}
]
},
pluginId: "hristudio-core",
category: "control",
sourceKind: "core"
});
// --- Step 2 Actions ---
// Loop
await db.insert(schema.actions).values({
stepId: step2.id,
name: "Repeat Message",
type: "loop",
orderIndex: 0,
parameters: {
iterations: 3,
children: [
{
id: uuidv4(),
name: "Say 'Echo'",
type: "nao6-ros2.say_text",
parameters: { text: "Echo" },
category: "interaction"
}
]
},
pluginId: "hristudio-core",
category: "control",
sourceKind: "core"
});
// Wait
await db.insert(schema.actions).values({
stepId: step2.id,
name: "Wait 5 Seconds",
type: "wait",
orderIndex: 1,
parameters: { duration: 5 },
pluginId: "hristudio-core",
category: "control",
sourceKind: "core"
});
// Branch (Controls step routing, not nested actions)
// Note: Branch configuration is stored in step.trigger.conditions, not action.parameters
// The branch action itself is just a marker that this step has conditional routing
await db.insert(schema.actions).values({
stepId: step2.id,
name: "Conditional Routing",
type: "branch",
orderIndex: 2,
parameters: {
// Branch actions don't have nested children
// Routing is configured at the step level via trigger.conditions
},
pluginId: "hristudio-core",
category: "control",
sourceKind: "core"
});
// Update step2 to have conditional routing
await db.update(schema.steps)
.set({
type: "conditional",
conditions: {
options: [
{
label: "High Score Path",
nextStepIndex: 2, // Would go to a hypothetical step 3
variant: "default"
},
{
label: "Low Score Path",
nextStepIndex: 0, // Loop back to step 1
variant: "outline"
}
]
}
})
.where(sql`id = ${step2.id}`);
} catch (err) {
console.error(err);
process.exit(1);
} finally {
await connection.end();
}
}
main();

View File

@@ -0,0 +1,703 @@
#!/usr/bin/env bun
/**
* Seed NAO6 Plugin into HRIStudio Database
*
* This script adds the NAO6 ROS2 integration plugin to the HRIStudio database,
* including the plugin repository and plugin definition with all available actions.
*/
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { env } from "~/env";
import {
plugins,
pluginRepositories,
robots,
users,
type InsertPlugin,
type InsertPluginRepository,
type InsertRobot,
} from "~/server/db/schema";
import { eq } from "drizzle-orm";
const connectionString = env.DATABASE_URL;
const client = postgres(connectionString);
const db = drizzle(client);
async function seedNAO6Plugin() {
console.log("🤖 Seeding NAO6 plugin into HRIStudio database...");
try {
// 0. Get system user for created_by fields
console.log("👤 Getting system user...");
const systemUser = await db
.select()
.from(users)
.where(eq(users.email, "sean@soconnor.dev"))
.limit(1);
if (systemUser.length === 0) {
throw new Error(
"System user not found. Please run 'bun db:seed' first to create initial users.",
);
}
const userId = systemUser[0]!.id;
console.log(`✅ Found system user: ${userId}`);
// 1. Create or update NAO6 robot entry
console.log("📋 Creating NAO6 robot entry...");
const existingRobot = await db
.select()
.from(robots)
.where(eq(robots.name, "NAO6"))
.limit(1);
let robotId: string;
if (existingRobot.length > 0) {
robotId = existingRobot[0]!.id;
console.log(`✅ Found existing NAO6 robot: ${robotId}`);
} else {
const newRobots = await db
.insert(robots)
.values({
name: "NAO6",
manufacturer: "SoftBank Robotics",
model: "NAO V6.0",
description:
"Humanoid robot designed for education, research, and human-robot interaction studies. Features bipedal walking, speech synthesis, cameras, and comprehensive sensor suite.",
capabilities: [
"bipedal_walking",
"speech_synthesis",
"head_movement",
"arm_gestures",
"touch_sensors",
"visual_sensors",
"audio_sensors",
"posture_control",
"balance_control",
],
communicationProtocol: "ros2",
} satisfies InsertRobot)
.returning();
robotId = newRobots[0]!.id;
console.log(`✅ Created NAO6 robot: ${robotId}`);
}
// 2. Create or update plugin repository
console.log("📦 Creating NAO6 plugin repository...");
const existingRepo = await db
.select()
.from(pluginRepositories)
.where(eq(pluginRepositories.name, "NAO6 ROS2 Integration Repository"))
.limit(1);
let repoId: string;
if (existingRepo.length > 0) {
repoId = existingRepo[0]!.id;
console.log(`✅ Found existing repository: ${repoId}`);
} else {
const newRepos = await db
.insert(pluginRepositories)
.values({
name: "NAO6 ROS2 Integration Repository",
url: "https://github.com/hristudio/nao6-ros2-plugins",
description:
"Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments",
trustLevel: "official",
isEnabled: true,
isOfficial: true,
createdBy: userId,
lastSyncAt: new Date(),
metadata: {
author: {
name: "HRIStudio Team",
email: "support@hristudio.com",
},
license: "MIT",
ros2: {
distro: "humble",
packages: [
"naoqi_driver2",
"naoqi_bridge_msgs",
"rosbridge_suite",
],
},
supportedRobots: ["NAO6"],
categories: [
"movement",
"speech",
"sensors",
"interaction",
"vision",
],
},
} satisfies InsertPluginRepository)
.returning();
repoId = newRepos[0]!.id;
console.log(`✅ Created repository: ${repoId}`);
}
// 3. Create or update NAO6 plugin
console.log("🔌 Creating NAO6 enhanced plugin...");
const existingPlugin = await db
.select()
.from(plugins)
.where(eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)"))
.limit(1);
const actionDefinitions = [
{
id: "nao_speak",
name: "Speak Text",
description:
"Make the NAO robot speak the specified text using text-to-speech synthesis",
category: "speech",
icon: "volume2",
parametersSchema: {
type: "object",
properties: {
text: {
type: "string",
title: "Text to Speak",
description: "The text that the robot should speak aloud",
minLength: 1,
maxLength: 500,
},
volume: {
type: "number",
title: "Volume",
description: "Speech volume level (0.1 = quiet, 1.0 = loud)",
default: 0.7,
minimum: 0.1,
maximum: 1.0,
step: 0.1,
},
speed: {
type: "number",
title: "Speech Speed",
description: "Speech rate multiplier (0.5 = slow, 2.0 = fast)",
default: 1.0,
minimum: 0.5,
maximum: 2.0,
step: 0.1,
},
},
required: ["text"],
},
implementation: {
type: "ros2_topic",
topic: "/speech",
messageType: "std_msgs/String",
},
},
{
id: "nao_move",
name: "Move Robot",
description:
"Move the NAO robot with specified linear and angular velocities",
category: "movement",
icon: "move",
parametersSchema: {
type: "object",
properties: {
direction: {
type: "string",
title: "Movement Direction",
description: "Predefined movement direction",
enum: [
"forward",
"backward",
"left",
"right",
"turn_left",
"turn_right",
],
default: "forward",
},
distance: {
type: "number",
title: "Distance (meters)",
description: "Distance to move in meters",
default: 0.1,
minimum: 0.01,
maximum: 2.0,
step: 0.01,
},
speed: {
type: "number",
title: "Movement Speed",
description: "Speed factor (0.1 = very slow, 1.0 = normal speed)",
default: 0.5,
minimum: 0.1,
maximum: 1.0,
step: 0.1,
},
},
required: ["direction"],
},
implementation: {
type: "ros2_topic",
topic: "/cmd_vel",
messageType: "geometry_msgs/Twist",
},
},
{
id: "nao_pose",
name: "Set Posture",
description: "Set the NAO robot to a specific posture or pose",
category: "movement",
icon: "user",
parametersSchema: {
type: "object",
properties: {
posture: {
type: "string",
title: "Posture",
description: "Target posture for the robot",
enum: ["Stand", "Sit", "SitRelax", "StandInit", "Crouch"],
default: "Stand",
},
speed: {
type: "number",
title: "Movement Speed",
description:
"Speed of posture transition (0.1 = slow, 1.0 = fast)",
default: 0.5,
minimum: 0.1,
maximum: 1.0,
step: 0.1,
},
},
required: ["posture"],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/robot_posture/go_to_posture",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
{
id: "nao_head_movement",
name: "Move Head",
description:
"Control NAO robot head movement for gaze direction and attention",
category: "movement",
icon: "eye",
parametersSchema: {
type: "object",
properties: {
headYaw: {
type: "number",
title: "Head Yaw (degrees)",
description:
"Left/right head rotation (-90° = right, +90° = left)",
default: 0.0,
minimum: -90.0,
maximum: 90.0,
step: 1.0,
},
headPitch: {
type: "number",
title: "Head Pitch (degrees)",
description: "Up/down head rotation (-25° = down, +25° = up)",
default: 0.0,
minimum: -25.0,
maximum: 25.0,
step: 1.0,
},
speed: {
type: "number",
title: "Movement Speed",
description: "Speed of head movement (0.1 = slow, 1.0 = fast)",
default: 0.3,
minimum: 0.1,
maximum: 1.0,
step: 0.1,
},
},
required: [],
},
implementation: {
type: "ros2_topic",
topic: "/joint_angles",
messageType: "naoqi_bridge_msgs/JointAnglesWithSpeed",
},
},
{
id: "nao_gesture",
name: "Perform Gesture",
description:
"Make NAO robot perform predefined gestures and animations",
category: "interaction",
icon: "hand",
parametersSchema: {
type: "object",
properties: {
gesture: {
type: "string",
title: "Gesture Type",
description: "Select a predefined gesture or animation",
enum: [
"wave",
"point_left",
"point_right",
"applause",
"thumbs_up",
],
default: "wave",
},
intensity: {
type: "number",
title: "Gesture Intensity",
description:
"Intensity of the gesture movement (0.5 = subtle, 1.0 = full)",
default: 0.8,
minimum: 0.3,
maximum: 1.0,
step: 0.1,
},
},
required: ["gesture"],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/animation_player/run_animation",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
{
id: "nao_sensor_monitor",
name: "Monitor Sensors",
description: "Monitor NAO robot sensors for interaction detection",
category: "sensors",
icon: "activity",
parametersSchema: {
type: "object",
properties: {
sensorType: {
type: "string",
title: "Sensor Type",
description: "Which sensors to monitor",
enum: ["touch", "bumper", "sonar", "all"],
default: "touch",
},
duration: {
type: "number",
title: "Duration (seconds)",
description: "How long to monitor sensors",
default: 10,
minimum: 1,
maximum: 300,
},
},
required: ["sensorType"],
},
implementation: {
type: "ros2_subscription",
topics: [
"/naoqi_driver/bumper",
"/naoqi_driver/hand_touch",
"/naoqi_driver/head_touch",
],
},
},
{
id: "nao_emergency_stop",
name: "Emergency Stop",
description: "Immediately stop all robot movement for safety",
category: "safety",
icon: "stop-circle",
parametersSchema: {
type: "object",
properties: {
stopType: {
type: "string",
title: "Stop Type",
description: "Type of emergency stop",
enum: ["movement", "all"],
default: "all",
},
},
required: [],
},
implementation: {
type: "ros2_topic",
topic: "/cmd_vel",
messageType: "geometry_msgs/Twist",
},
},
{
id: "nao_wake_rest",
name: "Wake Up / Rest Robot",
description: "Wake up the robot or put it to rest position",
category: "system",
icon: "power",
parametersSchema: {
type: "object",
properties: {
action: {
type: "string",
title: "Action",
description: "Wake up robot or put to rest",
enum: ["wake", "rest"],
default: "wake",
},
},
required: ["action"],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/motion/wake_up",
serviceType: "std_srvs/srv/Empty",
},
},
{
id: "nao_status_check",
name: "Check Robot Status",
description: "Get current robot status including battery and health",
category: "system",
icon: "info",
parametersSchema: {
type: "object",
properties: {
statusType: {
type: "string",
title: "Status Type",
description: "What status information to retrieve",
enum: ["basic", "battery", "sensors", "all"],
default: "basic",
},
},
required: ["statusType"],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/get_robot_config",
serviceType: "naoqi_bridge_msgs/srv/GetRobotInfo",
},
},
{
id: "nao_nod",
name: "Nod Head",
description: "Make the robot nod its head (Yes)",
category: "interaction",
icon: "check-circle",
parametersSchema: {
type: "object",
properties: {},
required: [],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/animation_player/run_animation",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
{
id: "nao_shake_head",
name: "Shake Head",
description: "Make the robot shake its head (No)",
category: "interaction",
icon: "x-circle",
parametersSchema: {
type: "object",
properties: {},
required: [],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/animation_player/run_animation",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
{
id: "nao_bow",
name: "Bow",
description: "Make the robot bow",
category: "interaction",
icon: "user-check",
parametersSchema: {
type: "object",
properties: {},
required: [],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/animation_player/run_animation",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
{
id: "nao_open_hand",
name: "Present (Open Hand)",
description: "Make the robot gesture with an open hand",
category: "interaction",
icon: "hand",
parametersSchema: {
type: "object",
properties: {
hand: {
type: "string",
enum: ["left", "right", "both"],
default: "right",
},
},
required: ["hand"],
},
implementation: {
type: "ros2_service",
service: "/naoqi_driver/animation_player/run_animation",
serviceType: "naoqi_bridge_msgs/srv/SetString",
},
},
];
const pluginData: InsertPlugin = {
robotId: robotId,
name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0",
description:
"Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2. Provides full robot control including movement, speech synthesis, posture control, sensor monitoring, and safety features.",
author: "HRIStudio RoboLab Team",
repositoryUrl: "https://github.com/hristudio/nao6-ros2-plugins",
trustLevel: "official",
status: "active",
configurationSchema: {
type: "object",
properties: {
robotIp: {
type: "string",
default: "nao.local",
title: "Robot IP Address",
description: "IP address or hostname of the NAO6 robot",
},
robotPassword: {
type: "string",
default: "robolab",
title: "Robot Password",
description: "Password for robot authentication",
format: "password",
},
websocketUrl: {
type: "string",
default: "ws://localhost:9090",
title: "WebSocket URL",
description: "ROS bridge WebSocket URL for robot communication",
},
maxLinearVelocity: {
type: "number",
default: 0.2,
minimum: 0.01,
maximum: 0.5,
title: "Max Linear Velocity (m/s)",
description: "Maximum allowed linear movement speed for safety",
},
speechVolume: {
type: "number",
default: 0.7,
minimum: 0.1,
maximum: 1.0,
title: "Speech Volume",
description: "Default volume for speech synthesis",
},
enableSafetyMonitoring: {
type: "boolean",
default: true,
title: "Enable Safety Monitoring",
description:
"Enable automatic safety monitoring and emergency stops",
},
},
required: ["robotIp", "websocketUrl"],
},
actionDefinitions: actionDefinitions,
metadata: {
robotModel: "NAO V6.0",
manufacturer: "SoftBank Robotics",
naoqiVersion: "2.8.7.4",
ros2Distro: "humble",
launchPackage: "nao_launch",
capabilities: [
"bipedal_walking",
"speech_synthesis",
"head_movement",
"arm_gestures",
"touch_sensors",
"visual_sensors",
"posture_control",
],
tags: [
"nao6",
"ros2",
"speech",
"movement",
"sensors",
"hri",
"production",
],
},
};
if (existingPlugin.length > 0) {
await db
.update(plugins)
.set({
...pluginData,
updatedAt: new Date(),
})
.where(eq(plugins.id, existingPlugin[0]!.id));
console.log(`✅ Updated existing NAO6 plugin: ${existingPlugin[0]!.id}`);
} else {
const newPlugins = await db
.insert(plugins)
.values(pluginData)
.returning();
console.log(`✅ Created NAO6 plugin: ${newPlugins[0]!.id}`);
}
console.log("\n🎉 NAO6 plugin seeding completed successfully!");
console.log("\nNext steps:");
console.log("1. Install the plugin in a study via the HRIStudio interface");
console.log("2. Configure the robot IP and WebSocket URL");
console.log(
"3. Launch ROS integration: ros2 launch nao_launch nao6_production.launch.py",
);
console.log("4. Test robot actions in the experiment designer");
console.log("\n📊 Plugin Summary:");
console.log(` Robot: NAO6 (${robotId})`);
console.log(` Repository: NAO6 ROS2 Integration (${repoId})`);
console.log(` Actions: ${actionDefinitions.length} available`);
console.log(
" Categories: speech, movement, interaction, sensors, safety, system",
);
} catch (error) {
console.error("❌ Error seeding NAO6 plugin:", error);
throw error;
} finally {
await client.end();
}
}
// Run the seeding script
seedNAO6Plugin()
.then(() => {
console.log("✅ Database seeding completed");
process.exit(0);
})
.catch((error) => {
console.error("❌ Database seeding failed:", error);
process.exit(1);
});

View File

@@ -0,0 +1,84 @@
// Mock of the logic in WizardInterface.tsx handleNextStep
const steps = [
{
id: "b9d43f8c-c40c-4f1c-9fdc-9076338d3c85",
name: "Step 3 (Conditional)",
order: 2
},
{
id: "3a2dc0b7-a43e-4236-9b9e-f957abafc1e5",
name: "Step 4 (Branch A)",
order: 3,
conditions: {
"nextStepId": "cc3fbc7f-29e5-45e0-8d46-e80813c54292"
}
},
{
id: "3ae2fe8a-fc5d-4a04-baa5-699a21f19e30",
name: "Step 5 (Branch B)",
order: 4,
conditions: {
"nextStepId": "cc3fbc7f-29e5-45e0-8d46-e80813c54292"
}
},
{
id: "cc3fbc7f-29e5-45e0-8d46-e80813c54292",
name: "Step 6 (Conclusion)",
order: 5
}
];
function simulateNextStep(currentStepIndex: number) {
const currentStep = steps[currentStepIndex];
if (!currentStep) {
console.log("No step found at index:", currentStepIndex);
return;
}
console.log(`\n--- Simulating Next Step from: ${currentStep.name} ---`);
console.log("Current Step Data:", JSON.stringify(currentStep, null, 2));
// Logic from WizardInterface.tsx
console.log("[WizardInterface] Checking for nextStepId condition:", currentStep?.conditions);
if (currentStep?.conditions?.nextStepId) {
const nextId = String(currentStep.conditions.nextStepId);
const targetIndex = steps.findIndex(s => s.id === nextId);
console.log(`Target ID: ${nextId}`);
console.log(`Target Index Found: ${targetIndex}`);
if (targetIndex !== -1) {
console.log(`[WizardInterface] Condition-based jump to step ${targetIndex} (${nextId})`);
return targetIndex;
} else {
console.warn(`[WizardInterface] Targeted nextStepId ${nextId} not found in steps list.`);
}
} else {
console.log("[WizardInterface] No nextStepId found in conditions, proceeding linearly.");
}
// Default: Linear progression
const nextIndex = currentStepIndex + 1;
console.log(`Proceeding linearly to index ${nextIndex}`);
return nextIndex;
}
// Simulate Branch A (Index 1 in this array, but 3 in real experiment?)
// In real exp, Step 3 is index 2. Step 4 (Branch A) is index 3.
console.log("Real experiment indices:");
// 0: Hook, 1: Narrative, 2: Conditional, 3: Branch A, 4: Branch B, 5: Conclusion
const indexStep4 = 1; // logical index in my mock array
const indexStep5 = 2; // logical index
console.log("Testing Branch A Logic:");
const resultA = simulateNextStep(indexStep4);
if (resultA === 3) console.log("SUCCESS: Branch A jumped to Conclusion");
else console.log("FAILURE: Branch A fell through");
console.log("\nTesting Branch B Logic:");
const resultB = simulateNextStep(indexStep5);
if (resultB === 3) console.log("SUCCESS: Branch B jumped to Conclusion");
else console.log("FAILURE: Branch B fell through");

View File

@@ -0,0 +1,60 @@
import { convertDatabaseToAction } from "../../src/lib/experiment-designer/block-converter";
const mockDbAction = {
id: "eaf8f85b-75cf-4973-b436-092516b4e0e4",
name: "Introduction Sequence",
description: null,
type: "sequence",
orderIndex: 0,
parameters: {
"children": [
{
"id": "75018b01-a964-41fb-8612-940a29020d4a",
"name": "Say Hello",
"type": "nao6-ros2.say_text",
"category": "interaction",
"parameters": {
"text": "Hello there!"
}
},
{
"id": "d7020530-6477-41f3-84a4-5141778c93da",
"name": "Wave Hand",
"type": "nao6-ros2.move_arm",
"category": "movement",
"parameters": {
"arm": "right",
"action": "wave"
}
}
]
},
timeout: null,
retryCount: 0,
sourceKind: "core",
pluginId: "hristudio-core",
pluginVersion: null,
robotId: null,
baseActionId: null,
category: "control",
transport: null,
ros2: null,
rest: null,
retryable: null,
parameterSchemaRaw: null
};
console.log("Testing convertDatabaseToAction...");
try {
const result = convertDatabaseToAction(mockDbAction);
console.log("Result:", JSON.stringify(result, null, 2));
if (result.children && result.children.length > 0) {
console.log("✅ Children hydrated successfully.");
} else {
console.error("❌ Children NOT hydrated.");
}
} catch (e) {
console.error("❌ Error during conversion:", e);
}

323
scripts/archive/test-seed-data.ts Executable file
View File

@@ -0,0 +1,323 @@
#!/usr/bin/env tsx
/**
* Test script to validate seed data structure
* Ensures all user relationships and study memberships are correct
*/
interface User {
id: string;
name: string;
email: string;
institution: string;
}
interface UserRole {
userId: string;
role: "administrator" | "researcher" | "wizard" | "observer";
assignedBy: string;
}
interface Study {
id: string;
name: string;
createdBy: string;
}
interface StudyMember {
studyId: string;
userId: string;
role: "owner" | "researcher" | "wizard" | "observer";
invitedBy: string | null;
}
function validateSeedData() {
console.log("🧪 Testing seed data structure...\n");
// Users data
const users: User[] = [
{
id: "01234567-89ab-cdef-0123-456789abcde0",
name: "Sean O'Connor",
email: "sean@soconnor.dev",
institution: "HRIStudio",
},
{
id: "01234567-89ab-cdef-0123-456789abcde1",
name: "Dr. Sarah Chen",
email: "sarah.chen@university.edu",
institution: "MIT Computer Science",
},
{
id: "01234567-89ab-cdef-0123-456789abcde2",
name: "Dr. Michael Rodriguez",
email: "m.rodriguez@research.org",
institution: "Stanford HCI Lab",
},
{
id: "01234567-89ab-cdef-0123-456789abcde3",
name: "Emma Thompson",
email: "emma.thompson@university.edu",
institution: "MIT Computer Science",
},
{
id: "01234567-89ab-cdef-0123-456789abcde4",
name: "Dr. James Wilson",
email: "james.wilson@university.edu",
institution: "MIT Computer Science",
},
];
// User roles
const userRoles: UserRole[] = [
{
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "administrator",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
userId: "01234567-89ab-cdef-0123-456789abcde4",
role: "observer",
assignedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
// Studies
const studies: Study[] = [
{
id: "11234567-89ab-cdef-0123-456789abcde1",
name: "Robot Navigation Assistance Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
id: "11234567-89ab-cdef-0123-456789abcde2",
name: "Social Robots in Healthcare Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
id: "11234567-89ab-cdef-0123-456789abcde3",
name: "Elderly Care Robot Interaction Study",
createdBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
// Study members
const studyMembers: StudyMember[] = [
// Sean as owner of all studies
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde0",
role: "owner",
invitedBy: null,
},
// Other team members
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde1",
userId: "01234567-89ab-cdef-0123-456789abcde3",
role: "wizard",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde2",
userId: "01234567-89ab-cdef-0123-456789abcde2",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
{
studyId: "11234567-89ab-cdef-0123-456789abcde3",
userId: "01234567-89ab-cdef-0123-456789abcde1",
role: "researcher",
invitedBy: "01234567-89ab-cdef-0123-456789abcde0",
},
];
let errors = 0;
console.log("👥 Validating users...");
console.log(` Users: ${users.length}`);
// Check for Sean as admin
const seanUser = users.find((u) => u.email === "sean@soconnor.dev");
if (seanUser) {
console.log(` ✅ Sean found: ${seanUser.name} (${seanUser.email})`);
} else {
console.error(` ❌ Sean not found as user`);
errors++;
}
console.log("\n🔐 Validating user roles...");
console.log(` User roles: ${userRoles.length}`);
// Check Sean's admin role
const seanRole = userRoles.find(
(r) => r.userId === "01234567-89ab-cdef-0123-456789abcde0",
);
if (seanRole && seanRole.role === "administrator") {
console.log(` ✅ Sean has administrator role`);
} else {
console.error(` ❌ Sean missing administrator role`);
errors++;
}
// Check all roles are assigned by Sean
const rolesAssignedBySean = userRoles.filter(
(r) => r.assignedBy === "01234567-89ab-cdef-0123-456789abcde0",
);
console.log(
`${rolesAssignedBySean.length}/${userRoles.length} roles assigned by Sean`,
);
console.log("\n📚 Validating studies...");
console.log(` Studies: ${studies.length}`);
// Check all studies created by Sean
const studiesCreatedBySean = studies.filter(
(s) => s.createdBy === "01234567-89ab-cdef-0123-456789abcde0",
);
if (studiesCreatedBySean.length === studies.length) {
console.log(` ✅ All ${studies.length} studies created by Sean`);
} else {
console.error(
` ❌ Only ${studiesCreatedBySean.length}/${studies.length} studies created by Sean`,
);
errors++;
}
console.log("\n👨💼 Validating study memberships...");
console.log(` Study memberships: ${studyMembers.length}`);
// Check Sean is owner of all studies
const seanOwnerships = studyMembers.filter(
(m) =>
m.userId === "01234567-89ab-cdef-0123-456789abcde0" && m.role === "owner",
);
if (seanOwnerships.length === studies.length) {
console.log(` ✅ Sean is owner of all ${studies.length} studies`);
} else {
console.error(
` ❌ Sean only owns ${seanOwnerships.length}/${studies.length} studies`,
);
errors++;
}
// Check invitation chain
const membersInvitedBySean = studyMembers.filter(
(m) => m.invitedBy === "01234567-89ab-cdef-0123-456789abcde0",
);
console.log(`${membersInvitedBySean.length} members invited by Sean`);
// Validate all user references exist
console.log("\n🔗 Validating references...");
const userIds = new Set(users.map((u) => u.id));
for (const role of userRoles) {
if (!userIds.has(role.userId)) {
console.error(` ❌ Invalid user reference in role: ${role.userId}`);
errors++;
}
if (!userIds.has(role.assignedBy)) {
console.error(
` ❌ Invalid assignedBy reference in role: ${role.assignedBy}`,
);
errors++;
}
}
for (const study of studies) {
if (!userIds.has(study.createdBy)) {
console.error(
` ❌ Invalid createdBy reference in study: ${study.createdBy}`,
);
errors++;
}
}
const studyIds = new Set(studies.map((s) => s.id));
for (const member of studyMembers) {
if (!studyIds.has(member.studyId)) {
console.error(
` ❌ Invalid study reference in membership: ${member.studyId}`,
);
errors++;
}
if (!userIds.has(member.userId)) {
console.error(
` ❌ Invalid user reference in membership: ${member.userId}`,
);
errors++;
}
if (member.invitedBy && !userIds.has(member.invitedBy)) {
console.error(
` ❌ Invalid invitedBy reference in membership: ${member.invitedBy}`,
);
errors++;
}
}
if (errors === 0) {
console.log(" ✅ All references are valid");
}
// Summary
console.log(`\n📊 Validation Summary:`);
console.log(` Users: ${users.length}`);
console.log(` User roles: ${userRoles.length}`);
console.log(` Studies: ${studies.length}`);
console.log(` Study memberships: ${studyMembers.length}`);
console.log(` Errors: ${errors}`);
if (errors === 0) {
console.log(`\n🎉 All validations passed! Seed data structure is correct.`);
console.log(` Sean (sean@soconnor.dev) is admin of everything:`);
console.log(` • System administrator role`);
console.log(` • Owner of all ${studies.length} studies`);
console.log(` • Assigned all user roles`);
console.log(` • Invited all study members`);
process.exit(0);
} else {
console.log(`\n❌ Validation failed with ${errors} error(s).`);
process.exit(1);
}
}
// Run the validation
if (import.meta.url === `file://${process.argv[1]}`) {
validateSeedData();
}
export { validateSeedData };

View File

@@ -0,0 +1,71 @@
import { appRouter } from "../../src/server/api/root";
import { createCallerFactory } from "../../src/server/api/trpc";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../../src/server/db/schema";
import { eq } from "drizzle-orm";
// 1. Setup DB Context
const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString);
const db = drizzle(connection, { schema });
// 2. Mock Session
const mockSession = {
user: {
id: "0e830889-ab46-4b48-a8ba-1d4bd3e665ed", // Admin user ID from seed
name: "Sean O'Connor",
email: "sean@soconnor.dev"
},
expires: new Date().toISOString()
};
// 3. Create Caller
const createCaller = createCallerFactory(appRouter);
const caller = createCaller({
db,
session: mockSession as any,
headers: new Headers()
});
async function main() {
console.log("🔍 Fetching experiment via TRPC caller...");
// Get ID first
const exp = await db.query.experiments.findFirst({
where: eq(schema.experiments.name, "Control Flow Demo"),
columns: { id: true }
});
if (!exp) {
console.error("❌ Experiment not found");
return;
}
const result = await caller.experiments.get({ id: exp.id });
console.log(`✅ Fetched experiment: ${result.name} (${result.id})`);
if (result.steps && result.steps.length > 0) {
console.log(`Checking ${result.steps.length} steps...`);
const actions = result.steps[0]!.actions; // Step 1 actions
console.log(`Step 1 has ${actions.length} actions.`);
actions.forEach(a => {
if (["sequence", "parallel", "loop", "branch"].includes(a.type)) {
console.log(`\nAction: ${a.name} (${a.type})`);
console.log(`Children Count: ${a.children ? a.children.length : 'UNDEFINED'}`);
if (a.children && a.children.length > 0) {
console.log(`First Child: ${a.children[0]!.name} (${a.children[0]!.type})`);
}
}
});
} else {
console.error("❌ No steps found in result.");
}
await connection.end();
}
main();

View File

@@ -0,0 +1,44 @@
import { db } from "../../src/server/db";
import { experiments } from "../../src/server/db/schema";
import { eq, asc } from "drizzle-orm";
import { convertDatabaseToSteps } from "../../src/lib/experiment-designer/block-converter";
async function verifyConversion() {
const experiment = await db.query.experiments.findFirst({
with: {
steps: {
orderBy: (steps, { asc }) => [asc(steps.orderIndex)],
with: {
actions: {
orderBy: (actions, { asc }) => [asc(actions.orderIndex)],
}
}
}
}
});
if (!experiment) {
console.log("No experiment found");
return;
}
console.log("Raw DB Steps Count:", experiment.steps.length);
const converted = convertDatabaseToSteps(experiment.steps);
console.log("Converted Steps:");
converted.forEach((s, idx) => {
console.log(`[${idx}] ${s.name} (${s.type})`);
console.log(` Trigger:`, JSON.stringify(s.trigger));
if (s.type === 'conditional') {
console.log(` Conditions populated?`, Object.keys(s.trigger.conditions).length > 0);
}
});
}
verifyConversion()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,561 @@
#!/bin/bash
#
# NAO6 HRIStudio Integration Verification Script
#
# This script performs comprehensive verification of the NAO6 integration with HRIStudio,
# checking all components from ROS2 workspace to database plugins and providing
# detailed status and next steps.
#
# Usage: ./verify-nao6-integration.sh [--robot-ip IP] [--verbose]
#
set -e
# =================================================================
# CONFIGURATION AND DEFAULTS
# =================================================================
NAO_IP="${1:-nao.local}"
VERBOSE=false
HRISTUDIO_DIR="${HOME}/Documents/Projects/hristudio"
ROS_WS="${HOME}/naoqi_ros2_ws"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# =================================================================
# UTILITY FUNCTIONS
# =================================================================
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[✅ PASS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[⚠️ WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[❌ FAIL]${NC} $1"
}
log_step() {
echo -e "${PURPLE}[STEP]${NC} $1"
}
log_verbose() {
if [ "$VERBOSE" = true ]; then
echo -e "${CYAN}[DEBUG]${NC} $1"
fi
}
show_header() {
echo -e "${CYAN}"
echo "================================================================="
echo " NAO6 HRIStudio Integration Verification"
echo "================================================================="
echo -e "${NC}"
echo "Target Robot: $NAO_IP"
echo "HRIStudio: $HRISTUDIO_DIR"
echo "ROS Workspace: $ROS_WS"
echo ""
}
# =================================================================
# VERIFICATION FUNCTIONS
# =================================================================
check_prerequisites() {
log_step "Checking prerequisites and dependencies..."
local errors=0
# Check ROS2 installation
if command -v ros2 >/dev/null 2>&1; then
local ros_distro=$(ros2 --version 2>/dev/null | grep -o "humble\|iron\|rolling" || echo "unknown")
log_success "ROS2 found (distro: $ros_distro)"
else
log_error "ROS2 not found - install ROS2 Humble"
((errors++))
fi
# Check required tools
local tools=("ping" "ssh" "sshpass" "bun" "docker")
for tool in "${tools[@]}"; do
if command -v $tool >/dev/null 2>&1; then
log_success "$tool available"
else
log_warning "$tool not found (may be optional)"
fi
done
# Check ROS workspace
if [ -d "$ROS_WS" ]; then
log_success "NAOqi ROS2 workspace found"
if [ -f "$ROS_WS/install/setup.bash" ]; then
log_success "ROS workspace built and ready"
else
log_warning "ROS workspace not built - run: cd $ROS_WS && colcon build"
fi
else
log_error "NAOqi ROS2 workspace not found at $ROS_WS"
((errors++))
fi
# Check HRIStudio directory
if [ -d "$HRISTUDIO_DIR" ]; then
log_success "HRIStudio directory found"
if [ -f "$HRISTUDIO_DIR/package.json" ]; then
log_success "HRIStudio package configuration found"
else
log_warning "HRIStudio package.json not found"
fi
else
log_error "HRIStudio directory not found at $HRISTUDIO_DIR"
((errors++))
fi
return $errors
}
check_nao_launch_package() {
log_step "Verifying nao_launch package..."
local errors=0
local package_dir="$ROS_WS/src/nao_launch"
if [ -d "$package_dir" ]; then
log_success "nao_launch package directory found"
# Check launch files
local launch_files=(
"nao6_hristudio.launch.py"
"nao6_production.launch.py"
"nao6_hristudio_enhanced.launch.py"
)
for launch_file in "${launch_files[@]}"; do
if [ -f "$package_dir/launch/$launch_file" ]; then
log_success "Launch file: $launch_file"
else
log_warning "Missing launch file: $launch_file"
fi
done
# Check scripts
if [ -d "$package_dir/scripts" ]; then
log_success "Scripts directory found"
local scripts=("nao_control.py" "start_nao6_hristudio.sh")
for script in "${scripts[@]}"; do
if [ -f "$package_dir/scripts/$script" ]; then
log_success "Script: $script"
else
log_warning "Missing script: $script"
fi
done
else
log_warning "Scripts directory not found"
fi
# Check if package is built
if [ -f "$ROS_WS/install/nao_launch/share/nao_launch/package.xml" ]; then
log_success "nao_launch package built and installed"
else
log_warning "nao_launch package not built - run: cd $ROS_WS && colcon build --packages-select nao_launch"
fi
else
log_error "nao_launch package directory not found"
((errors++))
fi
return $errors
}
check_robot_connectivity() {
log_step "Testing NAO robot connectivity..."
local errors=0
# Test ping
log_verbose "Testing ping to $NAO_IP..."
if ping -c 2 -W 3 "$NAO_IP" >/dev/null 2>&1; then
log_success "Robot responds to ping"
else
log_error "Cannot ping robot at $NAO_IP - check network/IP"
((errors++))
return $errors
fi
# Test NAOqi port
log_verbose "Testing NAOqi service on port 9559..."
if timeout 5 bash -c "echo >/dev/tcp/$NAO_IP/9559" 2>/dev/null; then
log_success "NAOqi service accessible on port 9559"
else
log_error "Cannot connect to NAOqi on $NAO_IP:9559"
((errors++))
fi
# Test SSH (optional)
log_verbose "Testing SSH connectivity (optional)..."
if timeout 5 ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes nao@$NAO_IP echo "SSH test" >/dev/null 2>&1; then
log_success "SSH connectivity working"
else
log_warning "SSH connectivity failed - may need password for wake-up"
fi
return $errors
}
check_hristudio_database() {
log_step "Checking HRIStudio database and plugins..."
local errors=0
# Check if database is running
if docker ps | grep -q postgres || ss -ln | grep -q :5432 || ss -ln | grep -q :5140; then
log_success "Database appears to be running"
# Try to query database (requires HRIStudio to be set up)
cd "$HRISTUDIO_DIR" 2>/dev/null || true
if [ -f "$HRISTUDIO_DIR/.env" ] || [ -n "$DATABASE_URL" ]; then
log_success "Database configuration found"
# Check for NAO6 plugin (this would require running a query)
log_info "Database plugins check would require HRIStudio connection"
else
log_warning "Database configuration not found - check .env file"
fi
else
log_warning "Database not running - start with: docker compose up -d"
fi
return $errors
}
check_ros_dependencies() {
log_step "Checking ROS dependencies and packages..."
local errors=0
# Source ROS if available
if [ -f "/opt/ros/humble/setup.bash" ]; then
source /opt/ros/humble/setup.bash 2>/dev/null || true
log_success "ROS2 Humble environment sourced"
fi
# Check required ROS packages
local required_packages=(
"rosbridge_server"
"rosapi"
"std_msgs"
"geometry_msgs"
"sensor_msgs"
)
for package in "${required_packages[@]}"; do
if ros2 pkg list 2>/dev/null | grep -q "^$package$"; then
log_success "ROS package: $package"
else
log_warning "ROS package missing: $package"
fi
done
# Check NAOqi-specific packages
local naoqi_packages=(
"naoqi_driver"
"naoqi_bridge_msgs"
)
for package in "${naoqi_packages[@]}"; do
if [ -d "$ROS_WS/src/naoqi_driver2" ] || [ -d "$ROS_WS/install/$package" ]; then
log_success "NAOqi package: $package"
else
log_warning "NAOqi package not found: $package"
fi
done
return $errors
}
check_plugin_files() {
log_step "Checking HRIStudio plugin files..."
local errors=0
local plugin_dir="$HRISTUDIO_DIR/public/nao6-plugins"
if [ -d "$plugin_dir" ]; then
log_success "NAO6 plugins directory found"
# Check repository metadata
if [ -f "$plugin_dir/repository.json" ]; then
log_success "Repository metadata file found"
# Validate JSON
if command -v jq >/dev/null 2>&1; then
if jq empty "$plugin_dir/repository.json" 2>/dev/null; then
log_success "Repository metadata is valid JSON"
else
log_error "Repository metadata has invalid JSON"
((errors++))
fi
fi
else
log_warning "Repository metadata not found"
fi
# Check plugin definition
if [ -f "$plugin_dir/nao6-ros2-enhanced.json" ]; then
log_success "NAO6 plugin definition found"
# Validate JSON
if command -v jq >/dev/null 2>&1; then
if jq empty "$plugin_dir/nao6-ros2-enhanced.json" 2>/dev/null; then
local action_count=$(jq '.actionDefinitions | length' "$plugin_dir/nao6-ros2-enhanced.json" 2>/dev/null || echo "0")
log_success "Plugin definition valid with $action_count actions"
else
log_error "Plugin definition has invalid JSON"
((errors++))
fi
fi
else
log_warning "NAO6 plugin definition not found"
fi
else
log_warning "NAO6 plugins directory not found - plugins may be in database only"
fi
return $errors
}
show_integration_status() {
echo ""
echo -e "${CYAN}================================================================="
echo " INTEGRATION STATUS SUMMARY"
echo -e "=================================================================${NC}"
echo ""
echo -e "${GREEN}🤖 NAO6 Robot Integration Components:${NC}"
echo " ✅ ROS2 Workspace: $ROS_WS"
echo " ✅ nao_launch Package: Enhanced launch files and scripts"
echo " ✅ HRIStudio Plugin: Database integration with 9 actions"
echo " ✅ Plugin Repository: Local and remote plugin definitions"
echo ""
echo -e "${BLUE}🔧 Available Launch Configurations:${NC}"
echo " 📦 Production: ros2 launch nao_launch nao6_production.launch.py"
echo " 🔍 Enhanced: ros2 launch nao_launch nao6_hristudio_enhanced.launch.py"
echo " ⚡ Basic: ros2 launch nao_launch nao6_hristudio.launch.py"
echo ""
echo -e "${PURPLE}🎮 Robot Control Options:${NC}"
echo " 🖥️ Command Line: python3 scripts/nao_control.py --ip $NAO_IP"
echo " 🌐 Web Interface: http://localhost:3000/nao-test"
echo " 🧪 HRIStudio: Experiment designer with NAO6 actions"
echo ""
echo -e "${YELLOW}📋 Available Actions in HRIStudio:${NC}"
echo " 🗣️ Speech: Text-to-speech synthesis"
echo " 🚶 Movement: Walking, turning, positioning"
echo " 🧍 Posture: Stand, sit, crouch poses"
echo " 👀 Head: Gaze control and attention direction"
echo " 👋 Gestures: Wave, point, applause, custom animations"
echo " 📡 Sensors: Touch, bumper, sonar monitoring"
echo " 🛑 Safety: Emergency stop and status checking"
echo " ⚡ System: Wake/rest and robot management"
echo ""
}
show_next_steps() {
echo -e "${GREEN}🚀 Next Steps to Start Using NAO6 Integration:${NC}"
echo ""
echo "1. 📡 Start ROS Integration:"
echo " cd $ROS_WS && source install/setup.bash"
echo " ros2 launch nao_launch nao6_production.launch.py nao_ip:=$NAO_IP password:=robolab"
echo ""
echo "2. 🌐 Start HRIStudio:"
echo " cd $HRISTUDIO_DIR"
echo " bun dev"
echo ""
echo "3. 🧪 Test Integration:"
echo " • Open: http://localhost:3000/nao-test"
echo " • Click 'Connect' to establish WebSocket connection"
echo " • Try robot commands (speech, movement, etc.)"
echo ""
echo "4. 🔬 Create Experiments:"
echo " • Login to HRIStudio: sean@soconnor.dev / password123"
echo " • Go to Study → Plugins → Install NAO6 plugin"
echo " • Configure robot IP: $NAO_IP"
echo " • Design experiments using NAO6 actions"
echo ""
echo "5. 🛠️ Troubleshooting:"
echo " • Robot not responding: Wake up with chest button (3 seconds)"
echo " • Connection issues: Check network and robot IP"
echo " • WebSocket problems: Verify rosbridge is running"
echo " • Emergency stop: Use Ctrl+C or emergency action"
echo ""
}
show_comprehensive_summary() {
echo -e "${CYAN}================================================================="
echo " COMPREHENSIVE INTEGRATION SUMMARY"
echo -e "=================================================================${NC}"
echo ""
echo -e "${GREEN}✅ COMPLETED ENHANCEMENTS:${NC}"
echo ""
echo -e "${BLUE}📦 Enhanced nao_launch Package:${NC}"
echo " • Production-optimized launch files with safety features"
echo " • Comprehensive robot control and monitoring scripts"
echo " • Automatic wake-up and error recovery"
echo " • Performance-tuned sensor frequencies for HRIStudio"
echo " • Emergency stop and safety monitoring capabilities"
echo ""
echo -e "${BLUE}🔌 Enhanced Plugin Integration:${NC}"
echo " • Complete NAO6 plugin with 9 comprehensive actions"
echo " • Type-safe configuration schema for robot settings"
echo " • WebSocket integration for real-time robot control"
echo " • Safety parameters and velocity limits"
echo " • Comprehensive action parameter validation"
echo ""
echo -e "${BLUE}🛠️ Utility Scripts and Tools:${NC}"
echo " • nao_control.py - Command-line robot control and monitoring"
echo " • start_nao6_hristudio.sh - Comprehensive startup automation"
echo " • Enhanced CMakeLists.txt and package metadata"
echo " • Database seeding scripts for plugin installation"
echo " • Comprehensive documentation and troubleshooting guides"
echo ""
echo -e "${BLUE}📚 Documentation and Guides:${NC}"
echo " • Complete README with setup and usage instructions"
echo " • Plugin repository metadata and action definitions"
echo " • Safety guidelines and emergency procedures"
echo " • Troubleshooting guide for common issues"
echo " • Integration examples and common use cases"
echo ""
echo -e "${PURPLE}🎯 Production-Ready Features:${NC}"
echo " • Automatic robot wake-up on experiment start"
echo " • Safety monitoring with emergency stop capabilities"
echo " • Optimized sensor publishing for experimental workflows"
echo " • Robust error handling and recovery mechanisms"
echo " • Performance tuning for stable long-running experiments"
echo " • Comprehensive logging and status monitoring"
echo ""
echo -e "${YELLOW}🔬 Research Capabilities:${NC}"
echo " • Complete speech synthesis with volume/speed control"
echo " • Precise movement control with safety limits"
echo " • Posture control for experimental positioning"
echo " • Head movement for gaze and attention studies"
echo " • Gesture library for social interaction research"
echo " • Comprehensive sensor monitoring for interaction detection"
echo " • Real-time status monitoring for experimental validity"
echo ""
}
# =================================================================
# MAIN EXECUTION
# =================================================================
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--robot-ip)
NAO_IP="$2"
shift 2
;;
--verbose)
VERBOSE=true
shift
;;
--help)
echo "Usage: $0 [--robot-ip IP] [--verbose]"
exit 0
;;
*)
NAO_IP="$1"
shift
;;
esac
done
# Show header
show_header
# Run verification checks
local total_errors=0
check_prerequisites
total_errors=$((total_errors + $?))
check_nao_launch_package
total_errors=$((total_errors + $?))
check_robot_connectivity
total_errors=$((total_errors + $?))
check_hristudio_database
total_errors=$((total_errors + $?))
check_ros_dependencies
total_errors=$((total_errors + $?))
check_plugin_files
total_errors=$((total_errors + $?))
# Show results
echo ""
if [ $total_errors -eq 0 ]; then
log_success "All verification checks passed! 🎉"
show_integration_status
show_next_steps
show_comprehensive_summary
echo -e "${GREEN}🎊 NAO6 HRIStudio Integration is ready for use!${NC}"
echo ""
else
log_warning "Verification completed with $total_errors issues"
echo ""
echo -e "${YELLOW}⚠️ Some components need attention before full integration.${NC}"
echo "Please resolve the issues above and run verification again."
echo ""
show_next_steps
fi
echo -e "${CYAN}================================================================="
echo " VERIFICATION COMPLETE"
echo -e "=================================================================${NC}"
}
# Run main function
main "$@"

View File

@@ -0,0 +1,90 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { eq, sql } from "drizzle-orm";
import postgres from "postgres";
import * as schema from "../../src/server/db/schema";
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);
const db = drizzle(client, { schema });
async function verify() {
console.log("🔍 Verifying Study Readiness...");
// 1. Check Study
const study = await db.query.studies.findFirst({
where: eq(schema.studies.name, "Comparative WoZ Study")
});
if (!study) {
console.error("❌ Study 'Comparative WoZ Study' not found.");
process.exit(1);
}
console.log("✅ Study found:", study.name);
// 2. Check Experiment
const experiment = await db.query.experiments.findFirst({
where: eq(schema.experiments.name, "The Interactive Storyteller")
});
if (!experiment) {
console.error("❌ Experiment 'The Interactive Storyteller' not found.");
process.exit(1);
}
console.log("✅ Experiment found:", experiment.name);
// 3. Check Steps
const steps = await db.query.steps.findMany({
where: eq(schema.steps.experimentId, experiment.id),
orderBy: schema.steps.orderIndex
});
console.log(` Found ${steps.length} steps.`);
if (steps.length < 5) {
console.error("❌ Expected at least 5 steps, found " + steps.length);
process.exit(1);
}
// Verify Step Names
const expectedSteps = ["The Hook", "The Narrative - Part 1", "Comprehension Check", "Positive Feedback", "Conclusion"];
for (let i = 0; i < expectedSteps.length; i++) {
const step = steps[i];
if (!step) continue;
if (step.name !== expectedSteps[i]) {
console.error(`❌ Step mismatch at index ${i}. Expected '${expectedSteps[i]}', got '${step.name}'`);
} else {
console.log(`✅ Step ${i + 1}: ${step.name}`);
}
}
// 4. Check Plugin Actions
// Find the NAO6 plugin
const plugin = await db.query.plugins.findFirst({
where: (plugins, { eq, and }) => and(eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)"), eq(plugins.status, "active"))
});
if (!plugin) {
console.error("❌ NAO6 Plugin not found.");
process.exit(1);
}
const actions = plugin.actionDefinitions as any[];
const requiredActions = ["nao_nod", "nao_shake_head", "nao_bow", "nao_open_hand"];
for (const actionId of requiredActions) {
const found = actions.find(a => a.id === actionId);
if (!found) {
console.error(`❌ Plugin missing action: ${actionId}`);
process.exit(1);
}
console.log(`✅ Plugin has action: ${actionId}`);
}
console.log("🎉 Verification Complete: Platform is ready for the study!");
process.exit(0);
}
verify().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,84 @@
import { db } from "~/server/db";
import { experiments, steps, actions } from "~/server/db/schema";
import { eq, asc, desc } from "drizzle-orm";
import { convertDatabaseToSteps } from "~/lib/experiment-designer/block-converter";
async function verifyTrpcLogic() {
console.log("Verifying TRPC Logic for Interactive Storyteller...");
// 1. Simulate the DB Query from experiments.ts
const experiment = await db.query.experiments.findFirst({
where: eq(experiments.name, "The Interactive Storyteller"),
with: {
study: {
columns: {
id: true,
name: true,
},
},
createdBy: {
columns: {
id: true,
name: true,
email: true,
},
},
robot: true,
steps: {
with: {
actions: {
orderBy: [asc(actions.orderIndex)],
},
},
orderBy: [asc(steps.orderIndex)],
},
},
});
if (!experiment) {
console.error("Experiment not found!");
return;
}
// 2. Simulate the Transformation
console.log("Transforming DB steps to Designer steps...");
const transformedSteps = convertDatabaseToSteps(experiment.steps);
// 3. Inspect Step 4 (Branch A)
// Step index 3 (0-based) is Branch A
const branchAStep = transformedSteps[3];
if (branchAStep) {
console.log("Step 4 (Branch A):", branchAStep.name);
console.log(" Type:", branchAStep.type);
console.log(" Trigger:", JSON.stringify(branchAStep.trigger, null, 2));
} else {
console.error("Step 4 (Branch A) not found in transformed steps!");
process.exit(1);
}
// Check conditions specifically
const conditions = branchAStep.trigger?.conditions as any;
if (conditions?.nextStepId) {
console.log("SUCCESS: nextStepId found in conditions:", conditions.nextStepId);
} else {
console.error("FAILURE: nextStepId MISSING in conditions!");
}
// Inspect Step 5 (Branch B) for completeness
const branchBStep = transformedSteps[4];
if (branchBStep) {
console.log("Step 5 (Branch B):", branchBStep.name);
console.log(" Trigger:", JSON.stringify(branchBStep.trigger, null, 2));
} else {
console.warn("Step 5 (Branch B) not found in transformed steps.");
}
}
verifyTrpcLogic()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});