mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 14:44:44 -05:00
Redesign experiment designer workspace and seed Bucknell data
- Overhauled designer UI: virtualized flow, slim action panel, improved drag - Added Bucknell studies, users, and NAO plugin to seed-dev script - Enhanced validation panel and inspector UX - Updated wizard-actions plugin options formatting - Removed Minio from docker-compose for local dev - Numerous UI and code quality improvements for experiment design
This commit is contained in:
@@ -3,6 +3,8 @@ 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";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
// Database connection
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
@@ -17,34 +19,57 @@ async function syncRepository(
|
||||
try {
|
||||
console.log(`🔄 Syncing repository: ${repoUrl}`);
|
||||
|
||||
// Use localhost for development
|
||||
const devUrl = repoUrl.includes("core.hristudio.com")
|
||||
? "http://localhost:3000/hristudio-core"
|
||||
: repoUrl;
|
||||
// Resolve source: use local public repo for core, remote URL otherwise
|
||||
const isCore = repoUrl.includes("core.hristudio.com");
|
||||
const devUrl = repoUrl;
|
||||
|
||||
// Fetch repository metadata
|
||||
const repoResponse = await fetch(`${devUrl}/repository.json`);
|
||||
if (!repoResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch repository metadata: ${repoResponse.status}`,
|
||||
);
|
||||
}
|
||||
const repoMetadata = (await repoResponse.json()) as {
|
||||
description?: string;
|
||||
author?: { name?: string };
|
||||
urls?: { git?: string };
|
||||
trust?: string;
|
||||
};
|
||||
// Fetch repository metadata (local filesystem for core)
|
||||
const repoMetadata = isCore
|
||||
? (JSON.parse(
|
||||
await readFile(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"hristudio-core",
|
||||
"repository.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
) as {
|
||||
description?: string;
|
||||
author?: { name?: string };
|
||||
urls?: { git?: string };
|
||||
trust?: string;
|
||||
})
|
||||
: await (async () => {
|
||||
const repoResponse = await fetch(`${devUrl}/repository.json`);
|
||||
if (!repoResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch repository metadata: ${repoResponse.status}`,
|
||||
);
|
||||
}
|
||||
return (await repoResponse.json()) as {
|
||||
description?: string;
|
||||
author?: { name?: string };
|
||||
urls?: { git?: string };
|
||||
trust?: string;
|
||||
};
|
||||
})();
|
||||
|
||||
// For core repository, create a single plugin with all block groups
|
||||
if (repoUrl.includes("core.hristudio.com")) {
|
||||
const indexResponse = await fetch(`${devUrl}/plugins/index.json`);
|
||||
if (!indexResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch plugin index: ${indexResponse.status}`,
|
||||
);
|
||||
}
|
||||
const indexData = (await indexResponse.json()) as {
|
||||
if (isCore) {
|
||||
const indexData = JSON.parse(
|
||||
await readFile(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"hristudio-core",
|
||||
"plugins",
|
||||
"index.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
) as {
|
||||
plugins?: Array<{ blockCount?: number }>;
|
||||
};
|
||||
|
||||
@@ -203,7 +228,7 @@ async function main() {
|
||||
.returning();
|
||||
console.log(`✅ Created ${insertedRobots.length} robots`);
|
||||
|
||||
// Create users
|
||||
// Create users (Bucknell University team)
|
||||
console.log("👥 Creating users...");
|
||||
const hashedPassword = await bcrypt.hash("password123", 12);
|
||||
|
||||
@@ -216,29 +241,29 @@ async function main() {
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
name: "Dr. Alice Rodriguez",
|
||||
email: "alice.rodriguez@university.edu",
|
||||
name: "Prof. Dana Miller",
|
||||
email: "dana.miller@bucknell.edu",
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(),
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
name: "Dr. Bob Chen",
|
||||
email: "bob.chen@research.org",
|
||||
name: "Chris Lee",
|
||||
email: "chris.lee@bucknell.edu",
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(),
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
name: "Emily Watson",
|
||||
email: "emily.watson@lab.edu",
|
||||
name: "Priya Singh",
|
||||
email: "priya.singh@bucknell.edu",
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(),
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
name: "Dr. Maria Santos",
|
||||
email: "maria.santos@tech.edu",
|
||||
name: "Jordan White",
|
||||
email: "jordan.white@bucknell.edu",
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(),
|
||||
image: null,
|
||||
@@ -321,32 +346,23 @@ async function main() {
|
||||
console.log("📚 Creating studies...");
|
||||
const studies = [
|
||||
{
|
||||
name: "Human-Robot Collaboration Study",
|
||||
name: "NAO Classroom Interaction",
|
||||
description:
|
||||
"Investigating collaborative tasks between humans and robots in shared workspace environments",
|
||||
institution: "MIT Computer Science",
|
||||
irbProtocol: "IRB-2024-001",
|
||||
"Evaluating student engagement with NAO-led prompts during lab sessions",
|
||||
institution: "Bucknell University",
|
||||
irbProtocol: "BU-IRB-2025-NAO-01",
|
||||
status: "active" as const,
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
name: "Robot Navigation Study",
|
||||
name: "Wizard-of-Oz Dialogue Study",
|
||||
description:
|
||||
"A comprehensive study on robot navigation and obstacle avoidance in dynamic environments",
|
||||
institution: "Stanford HCI Lab",
|
||||
irbProtocol: "IRB-2024-002",
|
||||
"WoZ-controlled NAO to assess timing and tone in instructional feedback",
|
||||
institution: "Bucknell University",
|
||||
irbProtocol: "BU-IRB-2025-WOZ-02",
|
||||
status: "draft" as const,
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
name: "Social Robot Interaction Study",
|
||||
description:
|
||||
"Examining social dynamics between humans and humanoid robots in educational settings",
|
||||
institution: "Carnegie Mellon",
|
||||
irbProtocol: "IRB-2024-003",
|
||||
status: "active" as const,
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
];
|
||||
|
||||
const insertedStudies = await db
|
||||
@@ -411,6 +427,27 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
// Install NAO plugin for first study if available
|
||||
console.log("🤝 Installing NAO plugin (if available)...");
|
||||
const naoPlugin = await db
|
||||
.select()
|
||||
.from(schema.plugins)
|
||||
.where(eq(schema.plugins.name, "NAO Humanoid Robot"))
|
||||
.limit(1);
|
||||
if (naoPlugin.length > 0 && insertedStudies[0]) {
|
||||
await db.insert(schema.studyPlugins).values({
|
||||
studyId: insertedStudies[0].id,
|
||||
pluginId: naoPlugin[0]!.id,
|
||||
configuration: { voice: "nao-tts", locale: "en-US" },
|
||||
installedBy: seanUser.id,
|
||||
});
|
||||
console.log("✅ Installed NAO plugin in first study");
|
||||
} else {
|
||||
console.log(
|
||||
"ℹ️ NAO plugin not found in repository sync; continuing without it",
|
||||
);
|
||||
}
|
||||
|
||||
// Create some participants
|
||||
console.log("👤 Creating participants...");
|
||||
const participants = [];
|
||||
@@ -447,24 +484,313 @@ async function main() {
|
||||
.returning();
|
||||
console.log(`✅ Created ${insertedParticipants.length} participants`);
|
||||
|
||||
// Create basic experiments
|
||||
// Create experiments (include one NAO-based)
|
||||
console.log("🧪 Creating experiments...");
|
||||
const experiments = insertedStudies.map((study, i) => ({
|
||||
studyId: study.id,
|
||||
name: `Basic Interaction Protocol ${i + 1}`,
|
||||
description: `A simple human-robot interaction experiment for ${study.name}`,
|
||||
version: 1,
|
||||
status: "ready" as const,
|
||||
estimatedDuration: 30 + i * 10,
|
||||
createdBy: seanUser.id,
|
||||
}));
|
||||
const experiments = [
|
||||
{
|
||||
studyId: insertedStudies[0]!.id,
|
||||
name: "Basic Interaction Protocol 1",
|
||||
description: "Wizard prompts + NAO speaks demo script",
|
||||
version: 1,
|
||||
status: "ready" as const,
|
||||
estimatedDuration: 25,
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
studyId: insertedStudies[1]!.id,
|
||||
name: "Dialogue Timing Pilot",
|
||||
description: "Compare response timing variants under WoZ control",
|
||||
version: 1,
|
||||
status: "draft" as const,
|
||||
estimatedDuration: 35,
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
];
|
||||
|
||||
const insertedExperiments = await db
|
||||
.insert(schema.experiments)
|
||||
.values(experiments)
|
||||
.values(
|
||||
experiments.map((e) => ({
|
||||
...e,
|
||||
visualDesign: {
|
||||
// minimal starter design; steps optionally overwritten below for DB tables
|
||||
steps: [],
|
||||
version: 1,
|
||||
lastSaved: new Date().toISOString(),
|
||||
},
|
||||
})),
|
||||
)
|
||||
.returning();
|
||||
console.log(`✅ Created ${insertedExperiments.length} experiments`);
|
||||
|
||||
// Seed a richer, multi-step design for the first experiment (wizard + robot)
|
||||
if (insertedExperiments[0]) {
|
||||
const exp = insertedExperiments[0];
|
||||
|
||||
// Step 1: Wizard demo + robot speaks
|
||||
const step1 = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp.id,
|
||||
name: "Step 1 • Introduction & Object Demo",
|
||||
description: "Wizard greets participant and demonstrates an object",
|
||||
type: "wizard",
|
||||
orderIndex: 0,
|
||||
required: true,
|
||||
conditions: {},
|
||||
})
|
||||
.returning();
|
||||
const step1Id = step1[0]!.id;
|
||||
|
||||
// Action 1.1: Wizard shows object
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: step1Id,
|
||||
name: "show object",
|
||||
description: "Wizard presents or demonstrates an object",
|
||||
type: "wizard_show_object",
|
||||
orderIndex: 0,
|
||||
parameters: { object: "Cube" },
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
// Resolve NAO plugin id/version for namespaced action type
|
||||
const naoDbPlugin1 = await db
|
||||
.select({ id: schema.plugins.id, version: schema.plugins.version })
|
||||
.from(schema.plugins)
|
||||
.where(eq(schema.plugins.name, "NAO Humanoid Robot"))
|
||||
.limit(1);
|
||||
const naoPluginRow1 = naoDbPlugin1[0];
|
||||
|
||||
// Action 1.2: Robot/NAO says text (or wizard says fallback)
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: step1Id,
|
||||
name: naoPluginRow1 ? "NAO Say Text" : "Wizard Say",
|
||||
description: naoPluginRow1
|
||||
? "Make the robot speak using text-to-speech"
|
||||
: "Wizard speaks to participant",
|
||||
type: naoPluginRow1 ? `${naoPluginRow1.id}.say_text` : "wizard_say",
|
||||
orderIndex: 1,
|
||||
parameters: naoPluginRow1
|
||||
? { text: "Hello, I am NAO. Let's begin!", speed: 110, volume: 0.75 }
|
||||
: { message: "Hello! Let's begin the session.", tone: "friendly" },
|
||||
sourceKind: naoPluginRow1 ? "plugin" : "core",
|
||||
pluginId: naoPluginRow1 ? naoPluginRow1.id : null,
|
||||
pluginVersion: naoPluginRow1 ? naoPluginRow1.version : null,
|
||||
category: naoPluginRow1 ? "robot" : "wizard",
|
||||
transport: naoPluginRow1 ? "rest" : "internal",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
// Step 2: Wait for response (wizard)
|
||||
const step2 = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp.id,
|
||||
name: "Step 2 • Participant Response",
|
||||
description: "Wizard waits for the participant's response",
|
||||
type: "wizard",
|
||||
orderIndex: 1,
|
||||
required: true,
|
||||
conditions: {},
|
||||
})
|
||||
.returning();
|
||||
const step2Id = step2[0]!.id;
|
||||
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: step2Id,
|
||||
name: "wait for response",
|
||||
description: "Wizard waits for participant to respond",
|
||||
type: "wizard_wait_for_response",
|
||||
orderIndex: 0,
|
||||
parameters: {
|
||||
response_type: "verbal",
|
||||
timeout: 20,
|
||||
prompt_text: "What did you notice about the object?",
|
||||
},
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
// Step 3: Robot LED feedback (or record note fallback)
|
||||
const step3 = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp.id,
|
||||
name: "Step 3 • Robot Feedback",
|
||||
description: "Provide feedback using robot LED color or record note",
|
||||
type: "robot",
|
||||
orderIndex: 2,
|
||||
required: false,
|
||||
conditions: {},
|
||||
})
|
||||
.returning();
|
||||
const step3Id = step3[0]!.id;
|
||||
|
||||
const naoDbPlugin2 = await db
|
||||
.select({ id: schema.plugins.id, version: schema.plugins.version })
|
||||
.from(schema.plugins)
|
||||
.where(eq(schema.plugins.name, "NAO Humanoid Robot"))
|
||||
.limit(1);
|
||||
const naoPluginRow2 = naoDbPlugin2[0];
|
||||
|
||||
if (naoPluginRow2) {
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: step3Id,
|
||||
name: "Set LED Color",
|
||||
description: "Change NAO's eye LEDs to reflect state",
|
||||
type: `${naoPluginRow2.id}.set_led_color`,
|
||||
orderIndex: 0,
|
||||
parameters: { color: "blue", intensity: 0.6 },
|
||||
sourceKind: "plugin",
|
||||
pluginId: naoPluginRow2.id,
|
||||
pluginVersion: naoPluginRow2.version,
|
||||
category: "robot",
|
||||
transport: "rest",
|
||||
retryable: false,
|
||||
});
|
||||
} else {
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: step3Id,
|
||||
name: "record note",
|
||||
description: "Wizard records an observation",
|
||||
type: "wizard_record_note",
|
||||
orderIndex: 0,
|
||||
parameters: {
|
||||
note_type: "observation",
|
||||
prompt: "No robot available",
|
||||
},
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Seed a richer design for the second experiment (timers + conditional/parallel)
|
||||
if (insertedExperiments[1]) {
|
||||
const exp2 = insertedExperiments[1];
|
||||
|
||||
// Step A: Baseline prompt
|
||||
const stepA = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp2.id,
|
||||
name: "Step A • Baseline Prompt",
|
||||
description: "Wizard provides a baseline instruction",
|
||||
type: "wizard",
|
||||
orderIndex: 0,
|
||||
required: true,
|
||||
conditions: {},
|
||||
})
|
||||
.returning();
|
||||
const stepAId = stepA[0]!.id;
|
||||
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: stepAId,
|
||||
name: "say",
|
||||
description: "Wizard speaks to participant",
|
||||
type: "wizard_say",
|
||||
orderIndex: 0,
|
||||
parameters: {
|
||||
message: "We'll try a short timing task next.",
|
||||
tone: "instructional",
|
||||
},
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
// Step B: Parallel gestures/animation
|
||||
const stepB = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp2.id,
|
||||
name: "Step B • Parallel Cues",
|
||||
description: "Provide multiple cues at once (gesture + animation)",
|
||||
type: "parallel",
|
||||
orderIndex: 1,
|
||||
required: false,
|
||||
conditions: {},
|
||||
})
|
||||
.returning();
|
||||
const stepBId = stepB[0]!.id;
|
||||
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: stepBId,
|
||||
name: "gesture",
|
||||
description: "Wizard performs a physical gesture",
|
||||
type: "wizard_gesture",
|
||||
orderIndex: 0,
|
||||
parameters: { type: "point", direction: "participant" },
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
|
||||
const naoDbPluginB = await db
|
||||
.select({ id: schema.plugins.id, version: schema.plugins.version })
|
||||
.from(schema.plugins)
|
||||
.where(eq(schema.plugins.name, "NAO Humanoid Robot"))
|
||||
.limit(1);
|
||||
const naoPluginRowB = naoDbPluginB[0];
|
||||
|
||||
if (naoPluginRowB) {
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: stepBId,
|
||||
name: "Play Animation",
|
||||
description: "NAO plays a greeting animation",
|
||||
type: `${naoPluginRowB.id}.play_animation`,
|
||||
orderIndex: 1,
|
||||
parameters: { animation: "Hello" },
|
||||
sourceKind: "plugin",
|
||||
pluginId: naoPluginRowB.id,
|
||||
pluginVersion: naoPluginRowB.version,
|
||||
category: "robot",
|
||||
transport: "rest",
|
||||
retryable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Step C: Conditional follow-up after a brief wait
|
||||
const stepC = await db
|
||||
.insert(schema.steps)
|
||||
.values({
|
||||
experimentId: exp2.id,
|
||||
name: "Step C • Conditional Follow-up",
|
||||
description: "Proceed based on observed response after timer",
|
||||
type: "conditional",
|
||||
orderIndex: 2,
|
||||
required: false,
|
||||
conditions: { predicate: "response_received", timer_ms: 3000 },
|
||||
})
|
||||
.returning();
|
||||
const stepCId = stepC[0]!.id;
|
||||
|
||||
await db.insert(schema.actions).values({
|
||||
stepId: stepCId,
|
||||
name: "record note",
|
||||
description: "Wizard records a follow-up note",
|
||||
type: "wizard_record_note",
|
||||
orderIndex: 0,
|
||||
parameters: {
|
||||
note_type: "participant_response",
|
||||
prompt: "Response after parallel cues",
|
||||
},
|
||||
sourceKind: "core",
|
||||
category: "wizard",
|
||||
transport: "internal",
|
||||
retryable: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Create some trials for dashboard demo
|
||||
console.log("🧪 Creating sample trials...");
|
||||
const trials = [];
|
||||
@@ -526,6 +852,65 @@ async function main() {
|
||||
.returning();
|
||||
console.log(`✅ Created ${insertedTrials.length} trials`);
|
||||
|
||||
// Create trial events time series for richer dashboards
|
||||
const trialEventRows = [];
|
||||
for (const t of insertedTrials) {
|
||||
const baseStart = t.startedAt ?? new Date(Date.now() - 60 * 60 * 1000);
|
||||
const t1 = new Date(baseStart.getTime() - 2 * 60 * 1000); // 2 min before start
|
||||
const t2 = new Date(baseStart.getTime()); // start
|
||||
const t3 = new Date(baseStart.getTime() + 3 * 60 * 1000); // +3 min
|
||||
const t4 = new Date(baseStart.getTime() + 8 * 60 * 1000); // +8 min
|
||||
const t5 =
|
||||
t.completedAt ?? new Date(baseStart.getTime() + 15 * 60 * 1000); // completion
|
||||
|
||||
trialEventRows.push(
|
||||
{
|
||||
trialId: t.id,
|
||||
eventType: "wizard_prompt_shown",
|
||||
actionId: null,
|
||||
timestamp: t1,
|
||||
data: { prompt: "Welcome and object demo" },
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
trialId: t.id,
|
||||
eventType: "action_started",
|
||||
actionId: null,
|
||||
timestamp: t2,
|
||||
data: { label: "demo_start" },
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
trialId: t.id,
|
||||
eventType: "robot_action_executed",
|
||||
actionId: null,
|
||||
timestamp: t3,
|
||||
data: { robot: "nao", action: "speak" },
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
trialId: t.id,
|
||||
eventType: "action_completed",
|
||||
actionId: null,
|
||||
timestamp: t4,
|
||||
data: { label: "demo_complete" },
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
{
|
||||
trialId: t.id,
|
||||
eventType: "trial_note",
|
||||
actionId: null,
|
||||
timestamp: t5,
|
||||
data: { summary: "Session ended successfully" },
|
||||
createdBy: seanUser.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (trialEventRows.length) {
|
||||
await db.insert(schema.trialEvents).values(trialEventRows);
|
||||
console.log(`✅ Created ${trialEventRows.length} trial events`);
|
||||
}
|
||||
|
||||
// Create some activity logs for dashboard demo
|
||||
console.log("📝 Creating activity logs...");
|
||||
const activityEntries = [];
|
||||
@@ -612,7 +997,7 @@ async function main() {
|
||||
console.log("\n✅ Seed script completed successfully!");
|
||||
console.log("\n📊 Created:");
|
||||
console.log(` • ${insertedRobots.length} robots`);
|
||||
console.log(` • ${insertedUsers.length} users`);
|
||||
console.log(` • ${insertedUsers.length} users (Bucknell)`);
|
||||
console.log(` • ${insertedRepos.length} plugin repositories`);
|
||||
console.log(` • ${totalPlugins} plugins (via repository sync)`);
|
||||
console.log(` • ${insertedStudies.length} studies`);
|
||||
|
||||
Reference in New Issue
Block a user