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
{error && ( -
+
{error}
)} @@ -85,48 +91,52 @@ export default function SignInPage() { setEmail(e.target.value)} required disabled={isLoading} + className="bg-background/50" />
- +
+ + Forgot password? +
setPassword(e.target.value)} required disabled={isLoading} + className="bg-background/50" />
-
-
+
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
{error && ( -
+
{error}
)} @@ -88,11 +94,12 @@ export default function SignUpPage() { setName(e.target.value)} required disabled={createUser.isPending} + className="bg-background/50" />
@@ -101,67 +108,73 @@ export default function SignUpPage() { setEmail(e.target.value)} required disabled={createUser.isPending} + className="bg-background/50" />
-
- - setPassword(e.target.value)} - required - disabled={createUser.isPending} - minLength={6} - /> -
+
+
+ + setPassword(e.target.value)} + required + disabled={createUser.isPending} + minLength={6} + className="bg-background/50" + /> +
-
- - setConfirmPassword(e.target.value)} - required - disabled={createUser.isPending} - minLength={6} - /> +
+ + setConfirmPassword(e.target.value)} + required + disabled={createUser.isPending} + minLength={6} + className="bg-background/50" + /> +
-
+
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.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: - - -
- - - {new Date().toLocaleDateString()} - + +
+ + +
- {/* 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. + +
+ +
+
+ + {!scheduledTrials?.length ? ( +
+ +

No scheduled trials found.

+ +
+ ) : ( +
+ {scheduledTrials.map((trial) => ( +
+
+
+ +
+
+

+ {trial.participant.participantCode} + โ€ข {trial.experiment.name} +

+
+ + {trial.scheduledAt ? format(trial.scheduledAt, "MMM d, h:mm a") : "Unscheduled"} +
+
+
+ +
+ ))} +
+ )} +
+
+ + {/* 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 */} +
+ + +
+ + {/* 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 */} +
+
+ + +
+
-
- -
-
-
-
- {/* 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. -

-
- - -
-
-
- - {/* 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. -

-
- - + {/* 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. +

+
+ +
+
+
+ + - +
); } 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;