diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts
index f7637dd..d995a2a 100755
--- a/scripts/seed-dev.ts
+++ b/scripts/seed-dev.ts
@@ -1,6 +1,6 @@
import bcrypt from "bcryptjs";
import { drizzle } from "drizzle-orm/postgres-js";
-import { eq, sql } from "drizzle-orm";
+import { sql } from "drizzle-orm";
import postgres from "postgres";
import * as schema from "../src/server/db/schema";
@@ -9,49 +9,19 @@ const connectionString = process.env.DATABASE_URL!;
const connection = postgres(connectionString);
const db = drizzle(connection, { schema });
-// --- NAO6 Plugin Definitions (Inlined for reliability) ---
+// --- NAO6 Plugin Definitions (Synced from seed-nao6-plugin.ts) ---
const NAO_PLUGIN_DEF = {
name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0",
description: "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2.",
actions: [
- {
- id: "nao_speak",
- name: "Speak Text",
- category: "speech",
- parametersSchema: {
- type: "object",
- properties: {
- text: { type: "string" },
- volume: { type: "number", default: 0.7 }
- },
- required: ["text"]
- }
- },
- {
- id: "nao_gesture",
- name: "Perform Gesture",
- category: "interaction",
- parametersSchema: {
- type: "object",
- properties: {
- gesture: { type: "string", enum: ["wave", "bow", "point"] },
- speed: { type: "number", default: 0.8 }
- }
- }
- },
- {
- id: "nao_look_at",
- name: "Look At",
- category: "movement",
- parametersSchema: {
- type: "object",
- properties: {
- target: { type: "string", enum: ["participant", "screen", "away"] },
- duration: { type: "number", default: 2.0 }
- }
- }
- }
+ { id: "nao_speak", name: "Speak Text", category: "speech", parametersSchema: { type: "object", properties: { text: { type: "string" }, volume: { type: "number", default: 0.7 } }, required: ["text"] } },
+ { id: "nao_gesture", name: "Perform Gesture", category: "interaction", parametersSchema: { type: "object", properties: { gesture: { type: "string", enum: ["wave", "bow", "point"] }, speed: { type: "number", default: 0.8 } } } },
+ { id: "nao_look_at", name: "Look At", category: "movement", parametersSchema: { type: "object", properties: { target: { type: "string", enum: ["participant", "screen", "away"] }, duration: { type: "number", default: 2.0 } } } },
+ { id: "nao_nod", name: "Nod Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } },
+ { id: "nao_shake_head", name: "Shake Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } },
+ { id: "nao_bow", name: "Bow", category: "interaction", parametersSchema: { type: "object", properties: {} } },
+ { id: "nao_open_hand", name: "Present (Open Hand)", category: "interaction", parametersSchema: { type: "object", properties: { hand: { type: "string", enum: ["left", "right", "both"], default: "right" } } } }
]
};
@@ -59,9 +29,8 @@ async function main() {
console.log("๐ฑ Starting realistic seed script...");
try {
- // 1. Clean existing data
+ // 1. Clean existing data (Full Wipe)
console.log("๐งน Cleaning existing data...");
- // Delete in reverse dependency order
await db.delete(schema.mediaCaptures).where(sql`1=1`);
await db.delete(schema.trialEvents).where(sql`1=1`);
await db.delete(schema.trials).where(sql`1=1`);
@@ -87,7 +56,7 @@ async function main() {
email: "sean@soconnor.dev",
password: hashedPassword,
emailVerified: new Date(),
- image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sean", // Consistent avatar
+ image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sean",
}).returning();
const [researcherUser] = await db.insert(schema.users).values({
@@ -100,10 +69,7 @@ async function main() {
if (!adminUser) throw new Error("Failed to create admin user");
- await db.insert(schema.userSystemRoles).values({
- userId: adminUser.id,
- role: "administrator",
- });
+ await db.insert(schema.userSystemRoles).values({ userId: adminUser.id, role: "administrator" });
// 3. Create Robots & Plugins
console.log("๐ค Creating robots and plugins...");
@@ -121,13 +87,13 @@ async function main() {
url: "https://github.com/hristudio/plugins",
description: "Official verified plugins",
trustLevel: "official",
- status: "active",
+ isEnabled: true,
+ isOfficial: true,
createdBy: adminUser.id,
}).returning();
const [naoPlugin] = await db.insert(schema.plugins).values({
robotId: naoRobot!.id,
- repositoryId: naoRepo!.id,
name: NAO_PLUGIN_DEF.name,
version: NAO_PLUGIN_DEF.version,
description: NAO_PLUGIN_DEF.description,
@@ -136,22 +102,17 @@ async function main() {
status: "active",
repositoryUrl: naoRepo!.url,
actionDefinitions: NAO_PLUGIN_DEF.actions,
- configurationSchema: {
- type: "object",
- properties: {
- robotIp: { type: "string", default: "192.168.1.100" }
- }
- },
+ configurationSchema: { type: "object", properties: { robotIp: { type: "string", default: "192.168.1.100" } } },
metadata: { category: "robot_control" }
}).returning();
- // 4. Create Study & Experiment
- console.log("๐ Creating study and experiment...");
+ // 4. Create Study & Experiment - Comparative WoZ Study
+ console.log("๐ Creating 'Comparative WoZ Study'...");
const [study] = await db.insert(schema.studies).values({
- name: "Social Robot Attention Study",
- description: "Investigating the effect of robot gaze on participant attention retention.",
+ name: "Comparative WoZ Study",
+ description: "Comparison of HRIStudio vs Choregraphe for The Interactive Storyteller scenario.",
institution: "Bucknell University",
- irbProtocol: "2024-HRI-055",
+ irbProtocol: "2024-HRI-COMP",
status: "active",
createdBy: adminUser.id,
}).returning();
@@ -170,147 +131,203 @@ async function main() {
const [experiment] = await db.insert(schema.experiments).values({
studyId: study!.id,
- name: "Attention Gaze Protocol A",
- description: "Condition A: Robot maintains eye contact.",
+ name: "The Interactive Storyteller",
+ description: "A storytelling scenario where the robot tells a story and asks questions to the participant.",
version: 1,
- status: "ready", // Correct enum value
+ status: "ready",
+ robotId: naoRobot!.id,
createdBy: adminUser.id,
}).returning();
- // 5. Participants
- console.log("๐ค Creating participants...");
+ // 5. Create Steps & Actions (The Interactive Storyteller Protocol)
+ console.log("๐ฌ Creating experiment steps (Interactive Storyteller)...");
+
+ // --- Step 1: The Hook ---
+ const [step1] = await db.insert(schema.steps).values({
+ experimentId: experiment!.id,
+ name: "The Hook",
+ description: "Initial greeting and engagement",
+ type: "robot",
+ orderIndex: 0,
+ required: true,
+ durationEstimate: 30
+ }).returning();
+
+ await db.insert(schema.actions).values([
+ {
+ stepId: step1!.id,
+ name: "Greet Participant",
+ type: "nao6.nao_speak",
+ orderIndex: 0,
+ parameters: { text: "Hello there! I have a wonderful story to share with you today.", volume: 0.8 },
+ pluginId: naoPlugin!.id,
+ category: "speech",
+ retryable: true
+ },
+ {
+ stepId: step1!.id,
+ name: "Wave Greeting",
+ type: "nao6.nao_gesture",
+ orderIndex: 1,
+ parameters: { gesture: "wave" },
+ pluginId: naoPlugin!.id,
+ category: "interaction",
+ retryable: true
+ }
+ ]);
+
+ // --- Step 2: The Narrative (Part 1) ---
+ const [step2] = await db.insert(schema.steps).values({
+ experimentId: experiment!.id,
+ name: "The Narrative - Part 1",
+ description: "Robot tells the first part of the story",
+ type: "robot",
+ orderIndex: 1,
+ required: true,
+ durationEstimate: 60
+ }).returning();
+
+ await db.insert(schema.actions).values([
+ {
+ stepId: step2!.id,
+ name: "Tell Story Part 1",
+ type: "nao6.nao_speak",
+ orderIndex: 0,
+ parameters: { text: "Once upon a time, in a land far away, there lived a curious robot named Alpha.", volume: 0.8 },
+ pluginId: naoPlugin!.id,
+ category: "speech"
+ },
+ {
+ stepId: step2!.id,
+ name: "Present Gesture",
+ type: "nao6.nao_open_hand",
+ orderIndex: 1,
+ parameters: { hand: "right" },
+ pluginId: naoPlugin!.id,
+ category: "interaction"
+ }
+ ]);
+
+ // --- Step 3: Comprehension Check (Wizard Decision) ---
+ // Note: In a real visual designer, this would be a Branch/Conditional.
+ // Here we model it as a Wizard Step where they explicitly choose the next robot action.
+ const [step3] = await db.insert(schema.steps).values({
+ experimentId: experiment!.id,
+ name: "Comprehension Check",
+ description: "Wizard verifies participant understanding",
+ type: "wizard",
+ orderIndex: 2,
+ required: true,
+ durationEstimate: 45
+ }).returning();
+
+ await db.insert(schema.actions).values([
+ {
+ stepId: step3!.id,
+ name: "Ask Question",
+ type: "nao6.nao_speak",
+ orderIndex: 0,
+ parameters: { text: "What was the robot's name?", volume: 0.8 },
+ pluginId: naoPlugin!.id,
+ category: "speech"
+ },
+ {
+ stepId: step3!.id,
+ name: "Wait for Wizard Input",
+ type: "core.wait_for_response",
+ orderIndex: 1,
+ parameters: { prompt: "Did participant answer 'Alpha'?", options: ["Yes", "No"] },
+ sourceKind: "core",
+ category: "wizard"
+ }
+ ]);
+
+ // --- Step 4: Feedback (Positive/Negative branches implied) ---
+ // For linear seed, we just add the Positive feedback step
+ const [step4] = await db.insert(schema.steps).values({
+ experimentId: experiment!.id,
+ name: "Positive Feedback",
+ description: "Correct answer response",
+ type: "robot",
+ orderIndex: 3,
+ required: true,
+ durationEstimate: 15
+ }).returning();
+
+ await db.insert(schema.actions).values([
+ {
+ stepId: step4!.id,
+ name: "Nod Affirmation",
+ type: "nao6.nao_nod",
+ orderIndex: 0,
+ parameters: { speed: 1.0 },
+ pluginId: naoPlugin!.id,
+ category: "interaction"
+ },
+ {
+ stepId: step4!.id,
+ name: "Say Correct",
+ type: "nao6.nao_speak",
+ orderIndex: 1,
+ parameters: { text: "That is correct! Well done.", volume: 0.8 },
+ pluginId: naoPlugin!.id,
+ category: "speech"
+ }
+ ]);
+
+ // --- Step 5: Conclusion ---
+ const [step5] = await db.insert(schema.steps).values({
+ experimentId: experiment!.id,
+ name: "Conclusion",
+ description: "Wrap up the story",
+ type: "robot",
+ orderIndex: 4,
+ required: true,
+ durationEstimate: 30
+ }).returning();
+
+ await db.insert(schema.actions).values([
+ {
+ stepId: step5!.id,
+ name: "Finish Story",
+ type: "nao6.nao_speak",
+ orderIndex: 0,
+ parameters: { text: "Alpha explored the world and learned many things. The end.", volume: 0.8 },
+ pluginId: naoPlugin!.id,
+ category: "speech"
+ },
+ {
+ stepId: step5!.id,
+ name: "Bow Goodbye",
+ type: "nao6.nao_bow",
+ orderIndex: 1,
+ parameters: {},
+ pluginId: naoPlugin!.id,
+ category: "interaction"
+ }
+ ]);
+
+ // 6. Participants (N=20 for study)
+ console.log("๐ค Creating 20 participants for N=20 study...");
const participants = [];
- for (let i = 1; i <= 5; i++) {
+ for (let i = 1; i <= 20; i++) {
participants.push({
studyId: study!.id,
participantCode: `P${100 + i}`,
name: `Participant ${100 + i}`,
consentGiven: true,
consentGivenAt: new Date(),
- notes: i % 2 === 0 ? "Condition A" : "Condition B"
+ notes: i % 2 === 0 ? "Condition: HRIStudio" : "Condition: Choregraphe"
});
}
const insertedParticipants = await db.insert(schema.participants).values(participants).returning();
- // 6. Trials & Realistic Logs
- console.log("๐งช Generating trials with dense logs...");
-
- for (const p of insertedParticipants) {
- const isCompleted = Math.random() > 0.2; // 80% completed
- const trialStart = new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000);
- const durationSecs = 300 + Math.floor(Math.random() * 300); // 5-10 mins
- const trialEnd = new Date(trialStart.getTime() + durationSecs * 1000);
-
- const [trial] = await db.insert(schema.trials).values({
- studyId: study!.id, // Ensure referencing study if schema allows, otherwise via experiment
- experimentId: experiment!.id,
- participantId: p.id,
- wizardId: adminUser.id,
- sessionNumber: 1,
- status: isCompleted ? "completed" : "in_progress",
- startedAt: trialStart,
- completedAt: isCompleted ? trialEnd : null,
- duration: isCompleted ? durationSecs : null,
- createdBy: adminUser.id,
- }).returning();
-
- // Generate dense events
- let currentTime = trialStart.getTime();
- const events = [];
-
- // Event: Trial Start
- events.push({
- trialId: trial!.id,
- eventType: "system",
- timestamp: new Date(currentTime),
- data: { message: "Trial started", system_check: "nominal" },
- createdBy: adminUser.id
- });
- currentTime += 2000;
-
- // Event: Wizard Introduction (Wizard Action)
- events.push({
- trialId: trial!.id,
- eventType: "wizard_action",
- timestamp: new Date(currentTime),
- data: { action: "read_script", section: "intro" },
- createdBy: adminUser.id
- });
- currentTime += 5000;
-
- // Loop for interaction events
- const interactionCount = 15;
- for (let k = 0; k < interactionCount; k++) {
- // Gap
- currentTime += 2000 + Math.random() * 5000;
-
- // 1. Robot Action (Speak/Gesture)
- const actionType = Math.random() > 0.6 ? "nao_gesture" : "nao_speak";
- events.push({
- trialId: trial!.id,
- eventType: "robot_action",
- timestamp: new Date(currentTime),
- data: actionType === "nao_gesture"
- ? { plugin: "nao6", action: "gesture", type: "wave", speed: 0.8 }
- : { plugin: "nao6", action: "speak", text: "Please look at the screen now.", volume: 0.7 },
- createdBy: adminUser.id
- });
-
- // Execution log
- currentTime += 100;
- events.push({
- trialId: trial!.id,
- eventType: "system",
- timestamp: new Date(currentTime),
- data: { source: "ros2_bridge", topic: "/nao/cmd", status: "sent" },
- createdBy: adminUser.id
- });
-
- // 2. Participant Reaction (Simulated Logs/Wizard Note)
- if (Math.random() > 0.7) {
- currentTime += 3000;
- events.push({
- trialId: trial!.id,
- eventType: "wizard_note",
- timestamp: new Date(currentTime),
- data: { note: "Participant looked away briefly.", tag: "distraction" },
- createdBy: adminUser.id
- });
- }
- }
-
- // End
- if (isCompleted) {
- events.push({
- trialId: trial!.id,
- eventType: "system",
- timestamp: trialEnd,
- data: { message: "Trial completed successfully" },
- createdBy: adminUser.id
- });
-
- // Fake Media Capture
- await db.insert(schema.mediaCaptures).values({
- trialId: trial!.id,
- mediaType: "video", // Changed from "video/webm" to general "video"
- storagePath: `trials/${trial!.id}/recording.webm`,
- fileSize: 15480000 + Math.floor(Math.random() * 5000000), // ~15-20MB
- duration: durationSecs,
- startTimestamp: trialStart,
- endTimestamp: trialEnd,
- });
- }
-
- await db.insert(schema.trialEvents).values(events);
- }
-
console.log("\nโ
Database seeded successfully!");
console.log(`Summary:`);
console.log(`- 1 Admin User (sean@soconnor.dev)`);
- console.log(`- 1 Study (Social Robot Attention)`);
+ console.log(`- Study: 'Comparative WoZ Study'`);
+ console.log(`- Experiment: 'The Interactive Storyteller' (${5} steps created)`);
console.log(`- ${insertedParticipants.length} Participants`);
- console.log(`- ${insertedParticipants.length} Trials created (mixed status)`);
- console.log(`- ~20 Events per trial`);
} catch (error) {
console.error("โ Seeding failed:", error);
diff --git a/scripts/seed-nao6-plugin.ts b/scripts/seed-nao6-plugin.ts
index 41e4d8f..6a2782c 100644
--- a/scripts/seed-nao6-plugin.ts
+++ b/scripts/seed-nao6-plugin.ts
@@ -112,11 +112,10 @@ async function seedNAO6Plugin() {
description:
"Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments",
trustLevel: "official",
- isActive: true,
- isPublic: true,
+ isEnabled: true,
+ isOfficial: true,
createdBy: userId,
- status: "active",
- lastSyncedAt: new Date(),
+ lastSyncAt: new Date(),
metadata: {
author: {
name: "HRIStudio Team",
@@ -487,10 +486,83 @@ async function seedNAO6Plugin() {
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 = {
- repositoryId: repoId,
robotId: robotId,
name: "NAO6 Robot (Enhanced ROS2 Integration)",
version: "2.0.0",
diff --git a/scripts/verify-study-readiness.ts b/scripts/verify-study-readiness.ts
new file mode 100644
index 0000000..2e4ab91
--- /dev/null
+++ b/scripts/verify-study-readiness.ts
@@ -0,0 +1,87 @@
+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++) {
+ if (steps[i].name !== expectedSteps[i]) {
+ console.error(`โ Step mismatch at index ${i}. Expected '${expectedSteps[i]}', got '${steps[i].name}'`);
+ } else {
+ console.log(`โ
Step ${i + 1}: ${steps[i].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);
+});
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
index 22c8bb8..e53f64c 100755
--- a/src/app/auth/signin/page.tsx
+++ b/src/app/auth/signin/page.tsx
@@ -6,14 +6,15 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
+import { Logo } from "~/components/ui/logo";
export default function SignInPage() {
const [email, setEmail] = useState("");
@@ -52,30 +53,35 @@ export default function SignInPage() {
};
return (
-
-
+
+ {/* Background Gradients */}
+
+
+
+
{/* Header */}
-
-
HRIStudio
+
+
-
+
Welcome back
+
Sign in to your research account
{/* Sign In Card */}
-
+
- Welcome back
+ Sign In
- Enter your credentials to access your account
+ Enter your credentials to access the platform
-
+
Don't have an account?{" "}
- Sign up here
+ Sign up
{/* Footer */}
-
+
- ยฉ 2024 HRIStudio. A platform for Human-Robot Interaction research.
+ © {new Date().getFullYear()} HRIStudio. All rights reserved.
diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx
index 71ff81e..fe18614 100755
--- a/src/app/auth/signup/page.tsx
+++ b/src/app/auth/signup/page.tsx
@@ -5,14 +5,15 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
+import { Logo } from "~/components/ui/logo";
import { api } from "~/trpc/react";
export default function SignUpPage() {
@@ -55,30 +56,35 @@ export default function SignUpPage() {
};
return (
-
-
+
+ {/* Background Gradients */}
+
+
+
+
{/* Header */}
-
-
HRIStudio
+
+
-
- Create your research account
+
Create an account
+
+ Start your journey in HRI research
{/* Sign Up Card */}
-
+
- Get started
+ Sign Up
- Create your account to begin your HRI research
+ Enter your details to create your research account
-
- Password
- setPassword(e.target.value)}
- required
- disabled={createUser.isPending}
- minLength={6}
- />
-
+
+
+ Password
+ setPassword(e.target.value)}
+ required
+ disabled={createUser.isPending}
+ minLength={6}
+ className="bg-background/50"
+ />
+
-
{createUser.isPending ? "Creating account..." : "Create Account"}
-
+
Already have an account?{" "}
- Sign in here
+ Sign in
{/* Footer */}
-
+
- ยฉ 2024 HRIStudio. A platform for Human-Robot Interaction research.
+ © {new Date().getFullYear()} HRIStudio. All rights reserved.
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index ab3a234..a27f097 100755
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -2,17 +2,23 @@
import * as React from "react";
import Link from "next/link";
-import {
- Building,
- FlaskConical,
- TestTube,
- Users,
- Calendar,
- Clock,
- AlertCircle,
- CheckCircle2,
-} from "lucide-react";
+import { format } from "date-fns";
import { formatDistanceToNow } from "date-fns";
+import {
+ Activity,
+ ArrowRight,
+ Bot,
+ Calendar,
+ CheckCircle2,
+ Clock,
+ LayoutDashboard,
+ MoreHorizontal,
+ Play,
+ PlayCircle,
+ Plus,
+ Settings,
+ Users,
+} from "lucide-react";
import { Button } from "~/components/ui/button";
import {
@@ -22,7 +28,14 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
-import { Badge } from "~/components/ui/badge";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/components/ui/dropdown-menu";
import { Progress } from "~/components/ui/progress";
import {
Select,
@@ -31,375 +44,270 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
+import { Badge } from "~/components/ui/badge";
+import { ScrollArea } from "~/components/ui/scroll-area";
import { api } from "~/trpc/react";
-// Dashboard Overview Cards
-function OverviewCards({ studyFilter }: { studyFilter: string | null }) {
- const { data: stats, isLoading } = api.dashboard.getStats.useQuery({
- studyId: studyFilter ?? undefined,
- });
-
- const cards = [
- {
- title: "Active Studies",
- value: stats?.totalStudies ?? 0,
- description: "Research studies you have access to",
- icon: Building,
- color: "text-blue-600",
- bg: "bg-blue-50",
- },
- {
- title: "Experiments",
- value: stats?.totalExperiments ?? 0,
- description: "Experiment protocols designed",
- icon: FlaskConical,
- color: "text-green-600",
- bg: "bg-green-50",
- },
- {
- title: "Participants",
- value: stats?.totalParticipants ?? 0,
- description: "Enrolled participants",
- icon: Users,
- color: "text-purple-600",
- bg: "bg-purple-50",
- },
- {
- title: "Trials",
- value: stats?.totalTrials ?? 0,
- description: "Total trials conducted",
- icon: TestTube,
- color: "text-orange-600",
- bg: "bg-orange-50",
- },
- ];
-
- if (isLoading) {
- return (
-
- {Array.from({ length: 4 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
- ))}
-
- );
- }
-
- return (
-
- {cards.map((card) => (
-
-
- {card.title}
-
-
-
-
-
- {card.value}
- {card.description}
-
-
- ))}
-
- );
-}
-
-// Recent Activity Component
-function RecentActivity({ studyFilter }: { studyFilter: string | null }) {
- const { data: activities = [], isLoading } =
- api.dashboard.getRecentActivity.useQuery({
- limit: 8,
- studyId: studyFilter ?? undefined,
- });
-
- const getStatusIcon = (status: string) => {
- switch (status) {
- case "success":
- return
;
- case "pending":
- return
;
- case "error":
- return
;
- default:
- return
;
- }
- };
-
- return (
-
-
- Recent Activity
-
- Latest updates from your research platform
-
-
-
- {isLoading ? (
-
- {Array.from({ length: 4 }).map((_, i) => (
-
- ))}
-
- ) : activities.length === 0 ? (
-
-
-
- No recent activity
-
-
- ) : (
-
- {activities.map((activity) => (
-
- {getStatusIcon(activity.status)}
-
-
- {activity.title}
-
-
- {activity.description}
-
-
-
- {formatDistanceToNow(activity.time, { addSuffix: true })}
-
-
- ))}
-
- )}
-
-
- );
-}
-
-// Quick Actions Component
-function QuickActions() {
- const actions = [
- {
- title: "Create Study",
- description: "Start a new research study",
- href: "/studies/new",
- icon: Building,
- color: "bg-blue-500 hover:bg-blue-600",
- },
- {
- title: "Browse Studies",
- description: "View and manage your studies",
- href: "/studies",
- icon: Building,
- color: "bg-green-500 hover:bg-green-600",
- },
- {
- title: "Create Experiment",
- description: "Design new experiment protocol",
- href: "/experiments/new",
- icon: FlaskConical,
- color: "bg-purple-500 hover:bg-purple-600",
- },
- {
- title: "Browse Experiments",
- description: "View experiment templates",
- href: "/experiments",
- icon: FlaskConical,
- color: "bg-orange-500 hover:bg-orange-600",
- },
- ];
-
- return (
-
- {actions.map((action) => (
-
-
-
-
-
- {action.title}
-
-
-
- {action.description}
-
-
-
- ))}
-
- );
-}
-
-// Study Progress Component
-function StudyProgress({ studyFilter }: { studyFilter: string | null }) {
- const { data: studies = [], isLoading } =
- api.dashboard.getStudyProgress.useQuery({
- limit: 5,
- studyId: studyFilter ?? undefined,
- });
-
- return (
-
-
- Study Progress
-
- Current status of active research studies
-
-
-
- {isLoading ? (
-
- {Array.from({ length: 3 }).map((_, i) => (
-
- ))}
-
- ) : studies.length === 0 ? (
-
-
-
- No active studies found
-
-
- Create a study to get started
-
-
- ) : (
-
- {studies.map((study) => (
-
-
-
-
- {study.name}
-
-
- {study.participants}/{study.totalParticipants} completed
- trials
-
-
-
- {study.status}
-
-
-
-
- {study.progress}% complete
-
-
- ))}
-
- )}
-
-
- );
-}
-
export default function DashboardPage() {
const [studyFilter, setStudyFilter] = React.useState
(null);
- // Get user studies for filter dropdown
+ // --- Data Fetching ---
const { data: userStudiesData } = api.studies.list.useQuery({
memberOnly: true,
limit: 100,
});
-
const userStudies = userStudiesData?.studies ?? [];
+ const { data: stats } = api.dashboard.getStats.useQuery({
+ studyId: studyFilter ?? undefined,
+ });
+
+ const { data: scheduledTrials } = api.trials.list.useQuery({
+ studyId: studyFilter ?? undefined,
+ status: "scheduled",
+ limit: 5,
+ });
+
+ const { data: recentActivity } = api.dashboard.getRecentActivity.useQuery({
+ limit: 10,
+ studyId: studyFilter ?? undefined,
+ });
+
+ const { data: studyProgress } = api.dashboard.getStudyProgress.useQuery({
+ limit: 5,
+ studyId: studyFilter ?? undefined,
+ });
+
return (
-
- {/* Header */}
-
+
+ {/* Header Section */}
+
-
- Dashboard
- {studyFilter && (
-
- {userStudies.find((s) => s.id === studyFilter)?.name}
-
- )}
-
+
Dashboard
- {studyFilter
- ? "Study-specific dashboard view"
- : "Welcome to your HRI Studio research platform"}
+ Overview of your research activities and upcoming tasks.
-
-
-
- Filter by study:
-
-
- setStudyFilter(value === "all" ? null : value)
- }
- >
-
-
-
-
- All Studies
- {userStudies.map((study) => (
-
- {study.name}
-
- ))}
-
-
-
-
-
- {new Date().toLocaleDateString()}
-
+
+
+
+ setStudyFilter(value === "all" ? null : value)
+ }
+ >
+
+
+
+
+ All Studies
+ {userStudies.map((study) => (
+
+ {study.name}
+
+ ))}
+
+
+
+
+
+ New Study
+
+
- {/* Overview Cards */}
-
+ {/* Stats Cards */}
+
+
+
-
+ />
+
+
+
+
+
+
+ {/* Main Column: Scheduled Trials & Study Progress */}
-
-
-
- {/* Quick Actions */}
-
-
Quick Actions
-
+ {/* Scheduled Trials */}
+
+
+
+
+ Upcoming Sessions
+
+ You have {scheduledTrials?.length ?? 0} scheduled trials coming up.
+
+
+
+ View All
+
+
+
+
+ {!scheduledTrials?.length ? (
+
+
+
No scheduled trials found.
+
+ Schedule a Trial
+
+
+ ) : (
+
+ {scheduledTrials.map((trial) => (
+
+
+
+
+
+
+
+ {trial.participant.participantCode}
+ โข {trial.experiment.name}
+
+
+
+ {trial.scheduledAt ? format(trial.scheduledAt, "MMM d, h:mm a") : "Unscheduled"}
+
+
+
+
+
+ Start
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Study Progress */}
+
+
+ Study Progress
+
+ Completion tracking for active studies
+
+
+
+ {studyProgress?.map((study) => (
+
+
+
{study.name}
+
{study.participants} / {study.totalParticipants} Participants
+
+
+
+ ))}
+ {!studyProgress?.length && (
+ No active studies to track.
+ )}
+
+
+
+
+
+ {/* Side Column: Recent Activity & Quick Actions */}
+
+ {/* Quick Actions */}
+
+
+
+
+ New Experim.
+
+
+
+
+
+ Run Trial
+
+
+
+
+ {/* Recent Activity */}
+
+
+ Recent Activity
+
+
+
+
+ {recentActivity?.map((activity) => (
+
+
+
{activity.title}
+
{activity.description}
+
+ {formatDistanceToNow(activity.time, { addSuffix: true })}
+
+
+ ))}
+ {!recentActivity?.length && (
+
No recent activity.
+ )}
+
+
+
+
+
);
}
+
+function StatsCard({
+ title,
+ value,
+ icon: Icon,
+ description,
+ trend,
+}: {
+ title: string;
+ value: string | number;
+ icon: React.ElementType;
+ description: string;
+ trend?: string;
+}) {
+ return (
+
+
+ {title}
+
+
+
+ {value}
+
+ {description}
+ {trend && {trend} }
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ae52270..dd8a3a9 100755
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,561 +5,290 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Logo } from "~/components/ui/logo";
import { auth } from "~/server/auth";
+import {
+ ArrowRight,
+ Beaker,
+ Bot,
+ Database,
+ LayoutTemplate,
+ Lock,
+ Network,
+ PlayCircle,
+ Settings2,
+ Share2,
+} from "lucide-react";
export default async function Home() {
const session = await auth();
- // Redirect authenticated users to their dashboard
if (session?.user) {
redirect("/dashboard");
}
return (
-
- {/* Header */}
-
-
-
-
+
+ {/* Navbar */}
+
+
+
+
+
+ Features
+
+
+ Architecture
+
+
+
+ Sign In
+
+
+ Get Started
+
+
+
+
-
-
- Sign In
+
+ {/* Hero Section */}
+
+ {/* Background Gradients */}
+
+
+
+
+ โจ The Modern Standard for HRI Research
+
+
+
+ Reproducible WoZ Studies
+
+ Made Simple
+
+
+
+
+ HRIStudio is the open-source platform that bridges the gap between
+ ease of use and scientific rigor. Design, execute, and analyze
+ human-robot interaction experiments with zero friction.
+
+
+
+
+
+ Start Researching
+
+
-
- Get Started
+
+
+ View on GitHub
+
-
-
-
- {/* Hero Section */}
-
-
-
- ๐ค Human-Robot Interaction Research Platform
-
-
- Standardize Your
-
- {" "}
- Wizard of Oz{" "}
-
- Studies
-
-
- A comprehensive web-based platform that enhances the scientific
- rigor of Human-Robot Interaction experiments while remaining
- accessible to researchers with varying levels of technical
- expertise.
-
-
-
- Start Your Research
-
-
- Learn More
-
-
-
-
-
- {/* Problem Section */}
-
-
-
-
-
- The Challenge of WoZ Studies
-
-
- While Wizard of Oz is a powerful paradigm for HRI research, it
- faces significant challenges
-
-
-
-
-
-
-
- Reproducibility Issues
-
-
-
-
- โข Wizard behavior variability across trials
- โข Inconsistent experimental conditions
- โข Lack of standardized terminology
- โข Insufficient documentation
-
-
-
-
-
-
-
- Technical Barriers
-
-
-
-
- โข Platform-specific robot control systems
- โข Extensive custom coding requirements
- โข Limited to domain experts
- โข Fragmented data collection
-
-
-
-
-
-
-
-
- {/* Features Section */}
-
-
-
-
-
- Six Key Design Principles
-
-
- Our platform addresses these challenges through comprehensive
- design principles
-
-
-
-
-
-
-
- Integrated Environment
-
-
-
- All functionalities unified in a single web-based platform
- with intuitive interfaces
-
-
-
-
-
-
-
- Visual Experiment Design
-
-
-
- Minimal-to-no coding required with drag-and-drop visual
- programming capabilities
-
-
-
-
-
-
-
- Real-time Control
-
-
-
- Fine-grained, real-time control of scripted experimental
- runs with multiple robot platforms
-
-
-
-
-
-
-
- Data Management
-
-
-
- Comprehensive data collection and logging with structured
- storage and retrieval
-
-
-
-
-
-
-
- Platform Agnostic
-
-
-
- Support for wide range of robot hardware through RESTful
- APIs, ROS, and custom plugins
-
-
-
-
-
-
-
- Collaboration Support
-
-
-
- Role-based access control and data sharing for effective
- research team collaboration
-
-
-
-
-
-
-
-
- {/* Architecture Section */}
-
-
-
-
-
- Three-Layer Architecture
-
-
- Modular web application with clear separation of concerns
-
-
-
-
-
-
-
-
- User Interface Layer
-
-
-
-
-
-
- Experiment Designer
-
-
- Visual programming for experimental protocols
-
-
-
-
- Wizard Interface
-
-
- Real-time control during trial execution
-
-
-
-
- Playback & Analysis
-
-
- Data exploration and visualization
-
-
-
-
-
-
-
-
-
-
- Data Management Layer
-
-
-
-
- Secure database functionality with role-based access control
- (Researcher, Wizard, Observer) for organizing experiment
- definitions, metadata, and media assets.
-
-
- PostgreSQL
- MinIO Storage
- Role-based Access
- Cloud/On-premise
-
-
-
-
-
-
-
-
- Robot Integration Layer
-
-
-
-
- Robot-agnostic communication layer supporting multiple
- integration methods for diverse hardware platforms.
-
-
- RESTful APIs
- ROS Integration
- Custom Plugins
- Docker Deployment
-
-
-
-
-
-
-
-
- {/* Workflow Section */}
-
-
-
-
-
- Hierarchical Experiment Structure
-
-
- Standardized terminology and organization for reproducible
- research
-
-
-
-
- {/* Hierarchy visualization */}
-
-
-
-
-
- 1
-
-
-
Study
-
- Top-level container comprising one or more experiments
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
Experiment
-
- Parameterized template specifying experimental
- protocol
-
-
-
-
-
-
-
-
-
-
- 3
-
-
-
Trial
-
- Executable instance with specific participant and
- conditions
-
-
-
-
-
-
-
-
-
-
- 4
-
-
-
Step
-
- Distinct phase containing wizard or robot instructions
-
-
-
-
-
-
-
-
-
-
- 5
-
-
-
Action
-
- Specific atomic task (speech, movement, input
- gathering, etc.)
-
-
-
-
-
+ {/* Mockup / Visual Interest */}
+
+
+
+ {/* Placeholder for actual app screenshot */}
+
+
+
+
Interactive Experiment Designer
+
-
-
+
- {/* CTA Section */}
-
-
-
-
- Ready to Revolutionize Your HRI Research?
-
-
- Join researchers worldwide who are using our platform to conduct
- more rigorous, reproducible Wizard of Oz studies.
-
-
-
- Get Started Free
-
-
- Sign In
-
+ {/* Features Bento Grid */}
+
+
+
Everything You Need
+
Built for the specific needs of HRI researchers and wizards.
+
+
+
+ {/* Visual Designer - Large Item */}
+
+
+
+
+ Visual Experiment Designer
+
+
+
+
+ Construct complex branching narratives without writing a single line of code.
+ Our node-based editor handles logic, timing, and robot actions automatically.
+
+
+
+
Start
+
+
Robot: Greet
+
+
Wait: 5s
+
+
+
+
+
+ {/* Robot Agnostic */}
+
+
+
+
+ Robot Agnostic
+
+
+
+
+ Switch between robots instantly. Whether it's a NAO, Pepper, or a custom ROS2 bot,
+ your experiment logic remains strictly separated from hardware implementation.
+
+
+
+
+ {/* Role Based */}
+
+
+
+
+ Role-Based Access
+
+
+
+
+ Granular permissions for Principal Investigators, Wizards, and Observers.
+
+
+
+
+ {/* Data Logging */}
+
+
+
+
+ Full Traceability
+
+
+
+
+ Every wizard action, automated response, and sensor reading is time-stamped and logged.
+
+
+
+
+
+
+ {/* Architecture Section */}
+
+
+
+
+
Enterprise-Grade Architecture
+
+ Designed for reliability and scale. HRIStudio uses a modern stack to ensure your data is safe and your experiments run smoothly.
+
+
+
+
+
+
+
+
+
3-Layer Design
+
Clear separation between UI, Data, and Hardware layers for maximum stability.
+
+
+
+
+
+
+
+
Collaborative by Default
+
Real-time state synchronization allows multiple researchers to monitor a single trial.
+
+
+
+
+
+
+
+
ROS2 Integration
+
Native support for ROS2 nodes, topics, and actions right out of the box.
+
+
+
+
+
+
+ {/* Abstract representation of architecture */}
+
+
+
+ APP LAYER
+
+
+ Next.js Dashboard + Experiment Designer
+
+
+
+
+ DATA LAYER
+
+
+ PostgreSQL + MinIO + TRPC API
+
+
+
+
+ HARDWARE LAYER
+
+
+ ROS2 Bridge + Robot Plugins
+
+
+
+ {/* Decorative blobs */}
+
+
-
-
+
- {/* Footer */}
-
-
-
-
-
-
-
- Advancing Human-Robot Interaction research through standardized
- Wizard of Oz methodologies
+ {/* CTA Section */}
+
+ Ready to upgrade your lab?
+
+ Join the community of researchers building the future of HRI with reproducible, open-source tools.
+
+
+
+ Get Started for Free
+
+
+
+
+
+
-
+
);
}
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 0d0f035..eaebf7b 100755
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -1230,6 +1230,11 @@ export const systemSettingsRelations = relations(systemSettings, ({ one }) => ({
}),
}));
+
export const auditLogsRelations = relations(auditLogs, ({ one }) => ({
user: one(users, { fields: [auditLogs.userId], references: [users.id] }),
}));
+
+export type InsertPlugin = typeof plugins.$inferInsert;
+export type InsertPluginRepository = typeof pluginRepositories.$inferInsert;
+export type InsertRobot = typeof robots.$inferInsert;